@principal-ai/principal-view-react 0.7.9 → 0.7.10

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,420 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { GraphRenderer } from '../components/GraphRenderer';
4
+ import { TestEventPanel } from '../components/TestEventPanel';
5
+ import type { ExtendedCanvas, GraphEvent } from '@principal-ai/principal-view-core';
6
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
+ import testSpans from './data/graph-converter-test-execution.json';
8
+
9
+ const meta = {
10
+ title: 'Features/Real Test Execution',
11
+ component: GraphRenderer,
12
+ parameters: {
13
+ layout: 'fullscreen',
14
+ docs: {
15
+ description: {
16
+ component:
17
+ 'Visualizes REAL test execution data from instrumented Bun tests using the "wide event" pattern. Shows actual spans with file/line information collected from running GraphConverter.test.ts. Hover over graph nodes to highlight related events in the panel.',
18
+ },
19
+ },
20
+ },
21
+ tags: ['autodocs'],
22
+ decorators: [
23
+ (Story) => (
24
+ <ThemeProvider theme={defaultEditorTheme}>
25
+ <div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
26
+ <Story />
27
+ </div>
28
+ </ThemeProvider>
29
+ ),
30
+ ],
31
+ } satisfies Meta<typeof GraphRenderer>;
32
+
33
+ export default meta;
34
+ type Story = StoryObj<typeof meta>;
35
+
36
+ // ============================================================================
37
+ // Test Execution Flow Canvas
38
+ // ============================================================================
39
+
40
+ const testExecutionCanvas: ExtendedCanvas = {
41
+ nodes: [
42
+ // Test Suite
43
+ {
44
+ id: 'test-suite',
45
+ type: 'text',
46
+ text: 'GraphConverter Test Suite',
47
+ x: -100,
48
+ y: -100,
49
+ width: 240,
50
+ height: 80,
51
+ pv: {
52
+ nodeType: 'test-suite',
53
+ name: 'Test Suite',
54
+ description: 'Collection of GraphConverter tests',
55
+ shape: 'rectangle',
56
+ fill: '#3b82f6',
57
+ },
58
+ },
59
+
60
+ // Test Phase Nodes
61
+ {
62
+ id: 'setup-phase',
63
+ type: 'text',
64
+ text: 'Setup',
65
+ x: -250,
66
+ y: 50,
67
+ width: 120,
68
+ height: 80,
69
+ pv: {
70
+ nodeType: 'test-phase',
71
+ name: 'Setup Phase',
72
+ description: 'Test data preparation',
73
+ shape: 'hexagon',
74
+ fill: '#10b981',
75
+ },
76
+ },
77
+ {
78
+ id: 'execution-phase',
79
+ type: 'text',
80
+ text: 'Execution',
81
+ x: -80,
82
+ y: 50,
83
+ width: 120,
84
+ height: 80,
85
+ pv: {
86
+ nodeType: 'test-phase',
87
+ name: 'Execution Phase',
88
+ description: 'Code under test runs',
89
+ shape: 'hexagon',
90
+ fill: '#f59e0b',
91
+ },
92
+ },
93
+ {
94
+ id: 'assertion-phase',
95
+ type: 'text',
96
+ text: 'Assertion',
97
+ x: 90,
98
+ y: 50,
99
+ width: 120,
100
+ height: 80,
101
+ pv: {
102
+ nodeType: 'test-phase',
103
+ name: 'Assertion Phase',
104
+ description: 'Verify results',
105
+ shape: 'hexagon',
106
+ fill: '#8b5cf6',
107
+ },
108
+ },
109
+
110
+ // Result Node
111
+ {
112
+ id: 'test-result',
113
+ type: 'text',
114
+ text: 'Test Result',
115
+ x: -100,
116
+ y: 200,
117
+ width: 240,
118
+ height: 80,
119
+ pv: {
120
+ nodeType: 'result',
121
+ name: 'Test Result',
122
+ description: 'Pass/Fail outcome',
123
+ shape: 'rectangle',
124
+ fill: '#10b981',
125
+ },
126
+ },
127
+ ],
128
+ edges: [
129
+ {
130
+ id: 'suite-to-setup',
131
+ fromNode: 'test-suite',
132
+ toNode: 'setup-phase',
133
+ fromSide: 'bottom',
134
+ toSide: 'top',
135
+ label: 'start test',
136
+ pv: {
137
+ edgeType: 'flow',
138
+ style: 'solid',
139
+ },
140
+ },
141
+ {
142
+ id: 'setup-to-execution',
143
+ fromNode: 'setup-phase',
144
+ toNode: 'execution-phase',
145
+ fromSide: 'right',
146
+ toSide: 'left',
147
+ label: 'data ready',
148
+ pv: {
149
+ edgeType: 'flow',
150
+ style: 'solid',
151
+ },
152
+ },
153
+ {
154
+ id: 'execution-to-assertion',
155
+ fromNode: 'execution-phase',
156
+ toNode: 'assertion-phase',
157
+ fromSide: 'right',
158
+ toSide: 'left',
159
+ label: 'got result',
160
+ pv: {
161
+ edgeType: 'flow',
162
+ style: 'solid',
163
+ },
164
+ },
165
+ {
166
+ id: 'assertion-to-result',
167
+ fromNode: 'assertion-phase',
168
+ toNode: 'test-result',
169
+ fromSide: 'bottom',
170
+ toSide: 'top',
171
+ label: 'complete',
172
+ pv: {
173
+ edgeType: 'flow',
174
+ style: 'solid',
175
+ },
176
+ },
177
+ ],
178
+ pv: {
179
+ version: '1.0.0',
180
+ name: 'Test Execution Flow',
181
+ description: 'Visualizes the flow of test execution through phases',
182
+ },
183
+ };
184
+
185
+ // ============================================================================
186
+ // Convert Test Spans to Graph Events
187
+ // ============================================================================
188
+
189
+ function convertSpansToEvents(spans: typeof testSpans): GraphEvent[] {
190
+ const events: GraphEvent[] = [];
191
+ let time = 0;
192
+
193
+ spans.forEach((testSpan) => {
194
+ // Pulse test suite node at start of each test
195
+ events.push({
196
+ timestamp: time,
197
+ category: 'node',
198
+ operation: 'animate',
199
+ payload: {
200
+ nodeId: 'test-suite',
201
+ animation: { type: 'pulse', duration: 500 },
202
+ },
203
+ });
204
+ time += 600;
205
+
206
+ // Animate through events in the span
207
+ testSpan.events.forEach((event) => {
208
+ const eventName = event.name;
209
+
210
+ // Determine which phase based on event name
211
+ let nodeId = '';
212
+ let edgeId = '';
213
+
214
+ if (eventName.startsWith('setup.')) {
215
+ nodeId = 'setup-phase';
216
+ edgeId = 'suite-to-setup';
217
+ } else if (eventName.startsWith('execution.')) {
218
+ nodeId = 'execution-phase';
219
+ edgeId = 'setup-to-execution';
220
+ } else if (eventName.startsWith('assertion.')) {
221
+ nodeId = 'assertion-phase';
222
+ edgeId = 'execution-to-assertion';
223
+ }
224
+
225
+ // Animate edge when phase starts
226
+ if (eventName.endsWith('.started') && edgeId) {
227
+ events.push({
228
+ timestamp: time,
229
+ category: 'edge',
230
+ operation: 'animate',
231
+ payload: {
232
+ edgeId,
233
+ animation: { type: 'particle', duration: 500 },
234
+ },
235
+ });
236
+ time += 600;
237
+ }
238
+
239
+ // Pulse node
240
+ if (nodeId) {
241
+ events.push({
242
+ timestamp: time,
243
+ category: 'node',
244
+ operation: 'animate',
245
+ payload: {
246
+ nodeId,
247
+ animation: { type: 'pulse', duration: 600 },
248
+ },
249
+ });
250
+ time += 700;
251
+ }
252
+ });
253
+
254
+ // Animate to result
255
+ events.push({
256
+ timestamp: time,
257
+ category: 'edge',
258
+ operation: 'animate',
259
+ payload: {
260
+ edgeId: 'assertion-to-result',
261
+ animation: { type: 'particle', duration: 500 },
262
+ },
263
+ });
264
+ time += 600;
265
+
266
+ events.push({
267
+ timestamp: time,
268
+ category: 'node',
269
+ operation: 'animate',
270
+ payload: {
271
+ nodeId: 'test-result',
272
+ animation: { type: 'pulse', duration: 800 },
273
+ },
274
+ });
275
+ time += 1200; // Pause between tests
276
+ });
277
+
278
+ return events;
279
+ }
280
+
281
+ // ============================================================================
282
+ // Animated Story
283
+ // ============================================================================
284
+
285
+ const AnimatedTestExecution = () => {
286
+ const [events, setEvents] = useState<GraphEvent[]>([]);
287
+ const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
288
+ const [currentEventIndex, setCurrentEventIndex] = useState(0);
289
+ const [highlightedPhase, setHighlightedPhase] = useState<string | undefined>();
290
+
291
+ useEffect(() => {
292
+ const graphEvents = convertSpansToEvents(testSpans);
293
+ const timers: NodeJS.Timeout[] = [];
294
+
295
+ let spanIndex = 0;
296
+ let eventIndex = 0;
297
+ let eventsPerTest = testSpans[0].events.length * 2 + 2; // ~2 graph events per span event + suite + result
298
+
299
+ graphEvents.forEach((event, index) => {
300
+ const timer = setTimeout(() => {
301
+ setEvents((prev) => [...prev, event]);
302
+
303
+ // Track which span and event we're on
304
+ spanIndex = Math.floor(index / eventsPerTest);
305
+ eventIndex = Math.floor((index % eventsPerTest) / 2);
306
+
307
+ setCurrentSpanIndex(Math.min(spanIndex, testSpans.length - 1));
308
+ setCurrentEventIndex(eventIndex);
309
+ }, event.timestamp);
310
+ timers.push(timer);
311
+ });
312
+
313
+ // Reset animation
314
+ const resetTimer = setTimeout(() => {
315
+ setEvents([]);
316
+ setCurrentSpanIndex(0);
317
+ setCurrentEventIndex(0);
318
+ }, graphEvents[graphEvents.length - 1].timestamp + 2000);
319
+
320
+ return () => {
321
+ timers.forEach(clearTimeout);
322
+ clearTimeout(resetTimer);
323
+ };
324
+ }, []);
325
+
326
+ // Map node IDs to phase names
327
+ const getPhaseFromNodeId = (nodeId: string): string | undefined => {
328
+ if (nodeId === 'setup-phase') return 'setup';
329
+ if (nodeId === 'execution-phase') return 'execution';
330
+ if (nodeId === 'assertion-phase') return 'assertion';
331
+ return undefined;
332
+ };
333
+
334
+ return (
335
+ <div style={{ display: 'flex', width: '100%', height: '100%' }}>
336
+ {/* Graph Visualization - Left Side */}
337
+ <div
338
+ style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}
339
+ onMouseLeave={() => setHighlightedPhase(undefined)}
340
+ >
341
+ <div
342
+ style={{ width: '100%', height: '100%' }}
343
+ onMouseOver={(e) => {
344
+ // Check if hovering over a phase node
345
+ const target = e.target as HTMLElement;
346
+ const textContent = target.textContent;
347
+ if (textContent === 'Setup') setHighlightedPhase('setup');
348
+ else if (textContent === 'Execution') setHighlightedPhase('execution');
349
+ else if (textContent === 'Assertion') setHighlightedPhase('assertion');
350
+ }}
351
+ >
352
+ <GraphRenderer
353
+ canvas={testExecutionCanvas}
354
+ showMinimap={true}
355
+ showControls={true}
356
+ events={events}
357
+ />
358
+ </div>
359
+ </div>
360
+
361
+ {/* Event Panel - Right Side */}
362
+ <div style={{ flex: '0 0 40%', height: '100%', borderLeft: '1px solid #333' }}>
363
+ <TestEventPanel
364
+ spans={testSpans as any}
365
+ currentSpanIndex={currentSpanIndex}
366
+ currentEventIndex={currentEventIndex}
367
+ highlightedPhase={highlightedPhase}
368
+ />
369
+ </div>
370
+ </div>
371
+ );
372
+ };
373
+
374
+ /**
375
+ * Animated visualization of real test execution data using the "wide event" pattern.
376
+ *
377
+ * This demonstrates the key concept from loggingsucks.com:
378
+ * - ONE comprehensive span per test (not multiple child spans)
379
+ * - Events show the narrative of what happened during execution
380
+ * - Context accumulates through event attributes with file/line information
381
+ * - Easy to search by test.name to get full execution story
382
+ *
383
+ * **Interaction:**
384
+ * - Hover over graph nodes (Setup, Execution, Assertion) to highlight related events
385
+ * - Watch the code journey: blue = test file, green = code under test
386
+ * - See how context builds up through events as the animation plays
387
+ */
388
+ export const Animated: Story = {
389
+ render: () => <AnimatedTestExecution />,
390
+ };
391
+
392
+ /**
393
+ * Static view of the test execution flow showing phases.
394
+ */
395
+ export const StaticView: Story = {
396
+ args: {
397
+ canvas: testExecutionCanvas,
398
+ showMinimap: true,
399
+ showControls: true,
400
+ },
401
+ };
402
+
403
+ /**
404
+ * Event panel component showing test execution narrative with file/line information.
405
+ *
406
+ * Shows how events accumulate context as tests execute, with automatic file/line
407
+ * capture from stack traces and manual override for code under test.
408
+ */
409
+ export const EventPanelOnly: StoryObj = {
410
+ render: () => (
411
+ <div style={{ width: '600px', height: '100vh' }}>
412
+ <TestEventPanel
413
+ spans={testSpans as any}
414
+ currentSpanIndex={0}
415
+ currentEventIndex={5} // Show all events
416
+ highlightedPhase={undefined}
417
+ />
418
+ </div>
419
+ ),
420
+ };
@@ -0,0 +1,160 @@
1
+ import React from 'react';
2
+ import type { Meta, StoryObj } from '@storybook/react';
3
+ import { GraphRenderer } from '../components/GraphRenderer';
4
+ import { TestEventPanel } from '../components/TestEventPanel';
5
+ import type { ExtendedCanvas } from '@principal-ai/principal-view-core';
6
+ import { ThemeProvider, defaultEditorTheme } from '@principal-ade/industry-theme';
7
+ import executionCanvas from '../../../../.principal-views/graph-converter-execution.otel.canvas';
8
+ import validatedSpans from './data/graph-converter-validated-execution.json';
9
+
10
+ const meta = {
11
+ title: 'Features/Validated Execution',
12
+ component: GraphRenderer,
13
+ parameters: {
14
+ layout: 'fullscreen',
15
+ docs: {
16
+ description: {
17
+ component:
18
+ 'Demonstrates type-safe event emission with schema validation. The canvas defines expected events, and production code is validated against this schema. Shows how events match the schema defined in graph-converter-execution.otel.canvas.',
19
+ },
20
+ },
21
+ },
22
+ tags: ['autodocs'],
23
+ decorators: [
24
+ (Story) => (
25
+ <ThemeProvider theme={defaultEditorTheme}>
26
+ <div style={{ width: '100vw', height: '100vh', background: '#0a0a0a' }}>
27
+ <Story />
28
+ </div>
29
+ </ThemeProvider>
30
+ ),
31
+ ],
32
+ } satisfies Meta<typeof GraphRenderer>;
33
+
34
+ export default meta;
35
+ type Story = StoryObj<typeof meta>;
36
+
37
+ /**
38
+ * Graph visualization of the execution flow with event schema definitions.
39
+ *
40
+ * This canvas defines:
41
+ * - `graph-converter` node with 5 event types
42
+ * - `validation` node with 2 event types
43
+ * - `graph-output` node with 1 event type
44
+ *
45
+ * Each event type has a schema defining:
46
+ * - Required/optional fields
47
+ * - Field types (string, number, boolean, etc.)
48
+ * - Field descriptions
49
+ *
50
+ * See `.principal-views/graph-converter-execution.otel.canvas` for the full schema.
51
+ */
52
+ export const ExecutionFlow: Story = {
53
+ args: {
54
+ canvas: executionCanvas as ExtendedCanvas,
55
+ showMinimap: true,
56
+ showControls: true,
57
+ },
58
+ };
59
+
60
+ /**
61
+ * Event panel showing validated execution data.
62
+ *
63
+ * These events were emitted using `createValidatedSpanEmitter()` which:
64
+ * - Validates events against the canvas schema
65
+ * - Ensures required fields are present
66
+ * - Checks field types match the schema
67
+ * - Throws errors in strict mode if validation fails
68
+ *
69
+ * All events in this panel passed schema validation.
70
+ */
71
+ export const ValidatedEvents: StoryObj = {
72
+ render: () => (
73
+ <div style={{ width: '800px', height: '100vh' }}>
74
+ <TestEventPanel
75
+ spans={validatedSpans as any}
76
+ currentSpanIndex={0}
77
+ currentEventIndex={10} // Show all events
78
+ highlightedPhase={undefined}
79
+ />
80
+ </div>
81
+ ),
82
+ };
83
+
84
+ /**
85
+ * Side-by-side view of execution flow and validated events.
86
+ *
87
+ * **How it works:**
88
+ * 1. Canvas defines event schemas (what events should be emitted)
89
+ * 2. Tests use `createValidatedSpanEmitter()` to emit events
90
+ * 3. Events are validated against the schema in strict mode
91
+ * 4. If validation fails, test throws `EventValidationError`
92
+ * 5. If validation passes, events are emitted and collected
93
+ *
94
+ * This ensures production code emits events that match the architecture.
95
+ */
96
+ export const FlowWithValidation: StoryObj = {
97
+ render: () => (
98
+ <div style={{ display: 'flex', width: '100%', height: '100%' }}>
99
+ {/* Graph Visualization - Left Side */}
100
+ <div style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}>
101
+ <GraphRenderer
102
+ canvas={executionCanvas as ExtendedCanvas}
103
+ showMinimap={true}
104
+ showControls={true}
105
+ />
106
+ </div>
107
+
108
+ {/* Event Panel - Right Side */}
109
+ <div style={{ flex: '0 0 40%', height: '100%', borderLeft: '1px solid #333' }}>
110
+ <div style={{ padding: '20px', color: '#fff', borderBottom: '1px solid #333' }}>
111
+ <h3 style={{ margin: '0 0 10px 0', fontSize: '16px' }}>
112
+ Type-Safe Validated Events
113
+ </h3>
114
+ <p style={{ margin: 0, fontSize: '12px', color: '#888' }}>
115
+ Events validated against canvas schema. See{' '}
116
+ <code>.principal-views/graph-converter-execution.otel.canvas</code>
117
+ </p>
118
+ </div>
119
+ <TestEventPanel
120
+ spans={validatedSpans as any}
121
+ currentSpanIndex={0}
122
+ currentEventIndex={10}
123
+ highlightedPhase={undefined}
124
+ />
125
+ </div>
126
+ </div>
127
+ ),
128
+ };
129
+
130
+ /**
131
+ * Canvas with event schema definitions (JSON view).
132
+ *
133
+ * Shows the raw canvas structure including event schemas.
134
+ * Notice the `pv.events` property on each node defining:
135
+ * - Event names (e.g., "conversion.started")
136
+ * - Event descriptions
137
+ * - Field schemas with types and requirements
138
+ */
139
+ export const CanvasSchema: StoryObj = {
140
+ render: () => (
141
+ <div
142
+ style={{
143
+ padding: '20px',
144
+ color: '#fff',
145
+ fontFamily: 'monospace',
146
+ fontSize: '12px',
147
+ overflow: 'auto',
148
+ height: '100vh',
149
+ }}
150
+ >
151
+ <h2>Event Schema Definition</h2>
152
+ <p>
153
+ This canvas defines event schemas for type-safe telemetry validation.
154
+ </p>
155
+ <pre style={{ background: '#1e1e1e', padding: '20px', borderRadius: '8px' }}>
156
+ {JSON.stringify(executionCanvas, null, 2)}
157
+ </pre>
158
+ </div>
159
+ ),
160
+ };