@principal-ai/principal-view-react 0.6.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/README.md +111 -0
- package/dist/components/ConfigurationSelector.d.ts +37 -0
- package/dist/components/ConfigurationSelector.d.ts.map +1 -0
- package/dist/components/ConfigurationSelector.js +67 -0
- package/dist/components/ConfigurationSelector.js.map +1 -0
- package/dist/components/EdgeInfoPanel.d.ts +16 -0
- package/dist/components/EdgeInfoPanel.d.ts.map +1 -0
- package/dist/components/EdgeInfoPanel.js +85 -0
- package/dist/components/EdgeInfoPanel.js.map +1 -0
- package/dist/components/EventLog.d.ts +20 -0
- package/dist/components/EventLog.d.ts.map +1 -0
- package/dist/components/EventLog.js +13 -0
- package/dist/components/EventLog.js.map +1 -0
- package/dist/components/EventLog.test.d.ts +2 -0
- package/dist/components/EventLog.test.d.ts.map +1 -0
- package/dist/components/EventLog.test.js +73 -0
- package/dist/components/EventLog.test.js.map +1 -0
- package/dist/components/GraphRenderer.d.ts +121 -0
- package/dist/components/GraphRenderer.d.ts.map +1 -0
- package/dist/components/GraphRenderer.js +809 -0
- package/dist/components/GraphRenderer.js.map +1 -0
- package/dist/components/GraphRenderer.test.d.ts +2 -0
- package/dist/components/GraphRenderer.test.d.ts.map +1 -0
- package/dist/components/GraphRenderer.test.js +88 -0
- package/dist/components/GraphRenderer.test.js.map +1 -0
- package/dist/components/MetricsDashboard.d.ts +14 -0
- package/dist/components/MetricsDashboard.d.ts.map +1 -0
- package/dist/components/MetricsDashboard.js +13 -0
- package/dist/components/MetricsDashboard.js.map +1 -0
- package/dist/components/NodeInfoPanel.d.ts +21 -0
- package/dist/components/NodeInfoPanel.d.ts.map +1 -0
- package/dist/components/NodeInfoPanel.js +217 -0
- package/dist/components/NodeInfoPanel.js.map +1 -0
- package/dist/edges/CustomEdge.d.ts +16 -0
- package/dist/edges/CustomEdge.d.ts.map +1 -0
- package/dist/edges/CustomEdge.js +200 -0
- package/dist/edges/CustomEdge.js.map +1 -0
- package/dist/edges/GenericEdge.d.ts +18 -0
- package/dist/edges/GenericEdge.d.ts.map +1 -0
- package/dist/edges/GenericEdge.js +14 -0
- package/dist/edges/GenericEdge.js.map +1 -0
- package/dist/hooks/usePathBasedEvents.d.ts +42 -0
- package/dist/hooks/usePathBasedEvents.d.ts.map +1 -0
- package/dist/hooks/usePathBasedEvents.js +122 -0
- package/dist/hooks/usePathBasedEvents.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/nodes/CustomNode.d.ts +18 -0
- package/dist/nodes/CustomNode.d.ts.map +1 -0
- package/dist/nodes/CustomNode.js +298 -0
- package/dist/nodes/CustomNode.js.map +1 -0
- package/dist/nodes/GenericNode.d.ts +20 -0
- package/dist/nodes/GenericNode.d.ts.map +1 -0
- package/dist/nodes/GenericNode.js +24 -0
- package/dist/nodes/GenericNode.js.map +1 -0
- package/dist/utils/animationMapping.d.ts +53 -0
- package/dist/utils/animationMapping.d.ts.map +1 -0
- package/dist/utils/animationMapping.js +133 -0
- package/dist/utils/animationMapping.js.map +1 -0
- package/dist/utils/graphConverter.d.ts +22 -0
- package/dist/utils/graphConverter.d.ts.map +1 -0
- package/dist/utils/graphConverter.js +176 -0
- package/dist/utils/graphConverter.js.map +1 -0
- package/dist/utils/iconResolver.d.ts +29 -0
- package/dist/utils/iconResolver.d.ts.map +1 -0
- package/dist/utils/iconResolver.js +68 -0
- package/dist/utils/iconResolver.js.map +1 -0
- package/package.json +61 -0
- package/src/components/ConfigurationSelector.tsx +147 -0
- package/src/components/EdgeInfoPanel.tsx +198 -0
- package/src/components/EventLog.test.tsx +85 -0
- package/src/components/EventLog.tsx +51 -0
- package/src/components/GraphRenderer.test.tsx +118 -0
- package/src/components/GraphRenderer.tsx +1222 -0
- package/src/components/MetricsDashboard.tsx +40 -0
- package/src/components/NodeInfoPanel.tsx +425 -0
- package/src/edges/CustomEdge.tsx +344 -0
- package/src/edges/GenericEdge.tsx +40 -0
- package/src/hooks/usePathBasedEvents.ts +182 -0
- package/src/index.ts +67 -0
- package/src/nodes/CustomNode.tsx +432 -0
- package/src/nodes/GenericNode.tsx +54 -0
- package/src/stories/AnimationWorkshop.stories.tsx +608 -0
- package/src/stories/EventDrivenAnimations.stories.tsx +499 -0
- package/src/stories/EventLog.stories.tsx +161 -0
- package/src/stories/GraphRenderer.stories.tsx +628 -0
- package/src/stories/Introduction.mdx +51 -0
- package/src/stories/MetricsDashboard.stories.tsx +227 -0
- package/src/stories/MultiConfig.stories.tsx +531 -0
- package/src/stories/MultiDirectionalConnections.stories.tsx +345 -0
- package/src/stories/NodeShapes.stories.tsx +769 -0
- package/src/utils/animationMapping.ts +170 -0
- package/src/utils/graphConverter.ts +218 -0
- package/src/utils/iconResolver.tsx +49 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { GraphMetrics } from '@principal-ai/principal-view-core';
|
|
3
|
+
|
|
4
|
+
export interface MetricsDashboardProps {
|
|
5
|
+
/** Current metrics */
|
|
6
|
+
metrics: GraphMetrics;
|
|
7
|
+
|
|
8
|
+
/** Optional class name */
|
|
9
|
+
className?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Metrics dashboard component for displaying graph statistics
|
|
14
|
+
* TODO: Implement full metrics visualization
|
|
15
|
+
*/
|
|
16
|
+
export const MetricsDashboard: React.FC<MetricsDashboardProps> = ({ metrics, className }) => {
|
|
17
|
+
return (
|
|
18
|
+
<div className={className} style={{ padding: '20px', border: '1px solid #ccc' }}>
|
|
19
|
+
<h3>Metrics Dashboard (TODO)</h3>
|
|
20
|
+
<div>
|
|
21
|
+
<p>Total Nodes: {metrics.nodes.total}</p>
|
|
22
|
+
<p>Total Edges: {metrics.edges.total}</p>
|
|
23
|
+
<p>Total Events: {metrics.events.total}</p>
|
|
24
|
+
<p>Violations: {metrics.validation.violations}</p>
|
|
25
|
+
<p>Health Score: {metrics.validation.healthScore}</p>
|
|
26
|
+
</div>
|
|
27
|
+
<div>
|
|
28
|
+
<strong>TODO:</strong>
|
|
29
|
+
<ul>
|
|
30
|
+
<li>Add visual charts/graphs</li>
|
|
31
|
+
<li>Show breakdown by node type</li>
|
|
32
|
+
<li>Show breakdown by edge type</li>
|
|
33
|
+
<li>Show event rate over time</li>
|
|
34
|
+
<li>Add health score indicator</li>
|
|
35
|
+
<li>Add performance metrics</li>
|
|
36
|
+
</ul>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
};
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import type { NodeState, NodeTypeDefinition } from '@principal-ai/principal-view-core';
|
|
3
|
+
import { resolveIcon } from '../utils/iconResolver';
|
|
4
|
+
|
|
5
|
+
// Common icons for the icon selector
|
|
6
|
+
const COMMON_ICONS = [
|
|
7
|
+
'Settings', 'Database', 'Package', 'Server', 'Cloud', 'Globe',
|
|
8
|
+
'File', 'Folder', 'Code', 'Terminal', 'Cpu', 'HardDrive',
|
|
9
|
+
'Network', 'Wifi', 'Lock', 'Unlock', 'Key', 'Shield',
|
|
10
|
+
'User', 'Users', 'Mail', 'MessageSquare', 'Bell', 'Calendar',
|
|
11
|
+
'Clock', 'Timer', 'Zap', 'Activity', 'BarChart', 'PieChart',
|
|
12
|
+
'CheckCircle', 'XCircle', 'AlertCircle', 'Info', 'HelpCircle',
|
|
13
|
+
'Play', 'Pause', 'Square', 'Circle', 'Triangle', 'Hexagon',
|
|
14
|
+
'Box', 'Layers', 'GitBranch', 'GitCommit', 'GitMerge', 'GitPullRequest',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export interface NodeInfoPanelProps {
|
|
18
|
+
node: NodeState;
|
|
19
|
+
typeDefinition: NodeTypeDefinition;
|
|
20
|
+
/** Available node types for the type selector */
|
|
21
|
+
availableNodeTypes?: Record<string, NodeTypeDefinition>;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
/** Optional callback to delete the node. If not provided, delete button is hidden. */
|
|
24
|
+
onDelete?: (nodeId: string) => void;
|
|
25
|
+
/** Optional callback to update the node. If not provided, edit fields are disabled. */
|
|
26
|
+
onUpdate?: (nodeId: string, updates: { type?: string; data?: Record<string, unknown> }) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Panel that displays information about a selected node with optional editing
|
|
31
|
+
*/
|
|
32
|
+
export const NodeInfoPanel: React.FC<NodeInfoPanelProps> = ({
|
|
33
|
+
node,
|
|
34
|
+
typeDefinition,
|
|
35
|
+
availableNodeTypes,
|
|
36
|
+
onClose,
|
|
37
|
+
onDelete,
|
|
38
|
+
onUpdate,
|
|
39
|
+
}) => {
|
|
40
|
+
const color = typeDefinition?.color || '#888';
|
|
41
|
+
const canEdit = Boolean(onUpdate);
|
|
42
|
+
|
|
43
|
+
// Local state for editing
|
|
44
|
+
const [editingName, setEditingName] = useState(false);
|
|
45
|
+
const [nameValue, setNameValue] = useState('');
|
|
46
|
+
const [showIconPicker, setShowIconPicker] = useState(false);
|
|
47
|
+
|
|
48
|
+
// Current icon - either from node data override or type definition
|
|
49
|
+
const currentIcon = (node.data?.icon as string) || typeDefinition?.icon;
|
|
50
|
+
|
|
51
|
+
// Find the name field from data schema
|
|
52
|
+
const nameField = typeDefinition?.dataSchema
|
|
53
|
+
? Object.entries(typeDefinition.dataSchema).find(([, schema]) => schema.displayInLabel)?.[0]
|
|
54
|
+
: null;
|
|
55
|
+
|
|
56
|
+
// Initialize name value when node changes
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (nameField && node.data?.[nameField]) {
|
|
59
|
+
setNameValue(String(node.data[nameField]));
|
|
60
|
+
}
|
|
61
|
+
}, [node.id, nameField, node.data]);
|
|
62
|
+
|
|
63
|
+
// Get fields to display based on dataSchema
|
|
64
|
+
const displayFields = typeDefinition?.dataSchema
|
|
65
|
+
? Object.entries(typeDefinition.dataSchema)
|
|
66
|
+
.filter(([, schema]) => schema.displayInLabel)
|
|
67
|
+
.map(([field]) => ({
|
|
68
|
+
field,
|
|
69
|
+
label: field,
|
|
70
|
+
value: node.data?.[field],
|
|
71
|
+
}))
|
|
72
|
+
: [];
|
|
73
|
+
|
|
74
|
+
// Always show basic node data if no schema is defined
|
|
75
|
+
const hasSchemaFields = displayFields.length > 0;
|
|
76
|
+
const nodeDataEntries = node.data ? Object.entries(node.data).filter(([key]) => key !== 'icon') : [];
|
|
77
|
+
|
|
78
|
+
const handleNameSave = () => {
|
|
79
|
+
if (onUpdate && nameField && nameValue !== node.data?.[nameField]) {
|
|
80
|
+
onUpdate(node.id, {
|
|
81
|
+
data: { ...node.data, [nameField]: nameValue },
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
setEditingName(false);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const handleTypeChange = (newType: string) => {
|
|
88
|
+
if (onUpdate && newType !== node.type) {
|
|
89
|
+
onUpdate(node.id, { type: newType });
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const handleIconSelect = (iconName: string) => {
|
|
94
|
+
if (onUpdate) {
|
|
95
|
+
onUpdate(node.id, {
|
|
96
|
+
data: { ...node.data, icon: iconName },
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
setShowIconPicker(false);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
style={{
|
|
105
|
+
position: 'absolute',
|
|
106
|
+
top: '60px',
|
|
107
|
+
right: '20px',
|
|
108
|
+
backgroundColor: 'white',
|
|
109
|
+
borderRadius: '8px',
|
|
110
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
111
|
+
padding: '16px',
|
|
112
|
+
minWidth: '250px',
|
|
113
|
+
maxWidth: '350px',
|
|
114
|
+
zIndex: 1000,
|
|
115
|
+
}}
|
|
116
|
+
>
|
|
117
|
+
{/* Header */}
|
|
118
|
+
<div style={{
|
|
119
|
+
display: 'flex',
|
|
120
|
+
justifyContent: 'space-between',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
marginBottom: '12px',
|
|
123
|
+
paddingBottom: '8px',
|
|
124
|
+
borderBottom: `2px solid ${color}`,
|
|
125
|
+
}}>
|
|
126
|
+
<div style={{ fontWeight: 'bold', fontSize: '14px' }}>
|
|
127
|
+
Node Information
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
onClick={onClose}
|
|
131
|
+
style={{
|
|
132
|
+
border: 'none',
|
|
133
|
+
background: 'none',
|
|
134
|
+
cursor: 'pointer',
|
|
135
|
+
fontSize: '18px',
|
|
136
|
+
color: '#666',
|
|
137
|
+
padding: '0',
|
|
138
|
+
width: '24px',
|
|
139
|
+
height: '24px',
|
|
140
|
+
display: 'flex',
|
|
141
|
+
alignItems: 'center',
|
|
142
|
+
justifyContent: 'center',
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
×
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{/* Icon Selector */}
|
|
150
|
+
<div style={{ marginBottom: '12px' }}>
|
|
151
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>
|
|
152
|
+
Icon
|
|
153
|
+
</div>
|
|
154
|
+
<div style={{ position: 'relative' }}>
|
|
155
|
+
<button
|
|
156
|
+
onClick={() => canEdit && setShowIconPicker(!showIconPicker)}
|
|
157
|
+
disabled={!canEdit}
|
|
158
|
+
style={{
|
|
159
|
+
display: 'flex',
|
|
160
|
+
alignItems: 'center',
|
|
161
|
+
gap: '8px',
|
|
162
|
+
padding: '6px 10px',
|
|
163
|
+
backgroundColor: '#f5f5f5',
|
|
164
|
+
border: canEdit ? '1px dashed #ccc' : '1px solid #eee',
|
|
165
|
+
borderRadius: '4px',
|
|
166
|
+
cursor: canEdit ? 'pointer' : 'default',
|
|
167
|
+
fontSize: '12px',
|
|
168
|
+
width: '100%',
|
|
169
|
+
justifyContent: 'flex-start',
|
|
170
|
+
}}
|
|
171
|
+
>
|
|
172
|
+
<span style={{ display: 'flex', alignItems: 'center' }}>
|
|
173
|
+
{resolveIcon(currentIcon, 18)}
|
|
174
|
+
</span>
|
|
175
|
+
<span>{currentIcon || 'No icon'}</span>
|
|
176
|
+
{canEdit && <span style={{ marginLeft: 'auto', color: '#999', fontSize: '10px' }}>✎</span>}
|
|
177
|
+
</button>
|
|
178
|
+
|
|
179
|
+
{/* Icon Picker Dropdown */}
|
|
180
|
+
{showIconPicker && (
|
|
181
|
+
<div
|
|
182
|
+
style={{
|
|
183
|
+
position: 'absolute',
|
|
184
|
+
top: '100%',
|
|
185
|
+
left: 0,
|
|
186
|
+
right: 0,
|
|
187
|
+
marginTop: '4px',
|
|
188
|
+
backgroundColor: 'white',
|
|
189
|
+
border: '1px solid #ddd',
|
|
190
|
+
borderRadius: '4px',
|
|
191
|
+
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
|
|
192
|
+
padding: '8px',
|
|
193
|
+
maxHeight: '200px',
|
|
194
|
+
overflowY: 'auto',
|
|
195
|
+
zIndex: 1001,
|
|
196
|
+
}}
|
|
197
|
+
>
|
|
198
|
+
<div
|
|
199
|
+
style={{
|
|
200
|
+
display: 'grid',
|
|
201
|
+
gridTemplateColumns: 'repeat(6, 1fr)',
|
|
202
|
+
gap: '4px',
|
|
203
|
+
}}
|
|
204
|
+
>
|
|
205
|
+
{COMMON_ICONS.map(iconName => (
|
|
206
|
+
<button
|
|
207
|
+
key={iconName}
|
|
208
|
+
onClick={() => handleIconSelect(iconName)}
|
|
209
|
+
title={iconName}
|
|
210
|
+
style={{
|
|
211
|
+
padding: '6px',
|
|
212
|
+
border: currentIcon === iconName ? `2px solid ${color}` : '1px solid #eee',
|
|
213
|
+
borderRadius: '4px',
|
|
214
|
+
backgroundColor: currentIcon === iconName ? '#f0f7ff' : 'white',
|
|
215
|
+
cursor: 'pointer',
|
|
216
|
+
display: 'flex',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
justifyContent: 'center',
|
|
219
|
+
}}
|
|
220
|
+
>
|
|
221
|
+
{resolveIcon(iconName, 16)}
|
|
222
|
+
</button>
|
|
223
|
+
))}
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
|
|
230
|
+
{/* Node Type - Editable if availableNodeTypes provided */}
|
|
231
|
+
<div style={{ marginBottom: '12px' }}>
|
|
232
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>
|
|
233
|
+
Type
|
|
234
|
+
</div>
|
|
235
|
+
{canEdit && availableNodeTypes && Object.keys(availableNodeTypes).length > 1 ? (
|
|
236
|
+
<select
|
|
237
|
+
value={node.type}
|
|
238
|
+
onChange={(e) => handleTypeChange(e.target.value)}
|
|
239
|
+
style={{
|
|
240
|
+
fontSize: '12px',
|
|
241
|
+
padding: '4px 8px',
|
|
242
|
+
borderRadius: '4px',
|
|
243
|
+
border: '1px solid #ccc',
|
|
244
|
+
backgroundColor: 'white',
|
|
245
|
+
cursor: 'pointer',
|
|
246
|
+
width: '100%',
|
|
247
|
+
}}
|
|
248
|
+
>
|
|
249
|
+
{Object.entries(availableNodeTypes).map(([typeName, typeDef]) => (
|
|
250
|
+
<option key={typeName} value={typeName}>
|
|
251
|
+
{typeName} ({typeDef.shape})
|
|
252
|
+
</option>
|
|
253
|
+
))}
|
|
254
|
+
</select>
|
|
255
|
+
) : (
|
|
256
|
+
<div style={{
|
|
257
|
+
fontSize: '12px',
|
|
258
|
+
padding: '4px 8px',
|
|
259
|
+
backgroundColor: color,
|
|
260
|
+
color: 'white',
|
|
261
|
+
borderRadius: '4px',
|
|
262
|
+
display: 'inline-block',
|
|
263
|
+
}}>
|
|
264
|
+
{node.type}
|
|
265
|
+
</div>
|
|
266
|
+
)}
|
|
267
|
+
</div>
|
|
268
|
+
|
|
269
|
+
{/* Node State */}
|
|
270
|
+
{node.state && (
|
|
271
|
+
<div style={{ marginBottom: '12px' }}>
|
|
272
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>
|
|
273
|
+
State
|
|
274
|
+
</div>
|
|
275
|
+
<div style={{
|
|
276
|
+
fontSize: '12px',
|
|
277
|
+
padding: '4px 8px',
|
|
278
|
+
backgroundColor: typeDefinition?.states?.[node.state]?.color || '#888',
|
|
279
|
+
color: 'white',
|
|
280
|
+
borderRadius: '4px',
|
|
281
|
+
display: 'inline-block',
|
|
282
|
+
}}>
|
|
283
|
+
{typeDefinition?.states?.[node.state]?.label || node.state}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{/* Editable Name Field */}
|
|
289
|
+
{nameField && (
|
|
290
|
+
<div style={{ marginBottom: '12px' }}>
|
|
291
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '4px' }}>
|
|
292
|
+
Name
|
|
293
|
+
</div>
|
|
294
|
+
{canEdit && editingName ? (
|
|
295
|
+
<div style={{ display: 'flex', gap: '4px' }}>
|
|
296
|
+
<input
|
|
297
|
+
type="text"
|
|
298
|
+
value={nameValue}
|
|
299
|
+
onChange={(e) => setNameValue(e.target.value)}
|
|
300
|
+
onKeyDown={(e) => {
|
|
301
|
+
if (e.key === 'Enter') handleNameSave();
|
|
302
|
+
if (e.key === 'Escape') setEditingName(false);
|
|
303
|
+
}}
|
|
304
|
+
autoFocus
|
|
305
|
+
style={{
|
|
306
|
+
flex: 1,
|
|
307
|
+
fontSize: '12px',
|
|
308
|
+
padding: '4px 8px',
|
|
309
|
+
borderRadius: '4px',
|
|
310
|
+
border: '1px solid #4A90E2',
|
|
311
|
+
outline: 'none',
|
|
312
|
+
}}
|
|
313
|
+
/>
|
|
314
|
+
<button
|
|
315
|
+
onClick={handleNameSave}
|
|
316
|
+
style={{
|
|
317
|
+
padding: '4px 8px',
|
|
318
|
+
backgroundColor: '#4A90E2',
|
|
319
|
+
color: 'white',
|
|
320
|
+
border: 'none',
|
|
321
|
+
borderRadius: '4px',
|
|
322
|
+
cursor: 'pointer',
|
|
323
|
+
fontSize: '11px',
|
|
324
|
+
}}
|
|
325
|
+
>
|
|
326
|
+
Save
|
|
327
|
+
</button>
|
|
328
|
+
</div>
|
|
329
|
+
) : (
|
|
330
|
+
<div
|
|
331
|
+
onClick={() => canEdit && setEditingName(true)}
|
|
332
|
+
style={{
|
|
333
|
+
fontSize: '12px',
|
|
334
|
+
padding: '4px 8px',
|
|
335
|
+
backgroundColor: '#f5f5f5',
|
|
336
|
+
borderRadius: '4px',
|
|
337
|
+
cursor: canEdit ? 'pointer' : 'default',
|
|
338
|
+
border: canEdit ? '1px dashed #ccc' : 'none',
|
|
339
|
+
}}
|
|
340
|
+
title={canEdit ? 'Click to edit' : undefined}
|
|
341
|
+
>
|
|
342
|
+
{node.data?.[nameField] ?? '-'}
|
|
343
|
+
{canEdit && <span style={{ marginLeft: '8px', color: '#999', fontSize: '10px' }}>✎</span>}
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
</div>
|
|
347
|
+
)}
|
|
348
|
+
|
|
349
|
+
{/* Display other schema-defined fields (non-editable for now) */}
|
|
350
|
+
{hasSchemaFields && displayFields.filter(f => f.field !== nameField).length > 0 && (
|
|
351
|
+
<div style={{ marginBottom: '12px' }}>
|
|
352
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '8px', fontWeight: 'bold' }}>
|
|
353
|
+
Properties
|
|
354
|
+
</div>
|
|
355
|
+
{displayFields.filter(f => f.field !== nameField).map(({ field, label, value }) => (
|
|
356
|
+
<div key={field} style={{ marginBottom: '8px' }}>
|
|
357
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '2px' }}>
|
|
358
|
+
{label}
|
|
359
|
+
</div>
|
|
360
|
+
<div style={{ fontSize: '12px', color: '#333' }}>
|
|
361
|
+
{value !== undefined && value !== null
|
|
362
|
+
? typeof value === 'object'
|
|
363
|
+
? JSON.stringify(value, null, 2)
|
|
364
|
+
: String(value)
|
|
365
|
+
: '-'}
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
))}
|
|
369
|
+
</div>
|
|
370
|
+
)}
|
|
371
|
+
|
|
372
|
+
{/* Show all node data if no schema is defined */}
|
|
373
|
+
{!hasSchemaFields && nodeDataEntries.length > 0 && (
|
|
374
|
+
<div style={{ marginBottom: '12px' }}>
|
|
375
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '8px', fontWeight: 'bold' }}>
|
|
376
|
+
Data
|
|
377
|
+
</div>
|
|
378
|
+
{nodeDataEntries.map(([key, value]) => (
|
|
379
|
+
<div key={key} style={{ marginBottom: '8px' }}>
|
|
380
|
+
<div style={{ fontSize: '10px', color: '#666', marginBottom: '2px' }}>
|
|
381
|
+
{key}
|
|
382
|
+
</div>
|
|
383
|
+
<div style={{ fontSize: '12px', color: '#333', wordBreak: 'break-word' }}>
|
|
384
|
+
{value !== undefined && value !== null
|
|
385
|
+
? typeof value === 'object'
|
|
386
|
+
? JSON.stringify(value, null, 2)
|
|
387
|
+
: String(value)
|
|
388
|
+
: '-'}
|
|
389
|
+
</div>
|
|
390
|
+
</div>
|
|
391
|
+
))}
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
|
|
395
|
+
{/* Metadata */}
|
|
396
|
+
<div style={{ fontSize: '10px', color: '#999', marginTop: '12px', paddingTop: '8px', borderTop: '1px solid #eee' }}>
|
|
397
|
+
ID: {node.id}
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
{/* Delete Button */}
|
|
401
|
+
{onDelete && (
|
|
402
|
+
<button
|
|
403
|
+
onClick={() => {
|
|
404
|
+
onDelete(node.id);
|
|
405
|
+
onClose();
|
|
406
|
+
}}
|
|
407
|
+
style={{
|
|
408
|
+
marginTop: '12px',
|
|
409
|
+
width: '100%',
|
|
410
|
+
padding: '8px 12px',
|
|
411
|
+
backgroundColor: '#dc3545',
|
|
412
|
+
color: 'white',
|
|
413
|
+
border: 'none',
|
|
414
|
+
borderRadius: '4px',
|
|
415
|
+
cursor: 'pointer',
|
|
416
|
+
fontSize: '12px',
|
|
417
|
+
fontWeight: 'bold',
|
|
418
|
+
}}
|
|
419
|
+
>
|
|
420
|
+
Delete Node
|
|
421
|
+
</button>
|
|
422
|
+
)}
|
|
423
|
+
</div>
|
|
424
|
+
);
|
|
425
|
+
};
|