mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
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:
parent
0600b2ab59
commit
300a12a4eb
2 changed files with 62 additions and 43 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue