Add run detail page, link run cards, and custom workflow icon colors

- Add id field to RunItem and assign IDs to all mock runs
- Create run detail page at /runs/:id with status, changes, PR, CI info
- Link run cards in both /runs kanban board and /workflows/:name/runs list
- Add per-workflow icon color with tinted border on workflow cards

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-28 14:53:14 -05:00
parent 62c401336f
commit bc1b5d54ab
6 changed files with 99 additions and 11 deletions

View file

@ -1,6 +1,7 @@
export type CiStatus = "passing" | "failing" | "pending";
export interface RunItem {
id: string;
repo: string;
title: string;
number?: number;
@ -39,18 +40,21 @@ export const columns: {
actions: ["Watch", "Steer"],
items: [
{
id: "run-1",
repo: "api-server",
title: "Add rate limiting to auth endpoints",
resources: "4 CPU / 8 GB",
elapsed: "7m",
},
{
id: "run-2",
repo: "web-dashboard",
title: "Migrate to React Router v7",
resources: "8 CPU / 16 GB",
elapsed: "2h 15m",
},
{
id: "run-3",
repo: "cli-tools",
title: "Fix config parsing for nested values",
resources: "2 CPU / 4 GB",
@ -67,6 +71,7 @@ export const columns: {
actions: ["Answer Question"],
items: [
{
id: "run-4",
repo: "api-server",
title: "Update OpenAPI spec for v3",
additions: 567,
@ -74,6 +79,7 @@ export const columns: {
elapsed: "1h 12m",
},
{
id: "run-5",
repo: "shared-types",
title: "Add pipeline event types",
additions: 145,
@ -91,6 +97,7 @@ export const columns: {
actions: ["Resolve"],
items: [
{
id: "run-6",
repo: "web-dashboard",
title: "Add dark mode toggle",
number: 889,
@ -101,6 +108,7 @@ export const columns: {
comments: 4,
},
{
id: "run-7",
repo: "infrastructure",
title: "Terraform module for Redis cluster",
number: 156,
@ -122,6 +130,7 @@ export const columns: {
actions: ["Merge"],
items: [
{
id: "run-8",
repo: "api-server",
title: "Implement webhook retry logic",
number: 1249,
@ -133,6 +142,7 @@ export const columns: {
comments: 7,
},
{
id: "run-9",
repo: "cli-tools",
title: "Add --verbose flag to run command",
number: 430,
@ -143,6 +153,7 @@ export const columns: {
comments: 2,
},
{
id: "run-10",
repo: "shared-types",
title: "Export utility type helpers",
number: 76,
@ -166,6 +177,10 @@ export function allRunsFlat(): RunWithStatus[] {
);
}
export function findRun(id: string): RunWithStatus | undefined {
return allRunsFlat().find((r) => r.id === id);
}
export const statusColors: Record<ColumnStatus, { dot: string; text: string }> = {
working: { dot: "bg-teal-500", text: "text-teal-500" },
pending: { dot: "bg-amber", text: "text-amber" },

View file

@ -16,6 +16,7 @@ export default [
route("runs", "routes/workflow-runs.tsx"),
]),
route("runs", "routes/pipelines.tsx"),
route("runs/:id", "routes/run-detail.tsx"),
route("insights", "routes/insights.tsx"),
route("settings", "routes/settings.tsx"),
]),

View file

@ -1,3 +1,4 @@
import { Link } from "react-router";
import { columns, ciConfig } from "../data/runs";
import type { CiStatus, RunItem } from "../data/runs";
import type { Route } from "./+types/pipelines";
@ -80,7 +81,7 @@ function PrCard({
actions?: string[];
}) {
return (
<div className="group rounded-lg border border-white/[0.06] bg-navy-800/80 p-4 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800 hover:shadow-lg hover:shadow-black/20">
<Link to={`/runs/${pr.id}`} className="group block rounded-lg border border-white/[0.06] bg-navy-800/80 p-4 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800 hover:shadow-lg hover:shadow-black/20">
<div className="mb-2 flex items-center gap-1.5">
<Icon className={`size-3.5 shrink-0 ${iconColor}`} />
<span className="font-mono text-xs font-medium text-teal-500">
@ -172,7 +173,7 @@ function PrCard({
{pr.ci != null && <span className="ml-auto flex items-center"><CiBadge status={pr.ci} /></span>}
</div>
)}
</div>
</Link>
);
}
@ -193,7 +194,7 @@ function BoardColumn({ column }: { column: (typeof columns)[number] }) {
<div className="flex flex-1 flex-col gap-3">
{column.items.map((pr) => (
<PrCard
key={`${pr.repo}-${pr.number ?? pr.title}`}
key={pr.id}
pr={pr}
icon={Icon}
iconColor={column.iconColor}

View file

@ -0,0 +1,66 @@
import { findRun, statusColors, ciConfig } from "../data/runs";
import type { Route } from "./+types/run-detail";
export function meta({ params }: Route.MetaArgs) {
const run = findRun(params.id);
return [{ title: run ? `${run.title} — Arc` : "Run — Arc" }];
}
export default function RunDetail({ params }: Route.ComponentProps) {
const run = findRun(params.id);
if (!run) {
return <p className="py-8 text-center text-sm text-navy-600">Run not found.</p>;
}
const colors = statusColors[run.status];
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-ice-100">{run.title}</h2>
<div className="mt-2 flex items-center gap-3 text-sm">
<span className="flex items-center gap-1.5">
<span className={`size-2 rounded-full ${colors.dot}`} />
<span className={`font-medium ${colors.text}`}>{run.statusLabel}</span>
</span>
<span className="font-mono text-xs text-navy-600">{run.repo}</span>
{run.elapsed && (
<span className={`font-mono text-xs ${run.elapsedWarning ? "text-amber" : "text-navy-600"}`}>{run.elapsed}</span>
)}
</div>
</div>
<div className="rounded-lg border border-white/[0.06] bg-navy-800/80 p-5 space-y-4">
{run.resources && (
<div className="flex items-center justify-between">
<span className="text-xs text-navy-600">Resources</span>
<span className="font-mono text-sm text-ice-300">{run.resources}</span>
</div>
)}
{(run.additions != null || run.deletions != null) && (
<div className="flex items-center justify-between">
<span className="text-xs text-navy-600">Changes</span>
<span className="font-mono text-sm">
{run.additions != null && <span className="text-mint">+{run.additions.toLocaleString()}</span>}
{run.additions != null && run.deletions != null && <span className="text-navy-600"> / </span>}
{run.deletions != null && <span className="text-coral">-{run.deletions.toLocaleString()}</span>}
</span>
</div>
)}
{run.number != null && (
<div className="flex items-center justify-between">
<span className="text-xs text-navy-600">Pull Request</span>
<span className="font-mono text-sm text-ice-300">#{run.number}</span>
</div>
)}
{run.ci && (
<div className="flex items-center justify-between">
<span className="text-xs text-navy-600">CI</span>
<span className={`font-mono text-sm ${ciConfig[run.ci].text}`}>{ciConfig[run.ci].label}</span>
</div>
)}
</div>
</div>
);
}

View file

@ -1,5 +1,6 @@
import { useState } from "react";
import { ChevronDownIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { Link } from "react-router";
import { allRunsFlat, ciConfig, columns, statusColors } from "../data/runs";
import type { ColumnStatus, RunWithStatus } from "../data/runs";
@ -16,7 +17,7 @@ function GitPullRequestIcon({ className }: { className?: string }) {
function RunRow({ run }: { run: RunWithStatus }) {
const colors = statusColors[run.status];
return (
<div className="grid items-center rounded-lg border border-white/[0.06] bg-navy-800/80 px-4 py-3 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
<Link to={`/runs/${run.id}`} className="grid items-center rounded-lg border border-white/[0.06] bg-navy-800/80 px-4 py-3 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800" style={{ gridColumn: "1 / -1", gridTemplateColumns: "subgrid" }}>
<span className="flex items-center gap-2 pr-2">
<span className={`size-2 shrink-0 rounded-full ${colors.dot}`} />
<span className={`text-xs font-medium ${colors.text}`}>{run.statusLabel}</span>
@ -52,7 +53,7 @@ function RunRow({ run }: { run: RunWithStatus }) {
</>
)}
</span>
</div>
</Link>
);
}
@ -96,7 +97,7 @@ export default function WorkflowRuns() {
</div>
<div className="grid gap-2" style={{ gridTemplateColumns: "auto 5rem 1fr 8rem auto" }}>
{filtered.map((run) => (
<RunRow key={`${run.repo}-${run.number ?? run.title}`} run={run} />
<RunRow key={run.id} run={run} />
))}
{filtered.length === 0 && (
<p className="py-8 text-center text-sm text-navy-600">No runs match "{query}"</p>

View file

@ -61,13 +61,14 @@ interface Workflow {
filename: string;
lastRun: string;
icon: ComponentType<{ className?: string }>;
color: string;
}
const workflows: Workflow[] = [
{ name: "Fix Build", slug: "fix_build", filename: "fix_build.dot", lastRun: "2 hours ago", icon: WrenchIcon },
{ name: "Implement Feature", slug: "implement", filename: "implement.dot", lastRun: "4 days ago", icon: CodeBracketIcon },
{ name: "Sync Drift", slug: "sync_drift", filename: "sync_drift.dot", lastRun: "1 day ago", icon: ArrowsRightLeftIcon },
{ name: "Expand Product", slug: "expand", filename: "expand.dot", lastRun: "2 weeks ago", icon: RocketLaunchIcon },
{ name: "Fix Build", slug: "fix_build", filename: "fix_build.dot", lastRun: "2 hours ago", icon: WrenchIcon, color: "#F0A45B" },
{ name: "Implement Feature", slug: "implement", filename: "implement.dot", lastRun: "4 days ago", icon: CodeBracketIcon, color: "#67B2D7" },
{ name: "Sync Drift", slug: "sync_drift", filename: "sync_drift.dot", lastRun: "1 day ago", icon: ArrowsRightLeftIcon, color: "#5AC8A8" },
{ name: "Expand Product", slug: "expand", filename: "expand.dot", lastRun: "2 weeks ago", icon: RocketLaunchIcon, color: "#E86B6B" },
];
function PlayIcon({ className }: { className?: string }) {
@ -91,7 +92,10 @@ function WorkflowCard({ workflow }: { workflow: Workflow }) {
return (
<div className="group flex items-center gap-4 rounded-lg border border-white/[0.06] bg-navy-800/80 p-4 transition-all duration-200 hover:border-white/[0.12] hover:bg-navy-800 hover:shadow-lg hover:shadow-black/20">
<Link to={`/workflows/${workflow.slug}`} className="flex min-w-0 flex-1 items-center gap-4">
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-white/[0.06] bg-navy-900/60 text-ice-300">
<div
className="flex size-9 shrink-0 items-center justify-center rounded-md border bg-navy-900/60"
style={{ borderColor: `${workflow.color}33`, color: workflow.color }}
>
<Icon className="size-4" />
</div>