Skip to content
131 linesCodeBlameRaw
1/**
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, CircleDot, 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({
30 state,
31 size = 16,
32 className,
33 over = false,
34}: {
35 state: DeploymentState;
36 size?: number;
37 className?: string;
38 /** A past status a later one followed: under way then, over now, so it does not spin. */
39 over?: boolean;
40}) {
41 const label = STATE_WORD[state];
42 switch (state) {
43 case "success":
44 return (
45 <span role="img" aria-label={label} className={cn("inline-flex shrink-0 rounded-full bg-success/15 p-0.5 text-success", className)}>
46 <Check size={size - 4} strokeWidth={3} />
47 </span>
48 );
49 case "failure":
50 case "error":
51 return (
52 <span role="img" aria-label={label} className={cn("inline-flex shrink-0 rounded-full bg-danger/15 p-0.5 text-danger", className)}>
53 <X size={size - 4} strokeWidth={3} />
54 </span>
55 );
56 case "in_progress":
57 if (over) return <CircleDot size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
58 return <Loader2 size={size} aria-label={label} className={cn("shrink-0 animate-spin text-warn", className)} />;
59 case "queued":
60 return <Clock size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
61 default:
62 return <CircleDashed size={size} aria-label={label} className={cn("shrink-0 text-faint", className)} />;
63 }
64}
65
66/** A state's icon and word together. */
67export function DeploymentStateBadge({ state, size = 14, className }: { state: DeploymentState; size?: number; className?: string }) {
68 return (
69 <span className={cn("inline-flex items-center gap-1.5 text-xs font-medium", STATE_TONE[state], className)}>
70 <DeploymentStateIcon state={state} size={size} />
71 {STATE_WORD[state]}
72 </span>
73 );
74}
75
76/** What made a deployment, in words. */
77export function sourceLabel(source: DeploymentSource): string {
78 return SOURCE_LABEL[source];
79}
80
81export function DeploymentsPanel({
82 base,
83 summary,
84 className,
85}: {
86 base: string;
87 summary: DeploymentEnvironments | null;
88 className?: string;
89}) {
90 if (!summary || summary.environments.length === 0) return null;
91 const environments = orderEnvironments(summary.environments);
92 const shown = environments.slice(0, SHOWN);
93 const more = environments.length - shown.length;
94 return (
95 <section className={className} aria-labelledby="deployments-panel">
96 <h2 id="deployments-panel" className="text-sm font-semibold">
97 <Link to={`${base}/deployments`} className="inline-flex items-center gap-2 hover:text-accent">
98 <Rocket size={14} className="text-faint" />
99 Deployments
100 <span className="rounded-full bg-line px-1.5 text-[0.6875rem] font-medium tabular-nums text-muted">
101 {summary.total_count.toLocaleString("en-US")}
102 </span>
103 </Link>
104 </h2>
105 <ul className="mt-3 space-y-2 text-sm">
106 {shown.map((env) => {
107 const latest = env.latest;
108 return (
109 <li key={env.name} className="min-w-0">
110 <Link
111 to={latest ? `${base}/deployments/${latest.id}` : `${base}/deployments?environment=${encodeURIComponent(env.name)}#history`}
112 className="group flex min-w-0 items-center gap-2"
113 >
114 {latest ? <DeploymentStateIcon state={latest.state} size={15} /> : <CircleDashed size={15} className="shrink-0 text-faint" />}
115 <span className="min-w-0 truncate font-medium text-fg-soft group-hover:text-accent">{environmentLabel(env.name)}</span>
116 <span className="shrink-0 text-xs text-faint">
117 · <TimeAgo at={latest?.updated_at ?? env.updated_at} />
118 </span>
119 </Link>
120 </li>
121 );
122 })}
123 </ul>
124 {more > 0 && (
125 <Link to={`${base}/deployments`} className="mt-2.5 inline-block text-xs text-muted hover:text-fg">
126 + {more} more {more === 1 ? "environment" : "environments"}
127 </Link>
128 )}
129 </section>
130 );
131}