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 { NodeConfig } from './types';
|
||||
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||
import { colorForType, isCompoundType } from './node-colors';
|
||||
import { DiagramNode } from './DiagramNode';
|
||||
import { DiagramEdge } from './DiagramEdge';
|
||||
@@ -17,6 +18,8 @@ interface CompoundNodeProps {
|
||||
selectedNodeId?: string;
|
||||
hoveredNodeId: string | null;
|
||||
nodeConfigs?: Map<string, NodeConfig>;
|
||||
/** Execution overlay for edge traversal coloring */
|
||||
executionOverlay?: Map<string, NodeExecutionState>;
|
||||
onNodeClick: (nodeId: string) => void;
|
||||
onNodeDoubleClick?: (nodeId: string) => void;
|
||||
onNodeEnter: (nodeId: string) => void;
|
||||
@@ -25,7 +28,7 @@ interface CompoundNodeProps {
|
||||
|
||||
export function CompoundNode({
|
||||
node, edges, parentX = 0, parentY = 0,
|
||||
selectedNodeId, hoveredNodeId, nodeConfigs,
|
||||
selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||
}: CompoundNodeProps) {
|
||||
const x = (node.x ?? 0) - parentX;
|
||||
@@ -78,15 +81,21 @@ export function CompoundNode({
|
||||
|
||||
{/* Internal edges (rendered after background, before children) */}
|
||||
<g className="edges">
|
||||
{internalEdges.map((edge, i) => (
|
||||
<DiagramEdge
|
||||
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
||||
edge={{
|
||||
...edge,
|
||||
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
{internalEdges.map((edge, i) => {
|
||||
const isTraversed = executionOverlay
|
||||
? (executionOverlay.has(edge.sourceId) && executionOverlay.has(edge.targetId))
|
||||
: undefined;
|
||||
return (
|
||||
<DiagramEdge
|
||||
key={`${edge.sourceId}-${edge.targetId}-${i}`}
|
||||
edge={{
|
||||
...edge,
|
||||
points: edge.points.map(p => [p[0] - absX, p[1] - absY]),
|
||||
}}
|
||||
traversed={isTraversed}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
|
||||
{/* Children — recurse into compound children, render leaves as DiagramNode */}
|
||||
@@ -102,6 +111,7 @@ export function CompoundNode({
|
||||
selectedNodeId={selectedNodeId}
|
||||
hoveredNodeId={hoveredNodeId}
|
||||
nodeConfigs={nodeConfigs}
|
||||
executionOverlay={executionOverlay}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeEnter={onNodeEnter}
|
||||
|
||||
@@ -3,9 +3,11 @@ import type { DiagramEdge as DiagramEdgeType } from '../../api/queries/diagrams'
|
||||
interface DiagramEdgeProps {
|
||||
edge: DiagramEdgeType;
|
||||
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;
|
||||
if (!pts || pts.length < 2) return null;
|
||||
|
||||
@@ -29,9 +31,10 @@ export function DiagramEdge({ edge, offsetY = 0 }: DiagramEdgeProps) {
|
||||
<path
|
||||
d={d}
|
||||
fill="none"
|
||||
stroke="#9CA3AF"
|
||||
strokeWidth={1.5}
|
||||
markerEnd="url(#arrowhead)"
|
||||
stroke={traversed === true ? '#3D7C47' : '#9CA3AF'}
|
||||
strokeWidth={traversed === true ? 1.5 : traversed === false ? 1 : 1.5}
|
||||
strokeDasharray={traversed === false ? '4,3' : undefined}
|
||||
markerEnd={traversed === true ? 'url(#arrowhead-green)' : traversed === false ? undefined : 'url(#arrowhead)'}
|
||||
/>
|
||||
{edge.label && pts.length >= 2 && (
|
||||
<text
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { DiagramSection } from './types';
|
||||
import type { NodeConfig } from './types';
|
||||
import type { NodeExecutionState } from '../ExecutionDiagram/types';
|
||||
import type { DiagramNode as DiagramNodeType } from '../../api/queries/diagrams';
|
||||
import { DiagramEdge } from './DiagramEdge';
|
||||
import { DiagramNode } from './DiagramNode';
|
||||
@@ -16,6 +17,8 @@ interface ErrorSectionProps {
|
||||
selectedNodeId?: string;
|
||||
hoveredNodeId: string | null;
|
||||
nodeConfigs?: Map<string, NodeConfig>;
|
||||
/** Execution overlay for edge traversal coloring */
|
||||
executionOverlay?: Map<string, NodeExecutionState>;
|
||||
onNodeClick: (nodeId: string) => void;
|
||||
onNodeDoubleClick?: (nodeId: string) => void;
|
||||
onNodeEnter: (nodeId: string) => void;
|
||||
@@ -28,7 +31,7 @@ const VARIANT_COLORS: Record<string, string> = {
|
||||
};
|
||||
|
||||
export function ErrorSection({
|
||||
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs,
|
||||
section, totalWidth, selectedNodeId, hoveredNodeId, nodeConfigs, executionOverlay,
|
||||
onNodeClick, onNodeDoubleClick, onNodeEnter, onNodeLeave,
|
||||
}: ErrorSectionProps) {
|
||||
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})`}>
|
||||
{/* Edges */}
|
||||
<g className="edges">
|
||||
{section.edges.map((edge, i) => (
|
||||
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
|
||||
))}
|
||||
{section.edges.map((edge, i) => {
|
||||
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>
|
||||
|
||||
{/* Nodes */}
|
||||
@@ -99,6 +107,7 @@ export function ErrorSection({
|
||||
selectedNodeId={selectedNodeId}
|
||||
hoveredNodeId={hoveredNodeId}
|
||||
nodeConfigs={nodeConfigs}
|
||||
executionOverlay={executionOverlay}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeEnter={onNodeEnter}
|
||||
|
||||
@@ -53,6 +53,7 @@ export function ProcessDiagram({
|
||||
knownRouteIds,
|
||||
className,
|
||||
diagramLayout,
|
||||
executionOverlay,
|
||||
}: ProcessDiagramProps) {
|
||||
// Route stack for drill-down navigation
|
||||
const [routeStack, setRouteStack] = useState<string[]>([routeId]);
|
||||
@@ -209,14 +210,29 @@ export function ProcessDiagram({
|
||||
>
|
||||
<polygon points="0 0, 8 3, 0 6" fill="#9CA3AF" />
|
||||
</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>
|
||||
|
||||
<g style={{ transform: zoom.transform, transformOrigin: '0 0' }}>
|
||||
{/* Main section top-level edges (not inside compounds) */}
|
||||
<g className="edges">
|
||||
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => (
|
||||
<DiagramEdge key={`${edge.sourceId}-${edge.targetId}-${i}`} edge={edge} />
|
||||
))}
|
||||
{mainSection.edges.filter(e => topLevelEdge(e, mainSection.nodes)).map((edge, i) => {
|
||||
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>
|
||||
|
||||
{/* Main section nodes */}
|
||||
@@ -231,6 +247,7 @@ export function ProcessDiagram({
|
||||
selectedNodeId={selectedNodeId}
|
||||
hoveredNodeId={toolbar.hoveredNodeId}
|
||||
nodeConfigs={nodeConfigs}
|
||||
executionOverlay={executionOverlay}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onNodeEnter={toolbar.onNodeEnter}
|
||||
@@ -265,6 +282,7 @@ export function ProcessDiagram({
|
||||
selectedNodeId={selectedNodeId}
|
||||
hoveredNodeId={toolbar.hoveredNodeId}
|
||||
nodeConfigs={nodeConfigs}
|
||||
executionOverlay={executionOverlay}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
onNodeEnter={toolbar.onNodeEnter}
|
||||
|
||||
Reference in New Issue
Block a user