@principal-ai/subsystems-react 0.17.0 → 0.19.0

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 (108) hide show
  1. package/README.md +11 -85
  2. package/dist/index.d.ts +7 -44
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +6 -32
  5. package/dist/index.js.map +1 -1
  6. package/dist/subsystem/SubsystemCarousel.d.ts +36 -0
  7. package/dist/subsystem/SubsystemCarousel.d.ts.map +1 -0
  8. package/dist/subsystem/SubsystemCarousel.js +163 -0
  9. package/dist/subsystem/SubsystemCarousel.js.map +1 -0
  10. package/dist/subsystem/SubsystemComponentGraph.d.ts +25 -0
  11. package/dist/subsystem/SubsystemComponentGraph.d.ts.map +1 -1
  12. package/dist/subsystem/SubsystemComponentGraph.js +329 -68
  13. package/dist/subsystem/SubsystemComponentGraph.js.map +1 -1
  14. package/package.json +4 -4
  15. package/src/index.ts +9 -199
  16. package/src/subsystem/SubsystemComponentGraph.tsx +396 -50
  17. package/src/components/ConfigurationSelector.tsx +0 -133
  18. package/src/components/GraphRenderer.tsx +0 -3195
  19. package/src/components/MultiCanvasRenderer.tsx +0 -295
  20. package/src/components/NodeTooltip.tsx +0 -279
  21. package/src/components/PendingChanges.test.tsx +0 -375
  22. package/src/components/SelectionSidebar.tsx +0 -352
  23. package/src/components/SequenceDiagramRenderer.tsx +0 -1156
  24. package/src/components/WorkflowSequenceDiagram.tsx +0 -270
  25. package/src/components/dashboard/DashboardRenderer.tsx +0 -317
  26. package/src/components/dashboard/MetricPanel.tsx +0 -254
  27. package/src/components/dashboard/MockDataProvider.ts +0 -330
  28. package/src/components/dashboard/components/BarChart.tsx +0 -299
  29. package/src/components/dashboard/components/LineChart.tsx +0 -279
  30. package/src/components/dashboard/components/MetricCard.tsx +0 -270
  31. package/src/components/dashboard/components/SourceLink.tsx +0 -63
  32. package/src/components/dashboard/components/TimeRangeSelector.tsx +0 -280
  33. package/src/components/dashboard/components/index.ts +0 -5
  34. package/src/components/dashboard/index.ts +0 -47
  35. package/src/components/dashboard/types.ts +0 -126
  36. package/src/components/state-view/PipelineView.tsx +0 -347
  37. package/src/components/state-view/index.ts +0 -14
  38. package/src/components/state-view/types.ts +0 -261
  39. package/src/components/state-view/useStateView.ts +0 -205
  40. package/src/contexts/GraphEditContext.tsx +0 -27
  41. package/src/contexts/TooltipPortalContext.ts +0 -8
  42. package/src/edges/CustomEdge.tsx +0 -402
  43. package/src/edges/GenericEdge.tsx +0 -36
  44. package/src/hooks/usePathBasedEvents.ts +0 -192
  45. package/src/hooks/useSequenceLayout.ts +0 -543
  46. package/src/hooks/useUndoRedo.ts +0 -144
  47. package/src/nodes/CustomNode.tsx +0 -1231
  48. package/src/nodes/GenericNode.tsx +0 -55
  49. package/src/nodes/otel/OtelBoundaryNode.tsx +0 -224
  50. package/src/nodes/otel/OtelEventNode.tsx +0 -225
  51. package/src/nodes/otel/OtelResourceNode.tsx +0 -251
  52. package/src/nodes/otel/OtelScopeNode.tsx +0 -221
  53. package/src/nodes/otel/OtelSpanConventionNode.tsx +0 -295
  54. package/src/nodes/otel/index.ts +0 -23
  55. package/src/nodes/otel/shared/NodeBadges.tsx +0 -295
  56. package/src/nodes/otel/shared/NodeContent.tsx +0 -204
  57. package/src/nodes/otel/shared/index.ts +0 -8
  58. package/src/nodes/otel/shared/types.ts +0 -138
  59. package/src/nodes/otel/shared/useNodeBehavior.ts +0 -114
  60. package/src/stories/Audit/BoundaryNodes.stories.tsx +0 -385
  61. package/src/stories/Audit/CanvasEdgeTypes.stories.tsx +0 -1448
  62. package/src/stories/Audit/CanvasNodeTypes.stories.tsx +0 -1049
  63. package/src/stories/Audit/ColorPriority.stories.tsx +0 -895
  64. package/src/stories/Audit/DiamondBadges.stories.tsx +0 -629
  65. package/src/stories/Audit/NodeDimensionsTesting.stories.tsx +0 -724
  66. package/src/stories/Audit/NodeFieldsAudit.stories.tsx +0 -996
  67. package/src/stories/Audit/NodeFontSizeTesting.stories.tsx +0 -715
  68. package/src/stories/Audit/NodeShapes.stories.tsx +0 -1208
  69. package/src/stories/Audit/WideNodesTesting.stories.tsx +0 -1045
  70. package/src/stories/Components/DashboardRenderer.stories.tsx +0 -263
  71. package/src/stories/Components/GraphRenderer.stories.tsx +0 -3236
  72. package/src/stories/Components/MultiCanvasRenderer.stories.tsx +0 -370
  73. package/src/stories/Components/SequenceDiagram.stories.tsx +0 -255
  74. package/src/stories/Components/StateView.stories.tsx +0 -417
  75. package/src/stories/Components/TraceViewer.stories.tsx +0 -442
  76. package/src/stories/Components/WorkflowSequenceDiagram.stories.tsx +0 -802
  77. package/src/stories/Features/AnimationWorkshop.stories.tsx +0 -727
  78. package/src/stories/Features/ChangeTypeVisual.stories.tsx +0 -67
  79. package/src/stories/Features/EventDrivenAnimations.stories.tsx +0 -513
  80. package/src/stories/Features/MultiConfig.stories.tsx +0 -539
  81. package/src/stories/Layout/ElkEdgeRouting.stories.tsx +0 -539
  82. package/src/stories/Layout/GraphOrientation.stories.tsx +0 -500
  83. package/src/stories/Layout/MultiDirectionalConnections.stories.tsx +0 -352
  84. package/src/stories/Nodes/NodeDefinitionComparison.stories.tsx +0 -560
  85. package/src/stories/Nodes/NodeNameFallbacks.stories.tsx +0 -491
  86. package/src/stories/Nodes/NumericColors.stories.tsx +0 -440
  87. package/src/stories/OTEL/EventNameDisplay.stories.tsx +0 -694
  88. package/src/stories/OTEL/EventNameFallback.stories.tsx +0 -578
  89. package/src/stories/OTEL/OtelComponents.stories.tsx +0 -422
  90. package/src/stories/OTEL/OtelNodeTypesPrototype.stories.tsx +0 -806
  91. package/src/stories/OTEL/RawEventCards.stories.tsx +0 -57
  92. package/src/stories/OTEL/SpanBadges.stories.tsx +0 -840
  93. package/src/stories/OTEL/ValidationEventsCanvas.stories.tsx +0 -209
  94. package/src/stories/RealWorld/BacklogMdCanvases.stories.tsx +0 -721
  95. package/src/stories/RealWorld/FileCitySequence.stories.tsx +0 -516
  96. package/src/stories/RealWorld/RealCanvasFiles.stories.tsx +0 -336
  97. package/src/stories/__traces__/test-run.canvas.json +0 -241
  98. package/src/stories/data/activity-feed-analytics.dashboard.json +0 -300
  99. package/src/stories/data/file-city-images-transformed.otel.canvas.json +0 -295
  100. package/src/stories/data/file-city-workflows.json +0 -269
  101. package/src/stories/data/graph-converter-test-execution.json +0 -443
  102. package/src/stories/data/graph-converter-test.workflow.json +0 -105
  103. package/src/stories/data/graph-converter-validated-execution.json +0 -58
  104. package/src/utils/animationMapping.ts +0 -166
  105. package/src/utils/canvasBounds.ts +0 -146
  106. package/src/utils/graphConverter.ts +0 -148
  107. package/src/utils/iconResolver.tsx +0 -53
  108. package/src/utils/orientationUtils.ts +0 -73
@@ -1,3236 +0,0 @@
1
- import React from 'react';
2
- import type { Meta, StoryObj } from '@storybook/react';
3
- import '@xyflow/react/dist/style.css';
4
- import { GraphRenderer } from '../../components/GraphRenderer';
5
- import type { GraphRendererHandle, PendingChanges } from '../../components/GraphRenderer';
6
- import type { ExtendedCanvas, PVEventSchema } from '@principal-ai/subsystems-core';
7
- import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
8
-
9
- const meta = {
10
- title: 'Components/GraphRenderer',
11
- component: GraphRenderer,
12
- parameters: {
13
- layout: 'fullscreen',
14
- },
15
- tags: ['autodocs'],
16
- decorators: [
17
- (Story) => (
18
- <ThemeProvider theme={defaultEditorTheme}>
19
- <Story />
20
- </ThemeProvider>
21
- ),
22
- ],
23
- } satisfies Meta<typeof GraphRenderer>;
24
-
25
- export default meta;
26
- type Story = StoryObj<typeof meta>;
27
-
28
- // ============================================================================
29
- // Sample Canvas Documents
30
- // ============================================================================
31
-
32
- /**
33
- * Basic canvas with process and data nodes
34
- */
35
- const sampleCanvas: ExtendedCanvas = {
36
- nodes: [
37
- {
38
- id: 'node-1',
39
- type: 'text',
40
- x: 100,
41
- y: 100,
42
- width: 140,
43
- height: 70,
44
- text: 'Input Processor',
45
- color: '#4A90E2',
46
- pv: {
47
- nodeType: 'process',
48
- shape: 'rectangle',
49
- icon: 'Settings',
50
- },
51
- },
52
- {
53
- id: 'node-2',
54
- type: 'text',
55
- x: 300,
56
- y: 100,
57
- width: 100,
58
- height: 100,
59
- text: 'Database',
60
- color: '#7B68EE',
61
- pv: {
62
- nodeType: 'data',
63
- shape: 'circle',
64
- icon: 'Database',
65
- },
66
- },
67
- {
68
- id: 'node-3',
69
- type: 'text',
70
- x: 500,
71
- y: 100,
72
- width: 140,
73
- height: 70,
74
- text: 'Output Handler',
75
- color: '#4A90E2',
76
- pv: {
77
- nodeType: 'process',
78
- shape: 'rectangle',
79
- icon: 'Settings',
80
- },
81
- },
82
- ],
83
- edges: [
84
- {
85
- id: 'edge-1',
86
- fromNode: 'node-1',
87
- toNode: 'node-2',
88
- edgeType: 'dataflow',
89
- },
90
- {
91
- id: 'edge-2',
92
- fromNode: 'node-2',
93
- toNode: 'node-3',
94
- edgeType: 'dataflow',
95
- },
96
- ],
97
- name: 'Sample Validation Graph',
98
- description: 'Example graph configuration',
99
- edgeTypes: {
100
- dataflow: {
101
- style: 'solid',
102
- color: '#50E3C2',
103
- directed: true,
104
- },
105
- dependency: {
106
- style: 'dashed',
107
- color: '#F5A623',
108
- directed: true,
109
- },
110
- },
111
- };
112
-
113
- /**
114
- * Empty canvas
115
- */
116
- const emptyCanvas: ExtendedCanvas = {
117
- nodes: [],
118
- edges: [],
119
- name: 'Empty Graph',
120
- edgeTypes: {},
121
- };
122
-
123
- /**
124
- * Single node canvas
125
- */
126
- const singleNodeCanvas: ExtendedCanvas = {
127
- nodes: [
128
- {
129
- id: 'node-1',
130
- type: 'text',
131
- x: 100,
132
- y: 100,
133
- width: 140,
134
- height: 70,
135
- text: 'Input Processor',
136
- color: '#4A90E2',
137
- pv: {
138
- nodeType: 'process',
139
- shape: 'rectangle',
140
- icon: 'Settings',
141
- },
142
- },
143
- ],
144
- edges: [],
145
- name: 'Single Node',
146
- edgeTypes: {},
147
- };
148
-
149
- /**
150
- * Larger canvas with more nodes
151
- */
152
- const largeCanvas: ExtendedCanvas = {
153
- nodes: [
154
- {
155
- id: 'node-1',
156
- type: 'text',
157
- x: 100,
158
- y: 100,
159
- width: 140,
160
- height: 70,
161
- text: 'Input Processor',
162
- color: '#4A90E2',
163
- pv: {
164
- nodeType: 'process',
165
- shape: 'rectangle',
166
- icon: 'Settings',
167
- },
168
- },
169
- {
170
- id: 'node-2',
171
- type: 'text',
172
- x: 300,
173
- y: 100,
174
- width: 100,
175
- height: 100,
176
- text: 'Database',
177
- color: '#7B68EE',
178
- pv: {
179
- nodeType: 'data',
180
- shape: 'circle',
181
- icon: 'Database',
182
- },
183
- },
184
- {
185
- id: 'node-3',
186
- type: 'text',
187
- x: 500,
188
- y: 100,
189
- width: 140,
190
- height: 70,
191
- text: 'Output Handler',
192
- color: '#4A90E2',
193
- pv: {
194
- nodeType: 'process',
195
- shape: 'rectangle',
196
- icon: 'Settings',
197
- },
198
- },
199
- {
200
- id: 'node-4',
201
- type: 'text',
202
- x: 200,
203
- y: 250,
204
- width: 140,
205
- height: 70,
206
- text: 'Validator',
207
- color: '#4A90E2',
208
- pv: {
209
- nodeType: 'process',
210
- shape: 'rectangle',
211
- icon: 'Check',
212
- },
213
- },
214
- {
215
- id: 'node-5',
216
- type: 'text',
217
- x: 400,
218
- y: 250,
219
- width: 100,
220
- height: 100,
221
- text: 'Cache',
222
- color: '#7B68EE',
223
- pv: {
224
- nodeType: 'data',
225
- shape: 'circle',
226
- icon: 'Zap',
227
- },
228
- },
229
- ],
230
- edges: [
231
- {
232
- id: 'edge-1',
233
- fromNode: 'node-1',
234
- toNode: 'node-2',
235
- edgeType: 'dataflow',
236
- },
237
- {
238
- id: 'edge-2',
239
- fromNode: 'node-2',
240
- toNode: 'node-3',
241
- edgeType: 'dataflow',
242
- },
243
- {
244
- id: 'edge-3',
245
- fromNode: 'node-1',
246
- toNode: 'node-4',
247
- edgeType: 'dataflow',
248
- },
249
- {
250
- id: 'edge-4',
251
- fromNode: 'node-4',
252
- toNode: 'node-5',
253
- edgeType: 'dataflow',
254
- },
255
- ],
256
- name: 'Large Graph',
257
- edgeTypes: {
258
- dataflow: {
259
- style: 'solid',
260
- color: '#50E3C2',
261
- directed: true,
262
- },
263
- },
264
- };
265
-
266
- /**
267
- * Service architecture canvas (richer example)
268
- */
269
- const serviceArchitectureCanvas: ExtendedCanvas = {
270
- nodes: [
271
- {
272
- id: 'client',
273
- type: 'text',
274
- x: 100,
275
- y: 200,
276
- width: 120,
277
- height: 120,
278
- text: '# Client',
279
- color: 5, // cyan preset
280
- pv: {
281
- nodeType: 'client',
282
- shape: 'circle',
283
- icon: 'User',
284
- states: {
285
- idle: { color: '#94a3b8', icon: 'User' },
286
- connected: { color: '#22c55e', icon: 'UserCheck' },
287
- error: { color: '#ef4444', icon: 'UserX' },
288
- },
289
- },
290
- },
291
- {
292
- id: 'api-server',
293
- type: 'text',
294
- x: 350,
295
- y: 180,
296
- width: 200,
297
- height: 140,
298
- text: '# API Server\n\nHandles REST endpoints',
299
- color: 6, // purple preset
300
- pv: {
301
- nodeType: 'api-server',
302
- shape: 'rectangle',
303
- icon: 'Server',
304
- states: {
305
- idle: { color: '#94a3b8' },
306
- processing: { color: '#3b82f6' },
307
- error: { color: '#ef4444' },
308
- },
309
- },
310
- },
311
- {
312
- id: 'database',
313
- type: 'text',
314
- x: 620,
315
- y: 200,
316
- width: 150,
317
- height: 100,
318
- text: '# Database',
319
- color: 4, // green preset
320
- pv: {
321
- nodeType: 'database',
322
- shape: 'hexagon',
323
- icon: 'Database',
324
- },
325
- },
326
- {
327
- id: 'cache',
328
- type: 'text',
329
- x: 350,
330
- y: 380,
331
- width: 140,
332
- height: 80,
333
- text: '# Cache',
334
- color: 2, // orange preset
335
- pv: {
336
- nodeType: 'cache',
337
- shape: 'diamond',
338
- icon: 'Zap',
339
- },
340
- },
341
- ],
342
- edges: [
343
- {
344
- id: 'client-to-api',
345
- fromNode: 'client',
346
- toNode: 'api-server',
347
- fromSide: 'right',
348
- toSide: 'left',
349
- label: 'HTTP',
350
- pv: {
351
- edgeType: 'http-request',
352
- },
353
- },
354
- {
355
- id: 'api-to-db',
356
- fromNode: 'api-server',
357
- toNode: 'database',
358
- fromSide: 'right',
359
- toSide: 'left',
360
- label: 'SQL',
361
- pv: {
362
- edgeType: 'db-query',
363
- },
364
- },
365
- {
366
- id: 'api-to-cache',
367
- fromNode: 'api-server',
368
- toNode: 'cache',
369
- fromSide: 'bottom',
370
- toSide: 'top',
371
- label: 'GET/SET',
372
- edgeType: 'cache-access',
373
- },
374
- ],
375
- name: 'Simple Service Architecture',
376
- description: 'A basic client-server architecture with caching',
377
- edgeTypes: {
378
- 'http-request': {
379
- style: 'solid',
380
- color: '#3b82f6',
381
- width: 3,
382
- directed: true,
383
- animation: {
384
- type: 'flow',
385
- duration: 1500,
386
- },
387
- },
388
- 'db-query': {
389
- style: 'dashed',
390
- color: '#22c55e',
391
- width: 2,
392
- directed: true,
393
- },
394
- 'cache-access': {
395
- style: 'dotted',
396
- color: '#f97316',
397
- width: 2,
398
- directed: true,
399
- animation: {
400
- type: 'pulse',
401
- duration: 1000,
402
- },
403
- },
404
- },
405
- display: {
406
- layout: 'manual',
407
- animations: {
408
- enabled: true,
409
- speed: 1.0,
410
- },
411
- },
412
- };
413
-
414
- // ============================================================================
415
- // Stories
416
- // ============================================================================
417
-
418
- const DefaultTemplate = () => (
419
- <div style={{ width: '100%', height: '600px' }}>
420
- <GraphRenderer canvas={sampleCanvas} />
421
- </div>
422
- );
423
-
424
- export const Default: Story = {
425
- render: () => <DefaultTemplate />,
426
- };
427
-
428
- const EmptyGraphTemplate = () => (
429
- <div style={{ width: '100%', height: '600px' }}>
430
- <GraphRenderer canvas={emptyCanvas} />
431
- </div>
432
- );
433
-
434
- export const EmptyGraph: Story = {
435
- render: () => <EmptyGraphTemplate />,
436
- };
437
-
438
- const SingleNodeTemplate = () => (
439
- <div style={{ width: '100%', height: '600px' }}>
440
- <GraphRenderer canvas={singleNodeCanvas} />
441
- </div>
442
- );
443
-
444
- export const SingleNode: Story = {
445
- render: () => <SingleNodeTemplate />,
446
- };
447
-
448
- const LargeGraphTemplate = () => (
449
- <div style={{ width: '100%', height: '600px' }}>
450
- <GraphRenderer canvas={largeCanvas} />
451
- </div>
452
- );
453
-
454
- export const LargeGraph: Story = {
455
- render: () => <LargeGraphTemplate />,
456
- };
457
-
458
- const ServiceArchitectureTemplate = () => (
459
- <div style={{ width: '100%', height: '600px' }}>
460
- <GraphRenderer canvas={serviceArchitectureCanvas} />
461
- </div>
462
- );
463
-
464
- export const ServiceArchitecture: Story = {
465
- render: () => <ServiceArchitectureTemplate />,
466
- parameters: {
467
- docs: {
468
- description: {
469
- story: `
470
- **Service Architecture** - A realistic example showing:
471
- - Multiple node shapes (circle, rectangle, hexagon, diamond)
472
- - Edge types with different styles (solid, dashed, dotted)
473
- - Edge animations (flow, pulse)
474
- - State definitions for dynamic coloring
475
- `,
476
- },
477
- },
478
- },
479
- };
480
-
481
- // Interactive story with editable mode
482
- const EditableTemplate = () => {
483
- const graphRef = React.useRef<GraphRendererHandle>(null);
484
- const [hasChanges, setHasChanges] = React.useState(false);
485
- const [lastSavedChanges, setLastSavedChanges] = React.useState<PendingChanges | null>(null);
486
-
487
- const handleSave = () => {
488
- const changes = graphRef.current?.getPendingChanges();
489
- if (changes?.hasChanges) {
490
- console.log('Saving changes:', changes);
491
- setLastSavedChanges(changes);
492
- }
493
- };
494
-
495
- const handleReset = () => {
496
- graphRef.current?.resetEditState();
497
- setHasChanges(false);
498
- setLastSavedChanges(null);
499
- };
500
-
501
- return (
502
- <div>
503
- <div style={{ marginBottom: 16, padding: 12, backgroundColor: '#f5f5f5', borderRadius: 4 }}>
504
- <strong>Interactive Graph Editor</strong>
505
- <div style={{ marginTop: 8, fontSize: 12, color: '#666' }}>
506
- <ul style={{ margin: '4px 0', paddingLeft: 20 }}>
507
- <li>Drag nodes to reposition them</li>
508
- <li>Drag from a node handle to another node to create an edge</li>
509
- <li>Click an edge to view info and delete it</li>
510
- <li>Click a node to view info, edit name/type, or delete it</li>
511
- </ul>
512
- </div>
513
- <div style={{ marginTop: 12, display: 'flex', gap: 8, alignItems: 'center' }}>
514
- <button
515
- onClick={handleSave}
516
- disabled={!hasChanges}
517
- style={{
518
- padding: '6px 12px',
519
- backgroundColor: hasChanges ? '#4A90E2' : '#ccc',
520
- color: 'white',
521
- border: 'none',
522
- borderRadius: 4,
523
- cursor: hasChanges ? 'pointer' : 'not-allowed',
524
- }}
525
- >
526
- Save Changes
527
- </button>
528
- <button
529
- onClick={handleReset}
530
- style={{
531
- padding: '6px 12px',
532
- backgroundColor: '#f0f0f0',
533
- color: '#333',
534
- border: '1px solid #ccc',
535
- borderRadius: 4,
536
- cursor: 'pointer',
537
- }}
538
- >
539
- Reset
540
- </button>
541
- {hasChanges && (
542
- <span style={{ fontSize: 12, color: '#f5a623', fontStyle: 'italic' }}>
543
- Unsaved changes
544
- </span>
545
- )}
546
- </div>
547
- {lastSavedChanges && (
548
- <div style={{ marginTop: 12 }}>
549
- <div style={{ fontSize: 11, fontWeight: 'bold', color: '#666' }}>
550
- Last saved changes:
551
- </div>
552
- <pre
553
- style={{
554
- marginTop: 4,
555
- fontSize: 10,
556
- backgroundColor: '#fff',
557
- padding: 8,
558
- borderRadius: 4,
559
- maxHeight: 150,
560
- overflow: 'auto',
561
- }}
562
- >
563
- {JSON.stringify(lastSavedChanges, null, 2)}
564
- </pre>
565
- </div>
566
- )}
567
- </div>
568
- <div style={{ width: '100%', height: '500px' }}>
569
- <GraphRenderer
570
- ref={graphRef}
571
- canvas={sampleCanvas}
572
- editable={true}
573
- onPendingChangesChange={setHasChanges}
574
- />
575
- </div>
576
- </div>
577
- );
578
- };
579
-
580
- export const Editable: Story = {
581
- render: () => <EditableTemplate />,
582
- parameters: {
583
- docs: {
584
- description: {
585
- story:
586
- 'Full editing mode with internal state management. Drag nodes, create/delete edges, delete nodes, and edit node properties. Changes are tracked internally and can be retrieved via the ref when saving.',
587
- },
588
- },
589
- },
590
- };
591
-
592
- const ServiceArchitectureEditableTemplate = () => {
593
- const graphRef = React.useRef<GraphRendererHandle>(null);
594
- const [hasChanges, setHasChanges] = React.useState(false);
595
-
596
- return (
597
- <div>
598
- <div
599
- style={{
600
- marginBottom: 16,
601
- padding: 12,
602
- backgroundColor: '#f0f9ff',
603
- borderRadius: 4,
604
- border: '1px solid #3b82f6',
605
- }}
606
- >
607
- <strong style={{ color: '#1e40af' }}>Service Architecture + Editing</strong>
608
- <div style={{ marginTop: 8, fontSize: 12, color: '#1e40af' }}>
609
- This graph is rendered from an ExtendedCanvas document. You can edit it interactively.
610
- {hasChanges && <span style={{ marginLeft: 8, color: '#f97316' }}>(unsaved changes)</span>}
611
- </div>
612
- </div>
613
- <div style={{ width: '100%', height: '500px' }}>
614
- <GraphRenderer
615
- ref={graphRef}
616
- canvas={serviceArchitectureCanvas}
617
- editable={true}
618
- onPendingChangesChange={setHasChanges}
619
- />
620
- </div>
621
- </div>
622
- );
623
- };
624
-
625
- export const ServiceArchitectureEditable: Story = {
626
- render: () => <ServiceArchitectureEditableTemplate />,
627
- parameters: {
628
- docs: {
629
- description: {
630
- story:
631
- 'Service architecture canvas with editing enabled. Edit nodes, create edges, and track changes.',
632
- },
633
- },
634
- },
635
- };
636
-
637
- // ============================================================================
638
- // OTEL Log Association Canvas
639
- // ============================================================================
640
-
641
- /**
642
- * OTEL Log Association architecture canvas
643
- * Demonstrates OTEL badges, tooltips, and info panel integration
644
- */
645
- const otelLogAssociationCanvas: ExtendedCanvas = {
646
- nodes: [
647
- {
648
- id: 'otel-log',
649
- type: 'text',
650
- text: 'OtelLog',
651
- x: -304,
652
- y: 147,
653
- width: 120,
654
- height: 118,
655
- pv: {
656
- nodeType: 'otel-type',
657
- name: 'OtelLog',
658
- description:
659
- 'OpenTelemetry log record with timestamp, severity, body, resource, and trace context',
660
- otel: {
661
- kind: 'type',
662
- category: 'log',
663
- },
664
- shape: 'rectangle',
665
- icon: 'FileText',
666
- fill: '#4A90E2',
667
- },
668
- },
669
- {
670
- id: 'otel-resource',
671
- type: 'text',
672
- text: 'OtelResource',
673
- x: -305,
674
- y: 316,
675
- width: 120,
676
- height: 117,
677
- pv: {
678
- nodeType: 'otel-type',
679
- name: 'OtelResource',
680
- description: 'OTEL resource attributes like service.name, k8s.*, deployment.*',
681
- otel: {
682
- kind: 'type',
683
- category: 'resource',
684
- },
685
- shape: 'rectangle',
686
- icon: 'Box',
687
- fill: '#4A90E2',
688
- },
689
- },
690
- {
691
- id: 'canvas-scope',
692
- type: 'text',
693
- text: 'CanvasScope',
694
- x: -77,
695
- y: -48,
696
- width: 130,
697
- height: 118,
698
- pv: {
699
- nodeType: 'service',
700
- name: 'CanvasScope',
701
- description: 'Filters which logs are relevant to this canvas based on resource attributes',
702
- shape: 'hexagon',
703
- icon: 'Filter',
704
- fill: '#9B59B6',
705
- },
706
- },
707
- {
708
- id: 'log-router',
709
- type: 'text',
710
- text: 'LogRouter',
711
- x: -80,
712
- y: 150,
713
- width: 130,
714
- height: 118,
715
- pv: {
716
- nodeType: 'otel-service',
717
- name: 'LogRouter',
718
- description:
719
- 'Routes incoming OTEL logs to canvas nodes based on scope and resourceMatch criteria',
720
- otel: {
721
- kind: 'service',
722
- category: 'router',
723
- },
724
- shape: 'hexagon',
725
- icon: 'GitBranch',
726
- fill: '#7ED321',
727
- },
728
- },
729
- {
730
- id: 'resource-matcher',
731
- type: 'text',
732
- text: 'ResourceMatcher',
733
- x: -82,
734
- y: 440,
735
- width: 130,
736
- height: 119,
737
- pv: {
738
- nodeType: 'service',
739
- name: 'ResourceMatcher',
740
- description: 'Matches log resources against node resourceMatch criteria',
741
- otel: {
742
- kind: 'service',
743
- category: 'match',
744
- },
745
- shape: 'hexagon',
746
- icon: 'Search',
747
- fill: '#7ED321',
748
- },
749
- },
750
- {
751
- id: 'resource-match',
752
- type: 'text',
753
- text: 'ResourceMatch',
754
- x: 179,
755
- y: 445,
756
- width: 120,
757
- height: 117,
758
- pv: {
759
- nodeType: 'otel-type',
760
- name: 'ResourceMatch',
761
- description: 'Criteria for matching OTEL resources to canvas nodes',
762
- otel: {
763
- kind: 'type',
764
- category: 'match',
765
- },
766
- shape: 'rectangle',
767
- icon: 'Target',
768
- fill: '#4A90E2',
769
- },
770
- },
771
- {
772
- id: 'match-operator',
773
- type: 'text',
774
- text: 'MatchOperator',
775
- x: 178,
776
- y: 632,
777
- width: 120,
778
- height: 117,
779
- pv: {
780
- nodeType: 'otel-type',
781
- name: 'MatchOperator',
782
- description: 'Matching operators: exact, glob, regex, exists, oneOf',
783
- otel: {
784
- kind: 'type',
785
- category: 'match',
786
- },
787
- shape: 'diamond',
788
- icon: 'Code',
789
- fill: '#F5A623',
790
- },
791
- },
792
- {
793
- id: 'canvas-node',
794
- type: 'text',
795
- text: 'Canvas Node',
796
- x: 172,
797
- y: 277,
798
- width: 132,
799
- height: 121,
800
- pv: {
801
- nodeType: 'service',
802
- name: 'Canvas Node',
803
- description: 'A node in the canvas that can receive routed logs',
804
- shape: 'rectangle',
805
- icon: 'Square',
806
- fill: '#00BCD4',
807
- },
808
- },
809
- {
810
- id: 'routing-result',
811
- type: 'text',
812
- text: 'LogRoutingResult',
813
- x: 169,
814
- y: 80,
815
- width: 150,
816
- height: 135,
817
- pv: {
818
- nodeType: 'otel-type',
819
- name: 'LogRoutingResult',
820
- description: 'Result of routing a log: routed, orphaned, or out-of-scope',
821
- otel: {
822
- kind: 'type',
823
- category: 'audit',
824
- },
825
- shape: 'rectangle',
826
- icon: 'CheckCircle',
827
- fill: '#4A90E2',
828
- },
829
- },
830
- {
831
- id: 'audit-collector',
832
- type: 'text',
833
- text: 'AuditCollector',
834
- x: 380,
835
- y: 150,
836
- width: 130,
837
- height: 117,
838
- pv: {
839
- nodeType: 'otel-service',
840
- name: 'AuditCollector',
841
- description:
842
- 'Tracks routing metrics, orphaned logs, silent nodes, and generates coverage reports',
843
- otel: {
844
- kind: 'service',
845
- category: 'collector',
846
- },
847
- shape: 'hexagon',
848
- icon: 'BarChart2',
849
- fill: '#7ED321',
850
- },
851
- },
852
- {
853
- id: 'orphaned-log',
854
- type: 'text',
855
- text: 'OrphanedLog',
856
- x: 618,
857
- y: 150,
858
- width: 127,
859
- height: 118,
860
- pv: {
861
- nodeType: 'otel-type',
862
- name: 'OrphanedLog',
863
- description:
864
- 'Logs that matched canvas scope but no node resourceMatch - indicates missing nodes or misconfiguration',
865
- otel: {
866
- kind: 'type',
867
- category: 'audit',
868
- },
869
- shape: 'rectangle',
870
- icon: 'AlertTriangle',
871
- fill: '#D0021B',
872
- },
873
- },
874
- {
875
- id: 'audit-report',
876
- type: 'text',
877
- text: 'AuditReport',
878
- x: 618,
879
- y: 288,
880
- width: 124,
881
- height: 123,
882
- pv: {
883
- nodeType: 'otel-type',
884
- name: 'AuditReport',
885
- description: 'Coverage report with node activity, orphans, and recommendations',
886
- otel: {
887
- kind: 'type',
888
- category: 'audit',
889
- },
890
- shape: 'rectangle',
891
- icon: 'FileText',
892
- fill: '#4A90E2',
893
- },
894
- },
895
- ],
896
- edges: [
897
- {
898
- id: 'log-to-router',
899
- fromNode: 'otel-log',
900
- toNode: 'log-router',
901
- fromSide: 'right',
902
- toSide: 'left',
903
- label: 'route()',
904
- edgeType: 'http-request',
905
- },
906
- {
907
- id: 'router-checks-scope',
908
- fromNode: 'log-router',
909
- toNode: 'canvas-scope',
910
- fromSide: 'top',
911
- toSide: 'bottom',
912
- label: 'isInScope()',
913
- edgeType: 'http-request',
914
- },
915
- {
916
- id: 'router-uses-matcher',
917
- fromNode: 'log-router',
918
- toNode: 'resource-matcher',
919
- fromSide: 'bottom',
920
- toSide: 'top',
921
- label: 'match()',
922
- edgeType: 'http-request',
923
- },
924
- {
925
- id: 'matcher-uses-match',
926
- fromNode: 'resource-matcher',
927
- toNode: 'resource-match',
928
- fromSide: 'right',
929
- toSide: 'left',
930
- label: 'criteria',
931
- edgeType: 'db-query',
932
- },
933
- {
934
- id: 'match-has-operator',
935
- fromNode: 'resource-match',
936
- toNode: 'match-operator',
937
- fromSide: 'bottom',
938
- toSide: 'top',
939
- label: 'operators',
940
- edgeType: 'db-query',
941
- },
942
- {
943
- id: 'router-to-node',
944
- fromNode: 'log-router',
945
- toNode: 'canvas-node',
946
- fromSide: 'right',
947
- toSide: 'left',
948
- label: 'routed',
949
- edgeType: 'http-request',
950
- },
951
- {
952
- id: 'router-returns-result',
953
- fromNode: 'log-router',
954
- toNode: 'routing-result',
955
- fromSide: 'right',
956
- toSide: 'left',
957
- label: 'returns',
958
- edgeType: 'http-request',
959
- },
960
- {
961
- id: 'result-to-audit',
962
- fromNode: 'routing-result',
963
- toNode: 'audit-collector',
964
- fromSide: 'right',
965
- toSide: 'left',
966
- label: 'track()',
967
- edgeType: 'http-request',
968
- },
969
- {
970
- id: 'audit-tracks-orphans',
971
- fromNode: 'audit-collector',
972
- toNode: 'orphaned-log',
973
- fromSide: 'right',
974
- toSide: 'left',
975
- label: 'unmatched',
976
- edgeType: 'db-query',
977
- },
978
- {
979
- id: 'audit-generates-report',
980
- fromNode: 'audit-collector',
981
- toNode: 'audit-report',
982
- fromSide: 'right',
983
- toSide: 'left',
984
- label: 'generate()',
985
- edgeType: 'db-query',
986
- },
987
- {
988
- id: 'node-has-match',
989
- fromNode: 'canvas-node',
990
- toNode: 'resource-match',
991
- fromSide: 'bottom',
992
- toSide: 'top',
993
- label: 'resourceMatch',
994
- edgeType: 'db-query',
995
- },
996
- {
997
- id: 'log-has-resource',
998
- fromNode: 'otel-log',
999
- toNode: 'otel-resource',
1000
- fromSide: 'bottom',
1001
- toSide: 'top',
1002
- label: 'resource',
1003
- edgeType: 'db-query',
1004
- },
1005
- ],
1006
- name: 'OTEL Log Association Architecture',
1007
- description: 'Shows how OtelLog flows through LogRouter to Canvas Nodes, with audit tracking',
1008
- nodeTypes: {
1009
- service: {
1010
- label: 'Component',
1011
- description: 'System component',
1012
- shape: 'hexagon',
1013
- },
1014
- 'otel-type': {
1015
- label: 'Type',
1016
- description: 'TypeScript type or interface representing OTEL concepts',
1017
- shape: 'rectangle',
1018
- },
1019
- 'otel-service': {
1020
- label: 'Service',
1021
- description: 'Runtime service component for OTEL processing',
1022
- shape: 'hexagon',
1023
- },
1024
- },
1025
- edgeTypes: {
1026
- 'http-request': {
1027
- label: 'Flow',
1028
- style: 'solid',
1029
- color: '#4A90E2',
1030
- directed: true,
1031
- },
1032
- 'db-query': {
1033
- label: 'Reference',
1034
- style: 'dashed',
1035
- color: '#999',
1036
- directed: true,
1037
- },
1038
- },
1039
- };
1040
-
1041
- const OtelLogAssociationTemplate = () => (
1042
- <div style={{ width: '100%', height: '800px' }}>
1043
- <GraphRenderer canvas={otelLogAssociationCanvas} />
1044
- </div>
1045
- );
1046
-
1047
- export const OtelLogAssociation: Story = {
1048
- render: () => <OtelLogAssociationTemplate />,
1049
- parameters: {
1050
- docs: {
1051
- description: {
1052
- story: `
1053
- **OTEL Log Association Architecture** - Demonstrates the new OTEL visualization features:
1054
- - **OTEL Badges**: Circular badges (T/S/I) in the top-right corner of nodes indicating Type, Service, or Instance
1055
- - **Hover Tooltips**: Hover over any node to see the OTEL kind, category, NEW indicator, and description
1056
- - **Node Info Panel**: Click a node to see the OpenTelemetry section in the info panel
1057
-
1058
- Color coding:
1059
- - **Blue (T)**: Type nodes - TypeScript types/interfaces
1060
- - **Green (S)**: Service nodes - Runtime services
1061
- - **Purple (I)**: Instance nodes - Specific running instances
1062
- `,
1063
- },
1064
- },
1065
- },
1066
- };
1067
-
1068
- // ============================================================================
1069
- // Sources Tooltip Canvas
1070
- // ============================================================================
1071
-
1072
- /**
1073
- * Canvas demonstrating source file associations in tooltips
1074
- */
1075
- const sourcesTooltipCanvas: ExtendedCanvas = {
1076
- nodes: [
1077
- {
1078
- id: 'auth-service',
1079
- type: 'text',
1080
- x: 100,
1081
- y: 150,
1082
- width: 160,
1083
- height: 100,
1084
- text: '# Auth Service\n\nHandles user authentication',
1085
- color: '#3b82f6',
1086
- pv: {
1087
- nodeType: 'service',
1088
- shape: 'rectangle',
1089
- icon: 'Lock',
1090
- sources: [
1091
- 'src/services/auth/AuthService.ts',
1092
- 'src/services/auth/TokenManager.ts',
1093
- ],
1094
- },
1095
- },
1096
- {
1097
- id: 'user-controller',
1098
- type: 'text',
1099
- x: 350,
1100
- y: 150,
1101
- width: 160,
1102
- height: 100,
1103
- text: '# User Controller\n\nManages user CRUD operations',
1104
- color: '#8b5cf6',
1105
- pv: {
1106
- nodeType: 'controller',
1107
- shape: 'rectangle',
1108
- icon: 'User',
1109
- sources: [
1110
- 'src/controllers/UserController.ts',
1111
- ],
1112
- },
1113
- },
1114
- {
1115
- id: 'database',
1116
- type: 'text',
1117
- x: 600,
1118
- y: 150,
1119
- width: 140,
1120
- height: 100,
1121
- text: '# Database\n\nPostgreSQL instance',
1122
- color: '#22c55e',
1123
- pv: {
1124
- nodeType: 'database',
1125
- shape: 'hexagon',
1126
- icon: 'Database',
1127
- sources: [
1128
- 'src/db/connection.ts',
1129
- 'src/db/migrations/001_init.sql',
1130
- 'src/db/queries/users.ts',
1131
- ],
1132
- },
1133
- },
1134
- {
1135
- id: 'cache-layer',
1136
- type: 'text',
1137
- x: 225,
1138
- y: 320,
1139
- width: 150,
1140
- height: 90,
1141
- text: '# Cache Layer\n\nRedis caching',
1142
- color: '#f59e0b',
1143
- pv: {
1144
- nodeType: 'cache',
1145
- shape: 'diamond',
1146
- icon: 'Zap',
1147
- sources: [
1148
- 'src/cache/RedisCache.ts',
1149
- 'src/cache/CacheManager.ts',
1150
- ],
1151
- },
1152
- },
1153
- {
1154
- id: 'logger',
1155
- type: 'text',
1156
- x: 450,
1157
- y: 320,
1158
- width: 140,
1159
- height: 90,
1160
- text: '# Logger\n\nCentralized logging',
1161
- color: '#6366f1',
1162
- pv: {
1163
- nodeType: 'utility',
1164
- shape: 'circle',
1165
- icon: 'FileText',
1166
- sources: [
1167
- 'src/utils/logger.ts',
1168
- ],
1169
- },
1170
- },
1171
- ],
1172
- edges: [
1173
- {
1174
- id: 'auth-to-user',
1175
- fromNode: 'auth-service',
1176
- toNode: 'user-controller',
1177
- fromSide: 'right',
1178
- toSide: 'left',
1179
- label: 'validates',
1180
- edgeType: 'dataflow',
1181
- },
1182
- {
1183
- id: 'user-to-db',
1184
- fromNode: 'user-controller',
1185
- toNode: 'database',
1186
- fromSide: 'right',
1187
- toSide: 'left',
1188
- label: 'queries',
1189
- edgeType: 'dataflow',
1190
- },
1191
- {
1192
- id: 'auth-to-cache',
1193
- fromNode: 'auth-service',
1194
- toNode: 'cache-layer',
1195
- fromSide: 'bottom',
1196
- toSide: 'top',
1197
- label: 'stores tokens',
1198
- edgeType: 'dataflow',
1199
- },
1200
- {
1201
- id: 'user-to-cache',
1202
- fromNode: 'user-controller',
1203
- toNode: 'cache-layer',
1204
- fromSide: 'bottom',
1205
- toSide: 'top',
1206
- label: 'caches',
1207
- edgeType: 'dataflow',
1208
- },
1209
- {
1210
- id: 'auth-to-logger',
1211
- fromNode: 'auth-service',
1212
- toNode: 'logger',
1213
- fromSide: 'bottom',
1214
- toSide: 'top',
1215
- label: 'logs',
1216
- edgeType: 'logging',
1217
- },
1218
- {
1219
- id: 'user-to-logger',
1220
- fromNode: 'user-controller',
1221
- toNode: 'logger',
1222
- fromSide: 'bottom',
1223
- toSide: 'top',
1224
- label: 'logs',
1225
- edgeType: 'logging',
1226
- },
1227
- ],
1228
- pv: {
1229
- version: '1.0.0',
1230
- name: 'Service Architecture with Sources',
1231
- description: 'Demonstrates source file associations in shift-hover tooltips',
1232
- edgeTypes: {
1233
- dataflow: {
1234
- style: 'solid',
1235
- color: '#3b82f6',
1236
- width: 2,
1237
- directed: true,
1238
- },
1239
- logging: {
1240
- style: 'dashed',
1241
- color: '#94a3b8',
1242
- width: 2,
1243
- directed: true,
1244
- },
1245
- },
1246
- },
1247
- };
1248
-
1249
- const SourcesInTooltipTemplate = () => {
1250
- return (
1251
- <div style={{ width: '100%', height: '550px' }}>
1252
- <GraphRenderer
1253
- canvas={sourcesTooltipCanvas}
1254
- />
1255
- </div>
1256
- );
1257
- };
1258
-
1259
- export const SourcesInTooltip: Story = {
1260
- render: () => <SourcesInTooltipTemplate />,
1261
- parameters: {
1262
- docs: {
1263
- description: {
1264
- story: `
1265
- **[DEPRECATED]** This story previously demonstrated source file associations in tooltips and info panels.
1266
-
1267
- **Note:** The sources display feature has been removed from the node rendering. Nodes can still have source data in their canvas definition, but it will not be displayed in the UI.
1268
- `,
1269
- },
1270
- },
1271
- },
1272
- };
1273
-
1274
- const EditModeToggleTemplate = () => {
1275
- const [isEditMode, setIsEditMode] = React.useState(false);
1276
- const graphRef = React.useRef<GraphRendererHandle>(null);
1277
- const [hasChanges, setHasChanges] = React.useState(false);
1278
-
1279
- const handleSave = () => {
1280
- if (graphRef.current) {
1281
- const changes = graphRef.current.getPendingChanges();
1282
- console.log('Saving changes:', changes);
1283
- alert('Changes saved! Check console for details.');
1284
- graphRef.current.resetEditState();
1285
- }
1286
- };
1287
-
1288
- return (
1289
- <div>
1290
- <div
1291
- style={{
1292
- marginBottom: 16,
1293
- padding: 12,
1294
- backgroundColor: '#f0f9ff',
1295
- borderRadius: 4,
1296
- border: '1px solid #3b82f6',
1297
- }}
1298
- >
1299
- <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
1300
- <button
1301
- onClick={() => setIsEditMode(!isEditMode)}
1302
- style={{
1303
- padding: '8px 16px',
1304
- backgroundColor: isEditMode ? '#ef4444' : '#3b82f6',
1305
- color: 'white',
1306
- border: 'none',
1307
- borderRadius: 4,
1308
- cursor: 'pointer',
1309
- fontWeight: 'bold',
1310
- fontSize: 14,
1311
- }}
1312
- >
1313
- {isEditMode ? '🔓 Exit Edit Mode' : '🔒 Enter Edit Mode'}
1314
- </button>
1315
- {isEditMode && hasChanges && (
1316
- <button
1317
- onClick={handleSave}
1318
- style={{
1319
- padding: '8px 16px',
1320
- backgroundColor: '#10b981',
1321
- color: 'white',
1322
- border: 'none',
1323
- borderRadius: 4,
1324
- cursor: 'pointer',
1325
- fontWeight: 'bold',
1326
- fontSize: 14,
1327
- }}
1328
- >
1329
- 💾 Save Changes
1330
- </button>
1331
- )}
1332
- <div style={{ fontSize: 13, color: '#1e40af' }}>
1333
- Mode: <strong>{isEditMode ? 'Editable' : 'Read-only'}</strong>
1334
- {isEditMode && hasChanges && (
1335
- <span style={{ marginLeft: 8, color: '#f97316', fontWeight: 'bold' }}>
1336
- (unsaved changes)
1337
- </span>
1338
- )}
1339
- </div>
1340
- </div>
1341
- <div style={{ marginTop: 8, fontSize: 12, color: '#475569' }}>
1342
- Toggle between read-only and edit mode. Try dragging nodes after switching modes to verify
1343
- the NaN error fix.
1344
- </div>
1345
- </div>
1346
- <div style={{ width: '100%', height: '500px' }}>
1347
- <GraphRenderer
1348
- ref={graphRef}
1349
- canvas={sampleCanvas}
1350
- editable={isEditMode}
1351
- onPendingChangesChange={setHasChanges}
1352
- />
1353
- </div>
1354
- </div>
1355
- );
1356
- };
1357
-
1358
- export const EditModeToggle: Story = {
1359
- render: () => <EditModeToggleTemplate />,
1360
- parameters: {
1361
- docs: {
1362
- description: {
1363
- story: `
1364
- **Edit Mode Toggle** - Demonstrates toggling between read-only and editable modes without remounting.
1365
-
1366
- This story tests the fix for the NaN coordinate error that occurred when transitioning between modes:
1367
- - Click "Enter Edit Mode" to enable editing
1368
- - Try dragging nodes (should work without NaN errors)
1369
- - Click "Exit Edit Mode" to return to read-only
1370
- - Toggle back to edit mode and drag again (should still work)
1371
-
1372
- Previously, this would cause \`NaN\` errors in edge coordinates due to ReactFlow's internal state corruption.
1373
- The fix uses \`updateNodeInternals()\` to reset ReactFlow's measurement tracking when entering edit mode.
1374
- `,
1375
- },
1376
- },
1377
- },
1378
- };
1379
-
1380
- /**
1381
- * Canvas for demonstrating the selected prop styling
1382
- */
1383
- const selectionStylingCanvas: ExtendedCanvas = {
1384
- nodes: [
1385
- {
1386
- id: 'node-1',
1387
- type: 'text',
1388
- x: 100,
1389
- y: 100,
1390
- width: 140,
1391
- height: 70,
1392
- text: 'Click Me!',
1393
- color: '#4A90E2',
1394
- pv: {
1395
- nodeType: 'process',
1396
- shape: 'rectangle',
1397
- icon: 'MousePointer',
1398
- eventRef: 'user.clicked',
1399
- },
1400
- },
1401
- {
1402
- id: 'node-2',
1403
- type: 'text',
1404
- x: 300,
1405
- y: 100,
1406
- width: 100,
1407
- height: 100,
1408
- text: 'Or Me!',
1409
- color: '#7B68EE',
1410
- pv: {
1411
- nodeType: 'data',
1412
- shape: 'circle',
1413
- icon: 'Circle',
1414
- eventRef: 'data.processed',
1415
- },
1416
- },
1417
- {
1418
- id: 'node-3',
1419
- type: 'text',
1420
- x: 500,
1421
- y: 100,
1422
- width: 140,
1423
- height: 70,
1424
- text: 'Me Three!',
1425
- color: '#22c55e',
1426
- pv: {
1427
- nodeType: 'process',
1428
- shape: 'hexagon',
1429
- icon: 'Hexagon',
1430
- eventRef: 'workflow.executed',
1431
- },
1432
- },
1433
- {
1434
- id: 'node-4',
1435
- type: 'text',
1436
- x: 200,
1437
- y: 250,
1438
- width: 120,
1439
- height: 120,
1440
- text: 'Diamond',
1441
- color: '#f59e0b',
1442
- pv: {
1443
- nodeType: 'decision',
1444
- shape: 'diamond',
1445
- icon: 'GitBranch',
1446
- eventRef: 'decision.evaluated',
1447
- },
1448
- },
1449
- {
1450
- id: 'node-5',
1451
- type: 'text',
1452
- x: 400,
1453
- y: 250,
1454
- width: 140,
1455
- height: 80,
1456
- text: 'Inline Event',
1457
- color: '#8b5cf6',
1458
- pv: {
1459
- nodeType: 'event',
1460
- shape: 'rectangle',
1461
- icon: 'Zap',
1462
- event: {
1463
- name: 'order.completed',
1464
- description: 'Triggered when an order is successfully completed and payment is confirmed',
1465
- attributes: {
1466
- orderId: { type: 'string', required: true, description: 'Unique order identifier' },
1467
- userId: { type: 'string', required: true, description: 'User who placed the order' },
1468
- amount: { type: 'number', required: true, description: 'Order total amount' },
1469
- timestamp: { type: 'string', description: 'Order completion timestamp' },
1470
- },
1471
- },
1472
- },
1473
- },
1474
- ],
1475
- edges: [
1476
- {
1477
- id: 'edge-1',
1478
- fromNode: 'node-1',
1479
- toNode: 'node-2',
1480
- edgeType: 'dataflow',
1481
- },
1482
- {
1483
- id: 'edge-2',
1484
- fromNode: 'node-2',
1485
- toNode: 'node-3',
1486
- edgeType: 'dataflow',
1487
- },
1488
- {
1489
- id: 'edge-3',
1490
- fromNode: 'node-1',
1491
- toNode: 'node-4',
1492
- edgeType: 'dataflow',
1493
- },
1494
- ],
1495
- pv: {
1496
- version: '1.0.0',
1497
- name: 'Selection Styling Demo',
1498
- description: 'Demonstrates ReactFlow selected prop visual styling',
1499
- edgeTypes: {
1500
- dataflow: {
1501
- style: 'solid',
1502
- color: '#50E3C2',
1503
- directed: true,
1504
- },
1505
- },
1506
- },
1507
- };
1508
-
1509
- const SelectionStylingTemplate = () => {
1510
- return (
1511
- <div>
1512
- <div
1513
- style={{
1514
- marginBottom: 16,
1515
- padding: 12,
1516
- backgroundColor: '#f0f9ff',
1517
- borderRadius: 4,
1518
- border: '1px solid #3b82f6',
1519
- }}
1520
- >
1521
- <strong style={{ color: '#1e40af' }}>ReactFlow Selection Styling Demo</strong>
1522
- <div style={{ marginTop: 8, fontSize: 12, color: '#475569' }}>
1523
- <p style={{ margin: '4px 0' }}>
1524
- This demonstrates the <code>selected</code> prop that ReactFlow passes to CustomNode.
1525
- </p>
1526
- <p style={{ margin: '4px 0' }}>
1527
- <strong>Visual effects when a node is selected:</strong>
1528
- </p>
1529
- <ul style={{ margin: '4px 0', paddingLeft: 20 }}>
1530
- <li><strong>2px box-shadow outline</strong> in the node's stroke color</li>
1531
- <li><strong>Resize handles</strong> (only visible in edit mode - not shown in this demo)</li>
1532
- </ul>
1533
- <p style={{ margin: '8px 0 4px 0' }}>
1534
- <strong>Try these interactions:</strong>
1535
- </p>
1536
- <ul style={{ margin: '4px 0', paddingLeft: 20 }}>
1537
- <li><strong>Click</strong> any node → see the outline appear</li>
1538
- <li><strong>Click another</strong> node → outline moves to new selection</li>
1539
- <li><strong>Click background</strong> → outline disappears</li>
1540
- <li><strong>Shift+Drag</strong> box around multiple nodes → all get outlined</li>
1541
- </ul>
1542
- <p style={{ margin: '8px 0 4px 0', fontSize: 11, fontStyle: 'italic', color: '#64748b' }}>
1543
- Note: The <code>selected</code> prop is purely for visual feedback.
1544
- The info panel uses a separate <code>selectedNodeIds</code> state.
1545
- </p>
1546
- </div>
1547
- </div>
1548
- <div style={{ width: '100%', height: '500px' }}>
1549
- <GraphRenderer
1550
- canvas={selectionStylingCanvas}
1551
- showNodeDetailPanel={false}
1552
- />
1553
- </div>
1554
- </div>
1555
- );
1556
- };
1557
-
1558
- export const SelectionStyling: Story = {
1559
- render: () => <SelectionStylingTemplate />,
1560
- parameters: {
1561
- docs: {
1562
- description: {
1563
- story: `
1564
- **Selection Styling** - Demonstrates the visual effects of ReactFlow's \`selected\` prop in read-only mode.
1565
-
1566
- When a node is selected (via click or box selection), ReactFlow sets \`selected={true}\` on the node component.
1567
- This triggers visual changes in \`CustomNode\`:
1568
-
1569
- 1. **Box-shadow outline** (line 253-254 in CustomNode.tsx):
1570
- - 2px solid outline using the node's stroke color
1571
- - Appears immediately on selection
1572
- - Removed when selection is cleared
1573
- - **Click any node to see the outline appear!**
1574
-
1575
- 2. **NodeResizer visibility** (line 441 in CustomNode.tsx):
1576
- - Shows resize handles when \`selected={true}\` AND \`editable={true}\`
1577
- - Not visible in this demo (read-only mode)
1578
- - See the "Editable" story for resize handle demos
1579
-
1580
- **Key Point:** The \`selected\` prop is purely for ReactFlow's visual feedback. It's separate from
1581
- \`selectedNodeIds\` state which controls the info panel display.
1582
-
1583
- In this demo, the info panel is hidden (\`showNodeDetailPanel={false}\`) and edit mode is disabled,
1584
- so you can focus on just the selection outline effect.
1585
- `,
1586
- },
1587
- },
1588
- },
1589
- };
1590
-
1591
- const HighlightedNodeSyncTemplate = () => {
1592
- const [highlightedNodeId, setHighlightedNodeId] = React.useState<string | null>(null);
1593
- const nodeIds = ['node-1', 'node-2', 'node-3', 'node-4', 'node-5'];
1594
- const [currentIndex, setCurrentIndex] = React.useState(0);
1595
-
1596
- const handleCycleNodes = () => {
1597
- const nextIndex = (currentIndex + 1) % nodeIds.length;
1598
- setCurrentIndex(nextIndex);
1599
- setHighlightedNodeId(nodeIds[nextIndex]);
1600
- };
1601
-
1602
- const handleClearHighlight = () => {
1603
- setHighlightedNodeId(null);
1604
- setCurrentIndex(0);
1605
- };
1606
-
1607
- // Mock event registry to demonstrate resolveEventRef
1608
- const resolveEventRef = (eventRef: string) => {
1609
- const eventRegistry: Record<string, PVEventSchema> = {
1610
- 'user.clicked': {
1611
- name: 'user.clicked',
1612
- description: 'Triggered when a user clicks on an interactive element',
1613
- attributes: {
1614
- elementId: { type: 'string', required: true, description: 'ID of the clicked element' },
1615
- userId: { type: 'string', required: true, description: 'ID of the user who clicked' },
1616
- timestamp: { type: 'string', required: true, description: 'ISO 8601 timestamp' },
1617
- position: { type: 'object', description: 'Click coordinates (x, y)' },
1618
- },
1619
- },
1620
- 'data.processed': {
1621
- name: 'data.processed',
1622
- description: 'Emitted when data processing completes successfully',
1623
- attributes: {
1624
- dataId: { type: 'string', required: true, description: 'Unique identifier for the data' },
1625
- recordsProcessed: { type: 'number', required: true, description: 'Number of records processed' },
1626
- duration: { type: 'number', description: 'Processing time in milliseconds' },
1627
- status: { type: 'string', required: true, description: 'Processing status (success/partial/failed)' },
1628
- },
1629
- },
1630
- 'workflow.executed': {
1631
- name: 'workflow.executed',
1632
- description: 'Fired when a workflow completes execution',
1633
- attributes: {
1634
- workflowId: { type: 'string', required: true, description: 'Workflow identifier' },
1635
- executionId: { type: 'string', required: true, description: 'Execution run identifier' },
1636
- steps: { type: 'array', description: 'List of executed step IDs' },
1637
- result: { type: 'object', description: 'Workflow execution result' },
1638
- },
1639
- },
1640
- 'decision.evaluated': {
1641
- name: 'decision.evaluated',
1642
- description: 'Published when a decision point is evaluated in the flow',
1643
- attributes: {
1644
- decisionId: { type: 'string', required: true, description: 'Decision point identifier' },
1645
- condition: { type: 'string', description: 'Evaluated condition expression' },
1646
- result: { type: 'boolean', required: true, description: 'Evaluation result' },
1647
- branchTaken: { type: 'string', required: true, description: 'Branch selected (true/false/error)' },
1648
- },
1649
- },
1650
- };
1651
-
1652
- return eventRegistry[eventRef];
1653
- };
1654
-
1655
- return (
1656
- <div>
1657
- <div
1658
- style={{
1659
- marginBottom: 16,
1660
- padding: 12,
1661
- backgroundColor: '#f0f9ff',
1662
- borderRadius: 4,
1663
- border: '1px solid #3b82f6',
1664
- }}
1665
- >
1666
- <strong style={{ color: '#1e40af' }}>Highlighted Node Sync Demo</strong>
1667
- <div style={{ marginTop: 8, fontSize: 12, color: '#475569' }}>
1668
- <p style={{ margin: '4px 0' }}>
1669
- This demonstrates how <code>highlightedNodeId</code> automatically syncs with selection
1670
- and shows the node info panel.
1671
- </p>
1672
- <p style={{ margin: '8px 0 4px 0' }}>
1673
- <strong>Current highlighted node:</strong>{' '}
1674
- {highlightedNodeId ? (
1675
- <span style={{ fontFamily: 'monospace', color: '#3b82f6' }}>{highlightedNodeId}</span>
1676
- ) : (
1677
- <span style={{ color: '#94a3b8' }}>None</span>
1678
- )}
1679
- </p>
1680
- <div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
1681
- <button
1682
- onClick={handleCycleNodes}
1683
- style={{
1684
- padding: '8px 16px',
1685
- backgroundColor: '#3b82f6',
1686
- color: 'white',
1687
- border: 'none',
1688
- borderRadius: 4,
1689
- cursor: 'pointer',
1690
- fontWeight: 'bold',
1691
- fontSize: 14,
1692
- }}
1693
- >
1694
- Cycle Nodes ({nodeIds[currentIndex]})
1695
- </button>
1696
- <button
1697
- onClick={handleClearHighlight}
1698
- style={{
1699
- padding: '8px 16px',
1700
- backgroundColor: '#94a3b8',
1701
- color: 'white',
1702
- border: 'none',
1703
- borderRadius: 4,
1704
- cursor: 'pointer',
1705
- fontSize: 14,
1706
- }}
1707
- >
1708
- Clear Highlight
1709
- </button>
1710
- </div>
1711
- <p style={{ margin: '12px 0 4px 0', fontSize: 11, fontStyle: 'italic', color: '#64748b' }}>
1712
- When you cycle nodes, watch how:
1713
- </p>
1714
- <ul style={{ margin: '4px 0', paddingLeft: 20, fontSize: 11, color: '#64748b' }}>
1715
- <li>The highlighted node gets a <strong>blue glow</strong></li>
1716
- <li>The node is automatically <strong>selected</strong> (selection outline)</li>
1717
- <li>The <strong>info panel shows</strong> at the bottom with node details</li>
1718
- </ul>
1719
- </div>
1720
- </div>
1721
- <div style={{ width: '100%', height: '500px' }}>
1722
- <GraphRenderer
1723
- canvas={selectionStylingCanvas}
1724
- highlightedNodeId={highlightedNodeId}
1725
- showNodeDetailPanel={true}
1726
- resolveEventRef={resolveEventRef}
1727
- />
1728
- </div>
1729
- </div>
1730
- );
1731
- };
1732
-
1733
- export const HighlightedNodeSync: Story = {
1734
- render: () => <HighlightedNodeSyncTemplate />,
1735
- parameters: {
1736
- docs: {
1737
- description: {
1738
- story: `
1739
- **Highlighted Node Sync** - Demonstrates automatic synchronization between \`highlightedNodeId\` and selection state.
1740
-
1741
- This simulates what happens during execution playback in the CanvasDetailPanel:
1742
-
1743
- **How it works:**
1744
- 1. When \`highlightedNodeId\` prop changes, GraphRenderer automatically:
1745
- - Selects that node (adds it to \`selectedNodeIds\`)
1746
- - Shows the node info panel (\`showNodePanel = true\`)
1747
- - Applies both the highlight glow and selection outline
1748
-
1749
- 2. This creates a seamless connection between:
1750
- - Workflow events on the left (which set \`highlightedNodeId\`)
1751
- - Visual feedback on the canvas (glow + outline)
1752
- - Node details panel at the bottom (showing node info)
1753
-
1754
- **Use case:**
1755
- In the CanvasDetailPanel, when users:
1756
- - Click a scenario template → Nodes involved in that scenario are highlighted
1757
- - Click a workflow event → The corresponding node on the canvas is highlighted
1758
- - Click a node on the canvas → The corresponding workflow event is highlighted
1759
-
1760
- All of these actions update \`highlightedNodeId\`, which now automatically shows the node info panel!
1761
- `,
1762
- },
1763
- },
1764
- },
1765
- };
1766
-
1767
- // ============================================================================
1768
- // Real-World Test Data: Version Registry Canvas
1769
- // ============================================================================
1770
-
1771
- /**
1772
- * Version Registry Canvas from web-ade project
1773
- * Real production canvas showing version registration workflows
1774
- */
1775
- const versionRegistryCanvas: ExtendedCanvas = {
1776
- "name": "Version Registry",
1777
- "description": "Maps customer version strings (semver, git SHA, build numbers) to git commit SHAs for contract-based observability",
1778
- "markdown": ".principal-views/version-registry/version-registry.md",
1779
- "edgeTypes": {
1780
- "flow": {
1781
- "label": "Flow"
1782
- },
1783
- "error": {
1784
- "label": "Error Path"
1785
- },
1786
- "cache": {
1787
- "label": "Cache Path"
1788
- }
1789
- },
1790
- "nodes": [
1791
- {
1792
- "id": "registration-started",
1793
- "type": "text",
1794
- "text": "# Registration Started\n\nVersion registration request received",
1795
- "x": 100,
1796
- "y": 50,
1797
- "width": 200,
1798
- "height": 100,
1799
- "pv": {
1800
- "event": {
1801
- "name": "version.registration.started",
1802
- "attributes": {}
1803
- },
1804
- "sources": [
1805
- "src/app/api/versions/route.ts"
1806
- ],
1807
- "status": "implemented",
1808
- "otel": {
1809
- "kind": "event",
1810
- "category": "lifecycle"
1811
- },
1812
- "shape": "rectangle",
1813
- "fill": "#22c55e",
1814
- "stroke": "#16a34a",
1815
- "dataSchema": {
1816
- "service.name": {
1817
- "type": "string",
1818
- "required": true
1819
- },
1820
- "version": {
1821
- "type": "string",
1822
- "required": true
1823
- },
1824
- "repository.url": {
1825
- "type": "string",
1826
- "required": true
1827
- },
1828
- "environment": {
1829
- "type": "string",
1830
- "required": true
1831
- },
1832
- "git.sha": {
1833
- "type": "string",
1834
- "required": true
1835
- }
1836
- }
1837
- }
1838
- },
1839
- {
1840
- "id": "registration-validated",
1841
- "type": "text",
1842
- "text": "# Validate Request\n\nRequest validated successfully",
1843
- "x": 89,
1844
- "y": 199,
1845
- "width": 200,
1846
- "height": 100,
1847
- "pv": {
1848
- "event": {
1849
- "name": "version.registration.validated",
1850
- "attributes": {}
1851
- },
1852
- "sources": [
1853
- "src/lib/version-registry/version-manager.ts"
1854
- ],
1855
- "status": "approved",
1856
- "otel": {
1857
- "kind": "event",
1858
- "category": "operation"
1859
- },
1860
- "shape": "rectangle",
1861
- "fill": "#3b82f6",
1862
- "stroke": "#1d4ed8",
1863
- "dataSchema": {
1864
- "customer.id": {
1865
- "type": "string",
1866
- "required": true
1867
- },
1868
- "service.name": {
1869
- "type": "string",
1870
- "required": true
1871
- },
1872
- "version": {
1873
- "type": "string",
1874
- "required": true
1875
- }
1876
- }
1877
- }
1878
- },
1879
- {
1880
- "id": "registration-s3-stored",
1881
- "type": "text",
1882
- "text": "# Store to S3\n\nVersion mapping stored to S3",
1883
- "x": 92,
1884
- "y": 350,
1885
- "width": 200,
1886
- "height": 100,
1887
- "pv": {
1888
- "event": {
1889
- "name": "version.registration.s3.stored",
1890
- "attributes": {}
1891
- },
1892
- "sources": [
1893
- "src/lib/version-registry/s3-storage.ts"
1894
- ],
1895
- "status": "draft",
1896
- "otel": {
1897
- "kind": "event",
1898
- "category": "operation"
1899
- },
1900
- "shape": "rectangle",
1901
- "fill": "#3b82f6",
1902
- "stroke": "#1d4ed8",
1903
- "dataSchema": {
1904
- "s3.key": {
1905
- "type": "string",
1906
- "required": true
1907
- },
1908
- "s3.bucket": {
1909
- "type": "string",
1910
- "required": true
1911
- },
1912
- "already.exists": {
1913
- "type": "boolean",
1914
- "required": false
1915
- }
1916
- }
1917
- }
1918
- },
1919
- {
1920
- "id": "schematic-fetching",
1921
- "type": "text",
1922
- "text": "# Fetch Schematic\n\nFetching schematic from GitHub at commit SHA",
1923
- "x": 91,
1924
- "y": 498,
1925
- "width": 200,
1926
- "height": 100,
1927
- "pv": {
1928
- "event": {
1929
- "name": "version.registration.schematic.fetching",
1930
- "attributes": {}
1931
- },
1932
- "sources": [
1933
- "src/lib/version-registry/version-manager.ts"
1934
- ],
1935
- "otel": {
1936
- "kind": "event",
1937
- "category": "operation"
1938
- },
1939
- "shape": "rectangle",
1940
- "fill": "#3b82f6",
1941
- "stroke": "#1d4ed8",
1942
- "dataSchema": {
1943
- "repository.url": {
1944
- "type": "string",
1945
- "required": true
1946
- },
1947
- "git.sha": {
1948
- "type": "string",
1949
- "required": true
1950
- },
1951
- "schematic.cached": {
1952
- "type": "boolean",
1953
- "required": false
1954
- }
1955
- }
1956
- }
1957
- },
1958
- {
1959
- "id": "schematic-fetched",
1960
- "type": "text",
1961
- "text": "# Schematic Retrieved\n\nSchematic fetched from GitHub successfully",
1962
- "x": 91,
1963
- "y": 649,
1964
- "width": 200,
1965
- "height": 100,
1966
- "pv": {
1967
- "event": {
1968
- "name": "version.registration.schematic.fetched",
1969
- "attributes": {}
1970
- },
1971
- "sources": [
1972
- "src/lib/version-registry/schematic-fetcher.ts"
1973
- ],
1974
- "otel": {
1975
- "kind": "event",
1976
- "category": "operation"
1977
- },
1978
- "shape": "rectangle",
1979
- "fill": "#3b82f6",
1980
- "stroke": "#1d4ed8",
1981
- "dataSchema": {
1982
- "schematic.id": {
1983
- "type": "string",
1984
- "required": true
1985
- },
1986
- "canvases.count": {
1987
- "type": "number",
1988
- "required": true
1989
- },
1990
- "storyboards.count": {
1991
- "type": "number",
1992
- "required": true
1993
- },
1994
- "workflows.count": {
1995
- "type": "number",
1996
- "required": true
1997
- },
1998
- "errors.count": {
1999
- "type": "number",
2000
- "required": false
2001
- }
2002
- }
2003
- }
2004
- },
2005
- {
2006
- "id": "schematic-stored",
2007
- "type": "text",
2008
- "text": "# Store Schematic\n\nSchematic stored to S3",
2009
- "x": 86,
2010
- "y": 801,
2011
- "width": 200,
2012
- "height": 100,
2013
- "pv": {
2014
- "event": {
2015
- "name": "version.registration.schematic.stored",
2016
- "attributes": {}
2017
- },
2018
- "sources": [
2019
- "src/lib/version-registry/s3-storage.ts"
2020
- ],
2021
- "otel": {
2022
- "kind": "event",
2023
- "category": "operation"
2024
- },
2025
- "shape": "rectangle",
2026
- "fill": "#3b82f6",
2027
- "stroke": "#1d4ed8",
2028
- "dataSchema": {
2029
- "s3.key": {
2030
- "type": "string",
2031
- "required": true
2032
- },
2033
- "s3.bucket": {
2034
- "type": "string",
2035
- "required": true
2036
- },
2037
- "schematic.id": {
2038
- "type": "string",
2039
- "required": true
2040
- }
2041
- }
2042
- }
2043
- },
2044
- {
2045
- "id": "registration-complete",
2046
- "type": "text",
2047
- "text": "# Registration Complete\n\nVersion registered successfully",
2048
- "x": 97,
2049
- "y": 954,
2050
- "width": 200,
2051
- "height": 100,
2052
- "pv": {
2053
- "event": {
2054
- "name": "version.registration.complete",
2055
- "attributes": {}
2056
- },
2057
- "sources": [
2058
- "src/app/api/versions/route.ts"
2059
- ],
2060
- "otel": {
2061
- "kind": "event",
2062
- "category": "lifecycle"
2063
- },
2064
- "shape": "rectangle",
2065
- "fill": "#22c55e",
2066
- "stroke": "#16a34a",
2067
- "dataSchema": {
2068
- "registration.id": {
2069
- "type": "string",
2070
- "required": true
2071
- },
2072
- "schematic.id": {
2073
- "type": "string",
2074
- "required": true
2075
- },
2076
- "duration.ms": {
2077
- "type": "number",
2078
- "required": false
2079
- }
2080
- }
2081
- }
2082
- },
2083
- {
2084
- "id": "registration-error",
2085
- "type": "text",
2086
- "text": "# Registration Error\n\nVersion registration failed",
2087
- "x": 410,
2088
- "y": 358,
2089
- "width": 200,
2090
- "height": 100,
2091
- "pv": {
2092
- "event": {
2093
- "name": "version.registration.error",
2094
- "attributes": {}
2095
- },
2096
- "sources": [
2097
- "src/app/api/versions/route.ts",
2098
- "src/lib/version-registry/version-manager.ts"
2099
- ],
2100
- "otel": {
2101
- "kind": "event",
2102
- "category": "error"
2103
- },
2104
- "shape": "rectangle",
2105
- "fill": "#ef4444",
2106
- "stroke": "#dc2626",
2107
- "dataSchema": {
2108
- "error.type": {
2109
- "type": "string",
2110
- "required": true
2111
- },
2112
- "error.message": {
2113
- "type": "string",
2114
- "required": true
2115
- },
2116
- "error.stage": {
2117
- "type": "string",
2118
- "required": true
2119
- },
2120
- "service.name": {
2121
- "type": "string",
2122
- "required": false
2123
- },
2124
- "version": {
2125
- "type": "string",
2126
- "required": false
2127
- }
2128
- }
2129
- }
2130
- },
2131
- {
2132
- "id": "lookup-started",
2133
- "type": "text",
2134
- "text": "# Lookup Started\n\nVersion lookup request received",
2135
- "x": 1137,
2136
- "y": 172,
2137
- "width": 200,
2138
- "height": 100,
2139
- "pv": {
2140
- "event": {
2141
- "name": "version.lookup.started",
2142
- "attributes": {}
2143
- },
2144
- "sources": [
2145
- "src/app/api/versions/lookup/route.ts"
2146
- ],
2147
- "otel": {
2148
- "kind": "event",
2149
- "category": "lifecycle"
2150
- },
2151
- "shape": "rectangle",
2152
- "fill": "#22c55e",
2153
- "stroke": "#16a34a",
2154
- "dataSchema": {
2155
- "customer.id": {
2156
- "type": "string",
2157
- "required": true
2158
- },
2159
- "service.name": {
2160
- "type": "string",
2161
- "required": true
2162
- },
2163
- "version": {
2164
- "type": "string",
2165
- "required": true
2166
- },
2167
- "environment": {
2168
- "type": "string",
2169
- "required": true
2170
- }
2171
- }
2172
- }
2173
- },
2174
- {
2175
- "id": "lookup-s3-retrieved",
2176
- "type": "text",
2177
- "text": "# Retrieve from S3\n\nVersion mapping retrieved from S3",
2178
- "x": 1138,
2179
- "y": 386,
2180
- "width": 200,
2181
- "height": 100,
2182
- "pv": {
2183
- "event": {
2184
- "name": "version.lookup.s3.retrieved",
2185
- "attributes": {}
2186
- },
2187
- "sources": [
2188
- "src/lib/version-registry/s3-storage.ts"
2189
- ],
2190
- "otel": {
2191
- "kind": "event",
2192
- "category": "operation"
2193
- },
2194
- "shape": "rectangle",
2195
- "fill": "#3b82f6",
2196
- "stroke": "#1d4ed8",
2197
- "dataSchema": {
2198
- "s3.key": {
2199
- "type": "string",
2200
- "required": true
2201
- },
2202
- "git.sha": {
2203
- "type": "string",
2204
- "required": true
2205
- },
2206
- "repository.url": {
2207
- "type": "string",
2208
- "required": true
2209
- },
2210
- "deployed.at": {
2211
- "type": "string",
2212
- "required": true
2213
- }
2214
- }
2215
- }
2216
- },
2217
- {
2218
- "id": "lookup-complete",
2219
- "type": "text",
2220
- "text": "# Lookup Complete\n\nVersion lookup successful",
2221
- "x": 1138,
2222
- "y": 596,
2223
- "width": 200,
2224
- "height": 100,
2225
- "pv": {
2226
- "event": {
2227
- "name": "version.lookup.complete",
2228
- "attributes": {}
2229
- },
2230
- "sources": [
2231
- "src/app/api/versions/lookup/route.ts"
2232
- ],
2233
- "otel": {
2234
- "kind": "event",
2235
- "category": "lifecycle"
2236
- },
2237
- "shape": "rectangle",
2238
- "fill": "#22c55e",
2239
- "stroke": "#16a34a",
2240
- "dataSchema": {
2241
- "git.sha": {
2242
- "type": "string",
2243
- "required": true
2244
- },
2245
- "duration.ms": {
2246
- "type": "number",
2247
- "required": false
2248
- }
2249
- }
2250
- }
2251
- },
2252
- {
2253
- "id": "lookup-not-found",
2254
- "type": "text",
2255
- "text": "# Not Found\n\nVersion not registered",
2256
- "x": 1470,
2257
- "y": 262,
2258
- "width": 200,
2259
- "height": 100,
2260
- "pv": {
2261
- "event": {
2262
- "name": "version.lookup.not_found",
2263
- "attributes": {}
2264
- },
2265
- "sources": [
2266
- "src/app/api/versions/lookup/route.ts",
2267
- "src/lib/version-registry/s3-storage.ts"
2268
- ],
2269
- "otel": {
2270
- "kind": "event",
2271
- "category": "operation"
2272
- },
2273
- "shape": "rectangle",
2274
- "fill": "#f59e0b",
2275
- "stroke": "#d97706",
2276
- "dataSchema": {
2277
- "customer.id": {
2278
- "type": "string",
2279
- "required": true
2280
- },
2281
- "service.name": {
2282
- "type": "string",
2283
- "required": true
2284
- },
2285
- "version": {
2286
- "type": "string",
2287
- "required": true
2288
- }
2289
- }
2290
- }
2291
- },
2292
- {
2293
- "id": "lookup-error",
2294
- "type": "text",
2295
- "text": "# Lookup Error\n\nVersion lookup failed",
2296
- "x": 1471,
2297
- "y": 93,
2298
- "width": 200,
2299
- "height": 100,
2300
- "pv": {
2301
- "event": {
2302
- "name": "version.lookup.error",
2303
- "attributes": {}
2304
- },
2305
- "sources": [
2306
- "src/app/api/versions/lookup/route.ts",
2307
- "src/lib/version-registry/version-manager.ts"
2308
- ],
2309
- "otel": {
2310
- "kind": "event",
2311
- "category": "error"
2312
- },
2313
- "shape": "rectangle",
2314
- "fill": "#ef4444",
2315
- "stroke": "#dc2626",
2316
- "dataSchema": {
2317
- "error.type": {
2318
- "type": "string",
2319
- "required": true
2320
- },
2321
- "error.message": {
2322
- "type": "string",
2323
- "required": true
2324
- },
2325
- "error.stage": {
2326
- "type": "string",
2327
- "required": true
2328
- }
2329
- }
2330
- }
2331
- },
2332
- {
2333
- "id": "list-started",
2334
- "type": "text",
2335
- "text": "# List Started\n\nVersion list request received",
2336
- "x": 678,
2337
- "y": 292,
2338
- "width": 200,
2339
- "height": 100,
2340
- "pv": {
2341
- "event": {
2342
- "name": "version.list.started",
2343
- "attributes": {}
2344
- },
2345
- "sources": [
2346
- "src/app/api/versions/list/route.ts"
2347
- ],
2348
- "otel": {
2349
- "kind": "event",
2350
- "category": "lifecycle"
2351
- },
2352
- "shape": "rectangle",
2353
- "fill": "#22c55e",
2354
- "stroke": "#16a34a",
2355
- "dataSchema": {
2356
- "customer.id": {
2357
- "type": "string",
2358
- "required": true
2359
- }
2360
- }
2361
- }
2362
- },
2363
- {
2364
- "id": "list-s3-retrieved",
2365
- "type": "text",
2366
- "text": "# Fetch All Versions\n\nAll registrations fetched from S3",
2367
- "x": 678,
2368
- "y": 465,
2369
- "width": 200,
2370
- "height": 100,
2371
- "pv": {
2372
- "event": {
2373
- "name": "version.list.s3.retrieved",
2374
- "attributes": {}
2375
- },
2376
- "sources": [
2377
- "src/lib/version-registry/s3-storage.ts"
2378
- ],
2379
- "otel": {
2380
- "kind": "event",
2381
- "category": "operation"
2382
- },
2383
- "shape": "rectangle",
2384
- "fill": "#3b82f6",
2385
- "stroke": "#2563eb",
2386
- "dataSchema": {
2387
- "customer.id": {
2388
- "type": "string",
2389
- "required": true
2390
- },
2391
- "registration.count": {
2392
- "type": "number",
2393
- "required": true
2394
- }
2395
- }
2396
- }
2397
- },
2398
- {
2399
- "id": "list-complete",
2400
- "type": "text",
2401
- "text": "# List Complete\n\nVersion list successful",
2402
- "x": 678,
2403
- "y": 675,
2404
- "width": 200,
2405
- "height": 100,
2406
- "pv": {
2407
- "event": {
2408
- "name": "version.list.complete",
2409
- "attributes": {}
2410
- },
2411
- "sources": [
2412
- "src/app/api/versions/list/route.ts"
2413
- ],
2414
- "otel": {
2415
- "kind": "event",
2416
- "category": "lifecycle"
2417
- },
2418
- "shape": "rectangle",
2419
- "fill": "#22c55e",
2420
- "stroke": "#16a34a",
2421
- "dataSchema": {
2422
- "customer.id": {
2423
- "type": "string",
2424
- "required": true
2425
- },
2426
- "registration.count": {
2427
- "type": "number",
2428
- "required": true
2429
- }
2430
- }
2431
- }
2432
- },
2433
- {
2434
- "id": "list-error",
2435
- "type": "text",
2436
- "text": "# List Error\n\nVersion list failed",
2437
- "x": 921,
2438
- "y": 291,
2439
- "width": 200,
2440
- "height": 100,
2441
- "pv": {
2442
- "event": {
2443
- "name": "version.list.error",
2444
- "attributes": {}
2445
- },
2446
- "sources": [
2447
- "src/app/api/versions/list/route.ts",
2448
- "src/lib/version-registry/s3-storage.ts"
2449
- ],
2450
- "otel": {
2451
- "kind": "event",
2452
- "category": "error"
2453
- },
2454
- "shape": "rectangle",
2455
- "fill": "#ef4444",
2456
- "stroke": "#dc2626",
2457
- "dataSchema": {
2458
- "error.type": {
2459
- "type": "string",
2460
- "required": true
2461
- },
2462
- "error.message": {
2463
- "type": "string",
2464
- "required": true
2465
- },
2466
- "error.stage": {
2467
- "type": "string",
2468
- "required": true
2469
- }
2470
- }
2471
- }
2472
- }
2473
- ],
2474
- "edges": [
2475
- {
2476
- "id": "edge-reg-start-to-validate",
2477
- "fromNode": "registration-started",
2478
- "fromSide": "bottom",
2479
- "toNode": "registration-validated",
2480
- "toSide": "top",
2481
- "edgeType": "flow"
2482
- },
2483
- {
2484
- "id": "edge-reg-validate-to-s3",
2485
- "fromNode": "registration-validated",
2486
- "fromSide": "bottom",
2487
- "toNode": "registration-s3-stored",
2488
- "toSide": "top",
2489
- "edgeType": "flow"
2490
- },
2491
- {
2492
- "id": "edge-reg-s3-to-schematic-fetching",
2493
- "fromNode": "registration-s3-stored",
2494
- "fromSide": "bottom",
2495
- "toNode": "schematic-fetching",
2496
- "toSide": "top",
2497
- "edgeType": "flow"
2498
- },
2499
- {
2500
- "id": "edge-schematic-fetching-to-fetched",
2501
- "fromNode": "schematic-fetching",
2502
- "fromSide": "bottom",
2503
- "toNode": "schematic-fetched",
2504
- "toSide": "top",
2505
- "edgeType": "flow"
2506
- },
2507
- {
2508
- "id": "edge-schematic-fetched-to-stored",
2509
- "fromNode": "schematic-fetched",
2510
- "fromSide": "bottom",
2511
- "toNode": "schematic-stored",
2512
- "toSide": "top",
2513
- "edgeType": "flow"
2514
- },
2515
- {
2516
- "id": "edge-schematic-stored-to-complete",
2517
- "fromNode": "schematic-stored",
2518
- "fromSide": "bottom",
2519
- "toNode": "registration-complete",
2520
- "toSide": "top",
2521
- "edgeType": "flow"
2522
- },
2523
- {
2524
- "id": "edge-schematic-fetching-to-error",
2525
- "fromNode": "schematic-fetching",
2526
- "fromSide": "right",
2527
- "toNode": "registration-error",
2528
- "toSide": "left",
2529
- "label": "schematic fetch failed",
2530
- "edgeType": "error"
2531
- },
2532
- {
2533
- "id": "edge-schematic-stored-to-error",
2534
- "fromNode": "schematic-stored",
2535
- "fromSide": "right",
2536
- "toNode": "registration-error",
2537
- "toSide": "left",
2538
- "label": "storage failed",
2539
- "edgeType": "error"
2540
- },
2541
- {
2542
- "id": "edge-reg-start-to-error",
2543
- "fromNode": "registration-started",
2544
- "fromSide": "right",
2545
- "toNode": "registration-error",
2546
- "toSide": "left",
2547
- "label": "validation error",
2548
- "edgeType": "error"
2549
- },
2550
- {
2551
- "id": "edge-reg-validate-to-error",
2552
- "fromNode": "registration-validated",
2553
- "fromSide": "right",
2554
- "toNode": "registration-error",
2555
- "toSide": "left",
2556
- "label": "storage error",
2557
- "edgeType": "error"
2558
- },
2559
- {
2560
- "id": "edge-lookup-start-to-s3",
2561
- "fromNode": "lookup-started",
2562
- "fromSide": "bottom",
2563
- "toNode": "lookup-s3-retrieved",
2564
- "toSide": "top",
2565
- "edgeType": "flow"
2566
- },
2567
- {
2568
- "id": "edge-lookup-s3-to-complete",
2569
- "fromNode": "lookup-s3-retrieved",
2570
- "fromSide": "bottom",
2571
- "toNode": "lookup-complete",
2572
- "toSide": "top",
2573
- "edgeType": "flow"
2574
- },
2575
- {
2576
- "id": "edge-lookup-start-to-not-found",
2577
- "fromNode": "lookup-started",
2578
- "fromSide": "right",
2579
- "toNode": "lookup-not-found",
2580
- "toSide": "left",
2581
- "label": "not registered",
2582
- "edgeType": "error"
2583
- },
2584
- {
2585
- "id": "edge-lookup-start-to-error",
2586
- "fromNode": "lookup-started",
2587
- "fromSide": "right",
2588
- "toNode": "lookup-error",
2589
- "toSide": "left",
2590
- "label": "storage error",
2591
- "edgeType": "error"
2592
- },
2593
- {
2594
- "id": "edge-list-start-to-s3",
2595
- "fromNode": "list-started",
2596
- "fromSide": "bottom",
2597
- "toNode": "list-s3-retrieved",
2598
- "toSide": "top",
2599
- "edgeType": "flow"
2600
- },
2601
- {
2602
- "id": "edge-list-s3-to-complete",
2603
- "fromNode": "list-s3-retrieved",
2604
- "fromSide": "bottom",
2605
- "toNode": "list-complete",
2606
- "toSide": "top",
2607
- "edgeType": "flow"
2608
- },
2609
- {
2610
- "id": "edge-list-start-to-error",
2611
- "fromNode": "list-started",
2612
- "fromSide": "right",
2613
- "toNode": "list-error",
2614
- "toSide": "left",
2615
- "label": "storage error",
2616
- "edgeType": "error"
2617
- }
2618
- ]
2619
- };
2620
-
2621
- const VersionRegistryTemplate = () => {
2622
- return (
2623
- <div>
2624
- <div
2625
- style={{
2626
- marginBottom: 16,
2627
- padding: 12,
2628
- backgroundColor: '#f0fdf4',
2629
- borderRadius: 4,
2630
- border: '1px solid #22c55e',
2631
- }}
2632
- >
2633
- <strong style={{ color: '#15803d' }}>Version Registry Canvas (Real Production Data)</strong>
2634
- <div style={{ marginTop: 8, fontSize: 12, color: '#166534' }}>
2635
- <p style={{ margin: '4px 0' }}>
2636
- This is a real production canvas from the web-ade project showing a version registry system.
2637
- </p>
2638
- <p style={{ margin: '8px 0 4px 0' }}>
2639
- <strong>Features demonstrated:</strong>
2640
- </p>
2641
- <ul style={{ margin: '4px 0', paddingLeft: 20 }}>
2642
- <li><strong>16 nodes</strong> across 3 separate workflows (Registration, Lookup, List)</li>
2643
- <li><strong>OTEL event metadata</strong> with dataSchemas defining expected attributes</li>
2644
- <li><strong>Multiple edge types</strong>: flow (normal), error (failure paths), cache</li>
2645
- <li><strong>Color-coded events</strong>: Green (lifecycle), Blue (operation), Red (error), Orange (warning)</li>
2646
- <li><strong>Source file associations</strong> linking events to implementation files</li>
2647
- <li><strong>Complex workflow patterns</strong> including happy paths and error handling</li>
2648
- </ul>
2649
- <p style={{ margin: '8px 0 4px 0' }}>
2650
- <strong>Try clicking nodes</strong> to see their OTEL metadata, dataSchemas, and source files!
2651
- </p>
2652
- </div>
2653
- </div>
2654
- <div style={{ width: '100%', height: '800px' }}>
2655
- <GraphRenderer
2656
- canvas={versionRegistryCanvas}
2657
- showNodeDetailPanel={true}
2658
- />
2659
- </div>
2660
- </div>
2661
- );
2662
- };
2663
-
2664
- export const VersionRegistry: Story = {
2665
- render: () => <VersionRegistryTemplate />,
2666
- parameters: {
2667
- docs: {
2668
- description: {
2669
- story: `
2670
- **Version Registry - Real Production Canvas**
2671
-
2672
- This story uses actual production data from the web-ade project's version registry system.
2673
- It demonstrates how Principal View handles complex, real-world workflows with:
2674
-
2675
- **Three Main Workflows:**
2676
-
2677
- 1. **Registration Flow (Left)**:
2678
- - \`version.registration.started\` → validated → s3.stored → schematic.fetching → schematic.fetched → schematic.stored → complete
2679
- - Handles registering new version mappings from customer version strings to git SHAs
2680
- - Includes error paths for validation, storage, and schematic fetch failures
2681
-
2682
- 2. **Lookup Flow (Middle)**:
2683
- - \`version.lookup.started\` → s3.retrieved → complete
2684
- - Retrieves existing version mappings from S3
2685
- - Error paths for not found and lookup failures
2686
-
2687
- 3. **List Flow (Right)**:
2688
- - \`version.list.started\` → s3.retrieved → complete
2689
- - Lists all version registrations for a customer
2690
- - Error path for storage errors
2691
-
2692
- **OTEL Integration:**
2693
- - Each node has \`pv.event\` with event name and attributes
2694
- - \`dataSchema\` defines expected OTEL attributes (type, required, description)
2695
- - OTEL categories: lifecycle (green), operation (blue), error (red)
2696
- - Source file associations show where events are emitted
2697
-
2698
- **Use Case:**
2699
- This canvas serves as the "contract" for version registry observability:
2700
- - Developers instrument these events in their code
2701
- - Runtime telemetry is validated against these schemas
2702
- - Missing or orphaned events are detected automatically
2703
- - Coverage reports show which nodes are "silent" vs "active"
2704
- `,
2705
- },
2706
- },
2707
- },
2708
- };
2709
-
2710
- // ============================================================================
2711
- // Fit View to Nodes Story
2712
- // ============================================================================
2713
-
2714
- /**
2715
- * Helper to select random nodes from an array
2716
- */
2717
- function selectRandomNodes(allNodeIds: string[], count: number): string[] {
2718
- const shuffled = [...allNodeIds].sort(() => Math.random() - 0.5);
2719
- return shuffled.slice(0, Math.min(count, allNodeIds.length));
2720
- }
2721
-
2722
- const FitViewToNodesTemplate = () => {
2723
- // Get all node IDs from the version registry canvas
2724
- const allNodeIds = versionRegistryCanvas.nodes.map((n) => n.id);
2725
-
2726
- const [selectedNodeIds, setSelectedNodeIds] = React.useState<string[]>([]);
2727
- const [nodeCount, setNodeCount] = React.useState(3);
2728
-
2729
- const handleSelectRandom = () => {
2730
- const randomNodes = selectRandomNodes(allNodeIds, nodeCount);
2731
- setSelectedNodeIds(randomNodes);
2732
- };
2733
-
2734
- const handleClear = () => {
2735
- setSelectedNodeIds([]);
2736
- };
2737
-
2738
- // Pre-defined scenarios for quick testing
2739
- const handleSelectRegistrationFlow = () => {
2740
- setSelectedNodeIds([
2741
- 'registration-started',
2742
- 'registration-validated',
2743
- 'registration-s3-stored',
2744
- 'schematic-fetching',
2745
- 'schematic-fetched',
2746
- 'schematic-stored',
2747
- 'registration-complete',
2748
- ]);
2749
- };
2750
-
2751
- const handleSelectErrorNodes = () => {
2752
- setSelectedNodeIds([
2753
- 'registration-error',
2754
- 'lookup-error',
2755
- 'list-error',
2756
- 'lookup-not-found',
2757
- ]);
2758
- };
2759
-
2760
- const handleSelectLookupFlow = () => {
2761
- setSelectedNodeIds([
2762
- 'lookup-started',
2763
- 'lookup-s3-retrieved',
2764
- 'lookup-complete',
2765
- ]);
2766
- };
2767
-
2768
- return (
2769
- <div>
2770
- <div
2771
- style={{
2772
- marginBottom: 16,
2773
- padding: 12,
2774
- backgroundColor: '#f0f9ff',
2775
- borderRadius: 4,
2776
- border: '1px solid #3b82f6',
2777
- }}
2778
- >
2779
- <strong style={{ color: '#1e40af' }}>Fit View to Specific Nodes</strong>
2780
- <div style={{ marginTop: 8, fontSize: 12, color: '#475569' }}>
2781
- <p style={{ margin: '4px 0' }}>
2782
- This demonstrates the <code>fitViewToNodeIds</code> prop that zooms/pans the viewport
2783
- to show only the specified nodes.
2784
- </p>
2785
- <p style={{ margin: '8px 0 4px 0' }}>
2786
- <strong>Currently showing:</strong>{' '}
2787
- {selectedNodeIds.length > 0 ? (
2788
- <span style={{ fontFamily: 'monospace', color: '#3b82f6' }}>
2789
- {selectedNodeIds.length} nodes: [{selectedNodeIds.join(', ')}]
2790
- </span>
2791
- ) : (
2792
- <span style={{ color: '#94a3b8' }}>All nodes (default fitView)</span>
2793
- )}
2794
- </p>
2795
- </div>
2796
-
2797
- <div style={{ marginTop: 12, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
2798
- <label style={{ fontSize: 12, color: '#475569' }}>
2799
- Random count:
2800
- <select
2801
- value={nodeCount}
2802
- onChange={(e) => setNodeCount(Number(e.target.value))}
2803
- style={{
2804
- marginLeft: 4,
2805
- padding: '4px 8px',
2806
- borderRadius: 4,
2807
- border: '1px solid #cbd5e1',
2808
- }}
2809
- >
2810
- {[1, 2, 3, 4, 5, 6, 7, 8].map((n) => (
2811
- <option key={n} value={n}>
2812
- {n}
2813
- </option>
2814
- ))}
2815
- </select>
2816
- </label>
2817
- <button
2818
- onClick={handleSelectRandom}
2819
- style={{
2820
- padding: '6px 12px',
2821
- backgroundColor: '#3b82f6',
2822
- color: 'white',
2823
- border: 'none',
2824
- borderRadius: 4,
2825
- cursor: 'pointer',
2826
- fontSize: 12,
2827
- fontWeight: 'bold',
2828
- }}
2829
- >
2830
- Select Random Nodes
2831
- </button>
2832
- <button
2833
- onClick={handleClear}
2834
- style={{
2835
- padding: '6px 12px',
2836
- backgroundColor: '#94a3b8',
2837
- color: 'white',
2838
- border: 'none',
2839
- borderRadius: 4,
2840
- cursor: 'pointer',
2841
- fontSize: 12,
2842
- }}
2843
- >
2844
- Clear (Show All)
2845
- </button>
2846
- </div>
2847
-
2848
- <div style={{ marginTop: 12, display: 'flex', gap: 8, flexWrap: 'wrap' }}>
2849
- <span style={{ fontSize: 11, color: '#64748b', alignSelf: 'center' }}>Quick scenarios:</span>
2850
- <button
2851
- onClick={handleSelectRegistrationFlow}
2852
- style={{
2853
- padding: '4px 10px',
2854
- backgroundColor: '#22c55e',
2855
- color: 'white',
2856
- border: 'none',
2857
- borderRadius: 4,
2858
- cursor: 'pointer',
2859
- fontSize: 11,
2860
- }}
2861
- >
2862
- Registration Flow (7 nodes)
2863
- </button>
2864
- <button
2865
- onClick={handleSelectLookupFlow}
2866
- style={{
2867
- padding: '4px 10px',
2868
- backgroundColor: '#3b82f6',
2869
- color: 'white',
2870
- border: 'none',
2871
- borderRadius: 4,
2872
- cursor: 'pointer',
2873
- fontSize: 11,
2874
- }}
2875
- >
2876
- Lookup Flow (3 nodes)
2877
- </button>
2878
- <button
2879
- onClick={handleSelectErrorNodes}
2880
- style={{
2881
- padding: '4px 10px',
2882
- backgroundColor: '#ef4444',
2883
- color: 'white',
2884
- border: 'none',
2885
- borderRadius: 4,
2886
- cursor: 'pointer',
2887
- fontSize: 11,
2888
- }}
2889
- >
2890
- Error Nodes (4 nodes)
2891
- </button>
2892
- </div>
2893
- </div>
2894
- <div style={{ width: '100%', height: '600px' }}>
2895
- <GraphRenderer
2896
- canvas={versionRegistryCanvas}
2897
- fitViewToNodeIds={selectedNodeIds.length > 0 ? selectedNodeIds : undefined}
2898
- fitViewPadding={0.1}
2899
- activeNodeIds={selectedNodeIds.length > 0 ? selectedNodeIds : undefined}
2900
- showNodeDetailPanel={true}
2901
- />
2902
- </div>
2903
- </div>
2904
- );
2905
- };
2906
-
2907
- export const FitViewToNodes: Story = {
2908
- render: () => <FitViewToNodesTemplate />,
2909
- parameters: {
2910
- docs: {
2911
- description: {
2912
- story: `
2913
- **Fit View to Specific Nodes** - Demonstrates the \`fitViewToNodeIds\` prop for focusing on subsets of a graph.
2914
-
2915
- **Use Cases:**
2916
- - **Scenario Playback**: When playing back a workflow scenario, fit the view to show only the nodes involved
2917
- - **Search Results**: When searching for specific events, zoom to show matching nodes
2918
- - **Error Investigation**: Focus on error paths without manually panning/zooming
2919
- - **Step-by-Step Tutorials**: Highlight different parts of the graph as users progress
2920
-
2921
- **How It Works:**
2922
- 1. Pass an array of node IDs to \`fitViewToNodeIds\`
2923
- 2. The viewport animates to fit those specific nodes
2924
- 3. Pass \`undefined\` or empty array to return to default behavior (fit all nodes)
2925
-
2926
- **Props:**
2927
- - \`fitViewToNodeIds\`: Array of node IDs to fit in view
2928
- - \`fitViewPadding\`: Padding around the nodes (default: 0.2)
2929
-
2930
- **In This Demo:**
2931
- - Select random nodes to see how the view adjusts
2932
- - Try the pre-defined scenarios (Registration Flow, Lookup Flow, Error Nodes)
2933
- - Clear selection to fit all nodes again
2934
- `,
2935
- },
2936
- },
2937
- },
2938
- };
2939
-
2940
- // ============================================================================
2941
- // Initial Viewport Comparison Story
2942
- // ============================================================================
2943
-
2944
- const InitialViewportComparisonTemplate = () => {
2945
- const [key, setKey] = React.useState(0);
2946
-
2947
- const handleRemount = () => {
2948
- setKey((k) => k + 1);
2949
- };
2950
-
2951
- return (
2952
- <div>
2953
- <div
2954
- style={{
2955
- marginBottom: 16,
2956
- padding: 12,
2957
- backgroundColor: '#fef3c7',
2958
- borderRadius: 4,
2959
- border: '1px solid #f59e0b',
2960
- }}
2961
- >
2962
- <strong style={{ color: '#92400e' }}>Initial Viewport Comparison</strong>
2963
- <div style={{ marginTop: 8, fontSize: 12, color: '#78350f' }}>
2964
- <p style={{ margin: '4px 0' }}>
2965
- Compare the loading behavior with and without pre-calculated viewport dimensions.
2966
- </p>
2967
- <p style={{ margin: '8px 0 4px 0' }}>
2968
- <strong>Left:</strong> Default behavior - starts at zoom=1, then animates to fit
2969
- </p>
2970
- <p style={{ margin: '4px 0' }}>
2971
- <strong>Right:</strong> With <code>containerWidth</code>/<code>containerHeight</code> - instant correct positioning
2972
- </p>
2973
- <button
2974
- onClick={handleRemount}
2975
- style={{
2976
- marginTop: 12,
2977
- padding: '8px 16px',
2978
- backgroundColor: '#f59e0b',
2979
- color: 'white',
2980
- border: 'none',
2981
- borderRadius: 4,
2982
- cursor: 'pointer',
2983
- fontWeight: 'bold',
2984
- fontSize: 14,
2985
- }}
2986
- >
2987
- Remount Both (to see initial load behavior)
2988
- </button>
2989
- </div>
2990
- </div>
2991
- <div style={{ display: 'flex', gap: 16 }}>
2992
- <div style={{ flex: 1 }}>
2993
- <div
2994
- style={{
2995
- padding: '8px 12px',
2996
- backgroundColor: '#fee2e2',
2997
- borderRadius: '4px 4px 0 0',
2998
- border: '1px solid #fca5a5',
2999
- borderBottom: 'none',
3000
- fontSize: 12,
3001
- fontWeight: 'bold',
3002
- color: '#991b1b',
3003
- }}
3004
- >
3005
- Without containerWidth/Height (animated)
3006
- </div>
3007
- <div
3008
- key={`animated-${key}`}
3009
- style={{ width: '100%', height: '500px', border: '1px solid #fca5a5' }}
3010
- >
3011
- <GraphRenderer canvas={versionRegistryCanvas} />
3012
- </div>
3013
- </div>
3014
- <div style={{ flex: 1 }}>
3015
- <div
3016
- style={{
3017
- padding: '8px 12px',
3018
- backgroundColor: '#dcfce7',
3019
- borderRadius: '4px 4px 0 0',
3020
- border: '1px solid #86efac',
3021
- borderBottom: 'none',
3022
- fontSize: 12,
3023
- fontWeight: 'bold',
3024
- color: '#166534',
3025
- }}
3026
- >
3027
- With containerWidth/Height (instant)
3028
- </div>
3029
- <div
3030
- key={`instant-${key}`}
3031
- style={{ width: '100%', height: '500px', border: '1px solid #86efac' }}
3032
- >
3033
- <GraphRenderer
3034
- canvas={versionRegistryCanvas}
3035
- containerWidth={600}
3036
- containerHeight={500}
3037
- />
3038
- </div>
3039
- </div>
3040
- </div>
3041
- </div>
3042
- );
3043
- };
3044
-
3045
- export const InitialViewportComparison: Story = {
3046
- render: () => <InitialViewportComparisonTemplate />,
3047
- parameters: {
3048
- docs: {
3049
- description: {
3050
- story: `
3051
- **Initial Viewport Comparison** - Demonstrates the difference between default and pre-calculated viewport behavior.
3052
-
3053
- **The Problem:**
3054
- By default, React Flow renders at zoom=1 centered at origin, then after ~100ms calls \`fitView()\` with a 200ms animation.
3055
- This creates a visible "zoom in then zoom out" effect on initial load.
3056
-
3057
- **The Solution:**
3058
- When you provide \`containerWidth\` and \`containerHeight\` props, the component calculates the correct viewport
3059
- (zoom level and pan position) synchronously before the first render. This eliminates the animation entirely.
3060
-
3061
- **How to Use:**
3062
- \`\`\`tsx
3063
- <GraphRenderer
3064
- canvas={myCanvas}
3065
- containerWidth={800} // Your container's width in pixels
3066
- containerHeight={600} // Your container's height in pixels
3067
- />
3068
- \`\`\`
3069
-
3070
- **When to Use:**
3071
- - When you know the container dimensions ahead of time (fixed layouts)
3072
- - When the container size comes from a layout system (CSS grid, flexbox with known dimensions)
3073
- - When you want to eliminate any visual "settling" on initial load
3074
-
3075
- **When NOT to Use:**
3076
- - When container dimensions are dynamic/unknown until mount
3077
- - When using percentage-based sizing without a parent with known dimensions
3078
- - In these cases, the default delayed fitView behavior is still appropriate
3079
- `,
3080
- },
3081
- },
3082
- },
3083
- };
3084
-
3085
- // ============================================================================
3086
- // ELK Layout with Edit Mode
3087
- // ============================================================================
3088
-
3089
- const elkEditableCanvas: ExtendedCanvas = {
3090
- nodes: [
3091
- {
3092
- id: 'a',
3093
- type: 'text',
3094
- x: 50,
3095
- y: 50,
3096
- width: 120,
3097
- height: 60,
3098
- text: 'Node A',
3099
- color: 5,
3100
- pv: { nodeType: 'component', shape: 'rectangle' },
3101
- },
3102
- {
3103
- id: 'b',
3104
- type: 'text',
3105
- x: 250,
3106
- y: 50,
3107
- width: 120,
3108
- height: 60,
3109
- text: 'Node B',
3110
- color: 4,
3111
- pv: { nodeType: 'component', shape: 'rectangle' },
3112
- },
3113
- {
3114
- id: 'c',
3115
- type: 'text',
3116
- x: 250,
3117
- y: 180,
3118
- width: 120,
3119
- height: 60,
3120
- text: 'Node C',
3121
- color: 2,
3122
- pv: { nodeType: 'component', shape: 'rectangle' },
3123
- },
3124
- ],
3125
- edges: [
3126
- { id: 'e1', fromNode: 'a', toNode: 'b', fromSide: 'right', toSide: 'left', edgeType: 'flow' },
3127
- { id: 'e2', fromNode: 'a', toNode: 'c', fromSide: 'bottom', toSide: 'left', edgeType: 'flow' },
3128
- ],
3129
- pv: {
3130
- version: '1.0.0',
3131
- name: 'ELK Editable Test',
3132
- edgeTypes: {
3133
- flow: { style: 'solid', color: '#3b82f6', width: 2, directed: true },
3134
- },
3135
- nodeTypes: {
3136
- component: { color: '#4A90E2' },
3137
- },
3138
- },
3139
- };
3140
-
3141
- const ElkEditModeTemplate = () => {
3142
- const [isEditMode, setIsEditMode] = React.useState(false);
3143
- const [hasChanges, setHasChanges] = React.useState(false);
3144
- const graphRef = React.useRef<GraphRendererHandle>(null);
3145
-
3146
- return (
3147
- <div style={{ height: '500px', display: 'flex', flexDirection: 'column' }}>
3148
- <div style={{ padding: '10px', background: '#f0f0f0', display: 'flex', gap: '10px', alignItems: 'center' }}>
3149
- <button
3150
- onClick={() => setIsEditMode(!isEditMode)}
3151
- style={{
3152
- padding: '8px 16px',
3153
- background: isEditMode ? '#ef4444' : '#22c55e',
3154
- color: 'white',
3155
- border: 'none',
3156
- borderRadius: '4px',
3157
- cursor: 'pointer',
3158
- }}
3159
- >
3160
- {isEditMode ? 'Exit Edit Mode' : 'Enter Edit Mode'}
3161
- </button>
3162
- {isEditMode && hasChanges && (
3163
- <>
3164
- <button
3165
- onClick={() => {
3166
- graphRef.current?.getPendingChanges();
3167
- setHasChanges(false);
3168
- }}
3169
- style={{
3170
- padding: '8px 16px',
3171
- background: '#3b82f6',
3172
- color: 'white',
3173
- border: 'none',
3174
- borderRadius: '4px',
3175
- cursor: 'pointer',
3176
- }}
3177
- >
3178
- Save
3179
- </button>
3180
- <button
3181
- onClick={() => {
3182
- graphRef.current?.resetEditState();
3183
- setHasChanges(false);
3184
- }}
3185
- style={{
3186
- padding: '8px 16px',
3187
- background: '#6b7280',
3188
- color: 'white',
3189
- border: 'none',
3190
- borderRadius: '4px',
3191
- cursor: 'pointer',
3192
- }}
3193
- >
3194
- Discard
3195
- </button>
3196
- </>
3197
- )}
3198
- <span style={{ marginLeft: 'auto', color: '#666' }}>
3199
- {isEditMode ? 'Drag nodes to test edge routing' : 'Click Edit to enable node dragging'}
3200
- </span>
3201
- </div>
3202
- <div style={{ flex: 1 }}>
3203
- <GraphRenderer
3204
- ref={graphRef}
3205
- canvas={elkEditableCanvas}
3206
- width="100%"
3207
- height="100%"
3208
- editable={isEditMode}
3209
- onPendingChangesChange={setHasChanges}
3210
- showBackground={true}
3211
- backgroundVariant="dots"
3212
- elkLayout={{ enabled: true, routingStyle: 'orthogonal' }}
3213
- />
3214
- </div>
3215
- </div>
3216
- );
3217
- };
3218
-
3219
- export const ElkLayoutEditMode: Story = {
3220
- render: () => <ElkEditModeTemplate />,
3221
- parameters: {
3222
- docs: {
3223
- description: {
3224
- story: `
3225
- **ELK Layout with Edit Mode** - Tests orthogonal edge routing with editable nodes.
3226
-
3227
- **Test Cases:**
3228
- 1. Enter edit mode and drag Node A - edges should maintain connection points and reroute
3229
- 2. Drag Node C - bottom-to-left edge should stay connected correctly
3230
- 3. Save/Discard should preserve or reset positions
3231
- 4. Edges should not disappear when entering edit mode
3232
- `,
3233
- },
3234
- },
3235
- },
3236
- };