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.

186 linesCodeBlameRaw
1/**
2 * A commit's checks at a glance: a mark beside the commit (a check when
3 * every check passed, a cross when one failed, an amber dot while any is
4 * running) that opens the list of them, each with how it went and a link
5 * to its details. Every reporter's checks are listed alike: workflow jobs,
6 * check runs and statuses from integrations and deployments.
7 *
8 * Loaders pass the page's checks as one promise (lib/commit-checks.server.ts);
9 * each badge shows a placeholder until it settles.
10 */
11import { Ban, Check, CircleDashed, X } from "lucide-react";
12import { Suspense } from "react";
13import { Await, Link } from "react-router";
14
15import type { CheckItem, CommitChecks } from "@g1t/contracts";
16
17import { useAddresses } from "../lib/addresses";
18import { cn } from "../lib/cn";
19import { checkDetail, checksHeadline, checksTally, detailsLink } from "../lib/commit-checks";
20import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover";
21import { Skeleton } from "./ui/skeleton";
22
23/** A page's commits' checks by SHA, as loaders return them: on their way, read, or not to be had. */
24export type ChecksSource = Promise<Record<string, CommitChecks> | null> | Record<string, CommitChecks> | null | undefined;
25
26/** One check's state as an icon. */
27export function CheckStateIcon({ state, size = 16 }: { state: CheckItem["state"] | CommitChecks["state"]; size?: number }) {
28 switch (state) {
29 case "success":
30 return (
31 <span className="inline-flex shrink-0 rounded-full bg-success/15 p-0.5 text-success" aria-label="Successful">
32 <Check size={size - 4} strokeWidth={3} />
33 </span>
34 );
35 case "failure":
36 return (
37 <span className="inline-flex shrink-0 rounded-full bg-danger/15 p-0.5 text-danger" aria-label="Failing">
38 <X size={size - 4} strokeWidth={3} />
39 </span>
40 );
41 case "pending":
42 return (
43 <span className="inline-flex shrink-0 items-center justify-center" style={{ width: size, height: size }} aria-label="In progress">
44 <span className="size-2 rounded-full bg-warn motion-safe:animate-pulse" />
45 </span>
46 );
47 case "cancelled":
48 return <Ban size={size} className="shrink-0 text-faint" aria-label="Cancelled" />;
49 default:
50 return <CircleDashed size={size} className="shrink-0 text-faint" aria-label="Skipped" />;
51 }
52}
53
54/** The mark itself: what all of a commit's checks add up to. */
55function Mark({ state }: { state: CommitChecks["state"] }) {
56 if (state === "success") return <Check size={15} strokeWidth={2.75} className="text-success" />;
57 if (state === "failure") return <X size={15} strokeWidth={2.75} className="text-danger" />;
58 return <span className="size-2 rounded-full bg-warn" />;
59}
60
61function CheckRow({ item }: { item: CheckItem }) {
62 const site = useAddresses().site;
63 const link = detailsLink(item, site);
64 const detail = checkDetail(item);
65 return (
66 <li className="flex items-center gap-2.5 px-4 py-2">
67 <CheckStateIcon state={item.state} size={16} />
68 <p className="min-w-0 grow truncate text-[0.8125rem]">
69 <span className="font-medium text-fg">{item.name}</span> <span className="text-muted">{detail}</span>
70 </p>
71 {link &&
72 (link.external ? (
73 <a href={link.href} target="_blank" rel="noreferrer" className="shrink-0 text-xs font-medium text-accent hover:underline">
74 Details
75 </a>
76 ) : (
77 <Link to={link.href} className="shrink-0 text-xs font-medium text-accent hover:underline">
78 Details
79 </Link>
80 ))}
81 </li>
82 );
83}
84
85/** The list a commit's mark opens. */
86export function ChecksList({ checks, className, showHeader = true }: { checks: CommitChecks; className?: string; showHeader?: boolean }) {
87 return (
88 <div className={className}>
89 {showHeader && (
90 <div className="flex items-start gap-3 border-b border-line px-4 py-3">
91 <span className="mt-0.5">
92 <CheckStateIcon state={checks.state} size={20} />
93 </span>
94 <div className="min-w-0">
95 <p className="font-semibold text-fg">{checksHeadline(checks)}</p>
96 <p className="text-xs text-muted">{checksTally(checks)}</p>
97 </div>
98 </div>
99 )}
100 <ul className="max-h-80 divide-y divide-line overflow-y-auto">
101 {checks.checks.map((item) => (
102 <CheckRow key={`${item.kind}:${item.id ?? item.name}`} item={item} />
103 ))}
104 </ul>
105 </div>
106 );
107}
108
109/** A commit's mark, opening its checks; nothing when nothing reported on it. */
110export function CommitChecksMark({ checks, className }: { checks: CommitChecks | null | undefined; className?: string }) {
111 if (!checks || checks.state === "none" || checks.total === 0) return null;
112 const headline = checksHeadline(checks);
113 return (
114 <Popover>
115 <PopoverTrigger
116 aria-label={`${headline}: ${checksTally(checks)}`}
117 className={cn(
118 "relative z-10 inline-flex size-6 shrink-0 pointer-coarse:size-8 animate-fade-in items-center justify-center rounded-md transition-colors hover:bg-raised data-[state=open]:bg-raised",
119 className,
120 )}
121 >
122 <Mark state={checks.state} />
123 </PopoverTrigger>
124 <PopoverContent align="start" className="w-md p-0">
125 <ChecksList checks={checks} />
126 </PopoverContent>
127 </Popover>
128 );
129}
130
131/** Holds a mark's place while the page's checks are read. */
132export function CommitChecksPlaceholder({ className }: { className?: string }) {
133 return (
134 <span aria-hidden="true" className={cn("inline-flex size-6 shrink-0 items-center justify-center", className)}>
135 <Skeleton className="size-3.5 rounded-full" />
136 </span>
137 );
138}
139
140/**
141 * The mark for commit `sha` from a page's checks: a placeholder while they
142 * are on their way, then the mark, or nothing.
143 */
144export function CommitChecksBadge({ checks, sha, className }: { checks: ChecksSource; sha: string; className?: string }) {
145 if (!checks) return null;
146 if (!(checks instanceof Promise)) return <CommitChecksMark checks={checks[sha]} className={className} />;
147 return (
148 <Suspense fallback={<CommitChecksPlaceholder className={className} />}>
149 <Await resolve={checks} errorElement={null}>
150 {(found) => <CommitChecksMark checks={found?.[sha]} className={className} />}
151 </Await>
152 </Suspense>
153 );
154}
155
156/** A readable summary with the same detailed checks as the repository orb. */
157export function CommitChecksSummary({ checks, sha, label }: { checks: ChecksSource; sha: string; label?: string }) {
158 const summary = (found: Record<string, CommitChecks> | null | undefined) => {
159 if (found == null) return <p className="text-xs text-muted">Checks could not be loaded.</p>;
160 const result = found[sha];
161 if (!result || result.total === 0 || result.state === "none") return <p className="text-xs text-muted">No checks reported for this commit.</p>;
162 return (
163 <details className="group rounded-lg border border-line">
164 <summary className="flex cursor-pointer list-none items-center gap-2 px-3 py-2 text-xs hover:bg-raised/40 [&::-webkit-details-marker]:hidden">
165 <CheckStateIcon state={result.state} />
166 <span className="min-w-0 flex-1">
167 <span className="block font-medium">{label ?? checksHeadline(result)}</span>
168 <span className="text-muted">{checksTally(result)}</span>
169 </span>
170 <span className="text-accent group-open:hidden">Show checks</span>
171 <span className="hidden text-accent group-open:inline">Hide checks</span>
172 </summary>
173 <ChecksList checks={result} className="border-t border-line" showHeader={false} />
174 </details>
175 );
176 };
177 return checks instanceof Promise ? (
178 <Suspense fallback={<Skeleton className="h-12 w-full rounded-lg" />}>
179 <Await resolve={checks} errorElement={<p className="text-xs text-muted">Checks could not be loaded.</p>}>
180 {summary}
181 </Await>
182 </Suspense>
183 ) : (
184 summary(checks)
185 );
186}