Files
CASAN/packages/casan-control-panel/frontend/src/components/trace/AssuranceRail.tsx
T

73 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { HarnessGateStatus, HarnessTraceGraph } from '../../lib/api';
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> = {
queued: '○',
running: '●',
pass: '✓',
warning: '!',
blocked: '×',
error: '×',
skipped: '–',
};
export function AssuranceRail({
graph,
selectedId,
onSelect,
}: {
graph: HarnessTraceGraph;
selectedId?: string;
onSelect?: (id: string) => void;
}) {
return (
<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>
);
}