Refine audit UI and vault settings placement

This commit is contained in:
Joe Julian
2026-04-01 17:11:34 -07:00
parent 99a798afbd
commit 4d35e7237d
4 changed files with 541 additions and 223 deletions
+211 -207
View File
@@ -165,210 +165,214 @@ const (
)
type ui struct {
mode string
theme *material.Theme
logoHorizontal paint.ImageOp
splashSquare paint.ImageOp
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
apiTokenName widget.Editor
apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor
securityCipher widget.Editor
securityKDF widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
customFieldKeys []widget.Editor
customFieldValues []widget.Editor
historyIndex widget.Editor
groupName widget.Editor
groupParentPath widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
list widget.List
groupList widget.List
detailList widget.List
apiPolicyList widget.List
lifecycleList widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable
openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable
closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable
runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable
clearVaultSelection widget.Clickable
clearRemoteSelection widget.Clickable
dismissBanner widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
replaceAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
goToRootGroup widget.Clickable
goToParentGroup widget.Clickable
createGroup widget.Clickable
moveGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable
toggleGroupControls widget.Clickable
toggleLifecycleAdvanced widget.Clickable
toggleHistory widget.Clickable
togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showAPITokens widget.Clickable
showAPIAudit widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable
showSyncRemote widget.Clickable
showSyncPull widget.Clickable
showSyncPush widget.Clickable
allowApproval widget.Clickable
denyApproval widget.Clickable
cancelApproval widget.Clickable
cancelLifecycleProgress widget.Clickable
retryLifecycleOpen widget.Clickable
approvalPermanent widget.Bool
rememberRemoteAuth widget.Bool
apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool
entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable
apiAuditClicks []widget.Clickable
historyClicks []widget.Clickable
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable
recentRemoteClicks []widget.Clickable
removeCustomFields []widget.Clickable
state appstate.State
visible []entry
currentPath []string
syncedPath []string
selectedHistoryIndex int
showPassword bool
generatedPasswordDraft bool
togglePassword widget.Clickable
copyAPITokenSecret widget.Clickable
issueAPIToken widget.Clickable
saveAPIToken widget.Clickable
rotateAPIToken widget.Clickable
disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable
addAPIPolicyRule widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon
chevronDownIcon *widget.Icon
settingsIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
loadingActionLabel string
lifecycleMode string
syncSourceMode syncSourceMode
syncDirection syncDirection
syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor
syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor
syncDialogOpen bool
syncMenuOpen bool
securityDialogOpen bool
showSyncPassword bool
keyboardFocus focusID
defaultSaveAsPath string
recentVaultsPath string
uiPreferencesPath string
recentRemotesPath string
autofillCachePath string
editingEntry bool
groupControlsHidden bool
lifecycleAdvancedHidden bool
historyHidden bool
recentVaults []string
recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string
recentVaultUsedAt map[string]time.Time
entriesState entriesSectionState
deleteGroupPath []string
apiPolicyGroupScope bool
apiTokenSecret string
selectedAuditIndex int
statusExpiresAt time.Time
now func() time.Time
apiHost *api.Host
auditLog *apiaudit.Log
grpcAddress string
backgroundResults chan backgroundActionResult
backgroundActionSerial int
activeBackgroundAction int
lastLifecycleAction string
requestMasterPassFocus bool
invalidate func()
mode string
theme *material.Theme
logoHorizontal paint.ImageOp
splashSquare paint.ImageOp
search widget.Editor
vaultPath widget.Editor
saveAsPath widget.Editor
remoteBaseURL widget.Editor
remotePath widget.Editor
remoteUsername widget.Editor
remotePassword widget.Editor
masterPassword widget.Editor
keyFilePath widget.Editor
apiTokenName widget.Editor
apiTokenClientName widget.Editor
apiTokenExpiresAt widget.Editor
apiPolicyOperation widget.Editor
apiPolicyPath widget.Editor
apiPolicyEntryID widget.Editor
securityCipher widget.Editor
securityKDF widget.Editor
entryID widget.Editor
entryTitle widget.Editor
entryUsername widget.Editor
entryPassword widget.Editor
entryURL widget.Editor
entryNotes widget.Editor
entryTags widget.Editor
entryPath widget.Editor
entryFields widget.Editor
customFieldKeys []widget.Editor
customFieldValues []widget.Editor
historyIndex widget.Editor
groupName widget.Editor
groupParentPath widget.Editor
passwordProfile widget.Editor
attachmentName widget.Editor
attachmentPath widget.Editor
exportAttachmentPath widget.Editor
list widget.List
groupList widget.List
detailList widget.List
apiPolicyList widget.List
lifecycleList widget.List
copyUser widget.Clickable
copyPass widget.Clickable
copyURL widget.Clickable
lockVault widget.Clickable
unlockVault widget.Clickable
createVault widget.Clickable
openVault widget.Clickable
saveVault widget.Clickable
saveAsVault widget.Clickable
openRemote widget.Clickable
changeMasterKey widget.Clickable
synchronizeVault widget.Clickable
toggleSyncMenu widget.Clickable
openAdvancedSync widget.Clickable
openSecuritySettings widget.Clickable
closeAdvancedSync widget.Clickable
closeSecuritySettings widget.Clickable
runAdvancedSync widget.Clickable
saveSecuritySettings widget.Clickable
editEntry widget.Clickable
cancelEdit widget.Clickable
pickVaultPath widget.Clickable
pickKeyFile widget.Clickable
pickSyncLocalPath widget.Clickable
clearVaultSelection widget.Clickable
clearRemoteSelection widget.Clickable
dismissBanner widget.Clickable
addEntry widget.Clickable
saveEntry widget.Clickable
duplicateEntry widget.Clickable
deleteEntry widget.Clickable
restoreEntry widget.Clickable
saveTemplate widget.Clickable
deleteTemplate widget.Clickable
instantiateTemplate widget.Clickable
addAttachment widget.Clickable
replaceAttachment widget.Clickable
removeAttachment widget.Clickable
exportAttachment widget.Clickable
restoreHistory widget.Clickable
generatePassword widget.Clickable
goToRootGroup widget.Clickable
goToParentGroup widget.Clickable
createGroup widget.Clickable
moveGroup widget.Clickable
renameGroup widget.Clickable
deleteGroup widget.Clickable
confirmDeleteGroup widget.Clickable
cancelDeleteGroup widget.Clickable
addCustomField widget.Clickable
toggleGroupControls widget.Clickable
toggleLifecycleAdvanced widget.Clickable
toggleHistory widget.Clickable
togglePasswordInline widget.Clickable
toggleSyncPassword widget.Clickable
showEntries widget.Clickable
showTemplates widget.Clickable
showRecycle widget.Clickable
showAPITokens widget.Clickable
showAPIAudit widget.Clickable
showLocalLifecycle widget.Clickable
showRemoteLifecycle widget.Clickable
showSyncLocal widget.Clickable
showSyncRemote widget.Clickable
showSyncPull widget.Clickable
showSyncPush widget.Clickable
allowApproval widget.Clickable
denyApproval widget.Clickable
cancelApproval widget.Clickable
cancelLifecycleProgress widget.Clickable
retryLifecycleOpen widget.Clickable
approvalPermanent widget.Bool
rememberRemoteAuth widget.Bool
apiPolicyAllow widget.Bool
apiPolicyGroupScopeW widget.Bool
apiTokenDisabled widget.Bool
entryClicks []widget.Clickable
apiTokenClicks []widget.Clickable
apiPolicyRemoves []widget.Clickable
apiAuditClicks []widget.Clickable
apiAuditTokenFilters []widget.Clickable
apiAuditDecisionFilters []widget.Clickable
apiAuditOperationFilters []widget.Clickable
clearAPIAuditFilters widget.Clickable
historyClicks []widget.Clickable
attachmentClicks []widget.Clickable
breadcrumbs []widget.Clickable
groupClicks []widget.Clickable
recentVaultClicks []widget.Clickable
recentRemoteClicks []widget.Clickable
removeCustomFields []widget.Clickable
state appstate.State
visible []entry
currentPath []string
syncedPath []string
selectedHistoryIndex int
showPassword bool
generatedPasswordDraft bool
togglePassword widget.Clickable
copyAPITokenSecret widget.Clickable
issueAPIToken widget.Clickable
saveAPIToken widget.Clickable
rotateAPIToken widget.Clickable
disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable
addAPIPolicyRule widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
splitBase float32
splitStartY float32
phoneSpan int
eyeIcon *widget.Icon
eyeOffIcon *widget.Icon
copyIcon *widget.Icon
expandMoreIcon *widget.Icon
expandLessIcon *widget.Icon
chevronDownIcon *widget.Icon
settingsIcon *widget.Icon
clipboardWriter clipboard.Writer
loadingMessage string
loadingActionLabel string
lifecycleMode string
syncSourceMode syncSourceMode
syncDirection syncDirection
syncLocalPath widget.Editor
syncRemoteBaseURL widget.Editor
syncRemotePath widget.Editor
syncRemoteUsername widget.Editor
syncRemotePassword widget.Editor
syncDialogOpen bool
syncMenuOpen bool
securityDialogOpen bool
showSyncPassword bool
keyboardFocus focusID
defaultSaveAsPath string
recentVaultsPath string
uiPreferencesPath string
recentRemotesPath string
autofillCachePath string
editingEntry bool
groupControlsHidden bool
lifecycleAdvancedHidden bool
historyHidden bool
recentVaults []string
recentRemotes []recentRemoteRecord
recentVaultGroups map[string][]string
recentVaultUsedAt map[string]time.Time
entriesState entriesSectionState
deleteGroupPath []string
apiPolicyGroupScope bool
apiTokenSecret string
selectedAuditIndex int
statusExpiresAt time.Time
now func() time.Time
apiHost *api.Host
auditLog *apiaudit.Log
grpcAddress string
backgroundResults chan backgroundActionResult
backgroundActionSerial int
activeBackgroundAction int
lastLifecycleAction string
requestMasterPassFocus bool
invalidate func()
}
type backgroundActionResult struct {
@@ -2265,7 +2269,7 @@ func (u *ui) listEmptyState() emptyState {
case appstate.SectionAPIAudit:
return emptyState{
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:
return emptyState{
@@ -2293,7 +2297,7 @@ func (u *ui) listEmptyState() emptyState {
case appstate.SectionAPIAudit:
return emptyState{
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:
return emptyState{
@@ -2332,7 +2336,7 @@ func (u *ui) detailPlaceholderMessage() string {
case appstate.SectionAPITokens:
return "Select an API token, issue a new one, or search to narrow the list."
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:
return "Select a template or start a reusable entry."
case appstate.SectionRecycleBin:
+64
View File
@@ -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.listEmptyMessage(); got != "No API audit events yet. Connect a trusted client, respond to approval prompts, or issue a token to start recording activity." {
t.Fatalf("listEmptyMessage() = %q, 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.listEmptyMessage(); got != `No audit events match "allowed". Clear the search or try a different quick filter.` {
t.Fatalf("listEmptyMessage() with search = %q, 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) {
t.Parallel()
+240 -11
View File
@@ -2,6 +2,7 @@ package main
import (
"fmt"
"image/color"
"strings"
"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 {
tokens, err := u.state.APITokens()
if err != nil {
@@ -274,15 +399,7 @@ func (u *ui) apiAuditEvents() []apiaudit.Event {
}
filtered := make([]apiaudit.Event, 0, len(events))
for _, event := range events {
haystack := strings.ToLower(strings.Join([]string{
string(event.Type),
event.TokenName,
event.ClientName,
string(event.Operation),
strings.Join(event.Resource.Path, " / "),
event.Resource.EntryID,
event.Message,
}, " "))
haystack := apiAuditEventSearchTerms(event)
if strings.Contains(haystack, query) {
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 {
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,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
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(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
return lbl.Layout(gtx)
}),
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 {
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
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 {
token, ok := u.selectedAPIToken()
removeClicks := u.ensureAPIPolicyRemoveClickables(0)
+26 -5
View File
@@ -269,11 +269,28 @@ func (u *ui) lifecycleControls(gtx layout.Context) layout.Dimensions {
if busy || u.lifecycleAdvancedHidden {
return layout.Dimensions{}
}
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(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "KDF", "Used for new vaults and future saves. Supported values: aes-kdf, argon2.", &u.securityKDF, false)),
)
if u.lifecycleMode == "remote" {
return layout.Dimensions{}
}
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
lbl := material.Label(u.theme, unit.Sp(13), "Vault settings")
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(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 {
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 {