g1t/apps/web/app/components/deployments-panel.tsx
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-a69aeabc4b0deeb97 | 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 | */ | |
| 6 | import { Check, CircleDashed, Clock, Loader2, Rocket, X } from "lucide-react"; | |
| 7 | import { Link } from "react-router"; | |
| 8 | ||
| 9 | import type { DeploymentEnvironments, DeploymentSource, DeploymentState } from "@g1t/contracts"; | |
| 10 | ||
| 11 | import { cn } from "../lib/cn"; | |
| 12 | import { SOURCE_LABEL, STATE_WORD, environmentLabel, orderEnvironments } from "../lib/deployments"; | |
| 13 | import { TimeAgo } from "./ui"; | |
| 14 | ||
| 15 | /** Environments listed before the rest are a link away. */ | |
| 16 | const SHOWN = 3; | |
| 17 | ||
| 18 | /** The color a state is shown in. */ | |
| 19 | export 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. */ | |
| 29 | export 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. */ | |
| 55 | export 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. */ | |
| 65 | export function sourceLabel(source: DeploymentSource): string { | |
| 66 | return SOURCE_LABEL[source]; | |
| 67 | } | |
| 68 | ||
| 69 | export 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.