Skip to content

g1t/apps/web/app/components/deployments-panel.tsx

119 lines4,773 bytesCodeBlame

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Merge branch 'main' into worktree-agent-a69aeabc4b0deeb971/**
2 * A repository's deployments at a glance, for a sidebar: each environment's
3 * latest deployment and when, linking to the Deployments page. Also the
4 * state icon and badge every page about deployments shares.
5 */
6import { Check, CircleDashed, Clock, Loader2, Rocket, X } from "lucide-react";
7import { Link } from "react-router";
8
9import type { DeploymentEnvironments, DeploymentSource, DeploymentState } from "@g1t/contracts";
10
11import { cn } from "../lib/cn";
12import { SOURCE_LABEL, STATE_WORD, environmentLabel, orderEnvironments } from "../lib/deployments";
13import { TimeAgo } from "./ui";
14
15/** Environments listed before the rest are a link away. */
16const SHOWN = 3;
17
18/** The color a state is shown in. */
19export const STATE_TONE: Record<DeploymentState, string> = {
20 queued: "text-faint",
21 in_progress: "text-warn",
22 success: "text-success",
23 failure: "text-danger",
24 error: "text-danger",
25 inactive: "text-faint",
26};
27
28/** A deployment's state as an icon: a check, a cross, a spinner, a clock. */
29export function DeploymentStateIcon({ state, size = 16, className }: { state: DeploymentState; size?: number; className?: string }) {
30 const label = STATE_WORD[state];
31 switch (state) {
32 case "success":
33 return (
34 <span role="img" aria-label={label} className={cn("inline-flex shrink-0 rounded-full bg-success/15 p-0.5 text-success", className)}>
35 <Check size={size - 4} strokeWidth={3} />
36 </span>
37 );
38 case "failure":
39 case "error":
40 return (
41 <span role="img" aria-label={label} className={cn("inline-flex shrink-0 rounded-full bg-danger/15 p-0.5 text-danger", className)}>
42 <X size={size - 4} strokeWidth={3} />
43 </span>
44 );
45 case "in_progress":
46 return <Loader2 size={size} aria-label={label} className={cn("shrink-0 animate-spin text-warn", className)} />;
47 case "queued":
48 return <Clock size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
49 default:
50 return <CircleDashed size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
51 }
52}
53
54/** A state's icon and word together. */
55export function DeploymentStateBadge({ state, size = 14, className }: { state: DeploymentState; size?: number; className?: string }) {
56 return (
57 <span className={cn("inline-flex items-center gap-1.5 text-xs font-medium", STATE_TONE[state], className)}>
58 <DeploymentStateIcon state={state} size={size} />
59 {STATE_WORD[state]}
60 </span>
61 );
62}
63
64/** What made a deployment, in words. */
65export function sourceLabel(source: DeploymentSource): string {
66 return SOURCE_LABEL[source];
67}
68
69export function DeploymentsPanel({
70 base,
71 summary,
72 className,
73}: {
74 base: string;
75 summary: DeploymentEnvironments | null;
76 className?: string;
77}) {
78 if (!summary || summary.environments.length === 0) return null;
79 const environments = orderEnvironments(summary.environments);
80 const shown = environments.slice(0, SHOWN);
81 const more = environments.length - shown.length;
82 return (
83 <section className={className} aria-labelledby="deployments-panel">
84 <h2 id="deployments-panel" className="text-sm font-semibold">
85 <Link to={`${base}/deployments`} className="inline-flex items-center gap-2 hover:text-accent">
86 <Rocket size={14} className="text-faint" />
87 Deployments
88 <span className="rounded-full bg-line px-1.5 text-[0.6875rem] font-medium tabular-nums text-muted">
89 {summary.total_count.toLocaleString("en-US")}
90 </span>
91 </Link>
92 </h2>
93 <ul className="mt-3 space-y-2 text-sm">
94 {shown.map((env) => {
95 const latest = env.latest;
96 return (
97 <li key={env.name} className="min-w-0">
98 <Link
99 to={latest ? `${base}/deployments/${latest.id}` : `${base}/deployments?environment=${encodeURIComponent(env.name)}#history`}
100 className="group flex min-w-0 items-center gap-2"
101 >
102 {latest ? <DeploymentStateIcon state={latest.state} size={15} /> : <CircleDashed size={15} className="shrink-0 text-faint" />}
103 <span className="min-w-0 truncate font-medium text-fg-soft group-hover:text-accent">{environmentLabel(env.name)}</span>
104 <span className="shrink-0 text-xs text-faint">
105 · <TimeAgo at={latest?.updated_at ?? env.updated_at} />
106 </span>
107 </Link>
108 </li>
109 );
110 })}
111 </ul>
112 {more > 0 && (
113 <Link to={`${base}/deployments`} className="mt-2.5 inline-block text-xs text-muted hover:text-fg">
114 + {more} more {more === 1 ? "environment" : "environments"}
115 </Link>
116 )}
117 </section>
118 );
119}

This file's history is long; its oldest lines are credited to the oldest commit read.