Refine workflow runs list: grid layout, search, CI dots, spacing

Use CSS grid with subgrid for guaranteed column alignment. Remove repo
name, add PR icon, move duration next to status, show CI as dot next
to PR number, add typeahead search. Reverse sort (Merge first).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-02-28 14:10:50 -05:00
parent 0600b2ab59
commit 300a12a4eb
2 changed files with 62 additions and 43 deletions

View file

@ -157,7 +157,7 @@ export const columns: {
];
export function allRunsFlat(): RunWithStatus[] {
return columns.flatMap((col) =>
return [...columns].reverse().flatMap((col) =>
col.items.map((item) => ({
...item,
status: col.id,

View file

@ -1,71 +1,90 @@
import { useState } from "react";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
import { allRunsFlat, ciConfig, statusColors } from "../data/runs";
import type { CiStatus, RunWithStatus } from "../data/runs";
import type { RunWithStatus } from "../data/runs";
const runs = allRunsFlat();
function CiBadge({ status }: { status: CiStatus }) {
const config = ciConfig[status];
function GitPullRequestIcon({ className }: { className?: string }) {
return (
<span className={`inline-flex items-center gap-1.5 font-mono text-xs ${config.text}`}>
<span className={`size-1.5 rounded-full ${config.dot}`} />
{config.label}
</span>
<svg viewBox="0 0 16 16" fill="currentColor" className={className} aria-hidden="true">
<path d="M1.5 3.25a2.25 2.25 0 1 1 3 2.122v5.256a2.251 2.251 0 1 1-1.5 0V5.372A2.25 2.25 0 0 1 1.5 3.25Zm5.677-.177L9.573.677A.25.25 0 0 1 10 .854V2.5h1A2.5 2.5 0 0 1 13.5 5v5.628a2.251 2.251 0 1 1-1.5 0V5a1 1 0 0 0-1-1h-1v1.646a.25.25 0 0 1-.427.177L7.177 3.427a.25.25 0 0 1 0-.354ZM3.75 2.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm0 9.5a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Zm8.25.75a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Z" />
</svg>
);
}
function RunRow({ run }: { run: RunWithStatus }) {
const colors = statusColors[run.status];
return (
<div className="flex items-center gap-4 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">
<span className="flex items-center gap-2 min-w-[80px]">
<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" }}>
<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>
</span>
<span className="font-mono text-xs font-medium text-teal-500 min-w-[110px]">
{run.repo}
{run.number != null && (
<span className="text-navy-600"> #{run.number}</span>
<span className={`font-mono text-xs pr-2 ${run.elapsedWarning ? "text-amber" : "text-navy-600"}`}>
{run.elapsed}
</span>
<span className="flex items-center gap-2 min-w-0">
<span className="truncate text-sm text-ice-100">{run.title}</span>
{run.comments != null && run.comments > 0 && (
<span className="inline-flex shrink-0 items-center gap-1 font-mono text-xs text-navy-600">
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
</svg>
{run.comments}
</span>
)}
</span>
<span className="flex-1 truncate text-sm text-ice-100">{run.title}</span>
<span className="flex items-center justify-end gap-2 pr-4 font-mono text-xs tabular-nums">
{run.additions != null && <span className="text-mint">+{run.additions.toLocaleString()}</span>}
{run.deletions != null && <span className="text-coral">-{run.deletions.toLocaleString()}</span>}
</span>
{run.additions != null && run.deletions != null && (
<span className="hidden sm:flex items-center gap-2 font-mono text-xs">
<span className="text-mint">+{run.additions.toLocaleString()}</span>
<span className="text-coral">-{run.deletions.toLocaleString()}</span>
</span>
)}
{run.ci != null && (
<span className="hidden md:block min-w-[120px]">
<CiBadge status={run.ci} />
</span>
)}
{run.comments != null && run.comments > 0 && (
<span className="hidden sm:inline-flex items-center gap-1 font-mono text-xs text-navy-600">
<svg viewBox="0 0 16 16" fill="currentColor" className="size-3" aria-hidden="true">
<path d="M1 2.75C1 1.784 1.784 1 2.75 1h10.5c.966 0 1.75.784 1.75 1.75v7.5A1.75 1.75 0 0 1 13.25 12H9.06l-2.573 2.573A1.458 1.458 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z" />
</svg>
{run.comments}
</span>
)}
<span className={`font-mono text-xs min-w-[60px] text-right ${run.elapsedWarning ? "text-amber" : "text-navy-600"}`}>
{run.elapsed}
<span className="inline-flex items-center justify-end gap-1.5 font-mono text-xs text-navy-600">
{run.number != null && (
<>
<GitPullRequestIcon className="size-3" />
#{run.number}
{run.ci != null && <span className={`size-1.5 rounded-full ${ciConfig[run.ci].dot}`} />}
</>
)}
</span>
</div>
);
}
export default function WorkflowRuns() {
const [query, setQuery] = useState("");
const filtered = runs.filter(
(r) =>
r.title.toLowerCase().includes(query.toLowerCase()) ||
r.statusLabel.toLowerCase().includes(query.toLowerCase()) ||
(r.number != null && `#${r.number}`.includes(query)),
);
return (
<div className="space-y-2">
{runs.map((run) => (
<RunRow key={`${run.repo}-${run.number ?? run.title}`} run={run} />
))}
<div className="space-y-4">
<div className="relative">
<MagnifyingGlassIcon className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-navy-600" />
<input
type="text"
placeholder="Search runs..."
value={query}
onChange={(e) => setQuery(e.target.value)}
className="w-full rounded-lg border border-white/[0.06] bg-navy-800/80 py-2 pl-9 pr-3 text-sm text-ice-100 placeholder-navy-600 outline-none transition-colors focus:border-teal-500/40 focus:ring-0"
/>
</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} />
))}
{filtered.length === 0 && (
<p className="py-8 text-center text-sm text-navy-600">No runs match "{query}"</p>
)}
</div>
</div>
);
}