Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

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