@principal-ai/principal-view-react 0.14.21 → 0.14.23

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 (57) hide show
  1. package/dist/components/GraphRenderer.d.ts.map +1 -1
  2. package/dist/components/GraphRenderer.js +23 -10
  3. package/dist/components/GraphRenderer.js.map +1 -1
  4. package/dist/components/state-view/PipelineView.d.ts +13 -0
  5. package/dist/components/state-view/PipelineView.d.ts.map +1 -0
  6. package/dist/components/state-view/PipelineView.js +195 -0
  7. package/dist/components/state-view/PipelineView.js.map +1 -0
  8. package/dist/components/state-view/index.d.ts +14 -0
  9. package/dist/components/state-view/index.d.ts.map +1 -0
  10. package/dist/components/state-view/index.js +12 -0
  11. package/dist/components/state-view/index.js.map +1 -0
  12. package/dist/components/state-view/types.d.ts +188 -0
  13. package/dist/components/state-view/types.d.ts.map +1 -0
  14. package/dist/components/state-view/types.js +10 -0
  15. package/dist/components/state-view/types.js.map +1 -0
  16. package/dist/components/state-view/useStateView.d.ts +32 -0
  17. package/dist/components/state-view/useStateView.d.ts.map +1 -0
  18. package/dist/components/state-view/useStateView.js +129 -0
  19. package/dist/components/state-view/useStateView.js.map +1 -0
  20. package/dist/index.d.ts +2 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +2 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/nodes/CustomNode.js +8 -8
  25. package/dist/nodes/CustomNode.js.map +1 -1
  26. package/dist/nodes/otel/OtelBoundaryNode.d.ts.map +1 -1
  27. package/dist/nodes/otel/OtelBoundaryNode.js +5 -3
  28. package/dist/nodes/otel/OtelBoundaryNode.js.map +1 -1
  29. package/dist/nodes/otel/OtelEventNode.d.ts.map +1 -1
  30. package/dist/nodes/otel/OtelEventNode.js +5 -3
  31. package/dist/nodes/otel/OtelEventNode.js.map +1 -1
  32. package/dist/nodes/otel/OtelResourceNode.d.ts.map +1 -1
  33. package/dist/nodes/otel/OtelResourceNode.js +5 -3
  34. package/dist/nodes/otel/OtelResourceNode.js.map +1 -1
  35. package/dist/nodes/otel/OtelScopeNode.d.ts.map +1 -1
  36. package/dist/nodes/otel/OtelScopeNode.js +5 -3
  37. package/dist/nodes/otel/OtelScopeNode.js.map +1 -1
  38. package/dist/nodes/otel/OtelSpanConventionNode.d.ts.map +1 -1
  39. package/dist/nodes/otel/OtelSpanConventionNode.js +5 -3
  40. package/dist/nodes/otel/OtelSpanConventionNode.js.map +1 -1
  41. package/package.json +2 -2
  42. package/src/components/GraphRenderer.tsx +24 -10
  43. package/src/components/state-view/PipelineView.tsx +347 -0
  44. package/src/components/state-view/index.ts +14 -0
  45. package/src/components/state-view/types.ts +261 -0
  46. package/src/components/state-view/useStateView.ts +205 -0
  47. package/src/index.ts +36 -0
  48. package/src/nodes/CustomNode.tsx +8 -8
  49. package/src/nodes/otel/OtelBoundaryNode.tsx +5 -3
  50. package/src/nodes/otel/OtelEventNode.tsx +5 -3
  51. package/src/nodes/otel/OtelResourceNode.tsx +5 -3
  52. package/src/nodes/otel/OtelScopeNode.tsx +5 -3
  53. package/src/nodes/otel/OtelSpanConventionNode.tsx +5 -4
  54. package/src/stories/CanvasEdgeTypes.stories.tsx +23 -27
  55. package/src/stories/GraphRenderer.stories.tsx +144 -200
  56. package/src/stories/StateView.stories.tsx +417 -0
  57. package/src/stories/__traces__/test-run.canvas.json +27 -30
@@ -0,0 +1,417 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import React, { useState, useCallback, useRef, useEffect } from 'react';
3
+ import {
4
+ PipelineView,
5
+ type PipelineEvent,
6
+ type EventSource,
7
+ type PipelineEventType,
8
+ } from '../components/state-view';
9
+
10
+ const meta = {
11
+ title: 'State View/Pipeline',
12
+ component: PipelineView,
13
+ parameters: {
14
+ layout: 'centered',
15
+ },
16
+ tags: ['autodocs'],
17
+ } satisfies Meta<typeof PipelineView>;
18
+
19
+ export default meta;
20
+ type Story = StoryObj<typeof meta>;
21
+
22
+ // =============================================================================
23
+ // Event Simulation Helpers
24
+ // =============================================================================
25
+
26
+ function createPipelineEvent(
27
+ type: PipelineEventType,
28
+ payload: PipelineEvent['payload'] = {}
29
+ ): PipelineEvent {
30
+ return {
31
+ type,
32
+ timestamp: Date.now(),
33
+ payload,
34
+ };
35
+ }
36
+
37
+ function simulatePipelineFlow(
38
+ emit: (event: PipelineEvent) => void,
39
+ repo: string
40
+ ) {
41
+ // Simulate: FS change -> Watch -> Cache -> Event
42
+ const delays = [0, 100, 200, 350];
43
+
44
+ setTimeout(() => emit(createPipelineEvent('FS_CHANGE', { repo })), delays[0]);
45
+ setTimeout(() => emit(createPipelineEvent('WATCH_DETECTED', { repo })), delays[1]);
46
+ setTimeout(() => emit(createPipelineEvent('CACHE_REBUILD', { repo, slice: 'fileTree' })), delays[2]);
47
+ setTimeout(() => emit(createPipelineEvent('EVENT_BROADCAST', { repo, eventType: 'commit' })), delays[3]);
48
+ }
49
+
50
+ // =============================================================================
51
+ // Live Event Source (simulated)
52
+ // =============================================================================
53
+
54
+ function useLiveEventSource(): {
55
+ eventSource: EventSource<PipelineEvent>;
56
+ triggerEvent: (type: PipelineEventType, payload?: PipelineEvent['payload']) => void;
57
+ triggerFlow: (repo: string) => void;
58
+ } {
59
+ const listenersRef = useRef<Set<(event: PipelineEvent) => void>>(new Set());
60
+
61
+ const eventSource: EventSource<PipelineEvent> = {
62
+ mode: 'live',
63
+ subscribe: (handler) => {
64
+ listenersRef.current.add(handler);
65
+ return () => listenersRef.current.delete(handler);
66
+ },
67
+ };
68
+
69
+ const emit = useCallback((event: PipelineEvent) => {
70
+ listenersRef.current.forEach((handler) => handler(event));
71
+ }, []);
72
+
73
+ const triggerEvent = useCallback(
74
+ (type: PipelineEventType, payload: PipelineEvent['payload'] = {}) => {
75
+ emit(createPipelineEvent(type, payload));
76
+ },
77
+ [emit]
78
+ );
79
+
80
+ const triggerFlow = useCallback(
81
+ (repo: string) => {
82
+ simulatePipelineFlow(emit, repo);
83
+ },
84
+ [emit]
85
+ );
86
+
87
+ return { eventSource, triggerEvent, triggerFlow };
88
+ }
89
+
90
+ // =============================================================================
91
+ // Replay Event Source
92
+ // =============================================================================
93
+
94
+ interface ReplayEventSourceOptions {
95
+ events: PipelineEvent[];
96
+ speed?: number;
97
+ }
98
+
99
+ function useReplayEventSource({ events, speed = 1 }: ReplayEventSourceOptions): {
100
+ eventSource: EventSource<PipelineEvent>;
101
+ controls: {
102
+ play: () => void;
103
+ pause: () => void;
104
+ reset: () => void;
105
+ isPlaying: boolean;
106
+ };
107
+ } {
108
+ const listenersRef = useRef<Set<(event: PipelineEvent) => void>>(new Set());
109
+ const [isPlaying, setIsPlaying] = useState(false);
110
+ const indexRef = useRef(0);
111
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
112
+
113
+ const eventSource: EventSource<PipelineEvent> = {
114
+ mode: 'replay',
115
+ subscribe: (handler) => {
116
+ listenersRef.current.add(handler);
117
+ return () => listenersRef.current.delete(handler);
118
+ },
119
+ };
120
+
121
+ const emit = useCallback((event: PipelineEvent) => {
122
+ // Update timestamp to "now" for display purposes
123
+ const adjustedEvent = { ...event, timestamp: Date.now() };
124
+ listenersRef.current.forEach((handler) => handler(adjustedEvent));
125
+ }, []);
126
+
127
+ const playNext = useCallback(() => {
128
+ if (indexRef.current >= events.length) {
129
+ setIsPlaying(false);
130
+ return;
131
+ }
132
+
133
+ const currentEvent = events[indexRef.current];
134
+ emit(currentEvent);
135
+ indexRef.current++;
136
+
137
+ if (indexRef.current < events.length) {
138
+ const nextEvent = events[indexRef.current];
139
+ const delay = (nextEvent.timestamp - currentEvent.timestamp) / speed;
140
+ timeoutRef.current = setTimeout(playNext, Math.max(50, delay));
141
+ } else {
142
+ setIsPlaying(false);
143
+ }
144
+ }, [events, speed, emit]);
145
+
146
+ const play = useCallback(() => {
147
+ if (isPlaying) return;
148
+ setIsPlaying(true);
149
+ playNext();
150
+ }, [isPlaying, playNext]);
151
+
152
+ const pause = useCallback(() => {
153
+ setIsPlaying(false);
154
+ if (timeoutRef.current) {
155
+ clearTimeout(timeoutRef.current);
156
+ timeoutRef.current = null;
157
+ }
158
+ }, []);
159
+
160
+ const reset = useCallback(() => {
161
+ pause();
162
+ indexRef.current = 0;
163
+ }, [pause]);
164
+
165
+ useEffect(() => {
166
+ return () => {
167
+ if (timeoutRef.current) clearTimeout(timeoutRef.current);
168
+ };
169
+ }, []);
170
+
171
+ return {
172
+ eventSource,
173
+ controls: { play, pause, reset, isPlaying },
174
+ };
175
+ }
176
+
177
+ // =============================================================================
178
+ // Stories
179
+ // =============================================================================
180
+
181
+ /**
182
+ * Interactive demo with manual event triggering.
183
+ * Click buttons to simulate events flowing through the pipeline.
184
+ */
185
+ export const Interactive: Story = {
186
+ render: () => {
187
+ const { eventSource, triggerEvent, triggerFlow } = useLiveEventSource();
188
+ const [selectedRepo, setSelectedRepo] = useState('myorg/backend');
189
+
190
+ const repos = ['myorg/backend', 'myorg/frontend', 'myorg/shared-lib'];
191
+
192
+ return (
193
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
194
+ <PipelineView eventSource={eventSource} title="Repository Monitoring Pipeline" />
195
+
196
+ {/* Control Panel */}
197
+ <div
198
+ style={{
199
+ padding: 16,
200
+ backgroundColor: '#111827',
201
+ borderRadius: 8,
202
+ fontFamily: 'system-ui, sans-serif',
203
+ }}
204
+ >
205
+ <div style={{ color: '#9ca3af', fontSize: 12, marginBottom: 12 }}>
206
+ Event Controls
207
+ </div>
208
+
209
+ <div style={{ display: 'flex', gap: 8, marginBottom: 12, flexWrap: 'wrap' }}>
210
+ <select
211
+ value={selectedRepo}
212
+ onChange={(e) => setSelectedRepo(e.target.value)}
213
+ style={{
214
+ padding: '8px 12px',
215
+ borderRadius: 4,
216
+ border: 'none',
217
+ backgroundColor: '#374151',
218
+ color: 'white',
219
+ }}
220
+ >
221
+ {repos.map((repo) => (
222
+ <option key={repo} value={repo}>
223
+ {repo}
224
+ </option>
225
+ ))}
226
+ </select>
227
+
228
+ <button
229
+ onClick={() => triggerFlow(selectedRepo)}
230
+ style={{
231
+ padding: '8px 16px',
232
+ borderRadius: 4,
233
+ border: 'none',
234
+ backgroundColor: '#3b82f6',
235
+ color: 'white',
236
+ cursor: 'pointer',
237
+ fontWeight: 'bold',
238
+ }}
239
+ >
240
+ Simulate Full Flow
241
+ </button>
242
+ </div>
243
+
244
+ <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
245
+ <button
246
+ onClick={() => triggerEvent('FS_CHANGE', { repo: selectedRepo })}
247
+ style={buttonStyle('#10b981')}
248
+ >
249
+ FS Change
250
+ </button>
251
+ <button
252
+ onClick={() => triggerEvent('WATCH_DETECTED', { repo: selectedRepo })}
253
+ style={buttonStyle('#6366f1')}
254
+ >
255
+ Watch Detect
256
+ </button>
257
+ <button
258
+ onClick={() => triggerEvent('CACHE_REBUILD', { repo: selectedRepo, slice: 'fileTree' })}
259
+ style={buttonStyle('#f59e0b')}
260
+ >
261
+ Cache Rebuild
262
+ </button>
263
+ <button
264
+ onClick={() => triggerEvent('EVENT_BROADCAST', { repo: selectedRepo, eventType: 'commit' })}
265
+ style={buttonStyle('#ef4444')}
266
+ >
267
+ Event Broadcast
268
+ </button>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ );
273
+ },
274
+ };
275
+
276
+ /**
277
+ * Auto-playing demo that continuously simulates pipeline activity.
278
+ */
279
+ export const AutoSimulation: Story = {
280
+ render: () => {
281
+ const { eventSource, triggerFlow } = useLiveEventSource();
282
+ const [isRunning, setIsRunning] = useState(true);
283
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
284
+
285
+ const repos = ['myorg/backend', 'myorg/frontend', 'myorg/shared-lib', 'myorg/docs'];
286
+
287
+ useEffect(() => {
288
+ if (isRunning) {
289
+ intervalRef.current = setInterval(() => {
290
+ const randomRepo = repos[Math.floor(Math.random() * repos.length)];
291
+ triggerFlow(randomRepo);
292
+ }, 1500);
293
+ } else {
294
+ if (intervalRef.current) clearInterval(intervalRef.current);
295
+ }
296
+
297
+ return () => {
298
+ if (intervalRef.current) clearInterval(intervalRef.current);
299
+ };
300
+ }, [isRunning, triggerFlow]);
301
+
302
+ return (
303
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
304
+ <PipelineView eventSource={eventSource} title="Auto-Simulation" />
305
+
306
+ <div style={{ textAlign: 'center' }}>
307
+ <button
308
+ onClick={() => setIsRunning(!isRunning)}
309
+ style={{
310
+ padding: '8px 24px',
311
+ borderRadius: 4,
312
+ border: 'none',
313
+ backgroundColor: isRunning ? '#ef4444' : '#10b981',
314
+ color: 'white',
315
+ cursor: 'pointer',
316
+ fontWeight: 'bold',
317
+ }}
318
+ >
319
+ {isRunning ? 'Stop Simulation' : 'Start Simulation'}
320
+ </button>
321
+ </div>
322
+ </div>
323
+ );
324
+ },
325
+ };
326
+
327
+ /**
328
+ * Replay mode - plays back a recorded sequence of events.
329
+ */
330
+ export const Replay: Story = {
331
+ render: () => {
332
+ // Pre-recorded events
333
+ const recordedEvents: PipelineEvent[] = [
334
+ { type: 'FS_CHANGE', timestamp: 0, payload: { repo: 'myorg/backend' } },
335
+ { type: 'WATCH_DETECTED', timestamp: 100, payload: { repo: 'myorg/backend' } },
336
+ { type: 'CACHE_REBUILD', timestamp: 200, payload: { repo: 'myorg/backend', slice: 'fileTree' } },
337
+ { type: 'EVENT_BROADCAST', timestamp: 350, payload: { repo: 'myorg/backend', eventType: 'commit' } },
338
+
339
+ { type: 'FS_CHANGE', timestamp: 800, payload: { repo: 'myorg/frontend' } },
340
+ { type: 'WATCH_DETECTED', timestamp: 900, payload: { repo: 'myorg/frontend' } },
341
+ { type: 'CACHE_HIT', timestamp: 950, payload: { repo: 'myorg/frontend', slice: 'fileTree' } },
342
+ { type: 'EVENT_BROADCAST', timestamp: 1000, payload: { repo: 'myorg/frontend', eventType: 'branch-switch' } },
343
+
344
+ { type: 'FS_CHANGE', timestamp: 1500, payload: { repo: 'myorg/backend' } },
345
+ { type: 'WATCH_DETECTED', timestamp: 1600, payload: { repo: 'myorg/backend' } },
346
+ { type: 'CACHE_REBUILD', timestamp: 1700, payload: { repo: 'myorg/backend', slice: 'packages' } },
347
+ { type: 'EVENT_BROADCAST', timestamp: 1850, payload: { repo: 'myorg/backend', eventType: 'dirty-change' } },
348
+ ];
349
+
350
+ const { eventSource, controls } = useReplayEventSource({
351
+ events: recordedEvents,
352
+ speed: 1,
353
+ });
354
+
355
+ return (
356
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
357
+ <PipelineView eventSource={eventSource} title="Replay Mode" />
358
+
359
+ <div
360
+ style={{
361
+ display: 'flex',
362
+ justifyContent: 'center',
363
+ gap: 8,
364
+ padding: 16,
365
+ backgroundColor: '#111827',
366
+ borderRadius: 8,
367
+ }}
368
+ >
369
+ <button
370
+ onClick={controls.isPlaying ? controls.pause : controls.play}
371
+ style={{
372
+ padding: '8px 24px',
373
+ borderRadius: 4,
374
+ border: 'none',
375
+ backgroundColor: controls.isPlaying ? '#f59e0b' : '#10b981',
376
+ color: 'white',
377
+ cursor: 'pointer',
378
+ fontWeight: 'bold',
379
+ }}
380
+ >
381
+ {controls.isPlaying ? 'Pause' : 'Play'}
382
+ </button>
383
+ <button
384
+ onClick={controls.reset}
385
+ style={{
386
+ padding: '8px 24px',
387
+ borderRadius: 4,
388
+ border: 'none',
389
+ backgroundColor: '#374151',
390
+ color: 'white',
391
+ cursor: 'pointer',
392
+ }}
393
+ >
394
+ Reset
395
+ </button>
396
+ </div>
397
+
398
+ <div style={{ color: '#9ca3af', fontSize: 12, textAlign: 'center' }}>
399
+ Replaying {recordedEvents.length} recorded events
400
+ </div>
401
+ </div>
402
+ );
403
+ },
404
+ };
405
+
406
+ // Helper style function
407
+ function buttonStyle(color: string): React.CSSProperties {
408
+ return {
409
+ padding: '6px 12px',
410
+ borderRadius: 4,
411
+ border: 'none',
412
+ backgroundColor: color,
413
+ color: 'white',
414
+ cursor: 'pointer',
415
+ fontSize: 12,
416
+ };
417
+ }
@@ -1,4 +1,30 @@
1
1
  {
2
+ "name": "Trace: principal-view-core-tests",
3
+ "description": "Exported at 2025-12-26T01:11:11.110Z",
4
+ "nodeTypes": {
5
+ "span": {
6
+ "description": "OpenTelemetry span from trace",
7
+ "shape": "rectangle"
8
+ },
9
+ "service": {
10
+ "description": "Service grouping",
11
+ "icon": "server",
12
+ "shape": "rectangle"
13
+ }
14
+ },
15
+ "edgeTypes": {
16
+ "span-child": {
17
+ "label": "Child span",
18
+ "directed": true
19
+ }
20
+ },
21
+ "display": {
22
+ "layout": "manual",
23
+ "animations": {
24
+ "enabled": true,
25
+ "speed": 1
26
+ }
27
+ },
2
28
  "nodes": [
3
29
  {
4
30
  "id": "service-principal-view-core-tests",
@@ -211,34 +237,5 @@
211
237
  }
212
238
  }
213
239
  ],
214
- "edges": [],
215
- "pv": {
216
- "version": "1.0.0",
217
- "name": "Trace: principal-view-core-tests",
218
- "description": "Exported at 2025-12-26T01:11:11.110Z",
219
- "nodeTypes": {
220
- "span": {
221
- "description": "OpenTelemetry span from trace",
222
- "shape": "rectangle"
223
- },
224
- "service": {
225
- "description": "Service grouping",
226
- "icon": "server",
227
- "shape": "rectangle"
228
- }
229
- },
230
- "edgeTypes": {
231
- "span-child": {
232
- "label": "Child span",
233
- "directed": true
234
- }
235
- },
236
- "display": {
237
- "layout": "manual",
238
- "animations": {
239
- "enabled": true,
240
- "speed": 1
241
- }
242
- }
243
- }
240
+ "edges": []
244
241
  }