From 03dd917e8ee81784159b7de377f2161a3aebb896 Mon Sep 17 00:00:00 2001 From: Severin Ibarluzea Date: Mon, 23 Mar 2026 13:29:17 -0700 Subject: [PATCH] Show active sub-solver trees in pipeline stages --- lib/react/PipelineStagesTable.tsx | 116 ++++++++++++++++++++++++------ tests/PipelineStagesTable.test.ts | 46 ++++++++++++ 2 files changed, 140 insertions(+), 22 deletions(-) create mode 100644 tests/PipelineStagesTable.test.ts diff --git a/lib/react/PipelineStagesTable.tsx b/lib/react/PipelineStagesTable.tsx index 1cf8b45..a2421f4 100644 --- a/lib/react/PipelineStagesTable.tsx +++ b/lib/react/PipelineStagesTable.tsx @@ -39,6 +39,41 @@ interface StageInfo { solverInstance: BaseSolver | null } +interface SolverTreeNode { + id: string + name: string + status: StageStatus + children: SolverTreeNode[] +} + +const getSolverTreeStatus = (solver: BaseSolver): StageStatus => { + if (solver.failed) return "Failed" + if (solver.solved) return "Completed" + if (solver.iterations > 0 || solver.activeSubSolver) return "In Progress" + return "Not Started" +} + +export const getActiveSubSolverTree = ( + solver: BaseSolver, + path = solver.getSolverName(), +): SolverTreeNode[] => { + if (!solver.activeSubSolver) { + return [] + } + + const childSolver = solver.activeSubSolver + const childPath = `${path}.${childSolver.getSolverName()}` + + return [ + { + id: childPath, + name: childSolver.getSolverName(), + status: getSolverTreeStatus(childSolver), + children: getActiveSubSolverTree(childSolver, childPath), + }, + ] +} + const getStageStatus = ( solver: BasePipelineSolver, stepIndex: number, @@ -253,12 +288,54 @@ const ChevronIcon = ({ expanded }: { expanded: boolean }) => ( > ) +const SkipIcon = () => ( + + + + +) + +const SolverTree = ({ + nodes, + depth = 0, +}: { + nodes: SolverTreeNode[] + depth?: number +}) => { + if (nodes.length === 0) { + return null + } + + return ( +
+ {nodes.map((node) => ( +
+
+ + {node.name} + +
+ +
+ ))} +
+ ) +} + /** Find the deepest activeSubSolver by traversing the chain */ const getDeepestActiveSubSolver = (solver: BaseSolver): BaseSolver => { let current = solver @@ -409,6 +486,9 @@ export const PipelineStagesTable = ({ {stages.map((stage) => { const isPipeline = isPipelineSolver(stage.solverInstance) const isExpanded = expandedStages.has(stage.name) + const activeSubSolverTree = stage.solverInstance + ? getActiveSubSolverTree(stage.solverInstance, stage.name) + : [] return ( @@ -417,23 +497,23 @@ export const PipelineStagesTable = ({ stage.status === "In Progress" ? "bg-yellow-50" : "" }`} > - +
{isPipeline ? ( ) : ( - + )} - + {String(stage.index + 1).padStart(2, "0")} - - {stage.name} - +
+ + {stage.name} + + +
diff --git a/tests/PipelineStagesTable.test.ts b/tests/PipelineStagesTable.test.ts new file mode 100644 index 0000000..7a2cfd6 --- /dev/null +++ b/tests/PipelineStagesTable.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from "bun:test" +import { getActiveSubSolverTree } from "../lib/react/PipelineStagesTable" +import { BaseSolver } from "../lib/BaseSolver" + +class LeafSolver extends BaseSolver { + override _step() {} +} + +class NestedSolver extends BaseSolver { + constructor(child?: BaseSolver) { + super() + this.activeSubSolver = child + } + + override _step() {} +} + +test("getActiveSubSolverTree returns the full nested active sub-solver chain", () => { + const leaf = new LeafSolver() + leaf.iterations = 2 + + const middle = new NestedSolver(leaf) + middle.solved = true + + const root = new NestedSolver(middle) + + expect(getActiveSubSolverTree(root, "stageOne")).toEqual([ + { + id: "stageOne.NestedSolver", + name: "NestedSolver", + status: "Completed", + children: [ + { + id: "stageOne.NestedSolver.LeafSolver", + name: "LeafSolver", + status: "In Progress", + children: [], + }, + ], + }, + ]) +}) + +test("getActiveSubSolverTree returns an empty list when no active sub-solver exists", () => { + expect(getActiveSubSolverTree(new LeafSolver(), "stageTwo")).toEqual([]) +})