Add API policy pickers and contextual search labels
This commit is contained in:
@@ -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
@@ -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{"Joe", "codex"}},
|
||||
},
|
||||
})
|
||||
u.state.NavigateToPath([]string{"Joe", "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()
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user