@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.
- package/dist/components/GraphRenderer.js +2 -2
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +41 -10
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +3 -3
- package/src/components/GraphRenderer.tsx +2 -2
- package/src/nodes/CustomNode.tsx +40 -12
- package/src/stories/OtelNodeTypesPrototype.stories.tsx +788 -0
- package/src/stories/data/graph-converter-test-execution.json +50 -50
|
@@ -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
|
+
};
|