@principal-ai/principal-view-react 0.15.0 → 0.15.2
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 +7 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +93 -4
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/NodeTooltip.d.ts +1 -0
- package/dist/components/NodeTooltip.d.ts.map +1 -1
- package/dist/components/NodeTooltip.js +10 -2
- package/dist/components/NodeTooltip.js.map +1 -1
- package/dist/contexts/GraphEditContext.d.ts +2 -0
- package/dist/contexts/GraphEditContext.d.ts.map +1 -1
- package/dist/contexts/GraphEditContext.js.map +1 -1
- package/dist/hooks/useUndoRedo.d.ts +5 -0
- package/dist/hooks/useUndoRedo.d.ts.map +1 -1
- package/dist/hooks/useUndoRedo.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 +84 -6
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +119 -2
- package/src/components/NodeTooltip.tsx +20 -1
- package/src/contexts/GraphEditContext.tsx +2 -0
- package/src/hooks/useUndoRedo.ts +6 -0
- package/src/nodes/CustomNode.tsx +111 -4
- package/src/stories/OtelComponents.stories.tsx +73 -1
|
@@ -84,12 +84,20 @@ export interface NodeDimensionChange {
|
|
|
84
84
|
dimensions: { width: number; height: number };
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/** Text change event for tracking inline text edits */
|
|
88
|
+
export interface NodeTextChange {
|
|
89
|
+
nodeId: string;
|
|
90
|
+
text: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
87
93
|
/** All pending changes that can be saved */
|
|
88
94
|
export interface PendingChanges {
|
|
89
95
|
/** Node position changes */
|
|
90
96
|
positionChanges: NodePositionChange[];
|
|
91
97
|
/** Node dimension changes (from resizing) */
|
|
92
98
|
dimensionChanges: NodeDimensionChange[];
|
|
99
|
+
/** Text changes for text and group nodes */
|
|
100
|
+
textChanges: NodeTextChange[];
|
|
93
101
|
/** Node updates (type, data changes) */
|
|
94
102
|
nodeUpdates: Array<{
|
|
95
103
|
nodeId: string;
|
|
@@ -398,6 +406,7 @@ interface AlignmentGuide {
|
|
|
398
406
|
interface EditState {
|
|
399
407
|
positionChanges: Map<string, { x: number; y: number }>;
|
|
400
408
|
dimensionChanges: Map<string, { width: number; height: number }>;
|
|
409
|
+
textChanges: Map<string, string>;
|
|
401
410
|
nodeUpdates: Map<string, { type?: string; data?: Record<string, unknown> }>;
|
|
402
411
|
deletedNodeIds: Set<string>;
|
|
403
412
|
createdEdges: Array<{
|
|
@@ -414,6 +423,7 @@ interface EditState {
|
|
|
414
423
|
const createEmptyEditState = (): EditState => ({
|
|
415
424
|
positionChanges: new Map(),
|
|
416
425
|
dimensionChanges: new Map(),
|
|
426
|
+
textChanges: new Map(),
|
|
417
427
|
nodeUpdates: new Map(),
|
|
418
428
|
deletedNodeIds: new Set(),
|
|
419
429
|
createdEdges: [],
|
|
@@ -573,6 +583,7 @@ interface GraphRendererInnerProps {
|
|
|
573
583
|
onEditStateChange?: (editState: EditState) => void;
|
|
574
584
|
editStateRef: React.MutableRefObject<EditState>;
|
|
575
585
|
resetVisualStateRef: React.MutableRefObject<(() => void) | null>;
|
|
586
|
+
resetTextChangesVersionRef: React.MutableRefObject<(() => void) | null>;
|
|
576
587
|
undoRedoFunctionsRef: React.MutableRefObject<UndoRedoFunctionsRef | null>;
|
|
577
588
|
pushHistory: (entries: HistoryEntry[]) => void;
|
|
578
589
|
clearHistory: () => void;
|
|
@@ -629,6 +640,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
629
640
|
onEditStateChange,
|
|
630
641
|
editStateRef,
|
|
631
642
|
resetVisualStateRef,
|
|
643
|
+
resetTextChangesVersionRef,
|
|
632
644
|
undoRedoFunctionsRef,
|
|
633
645
|
pushHistory,
|
|
634
646
|
clearHistory,
|
|
@@ -652,6 +664,9 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
652
664
|
// Track shift key state for tooltip control
|
|
653
665
|
const [shiftKeyPressed, setShiftKeyPressed] = useState(false);
|
|
654
666
|
|
|
667
|
+
// Track text changes version to force re-renders when text is edited
|
|
668
|
+
const [textChangesVersion, setTextChangesVersion] = useState(0);
|
|
669
|
+
|
|
655
670
|
// Track if we're currently processing a node hide operation
|
|
656
671
|
const hidingNodeRef = useRef(false);
|
|
657
672
|
|
|
@@ -822,6 +837,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
822
837
|
return (
|
|
823
838
|
state.positionChanges.size > 0 ||
|
|
824
839
|
state.dimensionChanges.size > 0 ||
|
|
840
|
+
state.textChanges.size > 0 ||
|
|
825
841
|
state.nodeUpdates.size > 0 ||
|
|
826
842
|
state.deletedNodeIds.size > 0 ||
|
|
827
843
|
state.createdEdges.length > 0 ||
|
|
@@ -841,6 +857,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
841
857
|
console.log('[GraphRenderer] Edit state updated:', {
|
|
842
858
|
positionChanges: newState.positionChanges.size,
|
|
843
859
|
dimensionChanges: newState.dimensionChanges.size,
|
|
860
|
+
textChanges: newState.textChanges.size,
|
|
844
861
|
nodeUpdates: newState.nodeUpdates.size,
|
|
845
862
|
hasChanges,
|
|
846
863
|
});
|
|
@@ -882,6 +899,39 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
882
899
|
[editable, updateEditState, pushHistory]
|
|
883
900
|
);
|
|
884
901
|
|
|
902
|
+
// Handler for node text change - called from CustomNode via context
|
|
903
|
+
const handleNodeTextChange = useCallback(
|
|
904
|
+
(nodeId: string, text: string) => {
|
|
905
|
+
if (!editable) return;
|
|
906
|
+
|
|
907
|
+
// Capture before text for undo
|
|
908
|
+
// For text nodes, the text is in the canvas node's 'text' field
|
|
909
|
+
// For group nodes, it's in the 'label' field
|
|
910
|
+
// We need to look at the original canvas to get the before value
|
|
911
|
+
const beforeText = editStateRef.current.textChanges.get(nodeId) ?? '';
|
|
912
|
+
|
|
913
|
+
// Push to history
|
|
914
|
+
pushHistory([
|
|
915
|
+
{
|
|
916
|
+
type: 'text',
|
|
917
|
+
nodeId,
|
|
918
|
+
before: beforeText,
|
|
919
|
+
after: text,
|
|
920
|
+
},
|
|
921
|
+
]);
|
|
922
|
+
|
|
923
|
+
updateEditState((prev) => {
|
|
924
|
+
const newTextChanges = new Map(prev.textChanges);
|
|
925
|
+
newTextChanges.set(nodeId, text);
|
|
926
|
+
return { ...prev, textChanges: newTextChanges };
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
// Force re-render by incrementing version
|
|
930
|
+
setTextChangesVersion((v) => v + 1);
|
|
931
|
+
},
|
|
932
|
+
[editable, updateEditState, pushHistory]
|
|
933
|
+
);
|
|
934
|
+
|
|
885
935
|
// Handle toggling node hidden state (Cmd/Ctrl+click)
|
|
886
936
|
// This is exposed via context so CustomNode can call it on mousedown
|
|
887
937
|
// (mousedown works in edit mode where click is intercepted by drag handling)
|
|
@@ -961,10 +1011,11 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
961
1011
|
const graphEditContextValue = useMemo(
|
|
962
1012
|
() => ({
|
|
963
1013
|
onNodeResizeEnd: handleNodeResizeEnd,
|
|
1014
|
+
onNodeTextChange: handleNodeTextChange,
|
|
964
1015
|
onToggleNodeHidden: handleToggleNodeHidden,
|
|
965
1016
|
onHideUnconnectedNodes: handleHideUnconnectedNodes,
|
|
966
1017
|
}),
|
|
967
|
-
[handleNodeResizeEnd, handleToggleNodeHidden, handleHideUnconnectedNodes]
|
|
1018
|
+
[handleNodeResizeEnd, handleNodeTextChange, handleToggleNodeHidden, handleHideUnconnectedNodes]
|
|
968
1019
|
);
|
|
969
1020
|
|
|
970
1021
|
// ============================================
|
|
@@ -1647,6 +1698,8 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1647
1698
|
const animation = animationState.nodeAnimations[node.id];
|
|
1648
1699
|
// Apply any pending position changes
|
|
1649
1700
|
const pendingPosition = editStateRef.current.positionChanges.get(node.id);
|
|
1701
|
+
// Apply any pending text changes
|
|
1702
|
+
const pendingText = editStateRef.current.textChanges.get(node.id);
|
|
1650
1703
|
// Allow specific nodes to be draggable even when not in edit mode
|
|
1651
1704
|
const isDraggable = editable || draggableNodeIds?.has(node.id);
|
|
1652
1705
|
// When draggableNodeIds is provided, we need to explicitly control each node's draggability
|
|
@@ -1668,6 +1721,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1668
1721
|
data: {
|
|
1669
1722
|
...node.data,
|
|
1670
1723
|
editable,
|
|
1724
|
+
pendingText,
|
|
1671
1725
|
tooltipsEnabled: showTooltips,
|
|
1672
1726
|
shiftKeyPressed,
|
|
1673
1727
|
isHighlighted: highlightedNodeId === node.id,
|
|
@@ -1682,7 +1736,7 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1682
1736
|
} as CustomNodeData,
|
|
1683
1737
|
};
|
|
1684
1738
|
});
|
|
1685
|
-
}, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, activeNodeIds, editStateRef, shiftKeyPressed, selectedNodeIds, hiddenNodeIds, draggableNodeIds]);
|
|
1739
|
+
}, [localNodes, configuration, violations, animationState.nodeAnimations, editable, showTooltips, highlightedNodeId, activeNodeIds, editStateRef, shiftKeyPressed, selectedNodeIds, hiddenNodeIds, draggableNodeIds, textChangesVersion]);
|
|
1686
1740
|
|
|
1687
1741
|
const baseNodesKey = useMemo(() => {
|
|
1688
1742
|
return nodes
|
|
@@ -1747,6 +1801,24 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1747
1801
|
);
|
|
1748
1802
|
}, [editable, hiddenNodeIds]);
|
|
1749
1803
|
|
|
1804
|
+
// Sync pending text changes to local nodes when textChangesVersion changes (edit mode only)
|
|
1805
|
+
const prevTextChangesVersionRef = useRef(textChangesVersion);
|
|
1806
|
+
useEffect(() => {
|
|
1807
|
+
if (!editable) return;
|
|
1808
|
+
if (prevTextChangesVersionRef.current === textChangesVersion) return;
|
|
1809
|
+
prevTextChangesVersionRef.current = textChangesVersion;
|
|
1810
|
+
|
|
1811
|
+
setXyflowLocalNodes((nodes) =>
|
|
1812
|
+
nodes.map((n) => {
|
|
1813
|
+
const pendingText = editStateRef.current.textChanges.get(n.id);
|
|
1814
|
+
return {
|
|
1815
|
+
...n,
|
|
1816
|
+
data: { ...n.data, pendingText },
|
|
1817
|
+
};
|
|
1818
|
+
})
|
|
1819
|
+
);
|
|
1820
|
+
}, [editable, textChangesVersion]);
|
|
1821
|
+
|
|
1750
1822
|
// Also sync when entering edit mode or when base nodes change content
|
|
1751
1823
|
const prevEditableRef = useRef(editable);
|
|
1752
1824
|
useEffect(() => {
|
|
@@ -1865,6 +1937,18 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1865
1937
|
});
|
|
1866
1938
|
break;
|
|
1867
1939
|
|
|
1940
|
+
case 'text':
|
|
1941
|
+
// Restore previous text
|
|
1942
|
+
// Update edit state
|
|
1943
|
+
updateEditState((prev) => {
|
|
1944
|
+
const newTextChanges = new Map(prev.textChanges);
|
|
1945
|
+
newTextChanges.set(entry.nodeId, entry.before);
|
|
1946
|
+
return { ...prev, textChanges: newTextChanges };
|
|
1947
|
+
});
|
|
1948
|
+
// Force re-render
|
|
1949
|
+
setTextChangesVersion((v) => v + 1);
|
|
1950
|
+
break;
|
|
1951
|
+
|
|
1868
1952
|
case 'edgeCreate':
|
|
1869
1953
|
// Remove the created edge
|
|
1870
1954
|
setLocalEdges((edges) => edges.filter((e) => e.id !== entry.edge.id));
|
|
@@ -1930,6 +2014,18 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
1930
2014
|
});
|
|
1931
2015
|
break;
|
|
1932
2016
|
|
|
2017
|
+
case 'text':
|
|
2018
|
+
// Apply new text
|
|
2019
|
+
// Update edit state
|
|
2020
|
+
updateEditState((prev) => {
|
|
2021
|
+
const newTextChanges = new Map(prev.textChanges);
|
|
2022
|
+
newTextChanges.set(entry.nodeId, entry.after);
|
|
2023
|
+
return { ...prev, textChanges: newTextChanges };
|
|
2024
|
+
});
|
|
2025
|
+
// Force re-render
|
|
2026
|
+
setTextChangesVersion((v) => v + 1);
|
|
2027
|
+
break;
|
|
2028
|
+
|
|
1933
2029
|
case 'edgeCreate':
|
|
1934
2030
|
// Re-create the edge
|
|
1935
2031
|
setLocalEdges((edges) => [...edges, entry.edge]);
|
|
@@ -2314,6 +2410,13 @@ const GraphRendererInner: React.FC<GraphRendererInnerProps> = ({
|
|
|
2314
2410
|
};
|
|
2315
2411
|
}, [xyflowNodesBase, xyflowEdgesWithElk, onPendingChangesChange]);
|
|
2316
2412
|
|
|
2413
|
+
// Set the reset text changes version function for use by resetEditState
|
|
2414
|
+
useEffect(() => {
|
|
2415
|
+
resetTextChangesVersionRef.current = () => {
|
|
2416
|
+
setTextChangesVersion(0);
|
|
2417
|
+
};
|
|
2418
|
+
}, []);
|
|
2419
|
+
|
|
2317
2420
|
// Use local edges in edit mode, base edges otherwise
|
|
2318
2421
|
const xyflowEdges = editable ? xyflowLocalEdges : xyflowEdgesWithElk;
|
|
2319
2422
|
|
|
@@ -2823,6 +2926,9 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
2823
2926
|
// Ref to hold the reset visual state function - will be set after xyflowLocalNodes is defined
|
|
2824
2927
|
const resetVisualStateRef = useRef<(() => void) | null>(null);
|
|
2825
2928
|
|
|
2929
|
+
// Ref to hold the reset text changes version function - will be set by inner component
|
|
2930
|
+
const resetTextChangesVersionRef = useRef<(() => void) | null>(null);
|
|
2931
|
+
|
|
2826
2932
|
// Undo/redo management
|
|
2827
2933
|
const {
|
|
2828
2934
|
canUndo: canUndoState,
|
|
@@ -2855,6 +2961,12 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
2855
2961
|
dimensions,
|
|
2856
2962
|
})
|
|
2857
2963
|
),
|
|
2964
|
+
textChanges: Array.from(state.textChanges.entries()).map(
|
|
2965
|
+
([nodeId, text]) => ({
|
|
2966
|
+
nodeId,
|
|
2967
|
+
text,
|
|
2968
|
+
})
|
|
2969
|
+
),
|
|
2858
2970
|
nodeUpdates: Array.from(state.nodeUpdates.entries()).map(([nodeId, updates]) => ({
|
|
2859
2971
|
nodeId,
|
|
2860
2972
|
updates,
|
|
@@ -2871,6 +2983,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
2871
2983
|
hasChanges:
|
|
2872
2984
|
state.positionChanges.size > 0 ||
|
|
2873
2985
|
state.dimensionChanges.size > 0 ||
|
|
2986
|
+
state.textChanges.size > 0 ||
|
|
2874
2987
|
state.nodeUpdates.size > 0 ||
|
|
2875
2988
|
state.deletedNodeIds.size > 0 ||
|
|
2876
2989
|
state.createdEdges.length > 0 ||
|
|
@@ -2879,6 +2992,8 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
2879
2992
|
},
|
|
2880
2993
|
resetEditState: () => {
|
|
2881
2994
|
editStateRef.current = createEmptyEditState();
|
|
2995
|
+
// Reset text changes version to trigger re-render
|
|
2996
|
+
resetTextChangesVersionRef.current?.();
|
|
2882
2997
|
// Also reset visual state (node positions/dimensions) if available
|
|
2883
2998
|
resetVisualStateRef.current?.();
|
|
2884
2999
|
// Clear undo/redo history
|
|
@@ -2889,6 +3004,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
2889
3004
|
return (
|
|
2890
3005
|
state.positionChanges.size > 0 ||
|
|
2891
3006
|
state.dimensionChanges.size > 0 ||
|
|
3007
|
+
state.textChanges.size > 0 ||
|
|
2892
3008
|
state.nodeUpdates.size > 0 ||
|
|
2893
3009
|
state.deletedNodeIds.size > 0 ||
|
|
2894
3010
|
state.createdEdges.length > 0 ||
|
|
@@ -2988,6 +3104,7 @@ export const GraphRenderer = forwardRef<GraphRendererHandle, GraphRendererProps>
|
|
|
2988
3104
|
onPendingChangesChange={onPendingChangesChange}
|
|
2989
3105
|
editStateRef={editStateRef}
|
|
2990
3106
|
resetVisualStateRef={resetVisualStateRef}
|
|
3107
|
+
resetTextChangesVersionRef={resetTextChangesVersionRef}
|
|
2991
3108
|
undoRedoFunctionsRef={undoRedoFunctionsRef}
|
|
2992
3109
|
pushHistory={pushHistory}
|
|
2993
3110
|
clearHistory={clearHistory}
|
|
@@ -7,6 +7,7 @@ import { TooltipPortalContext } from '../contexts/TooltipPortalContext';
|
|
|
7
7
|
export interface OtelInfo {
|
|
8
8
|
kind: 'type' | 'service' | 'instance';
|
|
9
9
|
category?: string;
|
|
10
|
+
scope?: string;
|
|
10
11
|
files?: string[];
|
|
11
12
|
}
|
|
12
13
|
|
|
@@ -138,7 +139,7 @@ export const NodeTooltip: React.FC<NodeTooltipProps> = ({
|
|
|
138
139
|
display: 'flex',
|
|
139
140
|
alignItems: 'center',
|
|
140
141
|
gap: '6px',
|
|
141
|
-
marginBottom: description ? '6px' : 0,
|
|
142
|
+
marginBottom: description || otel.scope ? '6px' : 0,
|
|
142
143
|
}}
|
|
143
144
|
>
|
|
144
145
|
<span
|
|
@@ -163,6 +164,24 @@ export const NodeTooltip: React.FC<NodeTooltipProps> = ({
|
|
|
163
164
|
</div>
|
|
164
165
|
)}
|
|
165
166
|
|
|
167
|
+
{/* Scope */}
|
|
168
|
+
{otel?.scope && (
|
|
169
|
+
<div style={{
|
|
170
|
+
marginBottom: description ? '6px' : 0,
|
|
171
|
+
fontSize: theme.fontSizes[0],
|
|
172
|
+
fontFamily: theme.fonts.body,
|
|
173
|
+
color: 'rgba(255,255,255,0.8)'
|
|
174
|
+
}}>
|
|
175
|
+
<span style={{
|
|
176
|
+
color: 'rgba(255,255,255,0.6)',
|
|
177
|
+
fontWeight: theme.fontWeights.semibold
|
|
178
|
+
}}>
|
|
179
|
+
Scope:
|
|
180
|
+
</span>{' '}
|
|
181
|
+
<span style={{ fontFamily: 'monospace' }}>{otel.scope}</span>
|
|
182
|
+
</div>
|
|
183
|
+
)}
|
|
184
|
+
|
|
166
185
|
{/* Description - rendered as markdown */}
|
|
167
186
|
{description ? (
|
|
168
187
|
<div style={{ lineHeight: '1.4', color: 'rgba(255,255,255,0.9)' }}>
|
|
@@ -3,6 +3,8 @@ import React, { createContext, useContext } from 'react';
|
|
|
3
3
|
export interface GraphEditContextValue {
|
|
4
4
|
/** Called when a node resize operation completes */
|
|
5
5
|
onNodeResizeEnd?: (nodeId: string, dimensions: { width: number; height: number }) => void;
|
|
6
|
+
/** Called when node text is edited (for text and group nodes) */
|
|
7
|
+
onNodeTextChange?: (nodeId: string, text: string) => void;
|
|
6
8
|
/** Called to toggle node hidden state (Cmd/Ctrl+click) */
|
|
7
9
|
onToggleNodeHidden?: (nodeId: string) => void;
|
|
8
10
|
/** Called to hide all nodes not directly connected to the given node (Cmd/Ctrl+Shift+click) */
|
package/src/hooks/useUndoRedo.ts
CHANGED
|
@@ -26,6 +26,12 @@ export type HistoryEntry =
|
|
|
26
26
|
before: { width: number; height: number };
|
|
27
27
|
after: { width: number; height: number };
|
|
28
28
|
}
|
|
29
|
+
| {
|
|
30
|
+
type: 'text';
|
|
31
|
+
nodeId: string;
|
|
32
|
+
before: string;
|
|
33
|
+
after: string;
|
|
34
|
+
}
|
|
29
35
|
| {
|
|
30
36
|
type: 'nodeDelete';
|
|
31
37
|
nodeId: string;
|
package/src/nodes/CustomNode.tsx
CHANGED
|
@@ -28,6 +28,8 @@ export interface CustomNodeData extends Record<string, unknown> {
|
|
|
28
28
|
animationDuration?: number;
|
|
29
29
|
// Edit mode - shows larger connection handles
|
|
30
30
|
editable?: boolean;
|
|
31
|
+
// Pending text change (from inline editing, not yet saved)
|
|
32
|
+
pendingText?: string;
|
|
31
33
|
// Whether tooltips are enabled (defaults to true)
|
|
32
34
|
tooltipsEnabled?: boolean;
|
|
33
35
|
// Whether shift key is currently pressed (for tooltip control)
|
|
@@ -77,10 +79,13 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
|
|
|
77
79
|
|
|
78
80
|
// Fall through to legacy rendering for non-OTEL nodes
|
|
79
81
|
const { theme } = useTheme();
|
|
80
|
-
const { onNodeResizeEnd, onToggleNodeHidden, onHideUnconnectedNodes } = useGraphEdit();
|
|
82
|
+
const { onNodeResizeEnd, onNodeTextChange, onToggleNodeHidden, onHideUnconnectedNodes } = useGraphEdit();
|
|
81
83
|
const nodeId = useNodeId();
|
|
82
84
|
const [isHovered, setIsHovered] = useState(false);
|
|
85
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
86
|
+
const [editingText, setEditingText] = useState('');
|
|
83
87
|
const nodeRef = useRef<HTMLDivElement>(null);
|
|
88
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
84
89
|
const nodeProps = data;
|
|
85
90
|
const {
|
|
86
91
|
typeDefinition,
|
|
@@ -449,8 +454,8 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
|
|
|
449
454
|
// Use fillColor as the primary "color" for backwards compatibility
|
|
450
455
|
const color = fillColor;
|
|
451
456
|
|
|
452
|
-
// Get display name - use
|
|
453
|
-
const displayName = nodeProps.name;
|
|
457
|
+
// Get display name - use pending text if available, otherwise use name from props
|
|
458
|
+
const displayName = nodeProps.pendingText ?? nodeProps.name;
|
|
454
459
|
|
|
455
460
|
// Extract identifier based on node type (for display below the label)
|
|
456
461
|
// Supports: event.name, otel.spanPattern, otel.scope, otel.resourceMatch, boundary.direction
|
|
@@ -545,8 +550,58 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
|
|
|
545
550
|
|
|
546
551
|
const animationClass = getAnimationClass();
|
|
547
552
|
|
|
548
|
-
// Check if this is a group node
|
|
553
|
+
// Check if this is a group node or text node (canvas types that support inline editing)
|
|
549
554
|
const isGroup = nodeData.canvasType === 'group';
|
|
555
|
+
const isTextNode = nodeData.canvasType === 'text';
|
|
556
|
+
const isInlineEditable = (isGroup || isTextNode) && editable;
|
|
557
|
+
|
|
558
|
+
// Double-click handler for inline text editing
|
|
559
|
+
const lastClickTimeRef = useRef<number>(0);
|
|
560
|
+
const handleNodeDoubleClick = useCallback((event: React.MouseEvent) => {
|
|
561
|
+
if (!isInlineEditable || !nodeId || !onNodeTextChange) return;
|
|
562
|
+
|
|
563
|
+
event.stopPropagation();
|
|
564
|
+
event.preventDefault();
|
|
565
|
+
|
|
566
|
+
setEditingText(displayName || '');
|
|
567
|
+
setIsEditing(true);
|
|
568
|
+
|
|
569
|
+
// Focus the textarea after state update
|
|
570
|
+
setTimeout(() => {
|
|
571
|
+
textareaRef.current?.focus();
|
|
572
|
+
textareaRef.current?.select();
|
|
573
|
+
}, 0);
|
|
574
|
+
}, [isInlineEditable, nodeId, onNodeTextChange, displayName]);
|
|
575
|
+
|
|
576
|
+
// Single click tracking for double-click detection
|
|
577
|
+
const handleNodeClick = useCallback((event: React.MouseEvent) => {
|
|
578
|
+
const now = Date.now();
|
|
579
|
+
const timeSinceLastClick = now - lastClickTimeRef.current;
|
|
580
|
+
|
|
581
|
+
if (timeSinceLastClick < 300) {
|
|
582
|
+
// Double-click detected
|
|
583
|
+
handleNodeDoubleClick(event);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
lastClickTimeRef.current = now;
|
|
587
|
+
}, [handleNodeDoubleClick]);
|
|
588
|
+
|
|
589
|
+
// Save text changes
|
|
590
|
+
const handleSaveText = useCallback(() => {
|
|
591
|
+
if (!nodeId || !onNodeTextChange) return;
|
|
592
|
+
|
|
593
|
+
if (editingText !== displayName) {
|
|
594
|
+
onNodeTextChange(nodeId, editingText);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
setIsEditing(false);
|
|
598
|
+
}, [nodeId, onNodeTextChange, editingText, displayName]);
|
|
599
|
+
|
|
600
|
+
// Cancel text editing
|
|
601
|
+
const handleCancelEdit = useCallback(() => {
|
|
602
|
+
setIsEditing(false);
|
|
603
|
+
setEditingText('');
|
|
604
|
+
}, []);
|
|
550
605
|
|
|
551
606
|
// Shape-specific styles
|
|
552
607
|
const getShapeStyles = () => {
|
|
@@ -946,6 +1001,7 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
|
|
|
946
1001
|
ref={nodeRef}
|
|
947
1002
|
style={{ position: 'relative', width: '100%', height: '100%' }}
|
|
948
1003
|
onMouseDown={handleMouseDown}
|
|
1004
|
+
onClick={handleNodeClick}
|
|
949
1005
|
onMouseEnter={() => setIsHovered(true)}
|
|
950
1006
|
onMouseLeave={() => setIsHovered(false)}
|
|
951
1007
|
>
|
|
@@ -1018,6 +1074,57 @@ export const CustomNode: React.FC<NodeProps<Node<CustomNodeData>>> = (props) =>
|
|
|
1018
1074
|
)}
|
|
1019
1075
|
</div>
|
|
1020
1076
|
</div>
|
|
1077
|
+
{/* Inline text editor overlay */}
|
|
1078
|
+
{isEditing && isInlineEditable && (
|
|
1079
|
+
<div
|
|
1080
|
+
style={{
|
|
1081
|
+
position: 'absolute',
|
|
1082
|
+
top: 0,
|
|
1083
|
+
left: 0,
|
|
1084
|
+
width: '100%',
|
|
1085
|
+
height: '100%',
|
|
1086
|
+
zIndex: 1000,
|
|
1087
|
+
display: 'flex',
|
|
1088
|
+
flexDirection: 'column',
|
|
1089
|
+
padding: theme.space[1],
|
|
1090
|
+
boxSizing: 'border-box',
|
|
1091
|
+
}}
|
|
1092
|
+
onClick={(e) => e.stopPropagation()}
|
|
1093
|
+
>
|
|
1094
|
+
<textarea
|
|
1095
|
+
ref={textareaRef}
|
|
1096
|
+
value={editingText}
|
|
1097
|
+
onChange={(e) => setEditingText(e.target.value)}
|
|
1098
|
+
placeholder="Enter text..."
|
|
1099
|
+
style={{
|
|
1100
|
+
flex: 1,
|
|
1101
|
+
width: '100%',
|
|
1102
|
+
padding: theme.space[2],
|
|
1103
|
+
fontSize: theme.fontSizes[1],
|
|
1104
|
+
fontFamily: theme.fonts.body,
|
|
1105
|
+
color: theme.colors.text,
|
|
1106
|
+
backgroundColor: theme.colors.background,
|
|
1107
|
+
border: `3px solid ${theme.colors.primary}`,
|
|
1108
|
+
borderRadius: theme.radii[1],
|
|
1109
|
+
outline: 'none',
|
|
1110
|
+
resize: 'none',
|
|
1111
|
+
boxSizing: 'border-box',
|
|
1112
|
+
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.3)',
|
|
1113
|
+
}}
|
|
1114
|
+
onKeyDown={(e) => {
|
|
1115
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1116
|
+
e.preventDefault();
|
|
1117
|
+
handleSaveText();
|
|
1118
|
+
} else if (e.key === 'Escape') {
|
|
1119
|
+
e.preventDefault();
|
|
1120
|
+
handleCancelEdit();
|
|
1121
|
+
}
|
|
1122
|
+
}}
|
|
1123
|
+
onBlur={handleSaveText}
|
|
1124
|
+
/>
|
|
1125
|
+
</div>
|
|
1126
|
+
)}
|
|
1127
|
+
|
|
1021
1128
|
{tooltipsEnabled && (
|
|
1022
1129
|
<NodeTooltip
|
|
1023
1130
|
description={description}
|
|
@@ -243,7 +243,79 @@ export const TooltipVariants: StoryObj = {
|
|
|
243
243
|
</div>
|
|
244
244
|
<NodeTooltip
|
|
245
245
|
description="A specific running instance of a service"
|
|
246
|
-
otel={{ kind: 'instance', category: 'runtime' }}
|
|
246
|
+
otel={{ kind: 'instance', category: 'runtime', scope: 'auth-flow' }}
|
|
247
|
+
visible={true}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</ThemeProvider>
|
|
252
|
+
),
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Demonstrates tooltips with scope information.
|
|
257
|
+
* Shows how scope names are displayed in the tooltip.
|
|
258
|
+
*/
|
|
259
|
+
export const TooltipWithScope: StoryObj = {
|
|
260
|
+
render: () => (
|
|
261
|
+
<ThemeProvider theme={defaultEditorTheme}>
|
|
262
|
+
<div style={{ padding: '40px', display: 'flex', gap: '80px', flexWrap: 'wrap' }}>
|
|
263
|
+
<div style={{ position: 'relative', width: '200px', height: '180px' }}>
|
|
264
|
+
<div
|
|
265
|
+
style={{
|
|
266
|
+
padding: '20px',
|
|
267
|
+
border: '2px solid #DC2626',
|
|
268
|
+
borderRadius: '8px',
|
|
269
|
+
textAlign: 'center',
|
|
270
|
+
backgroundColor: '#DC2626',
|
|
271
|
+
color: 'white',
|
|
272
|
+
}}
|
|
273
|
+
>
|
|
274
|
+
Auth Event
|
|
275
|
+
</div>
|
|
276
|
+
<NodeTooltip
|
|
277
|
+
description="User authentication completed successfully"
|
|
278
|
+
otel={{ kind: 'type', category: 'event', scope: 'auth-flow' }}
|
|
279
|
+
visible={true}
|
|
280
|
+
/>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
<div style={{ position: 'relative', width: '200px', height: '180px' }}>
|
|
284
|
+
<div
|
|
285
|
+
style={{
|
|
286
|
+
padding: '20px',
|
|
287
|
+
border: '2px solid #2563EB',
|
|
288
|
+
borderRadius: '8px',
|
|
289
|
+
textAlign: 'center',
|
|
290
|
+
backgroundColor: '#2563EB',
|
|
291
|
+
color: 'white',
|
|
292
|
+
}}
|
|
293
|
+
>
|
|
294
|
+
Terminal Event
|
|
295
|
+
</div>
|
|
296
|
+
<NodeTooltip
|
|
297
|
+
description="Command executed in terminal session"
|
|
298
|
+
otel={{ kind: 'type', category: 'event', scope: 'terminal-activity' }}
|
|
299
|
+
visible={true}
|
|
300
|
+
/>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div style={{ position: 'relative', width: '200px', height: '180px' }}>
|
|
304
|
+
<div
|
|
305
|
+
style={{
|
|
306
|
+
padding: '20px',
|
|
307
|
+
border: '2px solid #16A34A',
|
|
308
|
+
borderRadius: '8px',
|
|
309
|
+
textAlign: 'center',
|
|
310
|
+
backgroundColor: '#16A34A',
|
|
311
|
+
color: 'white',
|
|
312
|
+
}}
|
|
313
|
+
>
|
|
314
|
+
Quality Event
|
|
315
|
+
</div>
|
|
316
|
+
<NodeTooltip
|
|
317
|
+
description="Code quality check completed with metrics"
|
|
318
|
+
otel={{ kind: 'type', category: 'event', scope: 'quality-panel' }}
|
|
247
319
|
visible={true}
|
|
248
320
|
/>
|
|
249
321
|
</div>
|