feat: add traversed/not-traversed visual states to DiagramEdge
Add green solid edges for traversed paths and dashed gray for not-traversed when execution overlay is active. Includes green arrowhead marker and overlay threading through CompoundNode and ErrorSection. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
|
import type { DiagramNode as DiagramNodeType, DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams';
|
||||||
import type { NodeConfig } from './types';
|
import type { NodeConfig } from './types';
|
||||||
|
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||||
import { colorForType, isCompoundType } from './node-colors';
|
import { colorForType, isCompoundType } from './node-colors';
|
||||||
import { DiagramNode } from './DiagramNode';
|
import { DiagramNode } from './DiagramNode';
|
||||||
import { DiagramEdge } from './DiagramEdge';
|
import { DiagramEdge } from './DiagramEdge';
|
||||||
@@ -17,6 +18,8 @@ interface CompoundNodeProps {
|
|||||||
selectedNodeId?: string;
|
selectedNodeId?: string;
|
||||||
hoveredNodeId: string | null;
|
hoveredNodeId: string | null;
|
||||||
nodeConfigs?: Map<string, NodeConfig>;
|
nodeConfigs?: Map<string, NodeConfig>;
|
||||||
|
/** Execution overlay for edge traversal coloring */
|
||||||
|
executionOverlay?: Map<string, NodeExecutionState>;
|
||||||
onNodeClick: (nodeId: string) => void;
|
onNodeClick: (nodeId: string) => void;
|
||||||
onNodeDoubleClick?: (nodeId: string) => void;
|
onNodeDoubleClick?: (nodeId: string) => void;
|
||||||
onNodeEnter: (nodeId: string) => void;
|
onNodeEnter: (nodeId: string) => void;
|
||||||
@@ -25,7 +28,7 @@ interface CompoundNodeProps {
|
|||||||
|
|
||||||
export function CompoundNode({
|
export function CompoundNode({
|
||||||
node, edges, parentX = 0, parentY = 0,
|
node, edges, parentX = 0, parentY = 0,
|
||||||
selectedNodeId, hoveredNodeId, nodeConfigs,
|
selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||||
}: CompoundNodeProps) {
|
}: CompoundNodeProps) {
|
||||||
const x = (node.x ?? 0) - parentX;
|
const x = (node.x ?? 0) - parentX;
|
||||||
@@ -78,15 +81,21 @@ export function CompoundNode({
|
|||||||
|
|
||||||
{/* Internal edges (rendered after background, before children) */}
|
{/* Internal edges (rendered after background, before children) */}
|
||||||
<g className="edges">
|
<g className="edges">
|
||||||
{internalEdges.map((edge, i) => (
|
{internalEdges.map((edge, i) => {
|
||||||
|
const isTraversed = executionOverlay
|
||||||
|
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
||||||
|
: undefined;
|
||||||
|
return (
|
||||||
<DiagramEdge
|
<DiagramEdge
|
||||||
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
||||||
edge={{
|
edge={{
|
||||||
...edge,
|
...edge,
|
||||||
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
||||||
}}
|
}}
|
||||||
|
traversed={isTraversed}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Children — recurse into compound children, render leaves as DiagramNode */}
|
{/* Children — recurse into compound children, render leaves as DiagramNode */}
|
||||||
@@ -102,6 +111,7 @@ export function CompoundNode({
|
|||||||
selectedNodeId={selectedNodeId}
|
selectedNodeId={selectedNodeId}
|
||||||
hoveredNodeId={hoveredNodeId}
|
hoveredNodeId={hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
|
executionOverlay={executionOverlay}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeEnter={onNodeEnter}
|
onNodeEnter={onNodeEnter}
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ import type { DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'
|
|||||||
interface DiagramEdgeProps {
|
interface DiagramEdgeProps {
|
||||||
edge: DiagramEdgeType;
|
edge: DiagramEdgeType;
|
||||||
offsetY?: number;
|
offsetY?: number;
|
||||||
|
/** undefined = no overlay (default gray solid), true = traversed (green solid), false = not traversed (gray dashed) */
|
||||||
|
traversed?: boolean | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) {
|
export function DiagramEdge({ edge, offsetY = 0, traversed }: DiagramEdgeProps) {
|
||||||
const pts = edge.points;
|
const pts = edge.points;
|
||||||
if (!pts || pts.length < 2) return null;
|
if (!pts || pts.length < 2) return null;
|
||||||
|
|
||||||
@@ -29,9 +31,10 @@ export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) {
|
|||||||
<path
|
<path
|
||||||
d={d}
|
d={d}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#9CA3AF"
|
stroke={traversed === true ? '#3D7C47' : '#9CA3AF'}
|
||||||
strokeWidth={1.5}
|
strokeWidth={traversed === true ? 1.5 : traversed === false ? 1 : 1.5}
|
||||||
markerEnd="url(#arrowhead)"
|
strokeDasharray={traversed === false ? '4,3' : undefined}
|
||||||
|
markerEnd={traversed === true ? 'url(#arrowhead-green)' : traversed === false ? undefined : 'url(#arrowhead)'}
|
||||||
/>
|
/>
|
||||||
{edge.label && pts.length >= 2 && (
|
{edge.label && pts.length >= 2 && (
|
||||||
<text
|
<text
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { DiagramSection } from './types';
|
import type { DiagramSection } from './types';
|
||||||
import type { NodeConfig } from './types';
|
import type { NodeConfig } from './types';
|
||||||
|
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||||
import { DiagramEdge } from './DiagramEdge';
|
import { DiagramEdge } from './DiagramEdge';
|
||||||
import { DiagramNode } from './DiagramNode';
|
import { DiagramNode } from './DiagramNode';
|
||||||
@@ -16,6 +17,8 @@ interface ErrorSectionProps {
|
|||||||
selectedNodeId?: string;
|
selectedNodeId?: string;
|
||||||
hoveredNodeId: string | null;
|
hoveredNodeId: string | null;
|
||||||
nodeConfigs?: Map<string, NodeConfig>;
|
nodeConfigs?: Map<string, NodeConfig>;
|
||||||
|
/** Execution overlay for edge traversal coloring */
|
||||||
|
executionOverlay?: Map<string, NodeExecutionState>;
|
||||||
onNodeClick: (nodeId: string) => void;
|
onNodeClick: (nodeId: string) => void;
|
||||||
onNodeDoubleClick?: (nodeId: string) => void;
|
onNodeDoubleClick?: (nodeId: string) => void;
|
||||||
onNodeEnter: (nodeId: string) => void;
|
onNodeEnter: (nodeId: string) => void;
|
||||||
@@ -28,7 +31,7 @@ const VARIANT_COLORS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ErrorSection({
|
export function ErrorSection({
|
||||||
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs,
|
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||||
}: ErrorSectionProps) {
|
}: ErrorSectionProps) {
|
||||||
const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error;
|
const color = VARIANT_COLORS[section.variant ?? 'error'] ?? VARIANT_COLORS.error;
|
||||||
@@ -82,9 +85,14 @@ export function ErrorSection({
|
|||||||
<g transform={`translate(${CONTENT_PADDING_LEFT}, ${CONTENT_PADDING_Y})`}>
|
<g transform={`translate(${CONTENT_PADDING_LEFT}, ${CONTENT_PADDING_Y})`}>
|
||||||
{/* Edges */}
|
{/* Edges */}
|
||||||
<g className="edges">
|
<g className="edges">
|
||||||
{section.edges.map((edge, i) => (
|
{section.edges.map((edge, i) => {
|
||||||
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
|
const isTraversed = executionOverlay
|
||||||
))}
|
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
||||||
|
: undefined;
|
||||||
|
return (
|
||||||
|
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Nodes */}
|
{/* Nodes */}
|
||||||
@@ -99,6 +107,7 @@ export function ErrorSection({
|
|||||||
selectedNodeId={selectedNodeId}
|
selectedNodeId={selectedNodeId}
|
||||||
hoveredNodeId={hoveredNodeId}
|
hoveredNodeId={hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
|
executionOverlay={executionOverlay}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
onNodeDoubleClick={onNodeDoubleClick}
|
onNodeDoubleClick={onNodeDoubleClick}
|
||||||
onNodeEnter={onNodeEnter}
|
onNodeEnter={onNodeEnter}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export function ProcessDiagram({
|
|||||||
knownRouteIds,
|
knownRouteIds,
|
||||||
className,
|
className,
|
||||||
diagramLayout,
|
diagramLayout,
|
||||||
|
executionOverlay,
|
||||||
}: ProcessDiagramProps) {
|
}: ProcessDiagramProps) {
|
||||||
// Route stack for drill-down navigation
|
// Route stack for drill-down navigation
|
||||||
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
||||||
@@ -209,14 +210,29 @@ export function ProcessDiagram({
|
|||||||
>
|
>
|
||||||
<polygon points="0 0, 8 3, 0 6" fill="#9CA3AF" />
|
<polygon points="0 0, 8 3, 0 6" fill="#9CA3AF" />
|
||||||
</marker>
|
</marker>
|
||||||
|
<marker
|
||||||
|
id="arrowhead-green"
|
||||||
|
markerWidth="8"
|
||||||
|
markerHeight="6"
|
||||||
|
refX="7"
|
||||||
|
refY="3"
|
||||||
|
orient="auto"
|
||||||
|
>
|
||||||
|
<polygon points="0 0, 8 3, 0 6" fill="#3D7C47" />
|
||||||
|
</marker>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
|
<g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
|
||||||
{/* 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) => {
|
||||||
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
|
const isTraversed = executionOverlay
|
||||||
))}
|
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
||||||
|
: undefined;
|
||||||
|
return (
|
||||||
|
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} traversed={isTraversed} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
{/* Main section nodes */}
|
{/* Main section nodes */}
|
||||||
@@ -231,6 +247,7 @@ export function ProcessDiagram({
|
|||||||
selectedNodeId={selectedNodeId}
|
selectedNodeId={selectedNodeId}
|
||||||
hoveredNodeId={toolbar.hoveredNodeId}
|
hoveredNodeId={toolbar.hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
|
executionOverlay={executionOverlay}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeEnter={toolbar.onNodeEnter}
|
onNodeEnter={toolbar.onNodeEnter}
|
||||||
@@ -265,6 +282,7 @@ export function ProcessDiagram({
|
|||||||
selectedNodeId={selectedNodeId}
|
selectedNodeId={selectedNodeId}
|
||||||
hoveredNodeId={toolbar.hoveredNodeId}
|
hoveredNodeId={toolbar.hoveredNodeId}
|
||||||
nodeConfigs={nodeConfigs}
|
nodeConfigs={nodeConfigs}
|
||||||
|
executionOverlay={executionOverlay}
|
||||||
onNodeClick={handleNodeClick}
|
onNodeClick={handleNodeClick}
|
||||||
onNodeDoubleClick={handleNodeDoubleClick}
|
onNodeDoubleClick={handleNodeDoubleClick}
|
||||||
onNodeEnter={toolbar.onNodeEnter}
|
onNodeEnter={toolbar.onNodeEnter}
|
||||||
|
|||||||
Reference in New Issue
Block a user