diff options
Diffstat (limited to 'makima/frontend/src/components/directives/DirectiveDAG.tsx')
| -rw-r--r-- | makima/frontend/src/components/directives/DirectiveDAG.tsx | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/makima/frontend/src/components/directives/DirectiveDAG.tsx b/makima/frontend/src/components/directives/DirectiveDAG.tsx new file mode 100644 index 0000000..f288a0d --- /dev/null +++ b/makima/frontend/src/components/directives/DirectiveDAG.tsx @@ -0,0 +1,87 @@ +import { useMemo } from "react"; +import type { DirectiveStep } from "../../lib/api"; +import { StepNode } from "./StepNode"; + +interface DirectiveDAGProps { + steps: DirectiveStep[]; + onComplete?: (stepId: string) => void; + onFail?: (stepId: string) => void; + onSkip?: (stepId: string) => void; +} + +interface Layer { + steps: DirectiveStep[]; +} + +function topoSort(steps: DirectiveStep[]): Layer[] { + if (steps.length === 0) return []; + + const stepMap = new Map(steps.map((s) => [s.id, s])); + const assigned = new Set<string>(); + const layers: Layer[] = []; + + // Iteratively find steps whose dependencies are all assigned + let remaining = [...steps]; + while (remaining.length > 0) { + const layer: DirectiveStep[] = []; + for (const step of remaining) { + const depsResolved = step.dependsOn.every( + (depId) => assigned.has(depId) || !stepMap.has(depId) + ); + if (depsResolved) { + layer.push(step); + } + } + + if (layer.length === 0) { + // Cycle detected or orphaned — push all remaining + layers.push({ steps: remaining }); + break; + } + + for (const s of layer) { + assigned.add(s.id); + } + layers.push({ steps: layer.sort((a, b) => a.orderIndex - b.orderIndex) }); + remaining = remaining.filter((s) => !assigned.has(s.id)); + } + + return layers; +} + +export function DirectiveDAG({ steps, onComplete, onFail, onSkip }: DirectiveDAGProps) { + const layers = useMemo(() => topoSort(steps), [steps]); + + if (steps.length === 0) { + return ( + <div className="text-center py-8 text-[#7788aa] font-mono text-sm"> + No steps yet. Add steps to build the DAG. + </div> + ); + } + + return ( + <div className="flex flex-col gap-4 items-center py-4"> + {layers.map((layer, layerIdx) => ( + <div key={layerIdx}> + {layerIdx > 0 && ( + <div className="flex justify-center py-1"> + <div className="w-px h-4 bg-[#2a3a5a]" /> + </div> + )} + <div className="flex flex-wrap gap-3 justify-center"> + {layer.steps.map((step) => ( + <StepNode + key={step.id} + step={step} + onComplete={onComplete ? () => onComplete(step.id) : undefined} + onFail={onFail ? () => onFail(step.id) : undefined} + onSkip={onSkip ? () => onSkip(step.id) : undefined} + /> + ))} + </div> + </div> + ))} + </div> + ); +} |
