feat: redesign assurance reports for production

This commit is contained in:
thanhnv
2026-07-28 22:17:28 +07:00
parent cce3cbfd42
commit 3cc897c5bb
8 changed files with 878 additions and 309 deletions
@@ -1,13 +1,18 @@
import type { HarnessGateStatus, HarnessTraceGraph } from '../../lib/api';
const TONE: Record<HarnessGateStatus, string> = {
queued: 'border-slate-200 bg-slate-100 text-slate-400',
running: 'border-blue-300 bg-blue-50 text-blue-700',
pass: 'border-emerald-300 bg-emerald-50 text-emerald-700',
warning: 'border-amber-300 bg-amber-50 text-amber-700',
blocked: 'border-rose-300 bg-rose-50 text-rose-700',
error: 'border-rose-300 bg-rose-50 text-rose-700',
skipped: 'border-slate-200 bg-slate-100 text-slate-400',
const TONE: Record<HarnessGateStatus, {
ring: string;
dot: string;
text: string;
surface: string;
}> = {
queued: { ring: 'border-slate-300', dot: 'bg-slate-300', text: 'text-slate-500', surface: 'bg-slate-50' },
running: { ring: 'border-cyan-400', dot: 'bg-cyan-400', text: 'text-cyan-800', surface: 'bg-cyan-50' },
pass: { ring: 'border-emerald-400', dot: 'bg-emerald-500', text: 'text-emerald-800', surface: 'bg-emerald-50' },
warning: { ring: 'border-amber-400', dot: 'bg-amber-500', text: 'text-amber-800', surface: 'bg-amber-50' },
blocked: { ring: 'border-rose-500', dot: 'bg-rose-500', text: 'text-rose-800', surface: 'bg-rose-50' },
error: { ring: 'border-rose-500', dot: 'bg-rose-500', text: 'text-rose-800', surface: 'bg-rose-50' },
skipped: { ring: 'border-slate-300', dot: 'bg-slate-300', text: 'text-slate-500', surface: 'bg-slate-50' },
};
const MARK: Record<HarnessGateStatus, string> = {
@@ -20,22 +25,47 @@ const MARK: Record<HarnessGateStatus, string> = {
skipped: '–',
};
export function AssuranceRail({ graph }: { graph: HarnessTraceGraph }) {
export function AssuranceRail({
graph,
selectedId,
onSelect,
}: {
graph: HarnessTraceGraph;
selectedId?: string;
onSelect?: (id: string) => void;
}) {
return (
<div className="overflow-x-auto pb-1">
<div className="flex min-w-[760px] items-center">
{graph.nodes.map((node, index) => (
<div key={node.id} className="flex min-w-0 flex-1 items-center">
<div className={`min-w-0 flex-1 rounded-xl border p-3 ${TONE[node.status]}`}>
<div className="flex items-center justify-between gap-2">
<span className="text-[10px] font-black uppercase tracking-[0.12em]">{node.title.split(' · ')[0]}</span>
<span className={`flex h-5 w-5 items-center justify-center rounded-full border border-current text-[10px] ${node.status === 'running' ? 'animate-pulse' : ''}`}>{MARK[node.status]}</span>
</div>
<div className="mt-2 truncate text-xs font-semibold">{node.title.split(' · ')[1]}</div>
</div>
{index < graph.nodes.length - 1 && <div className={`h-px w-3 shrink-0 ${node.status === 'pass' ? 'bg-emerald-300' : 'bg-slate-200'}`} />}
</div>
))}
<div className="overflow-x-auto pb-2">
<div className="relative min-w-[860px] px-3 pt-2">
<div className="absolute left-[4.3rem] right-[4.3rem] top-[2.05rem] h-px bg-slate-200" aria-hidden="true" />
<div className="relative grid grid-cols-7">
{graph.nodes.map((node, index) => {
const tone = TONE[node.status];
const selected = selectedId === node.id;
const Element = onSelect ? 'button' : 'div';
return (
<Element
key={node.id}
{...(onSelect ? {
type: 'button' as const,
onClick: () => onSelect(node.id),
'aria-pressed': selected,
} : {})}
className={`group min-w-0 px-1.5 text-center ${onSelect ? 'cursor-pointer focus:outline-none' : ''}`}
>
<div className={`relative mx-auto flex h-11 w-11 items-center justify-center rounded-full border-2 bg-white text-sm font-black shadow-[0_0_0_5px_white] transition ${tone.ring} ${tone.text} ${selected ? 'scale-110 ring-4 ring-cyan-100' : onSelect ? 'group-hover:-translate-y-0.5 group-hover:shadow-md' : ''}`}>
<span className={`absolute right-0 top-0 h-2.5 w-2.5 rounded-full border-2 border-white ${tone.dot} ${node.status === 'running' ? 'animate-pulse' : ''}`} />
{MARK[node.status]}
</div>
<div className={`mx-auto mt-3 rounded-lg px-2 py-2 transition ${selected ? tone.surface : 'bg-transparent'}`}>
<p className="text-[10px] font-black uppercase tracking-[0.14em] text-slate-400">H{index + 1}</p>
<p className={`mt-1 truncate text-xs font-semibold ${tone.text}`}>{node.title.split(' · ')[1] ?? node.title}</p>
<p className="mt-1 truncate text-[10px] capitalize text-slate-400">{node.status}</p>
</div>
</Element>
);
})}
</div>
</div>
</div>
);