@principal-ai/principal-view-react 0.7.20 → 0.7.22
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.map +1 -1
- package/dist/components/GraphRenderer.js +42 -14
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/NodeTooltip.d.ts.map +1 -1
- package/dist/components/NodeTooltip.js +1 -4
- package/dist/components/NodeTooltip.js.map +1 -1
- package/dist/edges/CustomEdge.d.ts +1 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -1
- package/dist/edges/CustomEdge.js +2 -2
- package/dist/edges/CustomEdge.js.map +1 -1
- package/dist/nodes/CustomNode.d.ts +1 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +3 -3
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +47 -14
- package/src/components/NodeTooltip.tsx +3 -6
- package/src/edges/CustomEdge.tsx +5 -2
- package/src/nodes/CustomNode.tsx +6 -3
- package/src/stories/CanvasEdgeTypes.stories.tsx +312 -0
- package/src/stories/GraphRenderer.stories.tsx +106 -0
- package/src/stories/data/graph-converter-test-execution.json +25 -25
- package/src/stories/data/graph-converter-validated-execution.json +6 -6
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
MiniMap,
|
|
16
16
|
ReactFlowProvider,
|
|
17
17
|
useReactFlow,
|
|
18
|
+
useUpdateNodeInternals,
|
|
18
19
|
useViewport,
|
|
19
20
|
applyNodeChanges,
|
|
20
21
|
applyEdgeChanges,
|
|
@@ -368,8 +369,34 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
368
369
|
onSourceClick,
|
|
369
370
|
}) => {
|
|
370
371
|
const { fitView } = useReactFlow();
|
|
372
|
+
const updateNodeInternals = useUpdateNodeInternals();
|
|
371
373
|
const { theme } = useTheme();
|
|
372
374
|
|
|
375
|
+
// Track shift key state for tooltip control
|
|
376
|
+
const [shiftKeyPressed, setShiftKeyPressed] = useState(false);
|
|
377
|
+
|
|
378
|
+
// Setup keyboard event listeners for shift key
|
|
379
|
+
useEffect(() => {
|
|
380
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
381
|
+
if (e.key === 'Shift') {
|
|
382
|
+
setShiftKeyPressed(true);
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const handleKeyUp = (e: KeyboardEvent) => {
|
|
387
|
+
if (e.key === 'Shift') {
|
|
388
|
+
setShiftKeyPressed(false);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
window.addEventListener('keydown', handleKeyDown);
|
|
393
|
+
window.addEventListener('keyup', handleKeyUp);
|
|
394
|
+
|
|
395
|
+
return () => {
|
|
396
|
+
window.removeEventListener('keydown', handleKeyDown);
|
|
397
|
+
window.removeEventListener('keyup', handleKeyUp);
|
|
398
|
+
};
|
|
399
|
+
}, []);
|
|
373
400
|
|
|
374
401
|
// Track active animations
|
|
375
402
|
const [animationState, setAnimationState] = useState<AnimationState>({
|
|
@@ -848,20 +875,21 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
848
875
|
const originalEdge = localEdges.find((e) => e.id === oldEdge.id);
|
|
849
876
|
if (!originalEdge) return;
|
|
850
877
|
|
|
851
|
-
// Find source and target
|
|
878
|
+
// Find source and target nodes for validation
|
|
852
879
|
const sourceNode = nodes.find((n) => n.id === newConnection.source);
|
|
853
880
|
const targetNode = nodes.find((n) => n.id === newConnection.target);
|
|
854
881
|
if (!sourceNode || !targetNode) return;
|
|
855
882
|
|
|
856
883
|
// Check if the new connection is valid for this edge type
|
|
884
|
+
// Note: allowedConnections uses node IDs as the from/to values
|
|
857
885
|
const isValidConnection = configuration.allowedConnections.some(
|
|
858
886
|
(ac) =>
|
|
859
|
-
ac.from === sourceNode.
|
|
887
|
+
ac.from === sourceNode.id && ac.to === targetNode.id && ac.via === originalEdge.type
|
|
860
888
|
);
|
|
861
889
|
|
|
862
890
|
if (!isValidConnection) {
|
|
863
891
|
console.warn(
|
|
864
|
-
`Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.
|
|
892
|
+
`Cannot reconnect: ${originalEdge.type} edge not allowed from ${sourceNode.id} to ${targetNode.id}`
|
|
865
893
|
);
|
|
866
894
|
return;
|
|
867
895
|
}
|
|
@@ -1137,6 +1165,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1137
1165
|
...node.data,
|
|
1138
1166
|
editable,
|
|
1139
1167
|
tooltipsEnabled: showTooltips,
|
|
1168
|
+
shiftKeyPressed,
|
|
1140
1169
|
isHighlighted: highlightedNodeId === node.id,
|
|
1141
1170
|
...(animation
|
|
1142
1171
|
? {
|
|
@@ -1147,7 +1176,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1147
1176
|
} as CustomNodeData,
|
|
1148
1177
|
};
|
|
1149
1178
|
});
|
|
1150
|
-
}, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, editStateRef]);
|
|
1179
|
+
}, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, editStateRef, shiftKeyPressed]);
|
|
1151
1180
|
|
|
1152
1181
|
const baseNodesKey = useMemo(() => {
|
|
1153
1182
|
return nodes
|
|
@@ -1181,9 +1210,17 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1181
1210
|
if (editable && !prevEditableRef.current) {
|
|
1182
1211
|
// Entering edit mode - sync positions
|
|
1183
1212
|
setXyflowLocalNodes(xyflowNodesBase);
|
|
1213
|
+
|
|
1214
|
+
// Reset ReactFlow's internal state for all nodes to prevent NaN errors
|
|
1215
|
+
// This ensures ReactFlow remeasures nodes and updates drag tracking
|
|
1216
|
+
setTimeout(() => {
|
|
1217
|
+
xyflowNodesBase.forEach((node) => {
|
|
1218
|
+
updateNodeInternals(node.id);
|
|
1219
|
+
});
|
|
1220
|
+
}, 0);
|
|
1184
1221
|
}
|
|
1185
1222
|
prevEditableRef.current = editable;
|
|
1186
|
-
}, [editable, xyflowNodesBase]);
|
|
1223
|
+
}, [editable, xyflowNodesBase, updateNodeInternals]);
|
|
1187
1224
|
|
|
1188
1225
|
const xyflowNodes = editable ? xyflowLocalNodes : xyflowNodesBase;
|
|
1189
1226
|
|
|
@@ -1292,6 +1329,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1292
1329
|
data: {
|
|
1293
1330
|
...edge.data,
|
|
1294
1331
|
tooltipsEnabled: showTooltips,
|
|
1332
|
+
shiftKeyPressed,
|
|
1295
1333
|
...(animation
|
|
1296
1334
|
? {
|
|
1297
1335
|
animationType: animation.type,
|
|
@@ -1313,7 +1351,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1313
1351
|
if (!aSelected && bSelected) return -1; // b comes after a (rendered on top)
|
|
1314
1352
|
return 0; // maintain original order
|
|
1315
1353
|
});
|
|
1316
|
-
}, [edges, configuration, violations, animationState.edgeAnimations, showTooltips, selectedEdgeIds]);
|
|
1354
|
+
}, [edges, configuration, violations, animationState.edgeAnimations, showTooltips, selectedEdgeIds, shiftKeyPressed]);
|
|
1317
1355
|
|
|
1318
1356
|
// Local xyflow edges state for reconnection
|
|
1319
1357
|
const [xyflowLocalEdges, setXyflowLocalEdges] = useState<Edge<CustomEdgeData>[]>(xyflowEdgesBase);
|
|
@@ -1656,15 +1694,10 @@ function useCanvasToLegacy(
|
|
|
1656
1694
|
};
|
|
1657
1695
|
}
|
|
1658
1696
|
|
|
1659
|
-
//
|
|
1660
|
-
const fromNode = canvas.nodes?.find((n) => n.id === edge.fromNode);
|
|
1661
|
-
const toNode = canvas.nodes?.find((n) => n.id === edge.toNode);
|
|
1662
|
-
const fromType = fromNode?.pv?.nodeType || edge.fromNode;
|
|
1663
|
-
const toType = toNode?.pv?.nodeType || edge.toNode;
|
|
1664
|
-
|
|
1697
|
+
// Store allowed connections using node IDs
|
|
1665
1698
|
allowedConnections.push({
|
|
1666
|
-
from:
|
|
1667
|
-
to:
|
|
1699
|
+
from: edge.fromNode,
|
|
1700
|
+
to: edge.toNode,
|
|
1668
1701
|
via: edgeType,
|
|
1669
1702
|
});
|
|
1670
1703
|
}
|
|
@@ -42,9 +42,6 @@ export const NodeTooltip: React.FC<NodeTooltipProps> = ({
|
|
|
42
42
|
|
|
43
43
|
if (!visible) return null;
|
|
44
44
|
|
|
45
|
-
// Don't show tooltip if no useful info
|
|
46
|
-
if (!description && !otel) return null;
|
|
47
|
-
|
|
48
45
|
// If no nodeRef, render inline (for storybook demos)
|
|
49
46
|
const usePortal = Boolean(nodeRef);
|
|
50
47
|
|
|
@@ -156,9 +153,9 @@ export const NodeTooltip: React.FC<NodeTooltipProps> = ({
|
|
|
156
153
|
)}
|
|
157
154
|
|
|
158
155
|
{/* Description */}
|
|
159
|
-
{description
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
<div style={{ lineHeight: '1.4', color: description ? 'rgba(255,255,255,0.9)' : 'rgba(255,255,255,0.5)' }}>
|
|
157
|
+
{description || 'No description'}
|
|
158
|
+
</div>
|
|
162
159
|
</div>
|
|
163
160
|
);
|
|
164
161
|
|
package/src/edges/CustomEdge.tsx
CHANGED
|
@@ -14,6 +14,8 @@ export interface CustomEdgeData extends Record<string, unknown> {
|
|
|
14
14
|
animationDirection?: 'forward' | 'backward' | 'bidirectional';
|
|
15
15
|
// Whether tooltips are enabled (defaults to true)
|
|
16
16
|
tooltipsEnabled?: boolean;
|
|
17
|
+
// Whether shift key is currently pressed (for tooltip control)
|
|
18
|
+
shiftKeyPressed?: boolean;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -42,6 +44,7 @@ export const CustomEdge: React.FC<EdgeProps<any>> = ({
|
|
|
42
44
|
animationDuration = 1000,
|
|
43
45
|
animationDirection = 'forward',
|
|
44
46
|
tooltipsEnabled = true,
|
|
47
|
+
shiftKeyPressed = false,
|
|
45
48
|
} = edgeProps || ({} as CustomEdgeData);
|
|
46
49
|
|
|
47
50
|
const [particlePosition, setParticlePosition] = useState(0);
|
|
@@ -285,8 +288,8 @@ export const CustomEdge: React.FC<EdgeProps<any>> = ({
|
|
|
285
288
|
</EdgeLabelRenderer>
|
|
286
289
|
)}
|
|
287
290
|
|
|
288
|
-
{/* Hover tooltip showing edge type */}
|
|
289
|
-
{tooltipsEnabled && isHovered && (
|
|
291
|
+
{/* Hover tooltip showing edge type - only shown when shift key is pressed */}
|
|
292
|
+
{tooltipsEnabled && isHovered && shiftKeyPressed && (
|
|
290
293
|
<EdgeLabelRenderer>
|
|
291
294
|
<div
|
|
292
295
|
style={{
|
package/src/nodes/CustomNode.tsx
CHANGED
|
@@ -45,6 +45,8 @@ export interface CustomNodeData extends Record<string, unknown> {
|
|
|
45
45
|
editable?: boolean;
|
|
46
46
|
// Whether tooltips are enabled (defaults to true)
|
|
47
47
|
tooltipsEnabled?: boolean;
|
|
48
|
+
// Whether shift key is currently pressed (for tooltip control)
|
|
49
|
+
shiftKeyPressed?: boolean;
|
|
48
50
|
// Whether this node is highlighted (e.g., during execution playback)
|
|
49
51
|
isHighlighted?: boolean;
|
|
50
52
|
}
|
|
@@ -55,9 +57,6 @@ export interface CustomNodeData extends Record<string, unknown> {
|
|
|
55
57
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
56
58
|
export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging }) => {
|
|
57
59
|
const [isHovered, setIsHovered] = useState(false);
|
|
58
|
-
|
|
59
|
-
// Don't show tooltip while dragging
|
|
60
|
-
const showTooltip = isHovered && !dragging;
|
|
61
60
|
const nodeRef = useRef<HTMLDivElement>(null);
|
|
62
61
|
const nodeProps = data as CustomNodeData;
|
|
63
62
|
const {
|
|
@@ -69,9 +68,13 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
69
68
|
animationDuration = 1000,
|
|
70
69
|
editable = false,
|
|
71
70
|
tooltipsEnabled = true,
|
|
71
|
+
shiftKeyPressed = false,
|
|
72
72
|
isHighlighted = false,
|
|
73
73
|
} = nodeProps;
|
|
74
74
|
|
|
75
|
+
// Only show tooltip when hovering, not dragging, and shift key is pressed
|
|
76
|
+
const showTooltip = isHovered && !dragging && shiftKeyPressed;
|
|
77
|
+
|
|
75
78
|
// Extract OTEL info and description for tooltip
|
|
76
79
|
const otelInfo = nodeData?.otel as OtelInfo | undefined;
|
|
77
80
|
const description = nodeData?.description as string | undefined;
|
|
@@ -981,3 +981,315 @@ export const EdgeFieldsReference: Story = {
|
|
|
981
981
|
},
|
|
982
982
|
},
|
|
983
983
|
};
|
|
984
|
+
|
|
985
|
+
/**
|
|
986
|
+
* Canvas showing edge reconnection scenario from admin-dashboard
|
|
987
|
+
* Tests moving an edge from left to right of the same node
|
|
988
|
+
*/
|
|
989
|
+
const edgeReconnectionCanvas: ExtendedCanvas = {
|
|
990
|
+
nodes: [
|
|
991
|
+
{
|
|
992
|
+
id: 'client-components',
|
|
993
|
+
type: 'text' as const,
|
|
994
|
+
x: 100,
|
|
995
|
+
y: 200,
|
|
996
|
+
width: 200,
|
|
997
|
+
height: 100,
|
|
998
|
+
text: 'Client Components\n\nReact Query\nURL state (nuqs)\nReal-time updates',
|
|
999
|
+
color: '#06B6D4',
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
id: 'api-routes',
|
|
1003
|
+
type: 'text' as const,
|
|
1004
|
+
x: 400,
|
|
1005
|
+
y: 200,
|
|
1006
|
+
width: 220,
|
|
1007
|
+
height: 180,
|
|
1008
|
+
text: 'API Routes Layer\n\n• /api/otel/*\n• /api/dashboards/*\n• /api/teams/*\n• /api/github/*\n• /api/sentry/*\n• /api/billing/*\n• /api/bug-fix-agent/*',
|
|
1009
|
+
color: '#F59E0B',
|
|
1010
|
+
},
|
|
1011
|
+
{
|
|
1012
|
+
id: 'pages-layer',
|
|
1013
|
+
type: 'text' as const,
|
|
1014
|
+
x: 700,
|
|
1015
|
+
y: 100,
|
|
1016
|
+
width: 220,
|
|
1017
|
+
height: 180,
|
|
1018
|
+
text: 'Pages Layer\n\n• Dashboard (/, /dashboards)\n• Sessions (/sessions)\n• Events (/events)\n• Teams (/teams)\n• Integrations (/integrations)\n• Billing (/billing)',
|
|
1019
|
+
color: '#10B981',
|
|
1020
|
+
},
|
|
1021
|
+
],
|
|
1022
|
+
edges: [
|
|
1023
|
+
{
|
|
1024
|
+
id: 'e8',
|
|
1025
|
+
fromNode: 'client-components',
|
|
1026
|
+
toNode: 'api-routes',
|
|
1027
|
+
fromSide: 'right',
|
|
1028
|
+
toSide: 'left',
|
|
1029
|
+
label: 'React Query',
|
|
1030
|
+
pv: {
|
|
1031
|
+
edgeType: 'httpRest',
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: 'e4',
|
|
1036
|
+
fromNode: 'pages-layer',
|
|
1037
|
+
toNode: 'api-routes',
|
|
1038
|
+
fromSide: 'left',
|
|
1039
|
+
toSide: 'right',
|
|
1040
|
+
label: 'SSR',
|
|
1041
|
+
pv: {
|
|
1042
|
+
edgeType: 'httpRest',
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
],
|
|
1046
|
+
pv: {
|
|
1047
|
+
version: '1.0.0',
|
|
1048
|
+
name: 'Edge Reconnection Test - Admin Dashboard Scenario',
|
|
1049
|
+
description: 'Reproduces the httpRest edge reconnection issue when moving edge from left to right of same node',
|
|
1050
|
+
edgeTypes: {
|
|
1051
|
+
httpRest: {
|
|
1052
|
+
style: 'solid',
|
|
1053
|
+
color: '#3b82f6',
|
|
1054
|
+
directed: true,
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
},
|
|
1058
|
+
};
|
|
1059
|
+
|
|
1060
|
+
export const EdgeReconnectionScenario: Story = {
|
|
1061
|
+
args: {
|
|
1062
|
+
canvas: edgeReconnectionCanvas,
|
|
1063
|
+
width: 1100,
|
|
1064
|
+
height: 500,
|
|
1065
|
+
editable: true,
|
|
1066
|
+
},
|
|
1067
|
+
parameters: {
|
|
1068
|
+
docs: {
|
|
1069
|
+
description: {
|
|
1070
|
+
story: `
|
|
1071
|
+
**Edge Reconnection Test - Admin Dashboard Scenario**
|
|
1072
|
+
|
|
1073
|
+
This story reproduces the issue from the admin-dashboard.canvas file where:
|
|
1074
|
+
|
|
1075
|
+
1. We have text nodes connected by httpRest edges
|
|
1076
|
+
2. Edge e8 connects client-components → api-routes (left → right)
|
|
1077
|
+
3. Edge e4 connects pages-layer → api-routes (left → right)
|
|
1078
|
+
|
|
1079
|
+
**To Test the Bug:**
|
|
1080
|
+
Try to reconnect edge e8 or e4 to different sides of the api-routes node.
|
|
1081
|
+
You may see the error: "Cannot reconnect: httpRest edge not allowed from text to text"
|
|
1082
|
+
|
|
1083
|
+
**Root Cause:**
|
|
1084
|
+
The validation checks \`allowedConnections\` which is built from existing edges.
|
|
1085
|
+
When nodes don't have \`pv.nodeType\`, the system uses node IDs instead of node types,
|
|
1086
|
+
causing validation to fail when reconnecting edges between different node pairs.
|
|
1087
|
+
|
|
1088
|
+
**Expected Behavior:**
|
|
1089
|
+
Edges with edgeType "httpRest" should be allowed to reconnect between any text nodes,
|
|
1090
|
+
regardless of which specific nodes are connected.
|
|
1091
|
+
`,
|
|
1092
|
+
},
|
|
1093
|
+
},
|
|
1094
|
+
},
|
|
1095
|
+
};
|
|
1096
|
+
|
|
1097
|
+
/**
|
|
1098
|
+
* Full Admin Dashboard canvas data for comprehensive testing
|
|
1099
|
+
* Includes multiple httpRest edges and various node types
|
|
1100
|
+
*/
|
|
1101
|
+
const adminDashboardFullCanvas: ExtendedCanvas = {
|
|
1102
|
+
nodes: [
|
|
1103
|
+
{
|
|
1104
|
+
id: 'user-browser',
|
|
1105
|
+
type: 'text' as const,
|
|
1106
|
+
text: 'User Browser\n\nNext.js 16 React App',
|
|
1107
|
+
x: 92,
|
|
1108
|
+
y: 374,
|
|
1109
|
+
width: 180,
|
|
1110
|
+
height: 80,
|
|
1111
|
+
color: '#3B82F6',
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
id: 'pages-layer',
|
|
1115
|
+
type: 'text' as const,
|
|
1116
|
+
text: 'Pages Layer\n\n• Dashboard (/, /dashboards)\n• Sessions (/sessions)\n• Events (/events)',
|
|
1117
|
+
x: 400,
|
|
1118
|
+
y: 100,
|
|
1119
|
+
width: 220,
|
|
1120
|
+
height: 180,
|
|
1121
|
+
color: '#10B981',
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
id: 'api-routes',
|
|
1125
|
+
type: 'text' as const,
|
|
1126
|
+
text: 'API Routes Layer\n\n• /api/otel/*\n• /api/dashboards/*\n• /api/teams/*\n• /api/github/*',
|
|
1127
|
+
x: 400,
|
|
1128
|
+
y: 320,
|
|
1129
|
+
width: 220,
|
|
1130
|
+
height: 180,
|
|
1131
|
+
color: '#F59E0B',
|
|
1132
|
+
},
|
|
1133
|
+
{
|
|
1134
|
+
id: 'server-components',
|
|
1135
|
+
type: 'text' as const,
|
|
1136
|
+
text: 'Server Components\n\nSSR data fetching\nDirect DB queries',
|
|
1137
|
+
x: 700,
|
|
1138
|
+
y: 63,
|
|
1139
|
+
width: 200,
|
|
1140
|
+
height: 100,
|
|
1141
|
+
color: '#06B6D4',
|
|
1142
|
+
},
|
|
1143
|
+
{
|
|
1144
|
+
id: 'client-components',
|
|
1145
|
+
type: 'text' as const,
|
|
1146
|
+
text: 'Client Components\n\nReact Query\nURL state (nuqs)',
|
|
1147
|
+
x: 700,
|
|
1148
|
+
y: 230,
|
|
1149
|
+
width: 200,
|
|
1150
|
+
height: 100,
|
|
1151
|
+
color: '#06B6D4',
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
id: 'postgresql',
|
|
1155
|
+
type: 'text' as const,
|
|
1156
|
+
text: 'PostgreSQL\n\nDrizzle ORM\nUsers, orgs, teams',
|
|
1157
|
+
x: 1000,
|
|
1158
|
+
y: 124,
|
|
1159
|
+
width: 200,
|
|
1160
|
+
height: 120,
|
|
1161
|
+
color: '#336791',
|
|
1162
|
+
},
|
|
1163
|
+
],
|
|
1164
|
+
edges: [
|
|
1165
|
+
{
|
|
1166
|
+
id: 'e2',
|
|
1167
|
+
fromNode: 'user-browser',
|
|
1168
|
+
toNode: 'pages-layer',
|
|
1169
|
+
fromSide: 'right',
|
|
1170
|
+
toSide: 'left',
|
|
1171
|
+
label: 'Navigate',
|
|
1172
|
+
pv: { edgeType: 'httpRest' },
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
id: 'e3',
|
|
1176
|
+
fromNode: 'user-browser',
|
|
1177
|
+
toNode: 'api-routes',
|
|
1178
|
+
fromSide: 'right',
|
|
1179
|
+
toSide: 'left',
|
|
1180
|
+
label: 'API calls',
|
|
1181
|
+
pv: { edgeType: 'httpRest' },
|
|
1182
|
+
},
|
|
1183
|
+
{
|
|
1184
|
+
id: 'e4',
|
|
1185
|
+
fromNode: 'pages-layer',
|
|
1186
|
+
toNode: 'server-components',
|
|
1187
|
+
fromSide: 'right',
|
|
1188
|
+
toSide: 'left',
|
|
1189
|
+
label: 'SSR',
|
|
1190
|
+
pv: { edgeType: 'httpRest' },
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
id: 'e5',
|
|
1194
|
+
fromNode: 'pages-layer',
|
|
1195
|
+
toNode: 'client-components',
|
|
1196
|
+
fromSide: 'right',
|
|
1197
|
+
toSide: 'left',
|
|
1198
|
+
label: 'Hydrate',
|
|
1199
|
+
pv: { edgeType: 'httpRest' },
|
|
1200
|
+
},
|
|
1201
|
+
{
|
|
1202
|
+
id: 'e6',
|
|
1203
|
+
fromNode: 'server-components',
|
|
1204
|
+
toNode: 'postgresql',
|
|
1205
|
+
fromSide: 'right',
|
|
1206
|
+
toSide: 'left',
|
|
1207
|
+
label: 'Drizzle ORM',
|
|
1208
|
+
pv: { edgeType: 'postgresQuery' },
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
id: 'e8',
|
|
1212
|
+
fromNode: 'client-components',
|
|
1213
|
+
toNode: 'api-routes',
|
|
1214
|
+
fromSide: 'left',
|
|
1215
|
+
toSide: 'right',
|
|
1216
|
+
label: 'React Query',
|
|
1217
|
+
pv: { edgeType: 'httpRest' },
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
id: 'e9',
|
|
1221
|
+
fromNode: 'api-routes',
|
|
1222
|
+
toNode: 'postgresql',
|
|
1223
|
+
fromSide: 'right',
|
|
1224
|
+
toSide: 'left',
|
|
1225
|
+
label: 'Drizzle ORM',
|
|
1226
|
+
pv: { edgeType: 'postgresQuery' },
|
|
1227
|
+
},
|
|
1228
|
+
],
|
|
1229
|
+
pv: {
|
|
1230
|
+
version: '1.0.0',
|
|
1231
|
+
name: 'Admin Dashboard Architecture (Simplified)',
|
|
1232
|
+
description: 'Simplified version of admin-dashboard.canvas to test edge reconnection issues',
|
|
1233
|
+
edgeTypes: {
|
|
1234
|
+
httpRest: {
|
|
1235
|
+
style: 'solid',
|
|
1236
|
+
color: '#3b82f6',
|
|
1237
|
+
directed: true,
|
|
1238
|
+
},
|
|
1239
|
+
postgresQuery: {
|
|
1240
|
+
style: 'solid',
|
|
1241
|
+
color: '#336791',
|
|
1242
|
+
directed: true,
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
},
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
export const AdminDashboardFull: Story = {
|
|
1249
|
+
args: {
|
|
1250
|
+
canvas: adminDashboardFullCanvas,
|
|
1251
|
+
width: 1400,
|
|
1252
|
+
height: 650,
|
|
1253
|
+
editable: true,
|
|
1254
|
+
},
|
|
1255
|
+
parameters: {
|
|
1256
|
+
docs: {
|
|
1257
|
+
description: {
|
|
1258
|
+
story: `
|
|
1259
|
+
**Full Admin Dashboard Architecture Test**
|
|
1260
|
+
|
|
1261
|
+
This is a simplified version of the actual admin-dashboard.canvas file from /Users/griever/Developer/observability.
|
|
1262
|
+
|
|
1263
|
+
**Key Test Cases:**
|
|
1264
|
+
|
|
1265
|
+
1. **Edge e8 (client-components → api-routes)**:
|
|
1266
|
+
- Currently connects from LEFT side of client-components to RIGHT side of api-routes
|
|
1267
|
+
- Try dragging the edge handle to connect from RIGHT side instead
|
|
1268
|
+
- Expected: Should work seamlessly
|
|
1269
|
+
- Actual: May fail with "Cannot reconnect: httpRest edge not allowed from text to text"
|
|
1270
|
+
|
|
1271
|
+
2. **Multiple httpRest edges**:
|
|
1272
|
+
- Notice that e2, e3, e4, e5, and e8 all use edgeType "httpRest"
|
|
1273
|
+
- They connect various text nodes together
|
|
1274
|
+
- Try reconnecting any of these edges to different nodes or different sides
|
|
1275
|
+
|
|
1276
|
+
3. **Mixed edge types**:
|
|
1277
|
+
- Some edges use "httpRest", others use "postgresQuery"
|
|
1278
|
+
- This tests that the validation correctly distinguishes between different edge types
|
|
1279
|
+
|
|
1280
|
+
**The Issue:**
|
|
1281
|
+
The \`allowedConnections\` array is built by mapping each edge to its specific source and target node IDs.
|
|
1282
|
+
This means edge e8's allowed connection is registered as:
|
|
1283
|
+
\`\`\`
|
|
1284
|
+
{ from: "client-components", to: "api-routes", via: "httpRest" }
|
|
1285
|
+
\`\`\`
|
|
1286
|
+
|
|
1287
|
+
When you try to reconnect this edge to a different node pair (even with the same node types),
|
|
1288
|
+
the validation fails because it's checking node IDs, not node types.
|
|
1289
|
+
|
|
1290
|
+
**Enable editing mode** to test the reconnection behavior.
|
|
1291
|
+
`,
|
|
1292
|
+
},
|
|
1293
|
+
},
|
|
1294
|
+
},
|
|
1295
|
+
};
|
|
@@ -1075,3 +1075,109 @@ Color coding:
|
|
|
1075
1075
|
},
|
|
1076
1076
|
},
|
|
1077
1077
|
};
|
|
1078
|
+
|
|
1079
|
+
const EditModeToggleTemplate = () => {
|
|
1080
|
+
const [isEditMode, setIsEditMode] = React.useState(false);
|
|
1081
|
+
const graphRef = React.useRef<GraphRendererHandle>(null);
|
|
1082
|
+
const [hasChanges, setHasChanges] = React.useState(false);
|
|
1083
|
+
|
|
1084
|
+
const handleSave = () => {
|
|
1085
|
+
if (graphRef.current) {
|
|
1086
|
+
const changes = graphRef.current.getPendingChanges();
|
|
1087
|
+
console.log('Saving changes:', changes);
|
|
1088
|
+
alert('Changes saved! Check console for details.');
|
|
1089
|
+
graphRef.current.resetEditState();
|
|
1090
|
+
}
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
return (
|
|
1094
|
+
<div>
|
|
1095
|
+
<div
|
|
1096
|
+
style={{
|
|
1097
|
+
marginBottom: 16,
|
|
1098
|
+
padding: 12,
|
|
1099
|
+
backgroundColor: '#f0f9ff',
|
|
1100
|
+
borderRadius: 4,
|
|
1101
|
+
border: '1px solid #3b82f6',
|
|
1102
|
+
}}
|
|
1103
|
+
>
|
|
1104
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
|
|
1105
|
+
<button
|
|
1106
|
+
onClick={() => setIsEditMode(!isEditMode)}
|
|
1107
|
+
style={{
|
|
1108
|
+
padding: '8px 16px',
|
|
1109
|
+
backgroundColor: isEditMode ? '#ef4444' : '#3b82f6',
|
|
1110
|
+
color: 'white',
|
|
1111
|
+
border: 'none',
|
|
1112
|
+
borderRadius: 4,
|
|
1113
|
+
cursor: 'pointer',
|
|
1114
|
+
fontWeight: 'bold',
|
|
1115
|
+
fontSize: 14,
|
|
1116
|
+
}}
|
|
1117
|
+
>
|
|
1118
|
+
{isEditMode ? '🔓 Exit Edit Mode' : '🔒 Enter Edit Mode'}
|
|
1119
|
+
</button>
|
|
1120
|
+
{isEditMode && hasChanges && (
|
|
1121
|
+
<button
|
|
1122
|
+
onClick={handleSave}
|
|
1123
|
+
style={{
|
|
1124
|
+
padding: '8px 16px',
|
|
1125
|
+
backgroundColor: '#10b981',
|
|
1126
|
+
color: 'white',
|
|
1127
|
+
border: 'none',
|
|
1128
|
+
borderRadius: 4,
|
|
1129
|
+
cursor: 'pointer',
|
|
1130
|
+
fontWeight: 'bold',
|
|
1131
|
+
fontSize: 14,
|
|
1132
|
+
}}
|
|
1133
|
+
>
|
|
1134
|
+
💾 Save Changes
|
|
1135
|
+
</button>
|
|
1136
|
+
)}
|
|
1137
|
+
<div style={{ fontSize: 13, color: '#1e40af' }}>
|
|
1138
|
+
Mode: <strong>{isEditMode ? 'Editable' : 'Read-only'}</strong>
|
|
1139
|
+
{isEditMode && hasChanges && (
|
|
1140
|
+
<span style={{ marginLeft: 8, color: '#f97316', fontWeight: 'bold' }}>
|
|
1141
|
+
(unsaved changes)
|
|
1142
|
+
</span>
|
|
1143
|
+
)}
|
|
1144
|
+
</div>
|
|
1145
|
+
</div>
|
|
1146
|
+
<div style={{ marginTop: 8, fontSize: 12, color: '#475569' }}>
|
|
1147
|
+
Toggle between read-only and edit mode. Try dragging nodes after switching modes to verify
|
|
1148
|
+
the NaN error fix.
|
|
1149
|
+
</div>
|
|
1150
|
+
</div>
|
|
1151
|
+
<GraphRenderer
|
|
1152
|
+
ref={graphRef}
|
|
1153
|
+
canvas={sampleCanvas}
|
|
1154
|
+
width={800}
|
|
1155
|
+
height={500}
|
|
1156
|
+
editable={isEditMode}
|
|
1157
|
+
onPendingChangesChange={setHasChanges}
|
|
1158
|
+
/>
|
|
1159
|
+
</div>
|
|
1160
|
+
);
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
export const EditModeToggle: Story = {
|
|
1164
|
+
render: () => <EditModeToggleTemplate />,
|
|
1165
|
+
parameters: {
|
|
1166
|
+
docs: {
|
|
1167
|
+
description: {
|
|
1168
|
+
story: `
|
|
1169
|
+
**Edit Mode Toggle** - Demonstrates toggling between read-only and editable modes without remounting.
|
|
1170
|
+
|
|
1171
|
+
This story tests the fix for the NaN coordinate error that occurred when transitioning between modes:
|
|
1172
|
+
- Click "Enter Edit Mode" to enable editing
|
|
1173
|
+
- Try dragging nodes (should work without NaN errors)
|
|
1174
|
+
- Click "Exit Edit Mode" to return to read-only
|
|
1175
|
+
- Toggle back to edit mode and drag again (should still work)
|
|
1176
|
+
|
|
1177
|
+
Previously, this would cause \`NaN\` errors in edge coordinates due to ReactFlow's internal state corruption.
|
|
1178
|
+
The fix uses \`updateNodeInternals()\` to reset ReactFlow's measurement tracking when entering edit mode.
|
|
1179
|
+
`,
|
|
1180
|
+
},
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
};
|