@principal-ai/principal-view-react 0.14.4 → 0.14.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/GraphRenderer.d.ts +30 -7
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +29 -16
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/NodeTooltip.js +1 -1
- package/dist/components/NodeTooltip.js.map +1 -1
- package/dist/contexts/TooltipPortalContext.d.ts +8 -0
- package/dist/contexts/TooltipPortalContext.d.ts.map +1 -0
- package/dist/contexts/TooltipPortalContext.js +8 -0
- package/dist/contexts/TooltipPortalContext.js.map +1 -0
- package/dist/edges/CustomEdge.d.ts +5 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -1
- package/dist/edges/CustomEdge.js +7 -3
- package/dist/edges/CustomEdge.js.map +1 -1
- package/dist/hooks/useElkLayout.d.ts +66 -0
- package/dist/hooks/useElkLayout.d.ts.map +1 -0
- package/dist/hooks/useElkLayout.js +136 -0
- package/dist/hooks/useElkLayout.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/nodes/otel/OtelSpanConventionNode.js +3 -3
- package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
- package/dist/utils/elkLayout.d.ts +92 -0
- package/dist/utils/elkLayout.d.ts.map +1 -0
- package/dist/utils/elkLayout.js +281 -0
- package/dist/utils/elkLayout.js.map +1 -0
- package/package.json +4 -3
- package/src/components/GraphRenderer.tsx +70 -13
- package/src/components/NodeTooltip.tsx +1 -1
- package/src/contexts/TooltipPortalContext.ts +8 -0
- package/src/edges/CustomEdge.tsx +13 -2
- package/src/hooks/useElkLayout.test.ts +134 -0
- package/src/hooks/useElkLayout.ts +191 -0
- package/src/index.ts +6 -0
- package/src/nodes/otel/OtelSpanConventionNode.tsx +3 -3
- package/src/stories/ElkEdgeRouting.stories.tsx +415 -0
- package/src/stories/SpanBadges.stories.tsx +840 -0
- package/src/utils/elkLayout.test.ts +240 -0
- package/src/utils/elkLayout.ts +412 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for useElkLayout hook utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect } from 'bun:test';
|
|
6
|
+
import type { Edge } from '@xyflow/react';
|
|
7
|
+
import { applyElkPathsToEdges } from './useElkLayout';
|
|
8
|
+
|
|
9
|
+
describe('useElkLayout utilities', () => {
|
|
10
|
+
describe('applyElkPathsToEdges', () => {
|
|
11
|
+
const createEdge = (id: string, source: string, target: string): Edge => ({
|
|
12
|
+
id,
|
|
13
|
+
source,
|
|
14
|
+
target,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('should return original edges when edgePaths is empty', () => {
|
|
18
|
+
const edges = [createEdge('e1', 'a', 'b')];
|
|
19
|
+
const edgePaths = new Map<string, string>();
|
|
20
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
21
|
+
|
|
22
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
23
|
+
|
|
24
|
+
expect(result).toEqual(edges);
|
|
25
|
+
expect(result[0].data?.elkPath).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('should inject elkPath into edge data', () => {
|
|
29
|
+
const edges = [createEdge('e1', 'a', 'b')];
|
|
30
|
+
const edgePaths = new Map([['e1', 'M 0 0 L 100 100']]);
|
|
31
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
32
|
+
|
|
33
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
34
|
+
|
|
35
|
+
expect(result[0].data?.elkPath).toBe('M 0 0 L 100 100');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('should inject elkLabelPosition into edge data', () => {
|
|
39
|
+
const edges = [createEdge('e1', 'a', 'b')];
|
|
40
|
+
const edgePaths = new Map([['e1', 'M 0 0 L 100 100']]);
|
|
41
|
+
const edgeLabelPositions = new Map([['e1', { x: 50, y: 50 }]]);
|
|
42
|
+
|
|
43
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
44
|
+
|
|
45
|
+
expect(result[0].data?.elkLabelPosition).toEqual({ x: 50, y: 50 });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('should preserve existing edge data', () => {
|
|
49
|
+
const edges: Edge[] = [
|
|
50
|
+
{
|
|
51
|
+
id: 'e1',
|
|
52
|
+
source: 'a',
|
|
53
|
+
target: 'b',
|
|
54
|
+
data: { customField: 'value', edgeType: 'flow' },
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const edgePaths = new Map([['e1', 'M 0 0 L 100 100']]);
|
|
58
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
59
|
+
|
|
60
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
61
|
+
|
|
62
|
+
expect(result[0].data?.customField).toBe('value');
|
|
63
|
+
expect(result[0].data?.edgeType).toBe('flow');
|
|
64
|
+
expect(result[0].data?.elkPath).toBe('M 0 0 L 100 100');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should only modify edges that have corresponding paths', () => {
|
|
68
|
+
const edges = [
|
|
69
|
+
createEdge('e1', 'a', 'b'),
|
|
70
|
+
createEdge('e2', 'b', 'c'),
|
|
71
|
+
createEdge('e3', 'c', 'd'),
|
|
72
|
+
];
|
|
73
|
+
const edgePaths = new Map([
|
|
74
|
+
['e1', 'M 0 0 L 100 0'],
|
|
75
|
+
['e3', 'M 200 0 L 300 0'],
|
|
76
|
+
// e2 has no path
|
|
77
|
+
]);
|
|
78
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
79
|
+
|
|
80
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
81
|
+
|
|
82
|
+
expect(result[0].data?.elkPath).toBe('M 0 0 L 100 0');
|
|
83
|
+
expect(result[1].data?.elkPath).toBeUndefined();
|
|
84
|
+
expect(result[2].data?.elkPath).toBe('M 200 0 L 300 0');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('should handle edges with undefined data', () => {
|
|
88
|
+
const edges: Edge[] = [
|
|
89
|
+
{ id: 'e1', source: 'a', target: 'b', data: undefined },
|
|
90
|
+
];
|
|
91
|
+
const edgePaths = new Map([['e1', 'M 0 0 L 100 100']]);
|
|
92
|
+
const edgeLabelPositions = new Map([['e1', { x: 50, y: 50 }]]);
|
|
93
|
+
|
|
94
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
95
|
+
|
|
96
|
+
expect(result[0].data?.elkPath).toBe('M 0 0 L 100 100');
|
|
97
|
+
expect(result[0].data?.elkLabelPosition).toEqual({ x: 50, y: 50 });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('should return new array without mutating original', () => {
|
|
101
|
+
const edges = [createEdge('e1', 'a', 'b')];
|
|
102
|
+
const edgePaths = new Map([['e1', 'M 0 0 L 100 100']]);
|
|
103
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
104
|
+
|
|
105
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
106
|
+
|
|
107
|
+
// Result should be a new array
|
|
108
|
+
expect(result).not.toBe(edges);
|
|
109
|
+
// Original edge should not be modified
|
|
110
|
+
expect(edges[0].data).toBeUndefined();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('should handle complex path strings', () => {
|
|
114
|
+
const edges = [createEdge('e1', 'a', 'b')];
|
|
115
|
+
const complexPath = 'M 120 30 L 142 30 Q 150 30 150 38 L 150 72 Q 150 80 158 80 L 250 80';
|
|
116
|
+
const edgePaths = new Map([['e1', complexPath]]);
|
|
117
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
118
|
+
|
|
119
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
120
|
+
|
|
121
|
+
expect(result[0].data?.elkPath).toBe(complexPath);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('should handle empty edges array', () => {
|
|
125
|
+
const edges: Edge[] = [];
|
|
126
|
+
const edgePaths = new Map([['e1', 'M 0 0 L 100 100']]);
|
|
127
|
+
const edgeLabelPositions = new Map<string, { x: number; y: number }>();
|
|
128
|
+
|
|
129
|
+
const result = applyElkPathsToEdges(edges, edgePaths, edgeLabelPositions);
|
|
130
|
+
|
|
131
|
+
expect(result).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React hook for ELK layout integration
|
|
3
|
+
*
|
|
4
|
+
* Provides automatic edge routing with circuit-board style paths.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
|
|
8
|
+
import type { Node, Edge } from '@xyflow/react';
|
|
9
|
+
import { computeElkLayout, type ElkLayoutOptions } from '../utils/elkLayout';
|
|
10
|
+
|
|
11
|
+
export interface UseElkLayoutOptions extends ElkLayoutOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Whether ELK layout is enabled
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Debounce delay in ms before recomputing layout
|
|
20
|
+
* @default 100
|
|
21
|
+
*/
|
|
22
|
+
debounceMs?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UseElkLayoutResult {
|
|
26
|
+
/** Edge paths computed by ELK, keyed by edge ID */
|
|
27
|
+
edgePaths: Map<string, string>;
|
|
28
|
+
/** Edge label positions computed by ELK, keyed by edge ID */
|
|
29
|
+
edgeLabelPositions: Map<string, { x: number; y: number }>;
|
|
30
|
+
/** Whether layout is currently being computed */
|
|
31
|
+
isLayouting: boolean;
|
|
32
|
+
/** Any error that occurred during layout */
|
|
33
|
+
error: Error | null;
|
|
34
|
+
/** Manually trigger a layout recomputation */
|
|
35
|
+
recomputeLayout: () => void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Hook for computing ELK layout for edges
|
|
40
|
+
*
|
|
41
|
+
* @param nodes - xyflow nodes
|
|
42
|
+
* @param edges - xyflow edges
|
|
43
|
+
* @param options - Layout options
|
|
44
|
+
* @returns Layout result with edge paths
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```tsx
|
|
48
|
+
* const { edgePaths, isLayouting } = useElkLayout(nodes, edges, {
|
|
49
|
+
* routingStyle: 'orthogonal',
|
|
50
|
+
* edgeSpacing: 15,
|
|
51
|
+
* });
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function useElkLayout(
|
|
55
|
+
nodes: Node[],
|
|
56
|
+
edges: Edge[],
|
|
57
|
+
options: UseElkLayoutOptions = {}
|
|
58
|
+
): UseElkLayoutResult {
|
|
59
|
+
const { enabled = true, debounceMs = 100, ...layoutOptions } = options;
|
|
60
|
+
|
|
61
|
+
const [edgePaths, setEdgePaths] = useState<Map<string, string>>(new Map());
|
|
62
|
+
const [edgeLabelPositions, setEdgeLabelPositions] = useState<Map<string, { x: number; y: number }>>(
|
|
63
|
+
new Map()
|
|
64
|
+
);
|
|
65
|
+
const [isLayouting, setIsLayouting] = useState(false);
|
|
66
|
+
const [error, setError] = useState<Error | null>(null);
|
|
67
|
+
|
|
68
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
69
|
+
const abortRef = useRef<boolean>(false);
|
|
70
|
+
const hasComputedRef = useRef<string | null>(null);
|
|
71
|
+
|
|
72
|
+
// Create stable refs for nodes and edges
|
|
73
|
+
const nodesRef = useRef(nodes);
|
|
74
|
+
nodesRef.current = nodes;
|
|
75
|
+
const edgesRef = useRef(edges);
|
|
76
|
+
edgesRef.current = edges;
|
|
77
|
+
|
|
78
|
+
// Create a stable reference to layout options
|
|
79
|
+
const optionsRef = useRef(layoutOptions);
|
|
80
|
+
optionsRef.current = layoutOptions;
|
|
81
|
+
|
|
82
|
+
// Create a stable key based on node/edge structure
|
|
83
|
+
const layoutKey = useMemo(() => {
|
|
84
|
+
const nodeKey = nodes.map(n => `${n.id}:${n.position.x}:${n.position.y}:${n.width ?? 0}:${n.height ?? 0}`).join('|');
|
|
85
|
+
const edgeKey = edges.map(e => `${e.id}:${e.source}:${e.target}`).join('|');
|
|
86
|
+
return `${enabled}:${nodeKey}:${edgeKey}`;
|
|
87
|
+
}, [enabled, nodes, edges]);
|
|
88
|
+
|
|
89
|
+
// Compute layout when key changes
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
// Skip if already computed for this key
|
|
92
|
+
if (hasComputedRef.current === layoutKey) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!enabled || nodesRef.current.length === 0) {
|
|
97
|
+
setEdgePaths(new Map());
|
|
98
|
+
setEdgeLabelPositions(new Map());
|
|
99
|
+
hasComputedRef.current = layoutKey;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (timeoutRef.current) {
|
|
104
|
+
clearTimeout(timeoutRef.current);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
abortRef.current = false;
|
|
108
|
+
|
|
109
|
+
timeoutRef.current = setTimeout(async () => {
|
|
110
|
+
setIsLayouting(true);
|
|
111
|
+
setError(null);
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const result = await computeElkLayout(nodesRef.current, edgesRef.current, optionsRef.current);
|
|
115
|
+
|
|
116
|
+
if (abortRef.current) return;
|
|
117
|
+
|
|
118
|
+
setEdgePaths(result.edgePaths);
|
|
119
|
+
setEdgeLabelPositions(result.edgeLabelPositions);
|
|
120
|
+
hasComputedRef.current = layoutKey;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (!abortRef.current) {
|
|
123
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
124
|
+
console.error('ELK layout error:', err);
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if (!abortRef.current) {
|
|
128
|
+
setIsLayouting(false);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}, debounceMs);
|
|
132
|
+
|
|
133
|
+
return () => {
|
|
134
|
+
if (timeoutRef.current) {
|
|
135
|
+
clearTimeout(timeoutRef.current);
|
|
136
|
+
}
|
|
137
|
+
abortRef.current = true;
|
|
138
|
+
};
|
|
139
|
+
}, [layoutKey, enabled, debounceMs]);
|
|
140
|
+
|
|
141
|
+
const recomputeLayout = useCallback(() => {
|
|
142
|
+
hasComputedRef.current = null; // Force recompute
|
|
143
|
+
if (timeoutRef.current) {
|
|
144
|
+
clearTimeout(timeoutRef.current);
|
|
145
|
+
}
|
|
146
|
+
// Trigger effect by invalidating the key check
|
|
147
|
+
setIsLayouting(true);
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
edgePaths,
|
|
152
|
+
edgeLabelPositions,
|
|
153
|
+
isLayouting,
|
|
154
|
+
error,
|
|
155
|
+
recomputeLayout,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Apply ELK-computed paths to edges
|
|
161
|
+
*
|
|
162
|
+
* This utility injects the ELK path data into edge data so CustomEdge can use it.
|
|
163
|
+
*
|
|
164
|
+
* @param edges - Original edges
|
|
165
|
+
* @param edgePaths - ELK-computed paths
|
|
166
|
+
* @param edgeLabelPositions - ELK-computed label positions
|
|
167
|
+
* @returns Edges with ELK path data injected
|
|
168
|
+
*/
|
|
169
|
+
export function applyElkPathsToEdges<T extends Edge>(
|
|
170
|
+
edges: T[],
|
|
171
|
+
edgePaths: Map<string, string>,
|
|
172
|
+
edgeLabelPositions: Map<string, { x: number; y: number }>
|
|
173
|
+
): T[] {
|
|
174
|
+
return edges.map((edge) => {
|
|
175
|
+
const elkPath = edgePaths.get(edge.id);
|
|
176
|
+
const elkLabelPosition = edgeLabelPositions.get(edge.id);
|
|
177
|
+
|
|
178
|
+
if (elkPath) {
|
|
179
|
+
return {
|
|
180
|
+
...edge,
|
|
181
|
+
data: {
|
|
182
|
+
...edge.data,
|
|
183
|
+
elkPath,
|
|
184
|
+
elkLabelPosition,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return edge;
|
|
190
|
+
});
|
|
191
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -96,3 +96,9 @@ export {
|
|
|
96
96
|
} from './utils/orientationUtils';
|
|
97
97
|
export { getCanvasBounds, getCanvasDisplaySize, calculateInitialViewport } from './utils/canvasBounds';
|
|
98
98
|
export type { CanvasBounds, Viewport } from './utils/canvasBounds';
|
|
99
|
+
|
|
100
|
+
// ELK layout utilities for circuit-board style edge routing
|
|
101
|
+
export { computeElkLayout, createElkLayouter } from './utils/elkLayout';
|
|
102
|
+
export type { ElkLayoutOptions, ElkLayoutResult, ElkRoutingStyle } from './utils/elkLayout';
|
|
103
|
+
export { useElkLayout, applyElkPathsToEdges } from './hooks/useElkLayout';
|
|
104
|
+
export type { UseElkLayoutOptions, UseElkLayoutResult } from './hooks/useElkLayout';
|
|
@@ -144,9 +144,9 @@ export const OtelSpanConventionNode: React.FC<
|
|
|
144
144
|
const stateDefinitions = nodeData.states || typeDefinition.states;
|
|
145
145
|
|
|
146
146
|
// Workflow chips
|
|
147
|
-
const workflowChips = nodeData
|
|
148
|
-
const onWorkflowChipClick = nodeData
|
|
149
|
-
const selectedWorkflowId = nodeData
|
|
147
|
+
const workflowChips = nodeData?.workflowChips;
|
|
148
|
+
const onWorkflowChipClick = nodeData?.onWorkflowChipClick;
|
|
149
|
+
const selectedWorkflowId = nodeData?.selectedWorkflowId;
|
|
150
150
|
|
|
151
151
|
// Animation class
|
|
152
152
|
const getAnimationClass = () => {
|