@principal-ai/principal-view-react 0.7.11 → 0.7.12

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.
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useMemo } from 'react';
2
2
  import { useTheme } from '@principal-ade/industry-theme';
3
3
  import { HelpCircle } from 'lucide-react';
4
4
 
@@ -20,23 +20,115 @@ interface TestSpan {
20
20
  errorMessage?: string;
21
21
  }
22
22
 
23
+ // OTEL Log types
24
+ export type OtelSeverity = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'FATAL';
25
+
26
+ export interface OtelLog {
27
+ timestamp: number;
28
+ severity: OtelSeverity;
29
+ body: string | Record<string, unknown>;
30
+ resource: Record<string, string | number>;
31
+ attributes?: Record<string, any>;
32
+ traceId?: string;
33
+ spanId?: string;
34
+ }
35
+
36
+ // Timeline item (event or log)
37
+ interface TimelineItem {
38
+ type: 'event' | 'log';
39
+ time: number;
40
+ // For events
41
+ name?: string;
42
+ attributes?: Record<string, any>;
43
+ // For logs
44
+ severity?: OtelSeverity;
45
+ body?: string | Record<string, unknown>;
46
+ resource?: Record<string, string | number>;
47
+ }
48
+
23
49
  export interface TestEventPanelProps {
24
50
  spans: TestSpan[];
51
+ logs?: OtelLog[]; // Optional for backward compatibility
25
52
  currentSpanIndex: number;
26
53
  currentEventIndex: number;
27
54
  highlightedPhase?: string; // 'setup' | 'execution' | 'assertion'
28
55
  }
29
56
 
57
+ // Helper functions for log severity
58
+ function getSeverityColor(severity: OtelSeverity): string {
59
+ const colors = {
60
+ TRACE: '#6b7280',
61
+ DEBUG: '#60a5fa',
62
+ INFO: '#4ade80',
63
+ WARN: '#fbbf24',
64
+ ERROR: '#f87171',
65
+ FATAL: '#dc2626',
66
+ };
67
+ return colors[severity] || '#9ca3af';
68
+ }
69
+
70
+ function getSeverityIcon(severity: OtelSeverity): string {
71
+ const icons = {
72
+ TRACE: '○',
73
+ DEBUG: '◐',
74
+ INFO: '●',
75
+ WARN: '⚠',
76
+ ERROR: '✕',
77
+ FATAL: '☠',
78
+ };
79
+ return icons[severity] || '•';
80
+ }
81
+
30
82
  export const TestEventPanel: React.FC<TestEventPanelProps> = ({
31
83
  spans,
84
+ logs = [],
32
85
  currentSpanIndex,
33
86
  currentEventIndex,
34
87
  highlightedPhase,
35
88
  }) => {
36
89
  const { theme } = useTheme();
37
90
  const [showHelp, setShowHelp] = useState(false);
91
+ const [viewMode, setViewMode] = useState<'all' | 'events' | 'logs'>('all');
92
+
38
93
  const currentSpan = spans[currentSpanIndex];
39
- const eventsUpToNow = currentSpan?.events.slice(0, currentEventIndex + 1) || [];
94
+
95
+ // Build interleaved timeline
96
+ const timeline = useMemo(() => {
97
+ if (!currentSpan) return [];
98
+
99
+ const items: TimelineItem[] = [
100
+ // Span events
101
+ ...currentSpan.events.slice(0, currentEventIndex + 1).map((event) => ({
102
+ type: 'event' as const,
103
+ time: event.time,
104
+ name: event.name,
105
+ attributes: event.attributes,
106
+ })),
107
+
108
+ // Correlated logs (matching current span's traceId)
109
+ ...logs
110
+ .filter((log) => log.traceId === currentSpan.id)
111
+ .map((log) => ({
112
+ type: 'log' as const,
113
+ time: typeof log.timestamp === 'number' ? log.timestamp : new Date(log.timestamp).getTime(),
114
+ severity: log.severity,
115
+ body: log.body,
116
+ resource: log.resource,
117
+ attributes: log.attributes,
118
+ })),
119
+ ].sort((a, b) => a.time - b.time);
120
+
121
+ return items;
122
+ }, [currentSpan, currentEventIndex, logs]);
123
+
124
+ // Filter timeline based on view mode
125
+ const filteredTimeline = useMemo(() => {
126
+ if (viewMode === 'all') return timeline;
127
+ return timeline.filter((item) => item.type === viewMode.slice(0, -1)); // 'events' -> 'event', 'logs' -> 'log'
128
+ }, [timeline, viewMode]);
129
+
130
+ const eventCount = timeline.filter((i) => i.type === 'event').length;
131
+ const logCount = timeline.filter((i) => i.type === 'log').length;
40
132
 
41
133
  return (
42
134
  <div
@@ -54,7 +146,7 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
54
146
  >
55
147
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '15px' }}>
56
148
  <div style={{ fontWeight: 'bold', fontSize: '18px' }}>
57
- Wide Event Pattern - Code Journey
149
+ Execution Timeline
58
150
  </div>
59
151
  <button
60
152
  onClick={() => setShowHelp(true)}
@@ -77,6 +169,56 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
77
169
  <HelpCircle size={20} />
78
170
  </button>
79
171
  </div>
172
+
173
+ {/* Filter Tabs */}
174
+ <div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
175
+ <button
176
+ onClick={() => setViewMode('all')}
177
+ style={{
178
+ padding: '6px 12px',
179
+ background: viewMode === 'all' ? theme.colors.primary : 'transparent',
180
+ border: `1px solid ${theme.colors.border}`,
181
+ borderRadius: '4px',
182
+ color: viewMode === 'all' ? '#ffffff' : theme.colors.text,
183
+ cursor: 'pointer',
184
+ fontSize: '12px',
185
+ fontWeight: 500,
186
+ }}
187
+ >
188
+ All ({timeline.length})
189
+ </button>
190
+ <button
191
+ onClick={() => setViewMode('events')}
192
+ style={{
193
+ padding: '6px 12px',
194
+ background: viewMode === 'events' ? theme.colors.primary : 'transparent',
195
+ border: `1px solid ${theme.colors.border}`,
196
+ borderRadius: '4px',
197
+ color: viewMode === 'events' ? '#ffffff' : theme.colors.text,
198
+ cursor: 'pointer',
199
+ fontSize: '12px',
200
+ fontWeight: 500,
201
+ }}
202
+ >
203
+ Events ({eventCount})
204
+ </button>
205
+ <button
206
+ onClick={() => setViewMode('logs')}
207
+ style={{
208
+ padding: '6px 12px',
209
+ background: viewMode === 'logs' ? theme.colors.primary : 'transparent',
210
+ border: `1px solid ${theme.colors.border}`,
211
+ borderRadius: '4px',
212
+ color: viewMode === 'logs' ? '#ffffff' : theme.colors.text,
213
+ cursor: 'pointer',
214
+ fontSize: '12px',
215
+ fontWeight: 500,
216
+ }}
217
+ >
218
+ Logs ({logCount})
219
+ </button>
220
+ </div>
221
+
80
222
  <div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
81
223
  Test: {currentSpan?.name || 'Loading...'}
82
224
  </div>
@@ -114,9 +256,15 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
114
256
  </div>
115
257
  <div style={{ fontSize: '14px', marginBottom: '16px', lineHeight: '1.6' }}>
116
258
  <p style={{ marginBottom: '12px' }}>
117
- <strong>Watch how execution flows through files:</strong>
259
+ <strong>Timeline shows both events and logs:</strong>
118
260
  </p>
119
261
  <ul style={{ marginLeft: '20px', marginBottom: '16px' }}>
262
+ <li style={{ marginBottom: '8px' }}>
263
+ <span style={{ color: '#f59e0b' }}>🟧 Events</span> - Structured lifecycle points
264
+ </li>
265
+ <li style={{ marginBottom: '8px' }}>
266
+ <span style={{ color: '#4ade80' }}>● Logs</span> - Standalone log records (color = severity)
267
+ </li>
120
268
  <li style={{ marginBottom: '8px' }}>
121
269
  <span style={{ color: '#60a5fa' }}>Blue = Test file</span>
122
270
  </li>
@@ -125,30 +273,20 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
125
273
  </li>
126
274
  </ul>
127
275
  <p style={{ marginBottom: '12px' }}>
128
- <strong>Span Context (Static)</strong>
276
+ <strong>Use filter tabs to focus:</strong>
129
277
  </p>
130
- <pre
131
- style={{
132
- background: theme.colors.surface,
133
- padding: '12px',
134
- borderRadius: '4px',
135
- fontSize: '13px',
136
- overflow: 'auto',
137
- }}
138
- >
139
- {`{
140
- "test.file": "GraphConverter.test.ts",
141
- "test.suite": "GraphConverter",
142
- "test.result": "pass"
143
- }`}
144
- </pre>
278
+ <ul style={{ marginLeft: '20px' }}>
279
+ <li>All - Interleaved timeline</li>
280
+ <li>Events - Span events only</li>
281
+ <li>Logs - OTEL logs only</li>
282
+ </ul>
145
283
  </div>
146
284
  <button
147
285
  onClick={() => setShowHelp(false)}
148
286
  style={{
149
287
  padding: '8px 16px',
150
288
  backgroundColor: theme.colors.primary,
151
- color: theme.colors.background,
289
+ color: '#ffffff',
152
290
  border: 'none',
153
291
  borderRadius: '4px',
154
292
  cursor: 'pointer',
@@ -162,27 +300,18 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
162
300
  </div>
163
301
  )}
164
302
 
303
+ {/* Timeline Items */}
165
304
  {currentSpan && (
166
- <>
167
- {/* Event Timeline (context mutations) */}
168
- <div>
169
- <div
170
- style={{
171
- color: '#4ade80',
172
- fontWeight: 'bold',
173
- marginBottom: '8px',
174
- fontSize: '15px',
175
- }}
176
- >
177
- Event Timeline (Context Mutations)
178
- </div>
179
- {eventsUpToNow.map((event, idx) => {
180
- const filepath = event.attributes['code.filepath'] as string;
181
- const lineno = event.attributes['code.lineno'] as number;
305
+ <div>
306
+ {filteredTimeline.map((item, idx) => {
307
+ if (item.type === 'event') {
308
+ // SPAN EVENT RENDERING
309
+ const filepath = item.attributes?.['code.filepath'] as string;
310
+ const lineno = item.attributes?.['code.lineno'] as number;
182
311
  const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
183
312
 
184
313
  // Determine which phase this event belongs to
185
- const eventPhase = event.name.split('.')[0]; // 'setup', 'execution', 'assertion'
314
+ const eventPhase = item.name?.split('.')[0]; // 'setup', 'execution', 'assertion'
186
315
  const isHighlighted = highlightedPhase === eventPhase;
187
316
 
188
317
  return (
@@ -191,12 +320,14 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
191
320
  style={{
192
321
  marginBottom: '12px',
193
322
  paddingBottom: '12px',
194
- borderBottom: idx < eventsUpToNow.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
323
+ paddingLeft: '12px',
324
+ borderBottom: idx < filteredTimeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
325
+ borderLeft: '3px solid #f59e0b',
195
326
  opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
196
327
  transition: 'opacity 0.2s ease',
197
328
  transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
198
329
  backgroundColor: isHighlighted ? theme.colors.surface : 'transparent',
199
- padding: isHighlighted ? '8px' : '0',
330
+ padding: isHighlighted ? '8px 8px 8px 12px' : '0 0 12px 12px',
200
331
  borderRadius: '4px',
201
332
  }}
202
333
  >
@@ -209,8 +340,8 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
209
340
  gap: '8px',
210
341
  }}
211
342
  >
212
- <div style={{ color: '#f59e0b', fontSize: '13px', flexShrink: 0 }}>
213
- {idx + 1}. {event.name}
343
+ <div style={{ color: '#f59e0b', fontSize: '13px', fontWeight: 'bold', flexShrink: 0 }}>
344
+ EVENT: {item.name}
214
345
  </div>
215
346
  {filepath && (
216
347
  <div
@@ -247,7 +378,7 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
247
378
  >
248
379
  {JSON.stringify(
249
380
  Object.fromEntries(
250
- Object.entries(event.attributes).filter(
381
+ Object.entries(item.attributes || {}).filter(
251
382
  ([key]) => key !== 'code.filepath' && key !== 'code.lineno'
252
383
  )
253
384
  ),
@@ -257,9 +388,96 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
257
388
  </pre>
258
389
  </div>
259
390
  );
260
- })}
261
- </div>
262
- </>
391
+ } else {
392
+ // OTEL LOG RENDERING
393
+ const serviceName = item.resource?.['service.name'];
394
+ const severityColor = getSeverityColor(item.severity!);
395
+
396
+ return (
397
+ <div
398
+ key={idx}
399
+ style={{
400
+ marginBottom: '12px',
401
+ paddingBottom: '12px',
402
+ paddingLeft: '12px',
403
+ borderBottom: idx < filteredTimeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
404
+ borderLeft: `3px solid ${severityColor}`,
405
+ }}
406
+ >
407
+ <div
408
+ style={{
409
+ display: 'flex',
410
+ justifyContent: 'space-between',
411
+ alignItems: 'center',
412
+ marginBottom: '4px',
413
+ }}
414
+ >
415
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
416
+ <span style={{ fontSize: '16px' }}>{getSeverityIcon(item.severity!)}</span>
417
+ <span
418
+ style={{
419
+ color: severityColor,
420
+ fontSize: '13px',
421
+ fontWeight: 'bold',
422
+ }}
423
+ >
424
+ LOG: {item.severity}
425
+ </span>
426
+ </div>
427
+ {serviceName && (
428
+ <div
429
+ style={{
430
+ fontSize: '12px',
431
+ color: '#9ca3af',
432
+ background: '#1e293b',
433
+ padding: '2px 6px',
434
+ borderRadius: '3px',
435
+ }}
436
+ >
437
+ {serviceName}
438
+ </div>
439
+ )}
440
+ </div>
441
+
442
+ {/* Log body */}
443
+ <div
444
+ style={{
445
+ background: theme.colors.surface,
446
+ padding: '8px',
447
+ borderRadius: '4px',
448
+ marginBottom: item.attributes && Object.keys(item.attributes).length > 0 ? '8px' : '0',
449
+ fontSize: '13px',
450
+ }}
451
+ >
452
+ {typeof item.body === 'string' ? (
453
+ item.body
454
+ ) : (
455
+ <pre style={{ margin: 0, fontSize: '12px' }}>
456
+ {JSON.stringify(item.body, null, 2)}
457
+ </pre>
458
+ )}
459
+ </div>
460
+
461
+ {/* Log attributes */}
462
+ {item.attributes && Object.keys(item.attributes).length > 0 && (
463
+ <pre
464
+ style={{
465
+ background: theme.colors.surface,
466
+ padding: '8px',
467
+ borderRadius: '4px',
468
+ margin: 0,
469
+ fontSize: '11px',
470
+ opacity: 0.8,
471
+ }}
472
+ >
473
+ {JSON.stringify(item.attributes, null, 2)}
474
+ </pre>
475
+ )}
476
+ </div>
477
+ );
478
+ }
479
+ })}
480
+ </div>
263
481
  )}
264
482
 
265
483
  <div
@@ -275,7 +493,7 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
275
493
  <strong>Total tests:</strong> {spans.length}
276
494
  </div>
277
495
  <div style={{ marginBottom: '8px' }}>
278
- <strong>Pattern:</strong> One span per test + event timeline
496
+ <strong>Timeline:</strong> {eventCount} events, {logCount} logs
279
497
  </div>
280
498
  <div>
281
499
  <strong>Status:</strong>{' '}
@@ -193,11 +193,27 @@ const AnimatedTestExecution = () => {
193
193
  const [currentEventIndex] = useState(999);
194
194
  const [highlightedPhase, setHighlightedPhase] = useState<string | undefined>();
195
195
 
196
+ // Extract spans and logs from test data
197
+ const testData = testSpans as any;
198
+ const spans = Array.isArray(testData) ? testData : testData.spans || testData;
199
+ const logs = testData.logs || [];
200
+
196
201
  return (
197
202
  <div style={{ display: 'flex', width: '100vw', height: '100vh' }}>
198
- {/* Graph Visualization - Left Side */}
203
+ {/* Event Panel - Left Side */}
204
+ <div style={{ flex: '0 0 50%', height: '100%', borderRight: `1px solid #333`, overflow: 'hidden' }}>
205
+ <TestEventPanel
206
+ spans={spans}
207
+ logs={logs}
208
+ currentSpanIndex={currentSpanIndex}
209
+ currentEventIndex={currentEventIndex}
210
+ highlightedPhase={highlightedPhase}
211
+ />
212
+ </div>
213
+
214
+ {/* Graph Visualization - Right Side */}
199
215
  <div
200
- style={{ flex: '0 0 60%', height: '100%', position: 'relative' }}
216
+ style={{ flex: '0 0 50%', height: '100%', position: 'relative' }}
201
217
  onMouseLeave={() => setHighlightedPhase(undefined)}
202
218
  >
203
219
  <div
@@ -218,16 +234,6 @@ const AnimatedTestExecution = () => {
218
234
  />
219
235
  </div>
220
236
  </div>
221
-
222
- {/* Event Panel - Right Side */}
223
- <div style={{ flex: '0 0 40%', height: '100%', borderLeft: `1px solid #333`, overflow: 'hidden' }}>
224
- <TestEventPanel
225
- spans={testSpans as any}
226
- currentSpanIndex={currentSpanIndex}
227
- currentEventIndex={currentEventIndex}
228
- highlightedPhase={highlightedPhase}
229
- />
230
- </div>
231
237
  </div>
232
238
  );
233
239
  };
@@ -263,18 +269,25 @@ export const StaticView: Story = {
263
269
  /**
264
270
  * Event panel component showing test execution narrative with file/line information.
265
271
  *
266
- * Shows how events accumulate context as tests execute, with automatic file/line
267
- * capture from stack traces and manual override for code under test.
272
+ * Shows how events and logs are interleaved in chronological order, with automatic
273
+ * file/line capture from stack traces and severity-based color coding for logs.
268
274
  */
269
275
  export const EventPanelOnly: StoryObj = {
270
- render: () => (
271
- <div style={{ width: '600px', height: '100vh' }}>
272
- <TestEventPanel
273
- spans={testSpans as any}
274
- currentSpanIndex={0}
275
- currentEventIndex={999} // Show all events
276
- highlightedPhase={undefined}
277
- />
278
- </div>
279
- ),
276
+ render: () => {
277
+ const testData = testSpans as any;
278
+ const spans = Array.isArray(testData) ? testData : testData.spans || testData;
279
+ const logs = testData.logs || [];
280
+
281
+ return (
282
+ <div style={{ width: '600px', height: '100vh' }}>
283
+ <TestEventPanel
284
+ spans={spans}
285
+ logs={logs}
286
+ currentSpanIndex={0}
287
+ currentEventIndex={999} // Show all events
288
+ highlightedPhase={undefined}
289
+ />
290
+ </div>
291
+ );
292
+ },
280
293
  };