feat: color minimap nodes by execution overlay state
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 57s
CI / docker (push) Successful in 53s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s

Minimap reflects execution overlay: green for completed, red for failed,
grey for skipped nodes. ENDPOINT nodes are always green when overlay is
active (route entry point, same as main diagram logic).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-28 19:52:12 +01:00
parent c6f70968a2
commit f12f5f3c8d
2 changed files with 29 additions and 9 deletions

View File

@@ -1,6 +1,7 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import type { DiagramSection } from './types'; import type { DiagramSection } from './types';
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams'; import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
import type { NodeExecutionState } from '../ExecutionDiagram/types';
import { colorForType } from './node-colors'; import { colorForType } from './node-colors';
import styles from './ProcessDiagram.module.css'; import styles from './ProcessDiagram.module.css';
@@ -21,13 +22,15 @@ interface MinimapProps {
containerHeight: number; containerHeight: number;
/** Called when user clicks/drags on minimap to pan */ /** Called when user clicks/drags on minimap to pan */
onPan: (translateX: number, translateY: number) => void; onPan: (translateX: number, translateY: number) => void;
/** Execution overlay for coloring nodes */
executionOverlay?: Map<string, NodeExecutionState>;
} }
export function Minimap({ export function Minimap({
sections, totalWidth, totalHeight, sections, totalWidth, totalHeight,
scale, translateX, translateY, scale, translateX, translateY,
containerWidth, containerHeight, containerWidth, containerHeight,
onPan, onPan, executionOverlay,
}: MinimapProps) { }: MinimapProps) {
const svgRef = useRef<SVGSVGElement>(null); const svgRef = useRef<SVGSVGElement>(null);
const dragging = useRef(false); const dragging = useRef(false);
@@ -116,7 +119,7 @@ export function Minimap({
{/* Render simplified nodes for each section */} {/* Render simplified nodes for each section */}
{sections.map((section, si) => ( {sections.map((section, si) => (
<g key={si} transform={`translate(0, ${section.offsetY})`}> <g key={si} transform={`translate(0, ${section.offsetY})`}>
{renderMinimapNodes(section.nodes)} {renderMinimapNodes(section.nodes, executionOverlay)}
</g> </g>
))} ))}
</g> </g>
@@ -138,27 +141,43 @@ export function Minimap({
); );
} }
function renderMinimapNodes(nodes: DiagramNodeType[]): React.ReactNode[] { function nodeColor(
node: DiagramNodeType,
overlay?: Map<string, NodeExecutionState>,
): { fill: string; opacity: number } {
if (!overlay) return { fill: colorForType(node.type), opacity: 0.7 };
const state = node.id ? overlay.get(node.id) : undefined;
if (state?.status === 'COMPLETED') return { fill: '#3D7C47', opacity: 0.85 };
if (state?.status === 'FAILED') return { fill: '#C0392B', opacity: 0.85 };
// ENDPOINT is always traversed when overlay is active (route entry point)
if (node.type === 'ENDPOINT' && overlay.size > 0) return { fill: '#3D7C47', opacity: 0.85 };
// Skipped (overlay active but node not executed)
return { fill: '#9CA3AF', opacity: 0.35 };
}
function renderMinimapNodes(
nodes: DiagramNodeType[],
overlay?: Map<string, NodeExecutionState>,
): React.ReactNode[] {
const elements: React.ReactNode[] = []; const elements: React.ReactNode[] = [];
for (const node of nodes) { for (const node of nodes) {
const x = node.x ?? 0; const x = node.x ?? 0;
const y = node.y ?? 0; const y = node.y ?? 0;
const w = node.width ?? 160; const w = node.width ?? 160;
const h = node.height ?? 40; const h = node.height ?? 40;
const color = colorForType(node.type);
if (node.children && node.children.length > 0) { if (node.children && node.children.length > 0) {
// Compound: border only const { fill, opacity } = nodeColor(node, overlay);
elements.push( elements.push(
<rect key={node.id} x={x} y={y} width={w} height={h} <rect key={node.id} x={x} y={y} width={w} height={h}
fill="none" stroke={color} strokeWidth={2} rx={2} opacity={0.6} />, fill="none" stroke={fill} strokeWidth={2} rx={2} opacity={opacity} />,
); );
elements.push(...renderMinimapNodes(node.children)); elements.push(...renderMinimapNodes(node.children, overlay));
} else { } else {
// Leaf: filled rectangle const { fill, opacity } = nodeColor(node, overlay);
elements.push( elements.push(
<rect key={node.id} x={x} y={y} width={w} height={h} <rect key={node.id} x={x} y={y} width={w} height={h}
fill={color} rx={2} opacity={0.7} />, fill={fill} rx={2} opacity={opacity} />,
); );
} }
} }

View File

@@ -409,6 +409,7 @@ export function ProcessDiagram({
containerWidth={zoom.containerRef.current?.clientWidth ?? 0} containerWidth={zoom.containerRef.current?.clientWidth ?? 0}
containerHeight={zoom.containerRef.current?.clientHeight ?? 0} containerHeight={zoom.containerRef.current?.clientHeight ?? 0}
onPan={zoom.panTo} onPan={zoom.panTo}
executionOverlay={effectiveOverlay}
/> />
<ZoomControls <ZoomControls