feat: redesign assurance reports for production
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user