Add API policy pickers and contextual search labels

This commit is contained in:
Joe Julian
2026-04-03 10:55:17 -07:00
parent eebb270158
commit 05f43aa7bb
3 changed files with 183 additions and 8 deletions
+29 -4
View File
@@ -384,6 +384,9 @@ type ui struct {
disableAPIToken widget.Clickable
revokeAPIToken widget.Clickable
deleteAPIToken widget.Clickable
useCurrentGroupForPolicy widget.Clickable
useSelectedEntryForPolicy widget.Clickable
clearAPIPolicyTarget widget.Clickable
addAPIPolicyRule widget.Clickable
phoneSplit widget.Float
splitDrag gesture.Drag
@@ -669,6 +672,19 @@ func (u *ui) visibleEntrySnapshot() ([]entry, []*widget.Clickable) {
return visible, clicks
}
func (u *ui) searchPlaceholder() string {
switch u.state.Section {
case appstate.SectionAPITokens:
return "Search API tokens"
case appstate.SectionAPIAudit:
return "Search audit log"
case appstate.SectionRecycleBin:
return "Search recycle bin"
default:
return "Search vault"
}
}
func defaultStatePaths(stateDir string) statePaths {
baseDir := strings.TrimSpace(stateDir)
if baseDir == "" {
@@ -2592,7 +2608,7 @@ func (u *ui) listEmptyState() emptyState {
case appstate.SectionAPITokens:
return emptyState{
Title: "No matching API tokens",
Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search vault to find a token by name, client, or expiration.", query),
Body: fmt.Sprintf("No API tokens match %q. Clear or refine Search API tokens to find a token by name, client, or expiration.", query),
}
case appstate.SectionAPIAudit:
return emptyState{
@@ -2664,7 +2680,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 use Search vault or the quick filters above."
return "Select an audit event to inspect it, or use Search audit log or the quick filters above."
case appstate.SectionTemplates:
return "Select a template or start a reusable entry."
case appstate.SectionRecycleBin:
@@ -2996,6 +3012,15 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
for u.addAPIPolicyRule.Clicked(gtx) {
u.runAction("add API policy rule", u.addAPIPolicyRuleAction)
}
for u.useCurrentGroupForPolicy.Clicked(gtx) {
u.runAction("use current group for API policy", u.useCurrentGroupForPolicyAction)
}
for u.useSelectedEntryForPolicy.Clicked(gtx) {
u.runAction("use selected entry for API policy", u.useSelectedEntryForPolicyAction)
}
for u.clearAPIPolicyTarget.Clicked(gtx) {
u.runAction("clear API policy target", u.clearAPIPolicyTargetAction)
}
for i := range u.apiPolicyRemoves {
for u.apiPolicyRemoves[i].Clicked(gtx) {
index := i
@@ -4169,7 +4194,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
rows = append(rows, func(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
editor := material.Editor(u.theme, &u.search, "Search vault")
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
editor.Color = u.theme.Palette.Fg
editor.HintColor = mutedColor
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
@@ -4275,7 +4300,7 @@ func (u *ui) listPanel(gtx layout.Context) layout.Dimensions {
gtx.Constraints.Min.X = gtx.Constraints.Max.X
}
return u.outlinedFieldState(gtx, u.isFocused(focusSearch), func(gtx layout.Context) layout.Dimensions {
editor := material.Editor(u.theme, &u.search, "Search vault")
editor := material.Editor(u.theme, &u.search, u.searchPlaceholder())
editor.Color = u.theme.Palette.Fg
editor.HintColor = mutedColor
return layout.UniformInset(unit.Dp(10)).Layout(gtx, editor.Layout)
+65 -1
View File
@@ -973,7 +973,7 @@ func TestUIAPIAuditMessagesGuideQuickFilters(t *testing.T) {
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." {
if got := u.detailPlaceholderMessage(); got != "Select an audit event to inspect it, or use Search audit log or the quick filters above." {
t.Fatalf("detailPlaceholderMessage() = %q, want quick-filter guidance", got)
}
@@ -5217,6 +5217,70 @@ func TestUIListEmptyStateProvidesSectionSpecificGuidance(t *testing.T) {
})
}
func TestUISearchPlaceholderIsContextual(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{})
if got := u.searchPlaceholder(); got != "Search vault" {
t.Fatalf("default searchPlaceholder() = %q, want %q", got, "Search vault")
}
u.showRecycleBinSection()
if got := u.searchPlaceholder(); got != "Search recycle bin" {
t.Fatalf("recycle searchPlaceholder() = %q, want %q", got, "Search recycle bin")
}
u.showAPITokensSection()
if got := u.searchPlaceholder(); got != "Search API tokens" {
t.Fatalf("api token searchPlaceholder() = %q, want %q", got, "Search API tokens")
}
u.showAPIAuditSection()
if got := u.searchPlaceholder(); got != "Search audit log" {
t.Fatalf("api audit searchPlaceholder() = %q, want %q", got, "Search audit log")
}
}
func TestUIAPIPolicyTargetActionsUseCurrentContext(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{ID: "lights", Title: "Home Assistant", Path: []string{"Crew", "codex"}},
},
})
u.state.NavigateToPath([]string{"Crew", "codex"})
u.filter()
u.state.SelectedEntryID = "lights"
if err := u.useCurrentGroupForPolicyAction(); err != nil {
t.Fatalf("useCurrentGroupForPolicyAction() error = %v", err)
}
if got := u.apiPolicyPath.Text(); got != "codex" {
t.Fatalf("apiPolicyPath.Text() = %q, want %q", got, "codex")
}
if !u.apiPolicyGroupScopeW.Value {
t.Fatal("apiPolicyGroupScopeW.Value = false, want true")
}
if err := u.useSelectedEntryForPolicyAction(); err != nil {
t.Fatalf("useSelectedEntryForPolicyAction() error = %v", err)
}
if got := u.apiPolicyEntryID.Text(); got != "lights" {
t.Fatalf("apiPolicyEntryID.Text() = %q, want %q", got, "lights")
}
if u.apiPolicyGroupScopeW.Value {
t.Fatal("apiPolicyGroupScopeW.Value = true, want false")
}
if err := u.clearAPIPolicyTargetAction(); err != nil {
t.Fatalf("clearAPIPolicyTargetAction() error = %v", err)
}
if got := u.apiPolicyPath.Text(); got != "" {
t.Fatalf("apiPolicyPath.Text() = %q, want empty", got)
}
if got := u.apiPolicyEntryID.Text(); got != "" {
t.Fatalf("apiPolicyEntryID.Text() = %q, want empty", got)
}
}
func TestUIVisibleBreadcrumbsCompressesAggressivelyOnPhone(t *testing.T) {
t.Parallel()
+89 -3
View File
@@ -369,6 +369,57 @@ func (u *ui) addAPIPolicyRuleAction() error {
return nil
}
func (u *ui) apiPolicyGroupPathSummary() string {
path := parsePath(u.apiPolicyPath.Text())
if len(path) == 0 {
return "No group selected"
}
return strings.Join(path, " / ")
}
func (u *ui) apiPolicyEntrySummary() string {
id := strings.TrimSpace(u.apiPolicyEntryID.Text())
if id == "" {
return "No entry selected"
}
if item, ok := u.selectedEntry(); ok && item.ID == id {
if strings.TrimSpace(item.Title) != "" {
return item.Title + " (" + id + ")"
}
}
return id
}
func (u *ui) useCurrentGroupForPolicyAction() error {
u.syncCurrentPath()
path := u.displayPath()
if len(path) == 0 {
return fmt.Errorf("navigate to a group first")
}
u.apiPolicyGroupScope = true
u.apiPolicyGroupScopeW.Value = true
u.apiPolicyPath.SetText(strings.Join(path, " / "))
u.apiPolicyEntryID.SetText("")
return nil
}
func (u *ui) useSelectedEntryForPolicyAction() error {
item, ok := u.selectedEntry()
if !ok || strings.TrimSpace(item.ID) == "" {
return fmt.Errorf("select an entry first")
}
u.apiPolicyGroupScope = false
u.apiPolicyGroupScopeW.Value = false
u.apiPolicyEntryID.SetText(item.ID)
return nil
}
func (u *ui) clearAPIPolicyTargetAction() error {
u.apiPolicyPath.SetText("")
u.apiPolicyEntryID.SetText("")
return nil
}
func (u *ui) removeAPIPolicyRuleAction(index int) error {
token, ok := u.selectedAPIToken()
if !ok {
@@ -1002,9 +1053,44 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions {
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Operation", "Valid operations: "+strings.Join(stringOps(apiOperations()), ", "), &u.apiPolicyOperation, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Group Path", "Used when group scope is enabled.", &u.apiPolicyPath, false)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(labeledEditorHelp(u.theme, "Entry ID", "Used when group scope is disabled.", &u.apiPolicyEntryID, false)),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
if u.apiPolicyGroupScopeW.Value {
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(detailLine(u.theme, "Group Path", u.apiPolicyGroupPathSummary())),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useCurrentGroupForPolicy, "Use Current Group")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearAPIPolicyTarget, "Clear")
}),
)
}),
)
})
}
return compactCard(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(detailLine(u.theme, "Entry", u.apiPolicyEntrySummary())),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Spacing: layout.SpaceStart}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.useSelectedEntryForPolicy, "Use Selected Entry")
}),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.clearAPIPolicyTarget, "Clear")
}),
)
}),
)
})
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return tonedButton(gtx, u.theme, &u.addAPIPolicyRule, "Add Rule")