Skip to content
103 linesCodeBlameRaw
1/** The audit log's entries, as the workspace's Audit log page shows them. */
2
3import { ShieldAlert, ShieldCheck } from "lucide-react";
4import { Link } from "react-router";
5
6import type { AuditEntry } from "@g1t/contracts";
7
8import { actionLabel, ruleLabel, targetLabel } from "../lib/audit";
9import { TimeAgo } from "./ui";
10import { Avatar } from "./ui/avatar";
11import { Card } from "./ui/card";
12import { Hint } from "./ui/hint";
13
14/** Allowed or denied, and by which rule. */
15export function OutcomeMark({ entry }: { entry: AuditEntry }) {
16 const denied = entry.outcome === "denied";
17 return (
18 <Hint label={`${entry.outcome}: ${ruleLabel(entry.rule)} (${entry.rule})`}>
19 <span
20 className={`inline-flex shrink-0 items-center gap-1 rounded-full px-2 py-0.5 text-xs ${
21 denied ? "bg-danger/10 text-danger ring-1 ring-danger/30" : "bg-raised text-muted ring-1 ring-line"
22 }`}
23 >
24 {denied ? <ShieldAlert size={11} /> : <ShieldCheck size={11} />}
25 {entry.outcome}
26 </span>
27 </Hint>
28 );
29}
30
31/** Who acted, with whoever acted for someone (an app, or an old agent) shown as working for them. */
32export function ActorLine({ entry }: { entry: AuditEntry }) {
33 return (
34 <span className="flex min-w-0 items-center gap-2">
35 <Avatar name={entry.agent ?? entry.actor} size={18} square={entry.actorKind === "workspace"} />
36 <span className="truncate">
37 {entry.onBehalfOf ? (
38 <>
39 <span className="font-medium">{entry.agent ?? entry.actor}</span>
40 <span className="text-muted"> on behalf of </span>
41 <span className="font-medium">{entry.onBehalfOf}</span>
42 </>
43 ) : (
44 <span className="font-medium">{entry.actor}</span>
45 )}
46 </span>
47 </span>
48 );
49}
50
51/** The workspace's log, one row an entry, newest first. */
52export function AuditTable({ entries, base }: { entries: AuditEntry[]; base: string }) {
53 return (
54 <Card asChild divided>
55 <ol>
56 {entries.map((entry) => (
57 <li key={entry.id} className="grid gap-x-4 gap-y-1 px-4 py-3 text-sm sm:grid-cols-[9rem_1fr_auto]">
58 <span className="text-xs leading-5 text-faint">
59 <TimeAgo at={entry.time} />
60 </span>
61 <div className="min-w-0">
62 <div className="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
63 <ActorLine entry={entry} />
64 <span className="font-mono text-xs text-fg/85">{actionLabel(entry.action)}</span>
65 <span className="truncate font-mono text-xs text-muted">{targetLabel(entry)}</span>
66 </div>
67 <p className="mt-1 flex flex-wrap gap-x-3 text-xs text-faint">
68 <Hint label={entry.rule}>
69 <span>{ruleLabel(entry.rule)}</span>
70 </Hint>
71 <span>{entry.surface.toUpperCase()}</span>
72 {entry.result && entry.result !== "ok" && <span>result: {entry.result}</span>}
73 {entry.runId && (
74 <Link to={`${base}?run=${encodeURIComponent(entry.runId)}`} className="hover:text-fg">
75 everything this run did
76 </Link>
77 )}
78 {entry.credentialId && (
79 <Hint label="Credential">
80 <span className="font-mono">
81 <span className="sr-only">Credential </span>
82 {entry.credentialId}
83 </span>
84 </Hint>
85 )}
86 <Hint label="Request id">
87 <span className="font-mono">
88 <span className="sr-only">Request id </span>
89 {entry.requestId}
90 </span>
91 </Hint>
92 </p>
93 {entry.outcome === "denied" && entry.message && <p className="mt-1 text-xs text-danger">{entry.message}</p>}
94 </div>
95 <span className="sm:text-right">
96 <OutcomeMark entry={entry} />
97 </span>
98 </li>
99 ))}
100 </ol>
101 </Card>
102 );
103}