Compare commits
2 Commits
cb1447e554
...
d8db9f14b3
| Author | SHA1 | Date | |
|---|---|---|---|
| d8db9f14b3 | |||
| d1485a66c1 |
@@ -289,6 +289,10 @@ type ui struct {
|
|||||||
apiTokenClicks []widget.Clickable
|
apiTokenClicks []widget.Clickable
|
||||||
apiPolicyRemoves []widget.Clickable
|
apiPolicyRemoves []widget.Clickable
|
||||||
apiAuditClicks []widget.Clickable
|
apiAuditClicks []widget.Clickable
|
||||||
|
apiAuditTokenFilters []widget.Clickable
|
||||||
|
apiAuditDecisionFilters []widget.Clickable
|
||||||
|
apiAuditOperationFilters []widget.Clickable
|
||||||
|
clearAPIAuditFilters widget.Clickable
|
||||||
historyClicks []widget.Clickable
|
historyClicks []widget.Clickable
|
||||||
attachmentClicks []widget.Clickable
|
attachmentClicks []widget.Clickable
|
||||||
breadcrumbs []widget.Clickable
|
breadcrumbs []widget.Clickable
|
||||||
@@ -2265,7 +2269,7 @@ func (u *ui) listEmptyState() emptyState {
|
|||||||
case appstate.SectionAPIAudit:
|
case appstate.SectionAPIAudit:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
Title: "No matching audit events",
|
Title: "No matching audit events",
|
||||||
Body: fmt.Sprintf("No audit events match %q. Clear or refine Search vault to filter by token, decision, operation, or resource.", query),
|
Body: fmt.Sprintf("No audit events match %q. Clear the search or try a different quick filter.", query),
|
||||||
}
|
}
|
||||||
case appstate.SectionTemplates:
|
case appstate.SectionTemplates:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
@@ -2293,7 +2297,7 @@ func (u *ui) listEmptyState() emptyState {
|
|||||||
case appstate.SectionAPIAudit:
|
case appstate.SectionAPIAudit:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
Title: "No API audit events yet",
|
Title: "No API audit events yet",
|
||||||
Body: "Approval prompts, denials, and token actions will appear here.",
|
Body: "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity.",
|
||||||
}
|
}
|
||||||
case appstate.SectionTemplates:
|
case appstate.SectionTemplates:
|
||||||
return emptyState{
|
return emptyState{
|
||||||
@@ -2332,7 +2336,7 @@ func (u *ui) detailPlaceholderMessage() string {
|
|||||||
case appstate.SectionAPITokens:
|
case appstate.SectionAPITokens:
|
||||||
return "Select an API token, issue a new one, or search to narrow the list."
|
return "Select an API token, issue a new one, or search to narrow the list."
|
||||||
case appstate.SectionAPIAudit:
|
case appstate.SectionAPIAudit:
|
||||||
return "Select an audit event to inspect it, or filter the list with Search vault."
|
return "Select an audit event to inspect it, or use Search vault or the quick filters above."
|
||||||
case appstate.SectionTemplates:
|
case appstate.SectionTemplates:
|
||||||
return "Select a template or start a reusable entry."
|
return "Select a template or start a reusable entry."
|
||||||
case appstate.SectionRecycleBin:
|
case appstate.SectionRecycleBin:
|
||||||
|
|||||||
@@ -636,6 +636,70 @@ func TestUIAPIAuditSectionShowsRecordedEvents(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIAPIAuditEventsMatchFriendlyQuickFilters(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.auditLog = apiaudit.New(10)
|
||||||
|
u.auditLog.Record(apiaudit.Event{
|
||||||
|
Type: apiaudit.EventApprovalAllowed,
|
||||||
|
TokenName: "Browser Extension",
|
||||||
|
Operation: apitokens.OperationCopyPassword,
|
||||||
|
Message: "approved",
|
||||||
|
})
|
||||||
|
u.auditLog.Record(apiaudit.Event{
|
||||||
|
Type: apiaudit.EventApprovalDenied,
|
||||||
|
TokenName: "CLI",
|
||||||
|
Operation: apitokens.OperationListEntries,
|
||||||
|
Message: "denied",
|
||||||
|
})
|
||||||
|
|
||||||
|
u.showAPIAuditSection()
|
||||||
|
|
||||||
|
u.search.SetText("Allowed")
|
||||||
|
if got := u.apiAuditEvents(); len(got) != 1 || got[0].Type != apiaudit.EventApprovalAllowed {
|
||||||
|
t.Fatalf("apiAuditEvents() with Allowed = %#v, want allowed event", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.search.SetText("copy password")
|
||||||
|
if got := u.apiAuditEvents(); len(got) != 1 || got[0].Operation != apitokens.OperationCopyPassword {
|
||||||
|
t.Fatalf("apiAuditEvents() with copy password = %#v, want copy_password event", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.search.SetText("CLI")
|
||||||
|
if got := u.apiAuditEvents(); len(got) != 1 || got[0].TokenName != "CLI" {
|
||||||
|
t.Fatalf("apiAuditEvents() with CLI = %#v, want CLI token event", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIAPIAuditMessagesGuideQuickFilters(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.showAPIAuditSection()
|
||||||
|
|
||||||
|
if got := u.listEmptyState(); got.Title != "No API audit events yet" || got.Body != "Connect a trusted client, respond to approval prompts, or issue a token to start recording activity." {
|
||||||
|
t.Fatalf("listEmptyState() = %#v, want updated API audit guidance", got)
|
||||||
|
}
|
||||||
|
if got := u.detailPlaceholderMessage(); got != "Select an audit event to inspect it, or use Search vault or the quick filters above." {
|
||||||
|
t.Fatalf("detailPlaceholderMessage() = %q, want quick-filter guidance", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
u.search.SetText("allowed")
|
||||||
|
if got := u.listEmptyState(); got.Title != "No matching audit events" || got.Body != `No audit events match "allowed". Clear the search or try a different quick filter.` {
|
||||||
|
t.Fatalf("listEmptyState() with search = %#v, want quick-filter empty-state guidance", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUILifecycleSecuritySettingsSummaryMovesAdvancedFieldsOutOfOpenFlow(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
if got := u.lifecycleSecuritySettingsSummary(); got != "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices." {
|
||||||
|
t.Fatalf("lifecycleSecuritySettingsSummary() = %q, want focused lifecycle guidance", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUISelectedEntryFollowsApplicationStateSelection(t *testing.T) {
|
func TestUISelectedEntryFollowsApplicationStateSelection(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image/color"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,6 +28,130 @@ func apiOperations() []apitokens.Operation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type apiAuditQuickFilter struct {
|
||||||
|
Label string
|
||||||
|
Query string
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiAuditDecisionLabel(eventType apiaudit.EventType) string {
|
||||||
|
switch eventType {
|
||||||
|
case apiaudit.EventApprovalRequested:
|
||||||
|
return "Requested"
|
||||||
|
case apiaudit.EventApprovalAllowed:
|
||||||
|
return "Allowed"
|
||||||
|
case apiaudit.EventApprovalDenied:
|
||||||
|
return "Denied"
|
||||||
|
case apiaudit.EventApprovalCanceled:
|
||||||
|
return "Canceled"
|
||||||
|
case apiaudit.EventApprovalTimedOut:
|
||||||
|
return "Timed Out"
|
||||||
|
case apiaudit.EventAuthRejected:
|
||||||
|
return "Auth Rejected"
|
||||||
|
default:
|
||||||
|
return strings.ReplaceAll(string(eventType), "_", " ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiAuditOperationLabel(operation apitokens.Operation) string {
|
||||||
|
if strings.TrimSpace(string(operation)) == "" {
|
||||||
|
return "Other"
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(string(operation), "_", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
func compactAuditFilterLabel(label string) string {
|
||||||
|
label = strings.TrimSpace(label)
|
||||||
|
if len(label) <= 22 {
|
||||||
|
return label
|
||||||
|
}
|
||||||
|
return label[:19] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiAuditEventSearchTerms(event apiaudit.Event) string {
|
||||||
|
parts := []string{
|
||||||
|
string(event.Type),
|
||||||
|
apiAuditDecisionLabel(event.Type),
|
||||||
|
event.TokenName,
|
||||||
|
event.ClientName,
|
||||||
|
string(event.Operation),
|
||||||
|
apiAuditOperationLabel(event.Operation),
|
||||||
|
strings.Join(event.Resource.Path, " / "),
|
||||||
|
event.Resource.EntryID,
|
||||||
|
event.Message,
|
||||||
|
}
|
||||||
|
switch event.Type {
|
||||||
|
case apiaudit.EventApprovalAllowed:
|
||||||
|
parts = append(parts, "allow approved")
|
||||||
|
case apiaudit.EventApprovalDenied:
|
||||||
|
parts = append(parts, "deny denied")
|
||||||
|
case apiaudit.EventApprovalRequested:
|
||||||
|
parts = append(parts, "prompt requested")
|
||||||
|
case apiaudit.EventApprovalCanceled:
|
||||||
|
parts = append(parts, "cancel canceled")
|
||||||
|
case apiaudit.EventApprovalTimedOut:
|
||||||
|
parts = append(parts, "timeout timed out")
|
||||||
|
case apiaudit.EventAuthRejected:
|
||||||
|
parts = append(parts, "rejected unauthorized")
|
||||||
|
}
|
||||||
|
return strings.ToLower(strings.Join(parts, " "))
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiAuditFilterButtons(clicks *[]widget.Clickable, filters []apiAuditQuickFilter) []widget.Clickable {
|
||||||
|
if len(filters) == 0 {
|
||||||
|
*clicks = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if len(*clicks) < len(filters) {
|
||||||
|
next := make([]widget.Clickable, len(filters))
|
||||||
|
copy(next, *clicks)
|
||||||
|
*clicks = next
|
||||||
|
}
|
||||||
|
return (*clicks)[:len(filters)]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) apiAuditQuickFilters(events []apiaudit.Event) ([]apiAuditQuickFilter, []apiAuditQuickFilter, []apiAuditQuickFilter) {
|
||||||
|
tokenSeen := map[string]struct{}{}
|
||||||
|
decisionSeen := map[apiaudit.EventType]struct{}{}
|
||||||
|
operationSeen := map[apitokens.Operation]struct{}{}
|
||||||
|
var tokens []apiAuditQuickFilter
|
||||||
|
var decisions []apiAuditQuickFilter
|
||||||
|
var operations []apiAuditQuickFilter
|
||||||
|
|
||||||
|
for _, event := range events {
|
||||||
|
if name := strings.TrimSpace(event.TokenName); name != "" {
|
||||||
|
if _, ok := tokenSeen[name]; !ok {
|
||||||
|
tokenSeen[name] = struct{}{}
|
||||||
|
tokens = append(tokens, apiAuditQuickFilter{Label: name, Query: name})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := decisionSeen[event.Type]; !ok {
|
||||||
|
decisionSeen[event.Type] = struct{}{}
|
||||||
|
label := apiAuditDecisionLabel(event.Type)
|
||||||
|
decisions = append(decisions, apiAuditQuickFilter{Label: label, Query: label})
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(event.Operation)) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := operationSeen[event.Operation]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
operationSeen[event.Operation] = struct{}{}
|
||||||
|
label := apiAuditOperationLabel(event.Operation)
|
||||||
|
operations = append(operations, apiAuditQuickFilter{Label: label, Query: label})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tokens) > 4 {
|
||||||
|
tokens = tokens[:4]
|
||||||
|
}
|
||||||
|
if len(decisions) > 5 {
|
||||||
|
decisions = decisions[:5]
|
||||||
|
}
|
||||||
|
if len(operations) > 4 {
|
||||||
|
operations = operations[:4]
|
||||||
|
}
|
||||||
|
return tokens, decisions, operations
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) apiTokens() []apitokens.Token {
|
func (u *ui) apiTokens() []apitokens.Token {
|
||||||
tokens, err := u.state.APITokens()
|
tokens, err := u.state.APITokens()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -274,15 +399,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
|
|||||||
}
|
}
|
||||||
filtered := make([]apiaudit.Event, 0, len(events))
|
filtered := make([]apiaudit.Event, 0, len(events))
|
||||||
for _, event := range events {
|
for _, event := range events {
|
||||||
haystack := strings.ToLower(strings.Join([]string{
|
haystack := apiAuditEventSearchTerms(event)
|
||||||
string(event.Type),
|
|
||||||
event.TokenName,
|
|
||||||
event.ClientName,
|
|
||||||
string(event.Operation),
|
|
||||||
strings.Join(event.Resource.Path, " / "),
|
|
||||||
event.Resource.EntryID,
|
|
||||||
event.Message,
|
|
||||||
}, " "))
|
|
||||||
if strings.Contains(haystack, query) {
|
if strings.Contains(haystack, query) {
|
||||||
filtered = append(filtered, event)
|
filtered = append(filtered, event)
|
||||||
}
|
}
|
||||||
@@ -516,6 +633,11 @@ func (u *ui) apiTokenListPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
|
|
||||||
func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
||||||
events := u.apiAuditEvents()
|
events := u.apiAuditEvents()
|
||||||
|
allEvents := []apiaudit.Event(nil)
|
||||||
|
if u.auditLog != nil {
|
||||||
|
allEvents = u.auditLog.Events()
|
||||||
|
}
|
||||||
|
tokenFilters, decisionFilters, operationFilters := u.apiAuditQuickFilters(allEvents)
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
text := "Local gRPC audit history"
|
text := "Local gRPC audit history"
|
||||||
@@ -528,14 +650,18 @@ func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
lbl := material.Label(u.theme, unit.Sp(12), "Filter by typing a token name, decision, operation, or resource in Search vault.")
|
lbl := material.Label(u.theme, unit.Sp(12), "Filter by token, decision, or operation. Use the quick filters below or type a resource path in Search vault.")
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.apiAuditQuickFilterPanel(gtx, tokenFilters, decisionFilters, operationFilters)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
if len(events) == 0 {
|
if len(events) == 0 {
|
||||||
lbl := material.Label(u.theme, unit.Sp(14), "No audit events yet. Approval prompts, denials, token changes, and filled requests will appear here.")
|
lbl := material.Label(u.theme, unit.Sp(14), u.listEmptyMessage())
|
||||||
lbl.Color = mutedColor
|
lbl.Color = mutedColor
|
||||||
return lbl.Layout(gtx)
|
return lbl.Layout(gtx)
|
||||||
}
|
}
|
||||||
@@ -546,6 +672,109 @@ func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) apiAuditQuickFilterPanel(gtx layout.Context, tokenFilters, decisionFilters, operationFilters []apiAuditQuickFilter) layout.Dimensions {
|
||||||
|
hasTokens := len(tokenFilters) > 0
|
||||||
|
hasDecisions := len(decisionFilters) > 0
|
||||||
|
hasOperations := len(operationFilters) > 0
|
||||||
|
query := strings.TrimSpace(u.search.Text())
|
||||||
|
if !hasTokens && !hasDecisions && !hasOperations && query == "" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
|
||||||
|
children := make([]layout.FlexChild, 0, 8)
|
||||||
|
if query != "" {
|
||||||
|
children = append(children,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.auditQuickFilterButton(gtx, &u.clearAPIAuditFilters, "Clear filters", false, "")
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if hasTokens {
|
||||||
|
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.apiAuditQuickFilterRow(gtx, "Tokens", tokenFilters, &u.apiAuditTokenFilters)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if hasDecisions {
|
||||||
|
if len(children) > 0 {
|
||||||
|
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||||
|
}
|
||||||
|
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.apiAuditQuickFilterRow(gtx, "Decisions", decisionFilters, &u.apiAuditDecisionFilters)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
if hasOperations {
|
||||||
|
if len(children) > 0 {
|
||||||
|
children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||||
|
}
|
||||||
|
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.apiAuditQuickFilterRow(gtx, "Operations", operationFilters, &u.apiAuditOperationFilters)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) apiAuditQuickFilterRow(gtx layout.Context, title string, filters []apiAuditQuickFilter, clicks *[]widget.Clickable) layout.Dimensions {
|
||||||
|
buttons := apiAuditFilterButtons(clicks, filters)
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), title)
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
if gtx.Constraints.Max.X <= gtx.Dp(unit.Dp(460)) {
|
||||||
|
column := make([]layout.FlexChild, 0, len(filters)*2)
|
||||||
|
for i := range filters {
|
||||||
|
if i > 0 {
|
||||||
|
column = append(column, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout))
|
||||||
|
}
|
||||||
|
filter := filters[i]
|
||||||
|
click := &buttons[i]
|
||||||
|
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||||
|
column = append(column, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.auditQuickFilterButton(gtx, click, compactAuditFilterLabel(filter.Label), selected, filter.Query)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, column...)
|
||||||
|
}
|
||||||
|
flexChildren := make([]layout.FlexChild, 0, len(filters)*2)
|
||||||
|
for i := range filters {
|
||||||
|
if i > 0 {
|
||||||
|
flexChildren = append(flexChildren, layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout))
|
||||||
|
}
|
||||||
|
filter := filters[i]
|
||||||
|
click := &buttons[i]
|
||||||
|
selected := strings.EqualFold(strings.TrimSpace(u.search.Text()), strings.TrimSpace(filter.Query))
|
||||||
|
flexChildren = append(flexChildren, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return u.auditQuickFilterButton(gtx, click, compactAuditFilterLabel(filter.Label), selected, filter.Query)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx, flexChildren...)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) auditQuickFilterButton(gtx layout.Context, click *widget.Clickable, label string, selected bool, query string) layout.Dimensions {
|
||||||
|
for click.Clicked(gtx) {
|
||||||
|
u.search.SetText(strings.TrimSpace(query))
|
||||||
|
u.filter()
|
||||||
|
}
|
||||||
|
btn := material.Button(u.theme, click, label)
|
||||||
|
btn.CornerRadius = unit.Dp(10)
|
||||||
|
btn.TextSize = unit.Sp(11)
|
||||||
|
btn.Inset = layout.Inset{Top: 5, Bottom: 5, Left: 9, Right: 9}
|
||||||
|
if selected {
|
||||||
|
btn.Background = accentColor
|
||||||
|
btn.Color = color.NRGBA{R: 255, G: 248, B: 238, A: 255}
|
||||||
|
} else {
|
||||||
|
btn.Background = color.NRGBA{R: 231, G: 224, B: 214, A: 255}
|
||||||
|
btn.Color = accentColor
|
||||||
|
}
|
||||||
|
return btn.Layout(gtx)
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
|
||||||
token, ok := u.selectedAPIToken()
|
token, ok := u.selectedAPIToken()
|
||||||
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
|
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
|
||||||
|
|||||||
+24
-3
@@ -269,11 +269,28 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
if busy || u.lifecycleAdvancedHidden {
|
if busy || u.lifecycleAdvancedHidden {
|
||||||
return layout.Dimensions{}
|
return layout.Dimensions{}
|
||||||
}
|
}
|
||||||
|
if u.lifecycleMode == "remote" {
|
||||||
|
return layout.Dimensions{}
|
||||||
|
}
|
||||||
|
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Cipher", "Used for new vaults and future saves. Supported values: aes256, chacha20.", &u.securityCipher, false)),
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
lbl := material.Label(u.theme, unit.Sp(13), "Vault settings")
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Used for new vaults and future saves. Supported values: aes-kdf, argon2.", &u.securityKDF, false)),
|
lbl.Color = accentColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(12), u.lifecycleSecuritySettingsSummary())
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.openSecuritySettings, "Open Vault Settings")
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
@@ -313,6 +330,10 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) lifecycleSecuritySettingsSummary() string {
|
||||||
|
return "Cipher and KDF now live in Vault Settings so opening and creating a vault stays focused on the file, key material, and sync choices."
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) lifecycleAdvancedDisclosure(gtx layout.Context) layout.Dimensions {
|
func (u *ui) lifecycleAdvancedDisclosure(gtx layout.Context) layout.Dimensions {
|
||||||
return u.toggleLifecycleAdvanced.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return u.toggleLifecycleAdvanced.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
return layout.UniformInset(unit.Dp(2)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
|
|||||||
Reference in New Issue
Block a user