From 97e21c8296ec5f91912d56980ebf3b18a1ca3507 Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 7 Feb 2026 18:27:54 +0000 Subject: Add directive monitor contracts --- .../src/components/directives/StepDiagram.tsx | 152 +++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 makima/frontend/src/components/directives/StepDiagram.tsx (limited to 'makima/frontend/src/components/directives/StepDiagram.tsx') diff --git a/makima/frontend/src/components/directives/StepDiagram.tsx b/makima/frontend/src/components/directives/StepDiagram.tsx new file mode 100644 index 0000000..5c65ae1 --- /dev/null +++ b/makima/frontend/src/components/directives/StepDiagram.tsx @@ -0,0 +1,152 @@ +import { useNavigate } from "react-router"; +import type { ChainStep, ContractPhase } from "../../lib/api"; +import { PhaseProgressBarCompact } from "../contracts/PhaseProgressBar"; + +interface StepDiagramProps { + steps: ChainStep[]; +} + +const statusBorderColors: Record = { + pending: "border-[#555]", + running: "border-yellow-400", + passed: "border-green-400", + failed: "border-red-400", +}; + +const statusDotColors: Record = { + pending: "bg-[#555]", + running: "bg-yellow-400", + passed: "bg-green-400", + failed: "bg-red-400", +}; + +/** + * Assign depth to each step via topological sort. + * Steps with no dependsOn = depth 0. Steps depending only on depth-0 = depth 1. Etc. + */ +function assignDepths(steps: ChainStep[]): Map { + const depths = new Map(); + const stepMap = new Map(steps.map((s) => [s.id, s])); + + function getDepth(id: string): number { + if (depths.has(id)) return depths.get(id)!; + const step = stepMap.get(id); + if (!step || !step.dependsOn || step.dependsOn.length === 0) { + depths.set(id, 0); + return 0; + } + const maxParent = Math.max( + ...step.dependsOn.map((depId) => getDepth(depId)) + ); + const d = maxParent + 1; + depths.set(id, d); + return d; + } + + for (const step of steps) { + getDepth(step.id); + } + + return depths; +} + +export function StepDiagram({ steps }: StepDiagramProps) { + const navigate = useNavigate(); + + if (steps.length === 0) { + return ( +

No steps to display.

+ ); + } + + const depths = assignDepths(steps); + const maxDepth = Math.max(...Array.from(depths.values())); + + // Group steps by depth + const levels: ChainStep[][] = []; + for (let d = 0; d <= maxDepth; d++) { + levels.push( + steps + .filter((s) => depths.get(s.id) === d) + .sort((a, b) => a.orderIndex - b.orderIndex) + ); + } + + // Build position map for connectors + const stepPositions = new Map(); + levels.forEach((level, li) => { + level.forEach((step, si) => { + stepPositions.set(step.id, { level: li, index: si }); + }); + }); + + return ( +
+ {levels.map((level, li) => ( +
+ {li > 0 && ( +
+
+
+ )} + {level.map((step) => { + const borderColor = + statusBorderColors[step.status] || "border-[#555]"; + const dotColor = statusDotColors[step.status] || "bg-[#555]"; + const summary = step.contractSummary; + const hasContract = !!step.contractId; + + return ( +
{ + if (hasContract) navigate(`/contracts/${step.contractId}`); + }} + title={hasContract ? "View contract" : undefined} + > +
+
+ + {step.name} + + {hasContract && ( + + → + + )} +
+ {summary && ( + <> +
+ +
+
+ {summary.tasksDone}/{summary.taskCount} tasks + {summary.tasksRunning > 0 && ( + + {summary.tasksRunning} running + + )} + {summary.tasksFailed > 0 && ( + + {summary.tasksFailed} failed + + )} +
+ + )} +
+ ); + })} +
+ ))} +
+ ); +} -- cgit v1.2.3