@principal-ai/principal-view-react 0.13.27 → 0.13.29

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.
@@ -0,0 +1,788 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React from 'react';
3
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
4
+ import { GraphRenderer } from '../components/GraphRenderer';
5
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
6
+
7
+ /**
8
+ * =============================================================================
9
+ * OTEL Node Types - Examples
10
+ * =============================================================================
11
+ *
12
+ * This story demonstrates the implemented OTEL canvas node types:
13
+ * - otel-event: Telemetry events in workflows
14
+ * - otel-span-convention: Span naming patterns
15
+ * - otel-scope: Instrumentation scopes
16
+ * - otel-resource: Service/deployment resources
17
+ * - otel-boundary: External system interfaces
18
+ *
19
+ * See: docs/NODE_TYPE_MIGRATION.md for migration details.
20
+ *
21
+ * The new format uses:
22
+ * - Semantic `type` values (otel-event, otel-span-convention, etc.)
23
+ * - `label` for display text (required)
24
+ * - Top-level fields (no `pv` wrapper needed)
25
+ * - Consistent identifier display below label
26
+ */
27
+
28
+ // =============================================================================
29
+ // PROPOSED NEW FORMAT (what we want to migrate to)
30
+ // =============================================================================
31
+
32
+ /**
33
+ * This is what the NEW format would look like.
34
+ * Note: `type` is semantic, no `text` field, no `pv` wrapper
35
+ */
36
+ const proposedNewFormat = {
37
+ nodes: [
38
+ // otel-event: Telemetry events in workflows
39
+ {
40
+ type: 'otel-event',
41
+ id: 'analysis-started',
42
+ x: 100,
43
+ y: 100,
44
+ width: 180,
45
+ height: 80,
46
+ color: '4', // green
47
+ label: 'Analysis Started',
48
+ event: {
49
+ name: 'analysis.started',
50
+ attributes: {
51
+ 'file.count': { type: 'number', required: true, description: 'Number of files' },
52
+ },
53
+ },
54
+ otel: {
55
+ status: 'implemented',
56
+ scope: 'validation',
57
+ files: ['src/validation/analyzer.ts'],
58
+ },
59
+ },
60
+
61
+ // otel-span-convention: Span naming patterns
62
+ {
63
+ type: 'otel-span-convention',
64
+ id: 'validate-span',
65
+ x: 100,
66
+ y: 220,
67
+ width: 180,
68
+ height: 80,
69
+ color: '5', // cyan
70
+ label: 'Validation Operations',
71
+ description: 'All validation-related spans',
72
+ otel: {
73
+ status: 'approved',
74
+ spanPattern: 'validate.*',
75
+ spanKind: 'INTERNAL',
76
+ },
77
+ },
78
+
79
+ // otel-scope: Instrumentation scopes
80
+ {
81
+ type: 'otel-scope',
82
+ id: 'validation-scope',
83
+ x: 100,
84
+ y: 340,
85
+ width: 180,
86
+ height: 80,
87
+ color: '6', // purple
88
+ label: 'Validation Scope',
89
+ description: 'Tracer for validation operations',
90
+ otel: {
91
+ status: 'implemented',
92
+ scope: 'validation',
93
+ },
94
+ },
95
+
96
+ // otel-resource: Service/deployment resources
97
+ {
98
+ type: 'otel-resource',
99
+ id: 'cli-resource',
100
+ x: 100,
101
+ y: 460,
102
+ width: 180,
103
+ height: 80,
104
+ color: '2', // orange
105
+ label: 'CLI Service',
106
+ description: 'Principal View CLI tool',
107
+ otel: {
108
+ status: 'implemented',
109
+ resourceMatch: {
110
+ 'service.name': 'principal-view.cli',
111
+ },
112
+ },
113
+ },
114
+
115
+ // otel-boundary: External system interfaces
116
+ {
117
+ type: 'otel-boundary',
118
+ id: 'github-webhook',
119
+ x: 100,
120
+ y: 580,
121
+ width: 180,
122
+ height: 80,
123
+ color: '1', // red
124
+ label: 'GitHub Webhook',
125
+ description: 'Incoming webhook from GitHub',
126
+ otel: {
127
+ status: 'draft',
128
+ origin: 'external',
129
+ references: ['https://docs.github.com/webhooks'],
130
+ },
131
+ boundary: {
132
+ direction: 'inbound',
133
+ node: {
134
+ 'pv.event.name': 'webhook.repository-created',
135
+ 'pv.event.namespace': 'github',
136
+ },
137
+ },
138
+ },
139
+ ],
140
+ edges: [],
141
+ pv: {
142
+ version: '1.0.0',
143
+ name: 'New OTEL Node Types Prototype',
144
+ description: 'Demonstrates the proposed new node type format',
145
+ },
146
+ };
147
+
148
+ // =============================================================================
149
+ // ADAPTER: Convert new format to current format for rendering
150
+ // =============================================================================
151
+
152
+ interface NewFormatNode {
153
+ type: string;
154
+ id: string;
155
+ x: number;
156
+ y: number;
157
+ width: number;
158
+ height: number;
159
+ color?: string;
160
+ label: string;
161
+ description?: string;
162
+ icon?: string;
163
+ fill?: string;
164
+ event?: {
165
+ name: string;
166
+ attributes?: Record<string, unknown>;
167
+ };
168
+ otel?: {
169
+ status?: string;
170
+ scope?: string;
171
+ files?: string[];
172
+ spanPattern?: string;
173
+ spanKind?: string;
174
+ resourceMatch?: Record<string, string>;
175
+ origin?: string;
176
+ references?: string[];
177
+ };
178
+ boundary?: {
179
+ direction: string;
180
+ node: Record<string, string>;
181
+ };
182
+ }
183
+
184
+ /**
185
+ * Get the identifier that should be displayed under the label for each node type
186
+ */
187
+ function getNodeIdentifier(node: NewFormatNode): string | undefined {
188
+ switch (node.type) {
189
+ case 'otel-event':
190
+ return node.event?.name;
191
+ case 'otel-span-convention':
192
+ return node.otel?.spanPattern;
193
+ case 'otel-scope':
194
+ return node.otel?.scope;
195
+ case 'otel-resource':
196
+ // Show the primary resource match key/value
197
+ if (node.otel?.resourceMatch) {
198
+ const entries = Object.entries(node.otel.resourceMatch);
199
+ if (entries.length > 0) {
200
+ const [key, value] = entries[0];
201
+ return `${key}: ${value}`;
202
+ }
203
+ }
204
+ return undefined;
205
+ case 'otel-boundary':
206
+ return node.boundary?.direction;
207
+ default:
208
+ return undefined;
209
+ }
210
+ }
211
+
212
+ function adaptNewFormatToCurrentFormat(newFormat: {
213
+ nodes: NewFormatNode[];
214
+ edges: unknown[];
215
+ pv: unknown;
216
+ }): ExtendedCanvas {
217
+ const nodeTypeToIcon: Record<string, string> = {
218
+ 'otel-event': 'Zap',
219
+ 'otel-span-convention': 'GitCommit',
220
+ 'otel-scope': 'Layers',
221
+ 'otel-resource': 'Server',
222
+ 'otel-boundary': 'ArrowRightLeft',
223
+ };
224
+
225
+ const nodeTypeToShape: Record<string, string> = {
226
+ 'otel-event': 'rectangle',
227
+ 'otel-span-convention': 'hexagon',
228
+ 'otel-scope': 'circle',
229
+ 'otel-resource': 'diamond',
230
+ 'otel-boundary': 'rectangle',
231
+ };
232
+
233
+ return {
234
+ nodes: newFormat.nodes.map((node) => {
235
+ const identifier = getNodeIdentifier(node);
236
+
237
+ // For the prototype, we use the existing event.name mechanism to show identifiers
238
+ // by populating a synthetic event object with the identifier as the name.
239
+ // This gives us the same styling (75% size, monospace, 50% opacity) for all node types.
240
+ const syntheticEvent = identifier
241
+ ? { name: identifier, attributes: {} }
242
+ : node.event;
243
+
244
+ return {
245
+ id: node.id,
246
+ type: 'text' as const,
247
+ text: node.label,
248
+ x: node.x,
249
+ y: node.y,
250
+ width: node.width,
251
+ height: node.height,
252
+ color: node.color,
253
+ pv: {
254
+ nodeType: node.type,
255
+ name: node.label, // Just the label, identifier shown via event.name styling
256
+ description: buildDescription(node),
257
+ icon: node.icon || nodeTypeToIcon[node.type] || 'Circle',
258
+ shape: nodeTypeToShape[node.type] || 'rectangle',
259
+ status: node.otel?.status as 'draft' | 'approved' | 'implemented' | undefined,
260
+ otel: node.otel
261
+ ? {
262
+ scope: node.otel.scope,
263
+ files: node.otel.files,
264
+ spanPattern: node.otel.spanPattern,
265
+ spanKind: node.otel.spanKind as
266
+ | 'UNSPECIFIED'
267
+ | 'INTERNAL'
268
+ | 'SERVER'
269
+ | 'CLIENT'
270
+ | 'PRODUCER'
271
+ | 'CONSUMER'
272
+ | undefined,
273
+ resourceMatch: node.otel.resourceMatch,
274
+ }
275
+ : undefined,
276
+ // Use synthetic event to show identifier with same styling as event names
277
+ event: syntheticEvent,
278
+ boundary: node.boundary as
279
+ | {
280
+ direction: 'inbound' | 'outbound';
281
+ node: Record<string, string>;
282
+ }
283
+ | undefined,
284
+ origin: node.otel?.origin as 'internal' | 'external' | undefined,
285
+ references: node.otel?.references,
286
+ },
287
+ };
288
+ }),
289
+ edges: newFormat.edges as ExtendedCanvas['edges'],
290
+ pv: {
291
+ ...(newFormat.pv as object),
292
+ nodeTypes: {
293
+ 'otel-event': {
294
+ label: 'Event',
295
+ description: 'Telemetry event emitted during execution',
296
+ color: '#22c55e',
297
+ },
298
+ 'otel-span-convention': {
299
+ label: 'Span Convention',
300
+ description: 'Naming pattern for spans',
301
+ color: '#06b6d4',
302
+ },
303
+ 'otel-scope': {
304
+ label: 'Scope',
305
+ description: 'Instrumentation scope (tracer)',
306
+ color: '#8b5cf6',
307
+ },
308
+ 'otel-resource': {
309
+ label: 'Resource',
310
+ description: 'Service or deployment resource',
311
+ color: '#f97316',
312
+ },
313
+ 'otel-boundary': {
314
+ label: 'Boundary',
315
+ description: 'External system interface',
316
+ color: '#ef4444',
317
+ },
318
+ },
319
+ } as ExtendedCanvas['pv'],
320
+ };
321
+ }
322
+
323
+ function buildDescription(node: NewFormatNode): string {
324
+ const parts: string[] = [];
325
+
326
+ if (node.description) {
327
+ parts.push(node.description);
328
+ }
329
+
330
+ // Add type-specific info
331
+ if (node.type === 'otel-event' && node.event) {
332
+ parts.push(`\n\n**Event:** \`${node.event.name}\``);
333
+ if (node.event.attributes) {
334
+ const attrs = Object.entries(node.event.attributes)
335
+ .map(([key, val]) => {
336
+ const v = val as { type?: string; required?: boolean; description?: string };
337
+ return `- \`${key}\`: ${v.type || 'unknown'}${v.required ? ' (required)' : ''}`;
338
+ })
339
+ .join('\n');
340
+ parts.push(`\n**Attributes:**\n${attrs}`);
341
+ }
342
+ }
343
+
344
+ if (node.type === 'otel-span-convention' && node.otel?.spanPattern) {
345
+ parts.push(`\n\n**Pattern:** \`${node.otel.spanPattern}\``);
346
+ if (node.otel.spanKind) {
347
+ parts.push(`**SpanKind:** ${node.otel.spanKind}`);
348
+ }
349
+ }
350
+
351
+ if (node.type === 'otel-scope' && node.otel?.scope) {
352
+ parts.push(`\n\n**Scope:** \`${node.otel.scope}\``);
353
+ }
354
+
355
+ if (node.type === 'otel-resource' && node.otel?.resourceMatch) {
356
+ const matches = Object.entries(node.otel.resourceMatch)
357
+ .map(([k, v]) => `- \`${k}\`: ${v}`)
358
+ .join('\n');
359
+ parts.push(`\n\n**Resource Match:**\n${matches}`);
360
+ }
361
+
362
+ if (node.type === 'otel-boundary' && node.boundary) {
363
+ parts.push(`\n\n**Direction:** ${node.boundary.direction}`);
364
+ }
365
+
366
+ // Add status
367
+ if (node.otel?.status) {
368
+ const statusEmoji: Record<string, string> = {
369
+ draft: '\u{1F4DD}',
370
+ approved: '\u2705',
371
+ implemented: '\u{1F680}',
372
+ };
373
+ parts.push(`\n\n**Status:** ${statusEmoji[node.otel.status] || ''} ${node.otel.status}`);
374
+ }
375
+
376
+ return parts.join('');
377
+ }
378
+
379
+ // =============================================================================
380
+ // STORIES
381
+ // =============================================================================
382
+
383
+ const meta: Meta = {
384
+ title: 'OTEL/Node Types Prototype',
385
+ parameters: {
386
+ layout: 'fullscreen',
387
+ },
388
+ };
389
+
390
+ export default meta;
391
+
392
+ /**
393
+ * Shows all proposed OTEL node types rendered in the graph.
394
+ *
395
+ * Each node demonstrates:
396
+ * - Semantic type (otel-event, otel-span-convention, etc.)
397
+ * - Required `label` field
398
+ * - Type-specific fields (event, spanPattern, resourceMatch, etc.)
399
+ * - OTEL metadata grouped in `otel` field
400
+ *
401
+ * Hover over nodes to see the full metadata in tooltips.
402
+ */
403
+ export const AllNodeTypes: StoryObj = {
404
+ render: () => {
405
+ const canvas = adaptNewFormatToCurrentFormat(
406
+ proposedNewFormat as {
407
+ nodes: NewFormatNode[];
408
+ edges: unknown[];
409
+ pv: unknown;
410
+ }
411
+ );
412
+
413
+ return (
414
+ <ThemeProvider theme={defaultEditorTheme}>
415
+ <div style={{ width: '100%', height: '800px' }}>
416
+ <GraphRenderer canvas={canvas} initialViewport={{ x: 50, y: 20, zoom: 1 }} />
417
+ </div>
418
+ </ThemeProvider>
419
+ );
420
+ },
421
+ };
422
+
423
+ /**
424
+ * Shows the proposed JSON format for each node type.
425
+ * Use this as a reference for the migration.
426
+ */
427
+ export const FormatReference: StoryObj = {
428
+ render: () => {
429
+ // Helper to get identifier for each node type
430
+ const getIdentifier = (node: (typeof proposedNewFormat.nodes)[number]) => {
431
+ const n = node as NewFormatNode;
432
+ switch (n.type) {
433
+ case 'otel-event':
434
+ return n.event?.name;
435
+ case 'otel-span-convention':
436
+ return n.otel?.spanPattern;
437
+ case 'otel-scope':
438
+ return n.otel?.scope;
439
+ case 'otel-resource':
440
+ if (n.otel?.resourceMatch) {
441
+ const [key, value] = Object.entries(n.otel.resourceMatch)[0] || [];
442
+ return key ? `${key}: ${value}` : undefined;
443
+ }
444
+ return undefined;
445
+ case 'otel-boundary':
446
+ return n.boundary?.direction;
447
+ default:
448
+ return undefined;
449
+ }
450
+ };
451
+
452
+ return (
453
+ <ThemeProvider theme={defaultEditorTheme}>
454
+ <div style={{ padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>
455
+ <h1 style={{ marginBottom: '24px' }}>Proposed OTEL Node Type Formats</h1>
456
+ <p style={{ marginBottom: '24px', color: '#666' }}>
457
+ See <code>docs/NODE_TYPE_MIGRATION.md</code> for full documentation.
458
+ </p>
459
+
460
+ {proposedNewFormat.nodes.map((node, i) => {
461
+ const identifier = getIdentifier(node);
462
+ return (
463
+ <div
464
+ key={i}
465
+ style={{
466
+ marginBottom: '32px',
467
+ padding: '16px',
468
+ backgroundColor: '#f8f9fa',
469
+ borderRadius: '8px',
470
+ border: '1px solid #e9ecef',
471
+ }}
472
+ >
473
+ <div style={{ display: 'flex', alignItems: 'flex-start', gap: '24px', marginBottom: '12px' }}>
474
+ <div>
475
+ <h3 style={{ margin: 0, color: '#333' }}>
476
+ <code>{node.type}</code>
477
+ </h3>
478
+ </div>
479
+ <div
480
+ style={{
481
+ padding: '8px 16px',
482
+ backgroundColor: '#fff',
483
+ border: '2px solid #ddd',
484
+ borderRadius: '6px',
485
+ textAlign: 'center',
486
+ minWidth: '140px',
487
+ }}
488
+ >
489
+ <div style={{ fontWeight: 600, fontSize: '14px' }}>{node.label}</div>
490
+ {identifier && (
491
+ <div style={{ fontSize: '11px', color: '#666', fontFamily: 'monospace', marginTop: '4px' }}>
492
+ {identifier}
493
+ </div>
494
+ )}
495
+ </div>
496
+ </div>
497
+ <pre
498
+ style={{
499
+ backgroundColor: '#1e1e1e',
500
+ color: '#d4d4d4',
501
+ padding: '16px',
502
+ borderRadius: '4px',
503
+ overflow: 'auto',
504
+ fontSize: '13px',
505
+ lineHeight: '1.5',
506
+ }}
507
+ >
508
+ {JSON.stringify(node, null, 2)}
509
+ </pre>
510
+ </div>
511
+ );
512
+ })}
513
+ </div>
514
+ </ThemeProvider>
515
+ );
516
+ },
517
+ };
518
+
519
+ /**
520
+ * Side-by-side comparison of old format vs new format.
521
+ */
522
+ export const FormatComparison: StoryObj = {
523
+ render: () => {
524
+ const oldFormat = {
525
+ type: 'text',
526
+ text: 'Analysis Started',
527
+ id: 'analysis-started',
528
+ x: 100,
529
+ y: 100,
530
+ width: 180,
531
+ height: 80,
532
+ color: '4',
533
+ pv: {
534
+ nodeType: 'event',
535
+ name: 'Analysis Started',
536
+ description: 'Codebase composition analysis begins',
537
+ status: 'implemented',
538
+ event: {
539
+ name: 'analysis.started',
540
+ description: 'Analysis event',
541
+ attributes: {
542
+ 'file.count': { type: 'number', required: true },
543
+ },
544
+ },
545
+ otel: {
546
+ scope: 'validation',
547
+ files: ['src/validation/analyzer.ts'],
548
+ },
549
+ },
550
+ };
551
+
552
+ const newFormat = {
553
+ type: 'otel-event',
554
+ id: 'analysis-started',
555
+ x: 100,
556
+ y: 100,
557
+ width: 180,
558
+ height: 80,
559
+ color: '4',
560
+ label: 'Analysis Started',
561
+ event: {
562
+ name: 'analysis.started',
563
+ attributes: {
564
+ 'file.count': { type: 'number', required: true, description: 'Number of files' },
565
+ },
566
+ },
567
+ otel: {
568
+ status: 'implemented',
569
+ scope: 'validation',
570
+ files: ['src/validation/analyzer.ts'],
571
+ },
572
+ };
573
+
574
+ return (
575
+ <ThemeProvider theme={defaultEditorTheme}>
576
+ <div style={{ padding: '24px', maxWidth: '1400px', margin: '0 auto' }}>
577
+ <h1 style={{ marginBottom: '24px' }}>Format Comparison</h1>
578
+
579
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '24px' }}>
580
+ <div>
581
+ <h2 style={{ marginBottom: '16px', color: '#dc3545' }}>
582
+ Old Format (current)
583
+ </h2>
584
+ <ul style={{ marginBottom: '16px', color: '#666' }}>
585
+ <li>
586
+ <code>type: "text"</code> - not semantic
587
+ </li>
588
+ <li>
589
+ <code>text</code> field - duplicates pv.name
590
+ </li>
591
+ <li>
592
+ <code>pv</code> wrapper - extra nesting
593
+ </li>
594
+ <li>
595
+ <code>pv.description</code> - duplicates event.description
596
+ </li>
597
+ </ul>
598
+ <pre
599
+ style={{
600
+ backgroundColor: '#1e1e1e',
601
+ color: '#d4d4d4',
602
+ padding: '16px',
603
+ borderRadius: '4px',
604
+ overflow: 'auto',
605
+ fontSize: '12px',
606
+ lineHeight: '1.5',
607
+ }}
608
+ >
609
+ {JSON.stringify(oldFormat, null, 2)}
610
+ </pre>
611
+ </div>
612
+
613
+ <div>
614
+ <h2 style={{ marginBottom: '16px', color: '#28a745' }}>
615
+ New Format (proposed)
616
+ </h2>
617
+ <ul style={{ marginBottom: '16px', color: '#666' }}>
618
+ <li>
619
+ <code>type: "otel-event"</code> - semantic
620
+ </li>
621
+ <li>
622
+ <code>label</code> - single display field
623
+ </li>
624
+ <li>No <code>pv</code> wrapper - flat structure</li>
625
+ <li>
626
+ <code>otel</code> - instrumentation metadata only
627
+ </li>
628
+ </ul>
629
+ <pre
630
+ style={{
631
+ backgroundColor: '#1e1e1e',
632
+ color: '#d4d4d4',
633
+ padding: '16px',
634
+ borderRadius: '4px',
635
+ overflow: 'auto',
636
+ fontSize: '12px',
637
+ lineHeight: '1.5',
638
+ }}
639
+ >
640
+ {JSON.stringify(newFormat, null, 2)}
641
+ </pre>
642
+ </div>
643
+ </div>
644
+
645
+ <div style={{ marginTop: '32px', padding: '16px', backgroundColor: '#e8f5e9', borderRadius: '8px' }}>
646
+ <h3 style={{ marginBottom: '8px' }}>Key Changes</h3>
647
+ <ul style={{ marginBottom: 0 }}>
648
+ <li><strong>-15 lines</strong> (from 29 to 14 lines)</li>
649
+ <li><strong>No duplication</strong> - single source of truth for label, event name</li>
650
+ <li><strong>Semantic typing</strong> - <code>type</code> describes the node's purpose</li>
651
+ <li><strong>Flat structure</strong> - no more <code>pv.</code> prefix everywhere</li>
652
+ </ul>
653
+ </div>
654
+
655
+ <div style={{ marginTop: '32px' }}>
656
+ <h2 style={{ marginBottom: '16px' }}>Node Display: Label + Identifier</h2>
657
+ <p style={{ marginBottom: '16px', color: '#666' }}>
658
+ Each node shows its label plus identifier on the canvas:
659
+ </p>
660
+ <div style={{ display: 'flex', gap: '24px', flexWrap: 'wrap' }}>
661
+ {[
662
+ { type: 'otel-event', label: 'Analysis Started', identifier: 'analysis.started' },
663
+ { type: 'otel-span-convention', label: 'Validation Ops', identifier: 'validate.*' },
664
+ { type: 'otel-scope', label: 'Validation', identifier: 'validation' },
665
+ { type: 'otel-resource', label: 'CLI Service', identifier: 'service.name: pv.cli' },
666
+ { type: 'otel-boundary', label: 'GitHub Webhook', identifier: 'inbound' },
667
+ ].map((n, i) => (
668
+ <div
669
+ key={i}
670
+ style={{
671
+ padding: '12px 20px',
672
+ backgroundColor: '#fff',
673
+ border: '2px solid #ddd',
674
+ borderRadius: '8px',
675
+ textAlign: 'center',
676
+ minWidth: '140px',
677
+ }}
678
+ >
679
+ <div style={{ fontSize: '10px', color: '#999', marginBottom: '4px' }}>{n.type}</div>
680
+ <div style={{ fontWeight: 600, fontSize: '14px' }}>{n.label}</div>
681
+ <div style={{ fontSize: '11px', color: '#666', fontFamily: 'monospace', marginTop: '4px' }}>
682
+ {n.identifier}
683
+ </div>
684
+ </div>
685
+ ))}
686
+ </div>
687
+ </div>
688
+ </div>
689
+ </ThemeProvider>
690
+ );
691
+ },
692
+ };
693
+
694
+ /**
695
+ * Interactive workflow example using the new node types.
696
+ */
697
+ export const WorkflowExample: StoryObj = {
698
+ render: () => {
699
+ const workflowCanvas = adaptNewFormatToCurrentFormat({
700
+ nodes: [
701
+ {
702
+ type: 'otel-scope',
703
+ id: 'validation-scope',
704
+ x: 50,
705
+ y: 50,
706
+ width: 160,
707
+ height: 70,
708
+ color: '6',
709
+ label: 'Validation',
710
+ description: 'Validation instrumentation scope',
711
+ otel: { status: 'implemented', scope: 'validation' },
712
+ },
713
+ {
714
+ type: 'otel-event',
715
+ id: 'validation-started',
716
+ x: 250,
717
+ y: 50,
718
+ width: 160,
719
+ height: 70,
720
+ color: '4',
721
+ label: 'Validation Started',
722
+ event: { name: 'validation.started', attributes: {} },
723
+ otel: { status: 'implemented', scope: 'validation' },
724
+ },
725
+ {
726
+ type: 'otel-event',
727
+ id: 'file-parsed',
728
+ x: 450,
729
+ y: 50,
730
+ width: 160,
731
+ height: 70,
732
+ color: '4',
733
+ label: 'File Parsed',
734
+ event: {
735
+ name: 'validation.file.parsed',
736
+ attributes: { 'file.path': { type: 'string', required: true } },
737
+ },
738
+ otel: { status: 'implemented', scope: 'validation' },
739
+ },
740
+ {
741
+ type: 'otel-event',
742
+ id: 'validation-complete',
743
+ x: 650,
744
+ y: 50,
745
+ width: 160,
746
+ height: 70,
747
+ color: '4',
748
+ label: 'Validation Complete',
749
+ event: {
750
+ name: 'validation.complete',
751
+ attributes: { 'error.count': { type: 'number', required: true } },
752
+ },
753
+ otel: { status: 'implemented', scope: 'validation' },
754
+ },
755
+ {
756
+ type: 'otel-span-convention',
757
+ id: 'validate-span',
758
+ x: 350,
759
+ y: 170,
760
+ width: 180,
761
+ height: 70,
762
+ color: '5',
763
+ label: 'validate.*',
764
+ description: 'All validation operation spans',
765
+ otel: { status: 'approved', spanPattern: 'validate.*', spanKind: 'INTERNAL' },
766
+ },
767
+ ],
768
+ edges: [
769
+ { id: 'e1', fromNode: 'validation-scope', toNode: 'validation-started', fromSide: 'right', toSide: 'left' },
770
+ { id: 'e2', fromNode: 'validation-started', toNode: 'file-parsed', fromSide: 'right', toSide: 'left' },
771
+ { id: 'e3', fromNode: 'file-parsed', toNode: 'validation-complete', fromSide: 'right', toSide: 'left' },
772
+ { id: 'e4', fromNode: 'validate-span', toNode: 'file-parsed', fromSide: 'top', toSide: 'bottom', label: 'governs' },
773
+ ],
774
+ pv: {
775
+ version: '1.0.0',
776
+ name: 'Validation Workflow',
777
+ },
778
+ } as { nodes: NewFormatNode[]; edges: unknown[]; pv: unknown });
779
+
780
+ return (
781
+ <ThemeProvider theme={defaultEditorTheme}>
782
+ <div style={{ width: '100%', height: '400px' }}>
783
+ <GraphRenderer canvas={workflowCanvas} initialViewport={{ x: 20, y: 50, zoom: 1 }} />
784
+ </div>
785
+ </ThemeProvider>
786
+ );
787
+ },
788
+ };