| 1 | | − | import { AlertTriangle, BadgeCheck, FileCode2, GitBranch, Play, PlayCircle } from "lucide-react"; |
| 1 | + | import { Archive, ChevronRight, AlertTriangle, BadgeCheck, FileCode2, GitBranch, Play, PlayCircle } from "lucide-react"; |
| 2 | 2 | | import { useEffect, useRef, useState } from "react"; |
| 3 | 3 | | import { Form, Link, useSearchParams } from "react-router"; |
| 4 | 4 | | |
| 5 | 5 | | import { type DispatchInput, type Workflow, type WorkflowRun, isManagedWorkflow } from "@g1t/contracts"; |
| 6 | 6 | | |
| 7 | 7 | | import type { Route } from "./+types/actions"; |
| 8 | + | import { groupWorkflows, isArchivedWorkflow } from "../../lib/workflow-history"; |
| 8 | 9 | | import { page } from "../../lib/meta"; |
| 9 | 10 | | import { Duration, Notes, StatusIcon, shortRef } from "../../components/actions"; |
| 10 | 11 | | import { AddCiPrompt } from "../../components/add-ci"; |
| ⋯ |
| 306 | 307 | | } |
| 307 | 308 | | |
| 308 | 309 | | function WorkflowHeader({ workflow, base, member, manage }: { workflow: Workflow; base: string; member: boolean; manage: boolean }) { |
| 310 | + | const archived = isArchivedWorkflow(workflow); |
| 309 | 311 | | return ( |
| 310 | 312 | | <div className="space-y-4"> |
| 311 | 313 | | <div className="flex flex-wrap items-start justify-between gap-3"> |
| 312 | 314 | | <div className="min-w-0"> |
| 313 | 315 | | <h3 className="text-lg font-semibold tracking-tight">{workflow.name}</h3> |
| 314 | | − | {isManagedWorkflow(workflow.path) ? ( |
| 316 | + | {archived ? ( |
| 317 | + | <span className="inline-flex items-center gap-1 font-mono text-xs text-faint"><FileCode2 size={12} />{workflow.path}</span> |
| 318 | + | ) : isManagedWorkflow(workflow.path) ? ( |
| 315 | 319 | | <ManagedLine workflow={workflow} /> |
| 316 | 320 | | ) : ( |
| 317 | 321 | | <Link to={`${base}/blob/HEAD/${workflow.path}`} className="inline-flex items-center gap-1 font-mono text-xs text-faint hover:text-fg"> |
| ⋯ |
| 342 | 346 | | </div> |
| 343 | 347 | | )} |
| 344 | 348 | | </div> |
| 345 | | − | {workflow.error && <p className="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">{workflow.error}</p>} |
| 346 | | − | {workflow.state === "disabled" && ( |
| 349 | + | {archived ? ( |
| 350 | + | <p className="flex items-start gap-2 rounded-lg border border-line bg-surface px-3 py-3 text-sm text-muted"> |
| 351 | + | <Archive size={16} className="mt-0.5 shrink-0" /> |
| 352 | + | <span><strong className="font-medium text-fg">Archived workflow.</strong> Its file was removed from the default branch. Past runs remain available below. Restore the file on the default branch to return it to the workflow list.</span> |
| 353 | + | </p> |
| 354 | + | ) : workflow.error && <p className="rounded-lg bg-danger/10 px-3 py-2 text-sm text-danger">{workflow.error}</p>} |
| 355 | + | {!archived && workflow.state === "disabled" && ( |
| 347 | 356 | | <p className="rounded-lg bg-raised px-3 py-2 text-sm text-muted">Turned off: nothing starts it until someone with the Maintain role turns it on.</p> |
| 348 | 357 | | )} |
| 349 | | − | <Notes notes={workflow.notes} /> |
| 358 | + | {!archived && <Notes notes={workflow.notes} />} |
| 350 | 359 | | </div> |
| 351 | 360 | | ); |
| 352 | 361 | | } |
| ⋯ |
| 398 | 407 | | const workflowsFailed = isFailed(loaderData.workflows); |
| 399 | 408 | | const workflows = isFailed(loaderData.workflows) ? [] : loaderData.workflows; |
| 400 | 409 | | const workflow = workflows.find((w) => w.id === selected || w.path.endsWith(`/${selected}`)) ?? null; |
| 410 | + | const { current, archived } = groupWorkflows(workflows); |
| 411 | + | const archiveSelected = workflow != null && isArchivedWorkflow(workflow); |
| 401 | 412 | | useLiveWhile(runs.some((run) => run.status !== "completed")); |
| 402 | 413 | | const started = actionData && "started" in actionData ? actionData.started : null; |
| 403 | 414 | | return ( |
| ⋯ |
| 425 | 436 | | </div> |
| 426 | 437 | | )} |
| 427 | 438 | | |
| 428 | | − | {!workflowsFailed && workflows.length === 0 ? ( |
| 439 | + | {!workflowsFailed && workflows.length === 0 && runs.length === 0 ? ( |
| 429 | 440 | | <div className="grid gap-6 lg:grid-cols-2"> |
| 430 | 441 | | <div className="lg:col-span-2"> |
| 431 | 442 | | <AddCiPrompt owner={params.owner} repo={params.repo} canAdd={canPush} /> |
| ⋯ |
| 455 | 466 | | <Unavailable what="Workflows" compact /> |
| 456 | 467 | | </div> |
| 457 | 468 | | )} |
| 458 | | − | {workflows.map((w) => ( |
| 469 | + | {current.map((w) => ( |
| 459 | 470 | | <Link |
| 460 | 471 | | key={w.id} |
| 461 | 472 | | to={`?workflow=${w.id}`} |
| ⋯ |
| 479 | 490 | | {w.state === "disabled" && <span className="ml-auto shrink-0 text-xs text-faint">off</span>} |
| 480 | 491 | | </Link> |
| 481 | 492 | | ))} |
| 493 | + | {archived.length > 0 && ( |
| 494 | + | <details key={archiveSelected ? "selected-archive" : "archive"} open={archiveSelected || undefined} className="group mt-3 border-t border-line pt-3"> |
| 495 | + | <summary className="flex cursor-pointer list-none items-center gap-2 rounded-md px-2.5 py-2 text-muted hover:bg-raised/60 hover:text-fg [&::-webkit-details-marker]:hidden"> |
| 496 | + | <ChevronRight size={14} className="shrink-0 transition-transform group-open:rotate-90" /> |
| 497 | + | <span className="grow">Archived workflows</span> |
| 498 | + | <span className="text-xs tabular-nums text-faint">{archived.length}</span> |
| 499 | + | </summary> |
| 500 | + | <div className="mt-1 space-y-0.5"> |
| 501 | + | {archived.map((w) => ( |
| 502 | + | <Link key={w.id} to={`?workflow=${w.id}`} aria-current={w.id === workflow?.id ? "page" : undefined} |
| 503 | + | className={`flex items-center gap-2 rounded-md px-2.5 py-1.5 ${w.id === workflow?.id ? "bg-raised text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"}`}> |
| 504 | + | <Archive size={13} className="shrink-0 text-faint" /> |
| 505 | + | <span className="min-w-0 truncate">{w.name}<span className="block truncate font-mono text-[0.6875rem] text-faint">{w.path}</span></span> |
| 506 | + | </Link> |
| 507 | + | ))} |
| 508 | + | </div> |
| 509 | + | </details> |
| 510 | + | )} |
| 482 | 511 | | </nav> |
| 483 | 512 | | <div className="min-w-0 space-y-6"> |
| 484 | 513 | | {workflow && <WorkflowHeader workflow={workflow} base={base} member={member} manage={manage} />} |
| ⋯ |
| 493 | 522 | | )} |
| 494 | 523 | | <ErrorText>{actionData && "error" in actionData ? actionData.error : null}</ErrorText> |
| 495 | 524 | | {runs.length === 0 ? ( |
| 496 | | − | <EmptyState title="No runs yet"> |
| 497 | | − | {workflow?.dispatch ? "Run it by hand, or wait for what starts it." : "Runs appear here when something starts one."} |
| 525 | + | <EmptyState title={archiveSelected ? "No retained runs" : "No runs yet"}> |
| 526 | + | {archiveSelected ? "This archived workflow has no runs to display." : workflow?.dispatch ? "Run it by hand, or wait for what starts it." : "Runs appear here when something starts one."} |
| 498 | 527 | | </EmptyState> |
| 499 | 528 | | ) : ( |
| 500 | 529 | | <Card asChild className="overflow-hidden"> |