fix: ENDPOINT node execution state, badge position, and edge traversal
- Synthesize COMPLETED state for ENDPOINT nodes when overlay is active (endpoints are route entry points, not in the processor execution tree) - Move status badge (check/error) inside the card (top-right, below top bar) to avoid collision with ConfigBadge (TRACE/TAP) badges - Include ENDPOINT nodes in edge traversal check so the edge from endpoint to first processor renders as green/traversed Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -129,16 +129,16 @@ export function DiagramNode({
|
|||||||
{/* Config badges */}
|
{/* Config badges */}
|
||||||
{config && <ConfigBadge nodeWidth={w} config={config} />}
|
{config && <ConfigBadge nodeWidth={w} config={config} />}
|
||||||
|
|
||||||
{/* Execution overlay: status badge at top-right */}
|
{/* Execution overlay: status badge inside card, top-right corner */}
|
||||||
{isCompleted && (
|
{isCompleted && (
|
||||||
<>
|
<>
|
||||||
<circle cx={w - 8} cy={-8} r={8} fill="#3D7C47" />
|
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="#3D7C47" />
|
||||||
<text
|
<text
|
||||||
x={w - 8}
|
x={w - 10}
|
||||||
y={-4}
|
y={TOP_BAR_HEIGHT + 11}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fill="white"
|
fill="white"
|
||||||
fontSize={11}
|
fontSize={9}
|
||||||
fontWeight={700}
|
fontWeight={700}
|
||||||
>
|
>
|
||||||
✓
|
✓
|
||||||
@@ -147,13 +147,13 @@ export function DiagramNode({
|
|||||||
)}
|
)}
|
||||||
{isFailed && (
|
{isFailed && (
|
||||||
<>
|
<>
|
||||||
<circle cx={w - 8} cy={-8} r={8} fill="#C0392B" />
|
<circle cx={w - 10} cy={TOP_BAR_HEIGHT + 8} r={6} fill="#C0392B" />
|
||||||
<text
|
<text
|
||||||
x={w - 8}
|
x={w - 10}
|
||||||
y={-4}
|
y={TOP_BAR_HEIGHT + 11}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
fill="white"
|
fill="white"
|
||||||
fontSize={12}
|
fontSize={9}
|
||||||
fontWeight={700}
|
fontWeight={700}
|
||||||
>
|
>
|
||||||
!
|
!
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import type { ProcessDiagramProps } from './types';
|
import type { ProcessDiagramProps } from './types';
|
||||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||||
import { useDiagramData } from './useDiagramData';
|
import { useDiagramData } from './useDiagramData';
|
||||||
@@ -58,6 +58,7 @@ export function ProcessDiagram({
|
|||||||
onIterationChange,
|
onIterationChange,
|
||||||
}: ProcessDiagramProps) {
|
}: ProcessDiagramProps) {
|
||||||
const overlayActive = !!executionOverlay;
|
const overlayActive = !!executionOverlay;
|
||||||
|
|
||||||
// Route stack for drill-down navigation
|
// Route stack for drill-down navigation
|
||||||
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
||||||
|
|
||||||
@@ -72,6 +73,19 @@ export function ProcessDiagram({
|
|||||||
application, currentRouteId, direction, diagramLayout,
|
application, currentRouteId, direction, diagramLayout,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Collect ENDPOINT node IDs — these are always "traversed" when overlay is active
|
||||||
|
// because the endpoint is the route entry point (not in the processor execution tree).
|
||||||
|
const endpointNodeIds = useMemo(() => {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
if (!overlayActive || !sections.length) return ids;
|
||||||
|
for (const section of sections) {
|
||||||
|
for (const node of section.nodes) {
|
||||||
|
if (node.type === 'ENDPOINT' && node.id) ids.add(node.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}, [overlayActive, sections]);
|
||||||
|
|
||||||
const zoom = useZoomPan();
|
const zoom = useZoomPan();
|
||||||
const toolbar = useToolbarHover();
|
const toolbar = useToolbarHover();
|
||||||
|
|
||||||
@@ -85,6 +99,23 @@ export function ProcessDiagram({
|
|||||||
}
|
}
|
||||||
}, [totalWidth, totalHeight, currentRouteId]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [totalWidth, totalHeight, currentRouteId]); // 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).
|
||||||
|
const getNodeExecutionState = useCallback(
|
||||||
|
(nodeId: string | undefined, nodeType: string | undefined) => {
|
||||||
|
if (!nodeId || !executionOverlay) return undefined;
|
||||||
|
const state = executionOverlay.get(nodeId);
|
||||||
|
if (state) return state;
|
||||||
|
// Synthesize COMPLETED for ENDPOINT nodes when overlay is active
|
||||||
|
if (nodeType === 'ENDPOINT' && executionOverlay.size > 0) {
|
||||||
|
return { status: 'COMPLETED' as const, durationMs: 0, hasTraceData: false };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
[executionOverlay],
|
||||||
|
);
|
||||||
|
|
||||||
const handleNodeClick = useCallback(
|
const handleNodeClick = useCallback(
|
||||||
(nodeId: string) => { onNodeSelect?.(nodeId); },
|
(nodeId: string) => { onNodeSelect?.(nodeId); },
|
||||||
[onNodeSelect],
|
[onNodeSelect],
|
||||||
@@ -229,8 +260,10 @@ export function ProcessDiagram({
|
|||||||
{/* Main section top-level edges (not inside compounds) */}
|
{/* Main section top-level edges (not inside compounds) */}
|
||||||
<g className="edges">
|
<g className="edges">
|
||||||
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => {
|
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => {
|
||||||
|
const sourceHasState = executionOverlay?.has(edge.sourceId) || endpointNodeIds.has(edge.sourceId);
|
||||||
|
const targetHasState = executionOverlay?.has(edge.targetId) || endpointNodeIds.has(edge.targetId);
|
||||||
const isTraversed = executionOverlay
|
const isTraversed = executionOverlay
|
||||||
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
? (!!sourceHasState && !!targetHasState)
|
||||||
: undefined;
|
: undefined;
|
||||||
return (
|
return (
|
||||||
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
|
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
|
||||||
@@ -268,7 +301,7 @@ export function ProcessDiagram({
|
|||||||
isHovered={toolbar.hoveredNodeId === node.id}
|
isHovered={toolbar.hoveredNodeId === node.id}
|
||||||
isSelected={selectedNodeId === node.id}
|
isSelected={selectedNodeId === node.id}
|
||||||
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
config={node.id ? nodeConfigs?.get(node.id) : undefined}
|
||||||
executionState={executionOverlay?.get(node.id ?? '')}
|
executionState={getNodeExecutionState(node.id, node.type)}
|
||||||
overlayActive={overlayActive}
|
overlayActive={overlayActive}
|
||||||
onClick={() => node.id && handleNodeClick(node.id)}
|
onClick={() => node.id && handleNodeClick(node.id)}
|
||||||
onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)}
|
onDoubleClick={() => node.id && handleNodeDoubleClick(node.id)}
|
||||||
|
|||||||
Reference in New Issue
Block a user