summaryrefslogtreecommitdiff
path: root/makima/frontend/src/components/directives/DirectiveDAG.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src/components/directives/DirectiveDAG.tsx')
-rw-r--r--makima/frontend/src/components/directives/DirectiveDAG.tsx87
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>
+ );
+}