From 182b469b0b2e3285b0c13b45fc0df66fb30d2a08 Mon Sep 17 00:00:00 2001 From: Joe Julian Date: Mon, 30 Mar 2026 14:46:07 -0700 Subject: [PATCH] Fix API token detail panel panic --- main_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ ui_api.go | 29 +++++++++++++++++++++-------- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/main_test.go b/main_test.go index 1b381b6..1b2b6f3 100644 --- a/main_test.go +++ b/main_test.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "image" "io" "net/http" "net/http/httptest" @@ -15,6 +16,7 @@ import ( "gioui.org/io/key" "gioui.org/layout" + "gioui.org/op" "gioui.org/unit" "gioui.org/widget" @@ -237,6 +239,44 @@ func TestUIAPITokenPolicyRulesCanBeAddedAndRemoved(t *testing.T) { } } +func TestUIAPITokenDetailPanelHandlesMissingRemoveClickables(t *testing.T) { + t.Parallel() + + u := newUIWithSession("desktop", &session.Manager{}) + u.masterPassword.SetText("correct horse battery staple") + if err := u.createVaultAction(); err != nil { + t.Fatalf("createVaultAction() error = %v", err) + } + + u.showAPITokensSection() + u.apiTokenName.SetText("CLI") + u.apiTokenClientName.SetText("grpc-cli") + if err := u.issueAPITokenAction(); err != nil { + t.Fatalf("issueAPITokenAction() error = %v", err) + } + u.apiPolicyOperation.SetText(string(apitokens.OperationListEntries)) + u.apiPolicyPath.SetText("Crew / codex") + u.apiPolicyAllow.Value = true + u.apiPolicyGroupScopeW.Value = true + if err := u.addAPIPolicyRuleAction(); err != nil { + t.Fatalf("addAPIPolicyRuleAction() error = %v", err) + } + + u.apiPolicyRemoves = nil + ops := new(op.Ops) + gtx := layout.Context{ + Ops: ops, + Constraints: layout.Exact(image.Pt(800, 600)), + } + defer func() { + if r := recover(); r != nil { + t.Fatalf("apiTokenDetailPanel() panicked: %v", r) + } + }() + + _ = u.apiTokenDetailPanel(gtx) +} + func TestUIAPIAuditSectionShowsRecordedEvents(t *testing.T) { t.Parallel() diff --git a/ui_api.go b/ui_api.go index 1a2fe49..921cb7b 100644 --- a/ui_api.go +++ b/ui_api.go @@ -80,9 +80,9 @@ func (u *ui) loadSelectedAPITokenIntoEditor() { u.apiPolicyOperation.SetText(string(apitokens.OperationListEntries)) u.apiPolicyPath.SetText(strings.Join(u.displayPath(), " / ")) u.apiPolicyEntryID.SetText("") - u.apiPolicyAllow.Value = true - u.apiPolicyGroupScope = true - u.apiPolicyGroupScopeW.Value = true + u.apiPolicyAllow.Value = true + u.apiPolicyGroupScope = true + u.apiPolicyGroupScopeW.Value = true u.apiPolicyRemoves = nil return } @@ -444,6 +444,9 @@ func (u *ui) apiAuditListPanel(gtx layout.Context) layout.Dimensions { func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions { token, ok := u.selectedAPIToken() + if ok && len(u.apiPolicyRemoves) < len(token.Policies) { + u.apiPolicyRemoves = make([]widget.Clickable, len(token.Policies)) + } rows := []layout.Widget{ func(gtx layout.Context) layout.Dimensions { lbl := material.Label(u.theme, unit.Sp(20), "API Token") @@ -505,15 +508,25 @@ func (u *ui) apiTokenDetailPanel(gtx layout.Context) layout.Dimensions { layout.Spacer{Height: unit.Dp(10)}.Layout, 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.saveAPIToken, "Save Token") }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.saveAPIToken, "Save Token") + }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.rotateAPIToken, "Rotate") }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.rotateAPIToken, "Rotate") + }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.disableAPIToken, "Disable") }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.disableAPIToken, "Disable") + }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.revokeAPIToken, "Revoke") }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.revokeAPIToken, "Revoke") + }), layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { return tonedButton(gtx, u.theme, &u.deleteAPIToken, "Delete") }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return tonedButton(gtx, u.theme, &u.deleteAPIToken, "Delete") + }), ) }, layout.Spacer{Height: unit.Dp(14)}.Layout,