feat: Jump to Error centers the failed node in the viewport

Added centerOnNodeId prop to ProcessDiagram. When set, the diagram
pans to center the specified node in the viewport. Jump to Error
now selects the failed processor AND centers the viewport on it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 19:51:00 +01:00
parent 25e23c0b87
commit d7166b6d0a
3 changed files with 44 additions and 2 deletions

View File

@@ -80,8 +80,9 @@ export function ExecutionDiagram({
// 4. Compute overlay
const overlay = useExecutionOverlay(detail?.processors, iterationState);
// 5. Manage selection
// 5. Manage selection + center-on-node
const [selectedProcessorId, setSelectedProcessorId] = useState<string>('');
const [centerOnNodeId, setCenterOnNodeId] = useState<string>('');
// 6. Resizable splitter state
const [splitPercent, setSplitPercent] = useState(60);
@@ -105,12 +106,15 @@ export function ExecutionDiagram({
document.addEventListener('pointerup', onUp);
}, []);
// Jump to error: find first FAILED processor and select it
// Jump to error: find first FAILED processor, select it, and center the viewport
const handleJumpToError = useCallback(() => {
if (!detail?.processors) return;
const failed = findFailedProcessor(detail.processors);
if (failed?.processorId) {
setSelectedProcessorId(failed.processorId);
// Use a unique value to re-trigger centering even if the same node
setCenterOnNodeId('');
requestAnimationFrame(() => setCenterOnNodeId(failed.processorId));
}
}, [detail?.processors]);
@@ -187,6 +191,7 @@ export function ExecutionDiagram({
executionOverlay={overlay}
iterationState={iterationState}
onIterationChange={setIteration}
centerOnNodeId={centerOnNodeId}
/>
</div>

View File

@@ -56,6 +56,7 @@ export function ProcessDiagram({
executionOverlay,
iterationState,
onIterationChange,
centerOnNodeId,
}: ProcessDiagramProps) {
// Route stack for drill-down navigation
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
@@ -106,6 +107,33 @@ export function ProcessDiagram({
}
}, [totalWidth, totalHeight, currentRouteId]); // eslint-disable-line react-hooks/exhaustive-deps
// Center on a specific node when centerOnNodeId changes
useEffect(() => {
if (!centerOnNodeId || sections.length === 0) return;
const node = findNodeById(sections, centerOnNodeId);
if (!node) return;
const container = zoom.containerRef.current;
if (!container) return;
// Compute the node center in diagram coordinates
const nodeX = (node.x ?? 0) + (node.width ?? 160) / 2;
const nodeY = (node.y ?? 0) + (node.height ?? 40) / 2;
// Find which section the node is in to add its offsetY
let sectionOffsetY = 0;
for (const section of sections) {
const found = findNodeInSection(section.nodes, centerOnNodeId);
if (found) { sectionOffsetY = section.offsetY; break; }
}
const adjustedY = nodeY + sectionOffsetY;
// Pan so the node center is at the viewport center
const cw = container.clientWidth;
const ch = container.clientHeight;
const scale = zoom.state.scale;
zoom.panTo(
cw / 2 - nodeX * scale,
ch / 2 - adjustedY * scale,
);
}, [centerOnNodeId]); // eslint-disable-line react-hooks/exhaustive-deps
// Resolve execution state for a node. ENDPOINT nodes (the route's "from:")
// don't appear in the processor execution tree, but should be marked as
// COMPLETED when the route executed (i.e., overlay has any entries).
@@ -415,6 +443,13 @@ function findInChildren(
return undefined;
}
function findNodeInSection(
nodes: DiagramNodeType[],
nodeId: string,
): boolean {
return !!findInChildren(nodes, nodeId) || nodes.some(n => n.id === nodeId);
}
function topLevelEdge(
edge: import('../../api/queries/diagrams').DiagramEdge,
nodes: DiagramNodeType[],

View File

@@ -35,4 +35,6 @@ export interface ProcessDiagramProps {
iterationState?: Map<string, IterationInfo>;
/** Called when user changes iteration on a compound stepper */
onIterationChange?: (compoundNodeId: string, iterationIndex: number) => void;
/** When set, the diagram pans to center this node in the viewport */
centerOnNodeId?: string;
}