Refine audit UI and vault settings placement

This commit is contained in:
Joe Julian
2026-04-01 17:11:34 -07:00
parent 2aa859f7cb
commit 004a0278a7
4 changed files with 541 additions and 223 deletions
+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)