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 'mirroring' into artifacts-mode | 1 | import { CloudOff, Flag, Undo2, Workflow } from "lucide-react"; |
| 2 | import type { ReactNode } from "react"; | |
| 3 | import { Form, Link, useRouteLoaderData } from "react-router"; | |
| 4 | ||
| 5 | import type { RepoMirror } from "@g1t/contracts"; | |
| 6 | ||
| 7 | import { ConfirmDialog } from "./repo-lifecycle"; | |
| A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so. | 8 | import { ButtonLink, EmptyState, SubmitButton, TimeAgo } from "./ui"; |
| Merge branch 'mirroring' into artifacts-mode | 9 | import { Badge } from "./ui/badge"; |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 10 | import { Button } from "./ui/button"; |
| Merge branch 'mirroring' into artifacts-mode | 11 | import { Hint } from "./ui/hint"; |
| 12 | import { | |
| A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so. | 13 | MIRRORING_DOCS, |
| Merge branch 'mirroring' into artifacts-mode | 14 | type MirrorBrief, |
| 15 | type MirroredRepo, | |
| 16 | followersOf, | |
| 17 | leaderOf, | |
| 18 | mirrorBadge, | |
| 19 | mirrorBanner, | |
| 20 | mirrorReason, | |
| 21 | mirroringSettings, | |
| 22 | workflowReason, | |
| 23 | } from "../lib/mirror"; | |
| 24 | ||
| 25 | /** | |
| 26 | * The repository the page is in, its remotes in brief and whether the | |
| 27 | * viewer manages them, from the repository layout's loader. Empty outside | |
| 28 | * a repository. | |
| 29 | */ | |
| 30 | export function useRepoMirror(): { repo: MirroredRepo | null; briefs: MirrorBrief[]; admin: boolean } { | |
| 31 | const layout = useRouteLoaderData("routes/repo/layout") as | |
| 32 | | { repo?: MirroredRepo; mirrorBriefs?: MirrorBrief[]; access?: { can?: { manage_integrations?: boolean } } } | |
| 33 | | undefined; | |
| 34 | return { | |
| 35 | repo: layout?.repo ?? null, | |
| 36 | briefs: layout?.mirrorBriefs ?? [], | |
| 37 | admin: Boolean(layout?.access?.can?.manage_integrations), | |
| 38 | }; | |
| 39 | } | |
| 40 | ||
| 41 | /** | |
| 42 | * Why pushing, merging, opening issues and pull requests and assigning | |
| 43 | * agents are off in the repository the page is in; null when they are not | |
| 44 | * off because it is a mirror. | |
| 45 | */ | |
| 46 | export function useMirrorReason(): string | null { | |
| 47 | return mirrorReason(useRepoMirror().repo); | |
| 48 | } | |
| 49 | ||
| 50 | /** Why running workflows is off: a mirror standing by or handing back runs nothing. */ | |
| 51 | export function useWorkflowReason(): string | null { | |
| 52 | return workflowReason(useRepoMirror().repo); | |
| 53 | } | |
| 54 | ||
| 55 | /** What the badge means, said on hover. */ | |
| 56 | function badgeHint(mirror: RepoMirror | null | undefined, briefs: readonly MirrorBrief[]): string { | |
| 57 | if (mirror) { | |
| 58 | switch (mirror.state) { | |
| 59 | case "standby": | |
| 60 | return `A read-only copy that follows ${mirror.remote}. Work happens there; clone and fetch freely here.`; | |
| 61 | case "ci": | |
| 62 | return `${mirror.remote} keeps the code; g1t runs its workflows and sends the results back.`; | |
| 63 | case "takeover": | |
| 64 | return `g1t leads for now: everything works here until it is handed back to ${mirror.remote}.`; | |
| 65 | case "handing_back": | |
| 66 | return `Going back to ${mirror.remote} branch by branch. Read-only until it is done.`; | |
| 67 | } | |
| 68 | } | |
| 69 | const names = followersOf(briefs).map((brief) => brief.name); | |
| 70 | return `g1t leads. Every push here goes to ${names.join(", ")}.`; | |
| 71 | } | |
| 72 | ||
| 73 | /** | |
| 74 | * Beside the repository's name: what it is to its remotes. An amber dot | |
| 75 | * when the remote a mirror follows is not answering. | |
| 76 | */ | |
| 77 | export function MirrorBadge({ mirror, briefs }: { mirror: RepoMirror | null | undefined; briefs: readonly MirrorBrief[] }) { | |
| 78 | const badge = mirrorBadge(mirror, briefs); | |
| 79 | if (!badge) return null; | |
| 80 | const silent = Boolean(mirror) && leaderOf(briefs)?.reachable === false; | |
| 81 | return ( | |
| 82 | <Hint label={badgeHint(mirror, briefs)}> | |
| 83 | <Badge tone={badge.tone} tabIndex={0} className="min-w-0 max-w-full px-2 py-0.5 text-xs font-normal"> | |
| 84 | {silent && <span aria-label="Not answering" className="size-1.5 shrink-0 rounded-full bg-warn" />} | |
| 85 | <span className="truncate">{badge.label}</span> | |
| 86 | {badge.more > 0 && <span className="shrink-0 opacity-70">+{badge.more} more</span>} | |
| 87 | </Badge> | |
| 88 | </Hint> | |
| 89 | ); | |
| 90 | } | |
| 91 | ||
| 92 | const BANNER_TONES = { | |
| 93 | warn: { box: "border-warn/40 bg-warn/5", icon: "text-warn", action: "text-warn" }, | |
| 94 | info: { box: "border-info/40 bg-info/5", icon: "text-info", action: "text-info" }, | |
| 95 | } as const; | |
| 96 | ||
| 97 | /** A compact line across the top of the page, with what it is about and one action. */ | |
| 98 | function BannerBox({ tone, icon, title, children, action, className = "" }: { | |
| 99 | tone: keyof typeof BANNER_TONES; | |
| 100 | className?: string; | |
| 101 | icon: ReactNode; | |
| 102 | title: ReactNode; | |
| 103 | children?: ReactNode; | |
| 104 | action?: ReactNode; | |
| 105 | }) { | |
| 106 | const colours = BANNER_TONES[tone]; | |
| 107 | return ( | |
| 108 | <div | |
| 109 | role="status" | |
| 110 | className={`flex flex-col gap-2 rounded-lg border px-4 py-2.5 text-sm sm:flex-row sm:items-center sm:justify-between ${colours.box} ${className}`} | |
| 111 | > | |
| 112 | <p className="flex min-w-0 gap-2.5"> | |
| 113 | <span className={`mt-0.5 shrink-0 ${colours.icon} [&_svg]:size-4`}>{icon}</span> | |
| 114 | <span className="min-w-0"> | |
| 115 | <span className="font-medium text-fg">{title}</span> | |
| 116 | {children && <> <span className="text-muted">{children}</span></>} | |
| 117 | </span> | |
| 118 | </p> | |
| 119 | {action && <div className={`shrink-0 pl-6.5 font-medium sm:pl-0 ${colours.action}`}>{action}</div>} | |
| 120 | </div> | |
| 121 | ); | |
| 122 | } | |
| 123 | ||
| 124 | /** | |
| 125 | * Taking over, from anywhere: says what it does and posts to the | |
| 126 | * mirroring settings, which then show it. | |
| 127 | */ | |
| 128 | export function TakeOverDialog({ | |
| 129 | base, | |
| 130 | full, | |
| 131 | mirror, | |
| 132 | error, | |
| 133 | trigger, | |
| 134 | }: { | |
| 135 | base: string; | |
| 136 | full: string; | |
| 137 | mirror: Pick<RepoMirror, "remote" | "holdDeploys">; | |
| 138 | error?: string | null; | |
| 139 | trigger: (open: () => void) => ReactNode; | |
| 140 | }) { | |
| 141 | return ( | |
| 142 | <ConfirmDialog | |
| 143 | intent="take-over" | |
| 144 | action={mirroringSettings(base)} | |
| 145 | title={`Take over ${full}?`} | |
| 146 | description={`g1t leads ${full} until you hand it back. Pushes, pull requests, agents and workflows work here meanwhile.`} | |
| 147 | submit="Take over" | |
| 148 | busy="Taking over…" | |
| 149 | danger={false} | |
| 150 | error={error} | |
| 151 | trigger={trigger} | |
| 152 | > | |
| 153 | <li>{mirror.remote} keeps what it has. Nothing goes back to it until you hand it back, branch by branch.</li> | |
| 154 | {mirror.holdDeploys !== false && <li>Workflows that deploy wait for someone to approve them.</li>} | |
| 155 | </ConfirmDialog> | |
| 156 | ); | |
| 157 | } | |
| 158 | ||
| 159 | /** | |
| 160 | * Across the top of a mirror's pages, when there is something to know: its | |
| 161 | * remote is not answering, g1t runs its CI, has taken over, or is handing | |
| 162 | * back. A mirror standing by whose remote answers gets none: the badge | |
| 163 | * beside its name says it. | |
| 164 | */ | |
| 165 | export function MirrorBanner({ | |
| 166 | base, | |
| 167 | full, | |
| 168 | mirror, | |
| 169 | briefs, | |
| 170 | admin, | |
| 171 | className, | |
| 172 | }: { | |
| 173 | base: string; | |
| 174 | full: string; | |
| 175 | mirror: RepoMirror | null | undefined; | |
| 176 | briefs: readonly MirrorBrief[]; | |
| 177 | /** Whether the viewer manages its mirroring: they get its one action. */ | |
| 178 | admin: boolean; | |
| 179 | className?: string; | |
| 180 | }) { | |
| 181 | const kind = mirrorBanner(mirror, briefs); | |
| 182 | if (!kind || !mirror) return null; | |
| 183 | const settings = mirroringSettings(base); | |
| 184 | switch (kind) { | |
| 185 | case "unreachable": { | |
| 186 | const synced = leaderOf(briefs)?.syncedAt; | |
| 187 | return ( | |
| 188 | <BannerBox | |
| 189 | className={className} | |
| 190 | tone="warn" | |
| 191 | icon={<CloudOff />} | |
| 192 | title={`${mirror.remote} isn't answering.`} | |
| 193 | action={ | |
| 194 | admin ? ( | |
| 195 | <TakeOverDialog | |
| 196 | base={base} | |
| 197 | full={full} | |
| 198 | mirror={mirror} | |
| 199 | trigger={(open) => ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 200 | <Button variant="link" size="inline" onClick={open} className="text-inherit"> |
| Merge branch 'mirroring' into artifacts-mode | 201 | Take over |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 202 | </Button> |
| Merge branch 'mirroring' into artifacts-mode | 203 | )} |
| 204 | /> | |
| 205 | ) : undefined | |
| 206 | } | |
| 207 | > | |
| 208 | {synced ? ( | |
| 209 | <> | |
| 210 | g1t has its copy as of <TimeAgo at={synced} />. | |
| 211 | </> | |
| 212 | ) : ( | |
| 213 | "g1t has its copy as of the last sync." | |
| 214 | )} | |
| 215 | {mirror.state === "ci" && " Its workflows keep running here."} | |
| 216 | </BannerBox> | |
| 217 | ); | |
| 218 | } | |
| 219 | case "ci": | |
| 220 | return ( | |
| 221 | <BannerBox | |
| 222 | className={className} | |
| 223 | tone="info" | |
| 224 | icon={<Workflow />} | |
| 225 | title={`Running ${mirror.remote}'s workflows on g1t.`} | |
| 226 | action={ | |
| 227 | admin ? ( | |
| 228 | <Form method="post" action={settings}> | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 229 | <SubmitButton name="intent" value="ci-off" pending="Ending…" variant="link" size="inline" className="text-inherit"> |
| Merge branch 'mirroring' into artifacts-mode | 230 | End CI failover |
| 231 | </SubmitButton> | |
| 232 | </Form> | |
| 233 | ) : undefined | |
| 234 | } | |
| 235 | > | |
| 236 | CI failover started <TimeAgo at={mirror.since} />. | |
| 237 | {mirror.holdDeploys !== false && " Deploying workflows wait for approval."} | |
| 238 | </BannerBox> | |
| 239 | ); | |
| 240 | case "takeover": | |
| 241 | return ( | |
| 242 | <BannerBox | |
| 243 | className={className} | |
| 244 | tone="warn" | |
| 245 | icon={<Flag />} | |
| 246 | title={`g1t is leading ${full} for now.`} | |
| 247 | action={ | |
| 248 | admin ? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 249 | <ButtonLink to={`${settings}?plan=1#hand-back`} variant="link" size="inline" className="text-inherit"> |
| Merge branch 'mirroring' into artifacts-mode | 250 | Hand back… |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 251 | </ButtonLink> |
| Merge branch 'mirroring' into artifacts-mode | 252 | ) : undefined |
| 253 | } | |
| 254 | > | |
| 255 | Everything works here; when you're done, hand it back to {mirror.remote}. | |
| 256 | </BannerBox> | |
| 257 | ); | |
| 258 | case "handing_back": | |
| 259 | return ( | |
| 260 | <BannerBox | |
| 261 | className={className} | |
| 262 | tone="warn" | |
| 263 | icon={<Undo2 />} | |
| 264 | title={`Handing back to ${mirror.remote}…`} | |
| 265 | action={ | |
| 266 | admin ? ( | |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 267 | <ButtonLink to={settings} variant="link" size="inline" className="text-inherit"> |
| Merge branch 'mirroring' into artifacts-mode | 268 | Progress |
| The site is built from its shared components: one Button with its variants and sizes, one Badge, Card for every panel, Alert for notices, Kbd for keys, Table for the security tables, and Avatar, Command, Progress, ScrollArea, ToggleGroup, Collapsible and Accordion in the kit; raw buttons, hand-drawn cards and pills and per-file button class lists are gone, outline buttons stand the same height as filled ones, and the phone's Ask g1t loses its tile. Every page was compared pixel by pixel before and after. | 269 | </ButtonLink> |
| Merge branch 'mirroring' into artifacts-mode | 270 | ) : undefined |
| 271 | } | |
| 272 | > | |
| 273 | {full} is read-only until it's done. | |
| 274 | </BannerBox> | |
| 275 | ); | |
| 276 | } | |
| 277 | } | |
| 278 | ||
| 279 | /** | |
| 280 | * Under the clone address: what pushing here does when the repository | |
| 281 | * mirrors a remote. Null when it leads. | |
| 282 | */ | |
| 283 | export function MirrorCloneNote({ mirror, base, admin }: { mirror: RepoMirror | null | undefined; base: string; admin: boolean }) { | |
| 284 | if (!mirror) return null; | |
| 285 | const where = admin ? ( | |
| 286 | <Link to={mirroringSettings(base)} className="text-fg underline underline-offset-4"> | |
| 287 | Settings → Mirroring | |
| 288 | </Link> | |
| 289 | ) : ( | |
| 290 | "Settings → Mirroring" | |
| 291 | ); | |
| 292 | return ( | |
| 293 | <p className="mt-2 text-xs text-muted"> | |
| 294 | {mirror.state === "takeover" ? ( | |
| 295 | <>g1t leads for now: pushes here go back to {mirror.remote} when it is handed back.</> | |
| 296 | ) : mirror.state === "handing_back" ? ( | |
| 297 | <>Handing back to {mirror.remote}: clone and fetch freely; pushes wait until it is done.</> | |
| 298 | ) : ( | |
| 299 | <> | |
| 300 | A mirror of {mirror.remote}. Clone and fetch freely; to push, push there, or take over in {where}. | |
| 301 | </> | |
| 302 | )} | |
| 303 | </p> | |
| 304 | ); | |
| 305 | } | |
| 306 | ||
| 307 | /** | |
| A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so. | 308 | * In place of a page a mirror does not offer now (lib/chrome.ts |
| 309 | * `hiddenOnMirror`: issues, pull requests, agents, workflows, deployments, | |
| 310 | * a new release): what the repository is, where the work happens, and the | |
| 311 | * way to take over. | |
| 312 | */ | |
| 313 | export function MirrorReadOnlyPage({ base, repo, mirror, admin, what }: { | |
| 314 | base: string; | |
| 315 | repo: { namespace: string; name: string }; | |
| 316 | mirror: RepoMirror; | |
| 317 | /** Whether the viewer manages its mirroring: they get the way to its settings. */ | |
| 318 | admin: boolean; | |
| 319 | /** What the page would list, lowercase: "issues", "pull requests". */ | |
| 320 | what: string; | |
| 321 | }) { | |
| 322 | const full = `${repo.namespace}/${repo.name}`; | |
| 323 | const remote = ( | |
| 324 | <a href={mirror.url} target="_blank" rel="noreferrer" className="font-mono text-fg underline underline-offset-4 hover:text-accent"> | |
| 325 | {mirror.remote} | |
| 326 | </a> | |
| 327 | ); | |
| 328 | return ( | |
| 329 | <EmptyState title={`This is a mirror of ${mirror.remote}`}> | |
| 330 | <p className="mx-auto max-w-xl"> | |
| 331 | {mirror.state === "handing_back" ? ( | |
| 332 | <> | |
| 333 | {full} is handing back to {remote}, so there are no {what} here until that is done. | |
| 334 | </> | |
| 335 | ) : ( | |
| 336 | <> | |
| 337 | {full} is a read-only copy until someone takes over, so there are no {what} here. Its {what} are on {remote}. | |
| 338 | </> | |
| 339 | )} | |
| 340 | </p> | |
| 341 | <div className="mt-4 flex flex-wrap items-center justify-center gap-2"> | |
| 342 | {admin && ( | |
| 343 | <ButtonLink to={mirroringSettings(base)} variant="outline" size="sm"> | |
| 344 | Mirroring settings | |
| 345 | </ButtonLink> | |
| 346 | )} | |
| 347 | <ButtonLink to={MIRRORING_DOCS} variant="ghost" size="sm" target="_blank" rel="noreferrer"> | |
| 348 | How mirroring works | |
| 349 | </ButtonLink> | |
| 350 | </div> | |
| 351 | </EmptyState> | |
| 352 | ); | |
| 353 | } | |
| 354 | ||
| 355 | /** | |
| Merge branch 'mirroring' into artifacts-mode | 356 | * On the repository's overview, one quiet line when no banner says it: a |
| 357 | * mirror standing by, or the remotes that follow it, and when it last synced. | |
| 358 | */ | |
| 359 | export function MirrorOverviewNote({ | |
| 360 | base, | |
| 361 | mirror, | |
| 362 | briefs, | |
| 363 | admin, | |
| 364 | syncedAt, | |
| 365 | lastError, | |
| 366 | }: { | |
| 367 | base: string; | |
| 368 | mirror: RepoMirror | null | undefined; | |
| 369 | briefs: readonly MirrorBrief[]; | |
| 370 | admin: boolean; | |
| 371 | syncedAt?: string | null; | |
| 372 | lastError?: string | null; | |
| 373 | }) { | |
| 374 | const followers = followersOf(briefs); | |
| 375 | // A banner says it already. | |
| 376 | if (mirror && mirrorBanner(mirror, briefs)) return null; | |
| 377 | if (!mirror && followers.length === 0) return null; | |
| 378 | const stuck = followers.filter((brief) => brief.state === "stuck"); | |
| 379 | return ( | |
| 380 | <div className="flex flex-wrap items-baseline gap-x-3 gap-y-1 text-sm text-muted"> | |
| 381 | <p className="min-w-0"> | |
| 382 | {mirror ? ( | |
| 383 | <> | |
| 384 | A read-only mirror of{" "} | |
| 385 | <a href={mirror.url} target="_blank" rel="noreferrer" className="font-mono break-all text-fg hover:text-accent"> | |
| 386 | {mirror.remote} | |
| 387 | </a> | |
| 388 | . | |
| 389 | </> | |
| 390 | ) : ( | |
| 391 | <> | |
| 392 | Mirrored to <span className="font-mono text-fg">{followers[0]!.name}</span> | |
| 393 | {followers.length > 1 && ` and ${followers.length - 1} more`}: every push here goes there too. | |
| 394 | </> | |
| 395 | )} | |
| 396 | {syncedAt && ( | |
| 397 | <span className="text-faint"> | |
| 398 | {" "} | |
| 399 | Synced <TimeAgo at={syncedAt} />. | |
| 400 | </span> | |
| 401 | )} | |
| 402 | </p> | |
| 403 | {admin && ( | |
| 404 | <Link to={mirroringSettings(base)} className="text-xs text-muted hover:text-fg"> | |
| 405 | Mirroring settings | |
| 406 | </Link> | |
| 407 | )} | |
| 408 | {(lastError || stuck.length > 0) && ( | |
| 409 | <p className="w-full text-xs text-warn"> | |
| 410 | {lastError ?? `${stuck.map((brief) => brief.name).join(", ")} stopped taking pushes. See Settings → Mirroring.`} | |
| 411 | </p> | |
| 412 | )} | |
| 413 | </div> | |
| 414 | ); | |
| 415 | } | |
| 416 | ||
| 417 | /** What an action did that people should know, such as the pull requests it opened, with addresses as links. */ | |
| 418 | export function MirrorNotes({ notes }: { notes: readonly string[] | null | undefined }) { | |
| 419 | if (!notes || notes.length === 0) return null; | |
| 420 | return ( | |
| 421 | <div role="status" className="rounded-lg border border-success/40 bg-success/5 px-4 py-3 text-sm"> | |
| 422 | <ul className="space-y-1"> | |
| 423 | {notes.map((note, at) => ( | |
| 424 | <li key={at} className="text-fg-soft"> | |
| 425 | {note.split(/(https?:\/\/\S+?)(?=[.,;)]*(?:\s|$))/).map((piece, index) => | |
| 426 | /^https?:\/\//.test(piece) ? ( | |
| 427 | <a key={index} href={piece} className="break-all text-fg underline underline-offset-2 hover:text-accent"> | |
| 428 | {piece} | |
| 429 | </a> | |
| 430 | ) : ( | |
| 431 | piece | |
| 432 | ), | |
| 433 | )} | |
| 434 | </li> | |
| 435 | ))} | |
| 436 | </ul> | |
| 437 | </div> | |
| 438 | ); | |
| 439 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.