Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9c7a8ff
feat(audit-log): split changes from data access, add resource/action/…
Makisuo Nov 9, 2026
add9406
feat(audit-log): filter from an entry's detail, name members, show wh…
Makisuo Nov 9, 2026
8b8805a
feat(audit-log): day headings, a red edge on refusals, quieter reads
Makisuo Nov 9, 2026
7312a1b
chore(ui): index the day-label page kit module
Makisuo Nov 9, 2026
44f4017
fix(audit-log): mute reads only in the All view
Makisuo Nov 9, 2026
789d476
fix(audit-log): a shared entry link opens its entry however busy that…
Makisuo Nov 9, 2026
391e40d
Merge branch 'audit-log-ui/b-detail-pivots' into audit-log-ui/c-rows-…
Makisuo Nov 9, 2026
aba17d0
refactor(audit-log): one access-action list for the server and the web
Makisuo Nov 9, 2026
14c88ee
test(audit-log): a filter change drops the shared entry link
Makisuo Nov 9, 2026
bd28017
Merge branch 'audit-log-ui/b-detail-pivots' into audit-log-ui/c-rows-…
Makisuo Nov 9, 2026
ea5fbdc
feat(audit-log): icons for the kind of action, the actor, the surface…
Makisuo Nov 10, 2026
5bbc8e8
fix(audit-log): the action keeps its room beside the resource, every …
Makisuo Nov 10, 2026
9833c48
Merge remote-tracking branch 'origin/main' into audit-log-ui/a-views-…
Makisuo Nov 10, 2026
b491fc3
Merge branch 'audit-log-ui/b-detail-pivots' into audit-log-ui/c-rows-…
Makisuo Nov 10, 2026
924400a
Merge branch 'audit-log-ui/a-views-filters' into audit-log-ui/b-detai…
Makisuo Nov 10, 2026
f637218
Merge branch 'audit-log-ui/c-rows-layout' into audit-log-ui/d-icons
Makisuo Nov 10, 2026
9eea2ea
docs(audit-log): the action label's doc comment sits on its function …
Makisuo Nov 10, 2026
e9a39df
Merge branch 'audit-log-ui/c-rows-layout' into audit-log-ui/d-icons
Makisuo Nov 10, 2026
81ba0ce
fix(audit-log): an entry Maple itself made still names its surface
Makisuo Nov 10, 2026
0b85f10
Merge branch 'audit-log-ui/b-detail-pivots' into audit-log-ui/c-rows-…
Makisuo Nov 10, 2026
7bd013d
Merge branch 'audit-log-ui/c-rows-layout' into audit-log-ui/d-icons
Makisuo Nov 10, 2026
6b46d45
Merge branch 'main' into audit-log-ui/b-detail-pivots
Makisuo Nov 10, 2026
7fbd842
fix(audit-log): Filter from a shared entry link leaves that link's on…
Makisuo Nov 10, 2026
669d9ed
Merge branch 'audit-log-ui/b-detail-pivots' into audit-log-ui/c-rows-…
Makisuo Nov 10, 2026
33f2691
Merge branch 'main' into audit-log-ui/c-rows-layout
Makisuo Nov 10, 2026
c6d9ed8
Merge branch 'audit-log-ui/c-rows-layout' into audit-log-ui/d-icons
Makisuo Nov 10, 2026
d28aeb9
Merge branch 'main' into audit-log-ui/d-icons
Makisuo Nov 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions apps/web/src/page/settings/audit-log/icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { AuditResources, AuditResourceType } from "@maple/domain/audit-actions"
import type { AuditActorType, AuditLogSource, AuditOutcome } from "@maple/domain/http"
import type { IconName } from "@maple/views/ui/icon"
import { Array as Arr, Record as Rec, String as Str } from "effect"

// The audit log's glyphs: one per actor type, surface, outcome and kind of action, so a filter and
// the rows it lists carry the same picture.

export const ACTOR_ICONS = {
user: "UserIcon",
api_key: "KeyIcon",
agent: "FaceRobotIcon",
system: "GearIcon",
} as const satisfies { readonly [Actor in AuditActorType]: IconName }

export const SOURCE_ICONS = {
dashboard: "MonitorIcon",
api: "BracketsCurlyIcon",
mcp: "PlugIcon",
chat_platform: "ChatBubbleIcon",
system: "GearIcon",
} as const satisfies { readonly [Source in AuditLogSource]: IconName }

export const OUTCOME_ICONS = {
allowed: "CircleCheckIcon",
denied: "CircleXmarkIcon",
} as const satisfies { readonly [Outcome in AuditOutcome]: IconName }

type AuditVerb = (typeof AuditResources)[AuditResourceType]["verbs"][number]

/** A glyph for every declared verb: a new verb fails the build until it has one. */
const VERB_ICONS = {
created: "PlusIcon",
added: "PlusIcon",
registered: "PlusIcon",
submitted: "PlusIcon",
minted: "PlusIcon",
claimed: "PlusIcon",
install_started: "PlusIcon",
connect_started: "PlusIcon",
identity_link_started: "LinkIcon",
updated: "PencilIcon",
settings_updated: "PencilIcon",
zones_updated: "PencilIcon",
role_changed: "ShieldIcon",
status_changed: "PencilIcon",
catalog_curated: "PencilIcon",
components_reordered: "PencilIcon",
config_applied: "PencilIcon",
schema_applied: "PencilIcon",
organization_selected: "PencilIcon",
metrics_token_set: "KeyIcon",
region_chosen: "GlobeIcon",
triaged: "PencilIcon",
deleted: "TrashIcon",
removed: "TrashIcon",
removal_confirmed: "TrashIcon",
uninstalled: "TrashIcon",
disconnected: "TrashIcon",
identity_unlinked: "TrashIcon",
revoked: "LockIcon",
break_glass_revoked: "LockIcon",
cancelled: "SquareIcon",
ended: "SquareIcon",
rolled: "ArrowRotateClockwiseIcon",
rotated: "ArrowRotateClockwiseIcon",
break_glass_rotated: "ArrowRotateClockwiseIcon",
restarted: "ArrowRotateClockwiseIcon",
version_restored: "HistoryIcon",
catalog_refreshed: "ArrowRotateClockwiseIcon",
read: "EyeIcon",
called: "SquareTerminalIcon",
sql_executed: "DatabaseIcon",
request: "BracketsCurlyIcon",
tested: "MediaPlayIcon",
paused: "MediaPauseIcon",
resumed: "MediaPlayIcon",
published: "PaperPlaneIcon",
update_posted: "PaperPlaneIcon",
invite_sent: "PaperPlaneIcon",
resolved: "CheckIcon",
regression: "CircleWarningIcon",
state_change: "PencilIcon",
severity_change: "PencilIcon",
anomaly_linked: "LinkIcon",
lease_expired: "ClockIcon",
verification_started: "MediaPlayIcon",
verification_verdict: "ShieldCheckIcon",
comment: "ChatBubbleIcon",
agent_note: "ChatBubbleIcon",
ai_triage: "SparkleIcon",
fix_proposed: "SparkleIcon",
snooze: "BellSlashIcon",
unsnooze: "BellIcon",
assignment: "UserIcon",
claim: "UserIcon",
release: "UserIcon",
pr_linked: "PullRequestIcon",
pr_unlinked: "PullRequestIcon",
pr_merged: "PullRequestIcon",
merge: "BranchForkIcon",
unmerge: "BranchForkIcon",
} as const satisfies { readonly [Verb in AuditVerb]: IconName }

const hasVerbIcon = (verb: string): verb is keyof typeof VERB_ICONS =>
Rec.has<string, IconName>(VERB_ICONS, verb)

/** The glyph for "an action", and for one this build does not know. */
export const ACTION_ICON = "BoltIcon" satisfies IconName

/** The glyph for a `<resource>.<verb>` action, by its verb. */
export const auditActionIcon = (action: string): IconName => {
const verb = Arr.lastNonEmpty(Str.split(".")(action))
return hasVerbIcon(verb) ? VERB_ICONS[verb] : ACTION_ICON
}
32 changes: 23 additions & 9 deletions apps/web/src/page/settings/audit-log/view-detail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ import { inlineCode } from "@maple/views/ui/inline-code"
import { keyValue, keyValueList } from "@maple/views/ui/key-value"
import { table, tableBody, tableCell, tableHead, tableHeader, tableRow } from "@maple/views/ui/table"
import { Message, type Model } from "./model"
import { ACTOR_BADGES, SOURCE_LABELS } from "./view"
import { OUTCOME_ICONS, SOURCE_ICONS } from "./icons"
import { actorBadge, deniedBadge, SOURCE_LABELS } from "./view"
import { icon } from "@maple/views/ui/icon"
import { typography } from "@maple/views/ui/typography"
import { hstack } from "@maple/views/ui/stack"
import { timestamp } from "@maple/views/ui/timestamp"
Expand Down Expand Up @@ -164,6 +166,7 @@ const pivot = (h: H, key: string, value: string, what: string): Html =>
attributeFilterAction(h, {
label: `Show every entry with this ${what}`,
text: "Filter",
icon: "FilterIcon",
onClick: Message.ClickedPivot({ key, value }),
})

Expand Down Expand Up @@ -241,10 +244,16 @@ const originSection = (h: H, model: Model, entry: V2AuditLogEntry): ReadonlyArra
h.section(
[h.Class("space-y-1.5")],
[
eyebrow(h, { variant: "mono", as: "h4" }, ["Origin"]),
eyebrow(h, { variant: "mono", as: "h4", icon: "GlobeIcon" }, ["Origin"]),
keyValueList(h, { layout: "grid", className: DETAIL_GRID }, [
keyValue(h, { wrap: true, label: "Surface" }, [
h.span([], [SOURCE_LABELS[entry.source]]),
valueRow(h, [
icon(h, SOURCE_ICONS[entry.source], {
size: 13,
className: "text-muted-foreground shrink-0",
}),
h.span([], [SOURCE_LABELS[entry.source]]),
]),
]),
...(entry.origin_ip === null
? []
Expand Down Expand Up @@ -293,7 +302,6 @@ export const auditDetail = (
timeZone: string,
nowMs: number,
): Html => {
const actor = ACTOR_BADGES[entry.actor_type]
const full = (value: string) =>
timestamp(h, { value, timeZone, nowMs, withYear: true, className: "tabular-nums" })
const actionText = auditActionLabel(entry.action)
Expand All @@ -309,7 +317,7 @@ export const auditDetail = (
keyValue(h, { wrap: true, label: "Recorded" }, [full(entry.recorded_at)]),
keyValue(h, { wrap: true, label: "Actor" }, [
valueRow(h, [
badge(h, { variant: actor.variant, size: "sm" }, [actor.label]),
actorBadge(h, entry.actor_type),
...(actorName === null ? [] : [h.span([], [actorName])]),
...(entry.actor_id === null
? []
Expand All @@ -324,8 +332,11 @@ export const auditDetail = (
...(actionText === null ? [] : [h.span([], [actionText])]),
inlineCode(h, { variant: "plain", size: "2xs", tone: "foreground" }, [entry.action]),
entry.outcome === "denied"
? badge(h, { variant: "crit", size: "sm" }, ["Denied"])
: badge(h, { variant: "ok", size: "sm" }, ["Allowed"]),
? deniedBadge(h)
: badge(h, { variant: "ok", size: "sm" }, [
icon(h, OUTCOME_ICONS.allowed),
"Allowed",
]),
...(actionText === null ? [] : [pivot(h, "action", entry.action, "action")]),
]),
...(entry.denial_reason === null
Expand Down Expand Up @@ -379,6 +390,7 @@ export const auditDetail = (
"Entry id",
),
textLink(h, { href: entryHref(entry) }, [
icon(h, "LinkIcon", { size: 12 }),
typography(h, { size: "2xs" }, ["Link to entry"]),
]),
]),
Expand All @@ -390,7 +402,7 @@ export const auditDetail = (
h.section(
[h.Class("space-y-1.5")],
[
eyebrow(h, { variant: "mono", as: "h4" }, ["Changes"]),
eyebrow(h, { variant: "mono", as: "h4", icon: "PencilIcon" }, ["Changes"]),
changesTable(h, changes),
],
),
Expand All @@ -401,7 +413,9 @@ export const auditDetail = (
h.section(
[h.Class("space-y-1.5")],
[
eyebrow(h, { variant: "mono", as: "h4" }, ["Details"]),
eyebrow(h, { variant: "mono", as: "h4", icon: "CircleInfoIcon" }, [
"Details",
]),
...metadataList(h, metadata),
],
),
Expand Down
Loading
Loading