@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.
Files changed (41) hide show
  1. package/dist/components/GraphRenderer.d.ts +30 -7
  2. package/dist/components/GraphRenderer.d.ts.map +1 -1
  3. package/dist/components/GraphRenderer.js +29 -16
  4. package/dist/components/GraphRenderer.js.map +1 -1
  5. package/dist/components/NodeTooltip.js +1 -1
  6. package/dist/components/NodeTooltip.js.map +1 -1
  7. package/dist/contexts/TooltipPortalContext.d.ts +8 -0
  8. package/dist/contexts/TooltipPortalContext.d.ts.map +1 -0
  9. package/dist/contexts/TooltipPortalContext.js +8 -0
  10. package/dist/contexts/TooltipPortalContext.js.map +1 -0
  11. package/dist/edges/CustomEdge.d.ts +5 -0
  12. package/dist/edges/CustomEdge.d.ts.map +1 -1
  13. package/dist/edges/CustomEdge.js +7 -3
  14. package/dist/edges/CustomEdge.js.map +1 -1
  15. package/dist/hooks/useElkLayout.d.ts +66 -0
  16. package/dist/hooks/useElkLayout.d.ts.map +1 -0
  17. package/dist/hooks/useElkLayout.js +136 -0
  18. package/dist/hooks/useElkLayout.js.map +1 -0
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +3 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/nodes/otel/OtelSpanConventionNode.js +3 -3
  24. package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
  25. package/dist/utils/elkLayout.d.ts +92 -0
  26. package/dist/utils/elkLayout.d.ts.map +1 -0
  27. package/dist/utils/elkLayout.js +281 -0
  28. package/dist/utils/elkLayout.js.map +1 -0
  29. package/package.json +4 -3
  30. package/src/components/GraphRenderer.tsx +70 -13
  31. package/src/components/NodeTooltip.tsx +1 -1
  32. package/src/contexts/TooltipPortalContext.ts +8 -0
  33. package/src/edges/CustomEdge.tsx +13 -2
  34. package/src/hooks/useElkLayout.test.ts +134 -0
  35. package/src/hooks/useElkLayout.ts +191 -0
  36. package/src/index.ts +6 -0
  37. package/src/nodes/otel/OtelSpanConventionNode.tsx +3 -3
  38. package/src/stories/ElkEdgeRouting.stories.tsx +415 -0
  39. package/src/stories/SpanBadges.stories.tsx +840 -0
  40. package/src/utils/elkLayout.test.ts +240 -0
  41. 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.workflowChips;
148
- const onWorkflowChipClick = nodeData.onWorkflowChipClick;
149
- const selectedWorkflowId = nodeData.selectedWorkflowId;
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 = () => {