@principal-ai/principal-view-react 0.7.11 → 0.7.13
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/TestEventPanel.d.ts +12 -0
- package/dist/components/TestEventPanel.d.ts.map +1 -1
- package/dist/components/TestEventPanel.js +194 -49
- package/dist/components/TestEventPanel.js.map +1 -1
- package/package.json +1 -1
- package/src/components/TestEventPanel.tsx +369 -92
- package/src/stories/RealTestExecution.stories.tsx +39 -25
- package/src/stories/data/graph-converter-test-execution.json +326 -204
|
@@ -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,129 @@ 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'
|
|
55
|
+
onSpanIndexChange?: (index: number) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Helper functions for log severity
|
|
59
|
+
function getSeverityColor(severity: OtelSeverity): string {
|
|
60
|
+
const colors = {
|
|
61
|
+
TRACE: '#6b7280',
|
|
62
|
+
DEBUG: '#60a5fa',
|
|
63
|
+
INFO: '#4ade80',
|
|
64
|
+
WARN: '#fbbf24',
|
|
65
|
+
ERROR: '#f87171',
|
|
66
|
+
FATAL: '#dc2626',
|
|
67
|
+
};
|
|
68
|
+
return colors[severity] || '#9ca3af';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getSeverityIcon(severity: OtelSeverity): string {
|
|
72
|
+
const icons = {
|
|
73
|
+
TRACE: '○',
|
|
74
|
+
DEBUG: '◐',
|
|
75
|
+
INFO: '●',
|
|
76
|
+
WARN: '⚠',
|
|
77
|
+
ERROR: '✕',
|
|
78
|
+
FATAL: '☠',
|
|
79
|
+
};
|
|
80
|
+
return icons[severity] || '•';
|
|
28
81
|
}
|
|
29
82
|
|
|
30
83
|
export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
31
84
|
spans,
|
|
85
|
+
logs = [],
|
|
32
86
|
currentSpanIndex,
|
|
33
87
|
currentEventIndex,
|
|
34
88
|
highlightedPhase,
|
|
89
|
+
onSpanIndexChange,
|
|
35
90
|
}) => {
|
|
36
91
|
const { theme } = useTheme();
|
|
37
92
|
const [showHelp, setShowHelp] = useState(false);
|
|
93
|
+
const [viewMode, setViewMode] = useState<'all' | 'events' | 'logs'>('all');
|
|
94
|
+
|
|
38
95
|
const currentSpan = spans[currentSpanIndex];
|
|
39
|
-
|
|
96
|
+
|
|
97
|
+
const handlePrevTest = () => {
|
|
98
|
+
if (currentSpanIndex > 0 && onSpanIndexChange) {
|
|
99
|
+
onSpanIndexChange(currentSpanIndex - 1);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const handleNextTest = () => {
|
|
104
|
+
if (currentSpanIndex < spans.length - 1 && onSpanIndexChange) {
|
|
105
|
+
onSpanIndexChange(currentSpanIndex + 1);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Build interleaved timeline
|
|
110
|
+
const timeline = useMemo(() => {
|
|
111
|
+
if (!currentSpan) return [];
|
|
112
|
+
|
|
113
|
+
const items: TimelineItem[] = [
|
|
114
|
+
// Span events
|
|
115
|
+
...currentSpan.events.slice(0, currentEventIndex + 1).map((event) => ({
|
|
116
|
+
type: 'event' as const,
|
|
117
|
+
time: event.time,
|
|
118
|
+
name: event.name,
|
|
119
|
+
attributes: event.attributes,
|
|
120
|
+
})),
|
|
121
|
+
|
|
122
|
+
// Correlated logs (matching current span's traceId)
|
|
123
|
+
...logs
|
|
124
|
+
.filter((log) => log.traceId === currentSpan.id)
|
|
125
|
+
.map((log) => ({
|
|
126
|
+
type: 'log' as const,
|
|
127
|
+
time: typeof log.timestamp === 'number' ? log.timestamp : new Date(log.timestamp).getTime(),
|
|
128
|
+
severity: log.severity,
|
|
129
|
+
body: log.body,
|
|
130
|
+
resource: log.resource,
|
|
131
|
+
attributes: log.attributes,
|
|
132
|
+
})),
|
|
133
|
+
].sort((a, b) => a.time - b.time);
|
|
134
|
+
|
|
135
|
+
return items;
|
|
136
|
+
}, [currentSpan, currentEventIndex, logs]);
|
|
137
|
+
|
|
138
|
+
// Filter timeline based on view mode
|
|
139
|
+
const filteredTimeline = useMemo(() => {
|
|
140
|
+
if (viewMode === 'all') return timeline;
|
|
141
|
+
return timeline.filter((item) => item.type === viewMode.slice(0, -1)); // 'events' -> 'event', 'logs' -> 'log'
|
|
142
|
+
}, [timeline, viewMode]);
|
|
143
|
+
|
|
144
|
+
const eventCount = timeline.filter((i) => i.type === 'event').length;
|
|
145
|
+
const logCount = timeline.filter((i) => i.type === 'log').length;
|
|
40
146
|
|
|
41
147
|
return (
|
|
42
148
|
<div
|
|
@@ -45,40 +151,147 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
45
151
|
height: '100%',
|
|
46
152
|
backgroundColor: theme.colors.background,
|
|
47
153
|
color: theme.colors.text,
|
|
48
|
-
padding: '20px',
|
|
49
154
|
fontFamily: theme.fonts.monospace,
|
|
50
155
|
fontSize: '14px',
|
|
51
|
-
overflow: 'auto',
|
|
52
156
|
boxSizing: 'border-box',
|
|
157
|
+
display: 'flex',
|
|
158
|
+
flexDirection: 'column',
|
|
53
159
|
}}
|
|
54
160
|
>
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
161
|
+
{/* Static Header */}
|
|
162
|
+
<div
|
|
163
|
+
style={{
|
|
164
|
+
padding: '20px 20px 0 20px',
|
|
165
|
+
backgroundColor: theme.colors.background,
|
|
166
|
+
borderBottom: `1px solid ${theme.colors.border}`,
|
|
167
|
+
flexShrink: 0,
|
|
168
|
+
}}
|
|
169
|
+
>
|
|
170
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
|
|
171
|
+
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
|
172
|
+
Execution Timeline
|
|
173
|
+
</div>
|
|
174
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
|
175
|
+
<div style={{ fontSize: '13px', color: theme.colors.textMuted }}>
|
|
176
|
+
<span style={{ color: '#4ade80' }}>All Passed ✓</span>
|
|
177
|
+
</div>
|
|
178
|
+
<button
|
|
179
|
+
onClick={() => setShowHelp(true)}
|
|
180
|
+
style={{
|
|
181
|
+
background: 'transparent',
|
|
182
|
+
border: 'none',
|
|
183
|
+
cursor: 'pointer',
|
|
184
|
+
padding: '4px',
|
|
185
|
+
display: 'flex',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
color: theme.colors.textMuted,
|
|
188
|
+
}}
|
|
189
|
+
onMouseEnter={(e) => {
|
|
190
|
+
e.currentTarget.style.color = theme.colors.text;
|
|
191
|
+
}}
|
|
192
|
+
onMouseLeave={(e) => {
|
|
193
|
+
e.currentTarget.style.color = theme.colors.textMuted;
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<HelpCircle size={20} />
|
|
197
|
+
</button>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{/* Test Navigation */}
|
|
202
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '12px' }}>
|
|
203
|
+
<button
|
|
204
|
+
onClick={handlePrevTest}
|
|
205
|
+
disabled={currentSpanIndex === 0}
|
|
206
|
+
style={{
|
|
207
|
+
padding: '4px 12px',
|
|
208
|
+
background: theme.colors.surface,
|
|
209
|
+
border: `1px solid ${theme.colors.border}`,
|
|
210
|
+
borderRadius: '4px',
|
|
211
|
+
color: currentSpanIndex === 0 ? theme.colors.textMuted : theme.colors.text,
|
|
212
|
+
cursor: currentSpanIndex === 0 ? 'not-allowed' : 'pointer',
|
|
213
|
+
fontSize: '14px',
|
|
214
|
+
opacity: currentSpanIndex === 0 ? 0.5 : 1,
|
|
215
|
+
}}
|
|
216
|
+
>
|
|
217
|
+
← Prev
|
|
218
|
+
</button>
|
|
219
|
+
<div style={{ fontSize: '13px', color: theme.colors.textMuted }}>
|
|
220
|
+
Test {currentSpanIndex + 1} of {spans.length}
|
|
221
|
+
</div>
|
|
222
|
+
<button
|
|
223
|
+
onClick={handleNextTest}
|
|
224
|
+
disabled={currentSpanIndex === spans.length - 1}
|
|
225
|
+
style={{
|
|
226
|
+
padding: '4px 12px',
|
|
227
|
+
background: theme.colors.surface,
|
|
228
|
+
border: `1px solid ${theme.colors.border}`,
|
|
229
|
+
borderRadius: '4px',
|
|
230
|
+
color: currentSpanIndex === spans.length - 1 ? theme.colors.textMuted : theme.colors.text,
|
|
231
|
+
cursor: currentSpanIndex === spans.length - 1 ? 'not-allowed' : 'pointer',
|
|
232
|
+
fontSize: '14px',
|
|
233
|
+
opacity: currentSpanIndex === spans.length - 1 ? 0.5 : 1,
|
|
234
|
+
}}
|
|
235
|
+
>
|
|
236
|
+
Next →
|
|
237
|
+
</button>
|
|
238
|
+
<div style={{ fontSize: '12px', color: theme.colors.textMuted, marginLeft: 'auto' }}>
|
|
239
|
+
{eventCount} events, {logCount} logs
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{/* Filter Tabs */}
|
|
244
|
+
<div style={{ display: 'flex', gap: '8px', marginBottom: '12px' }}>
|
|
245
|
+
<button
|
|
246
|
+
onClick={() => setViewMode('all')}
|
|
247
|
+
style={{
|
|
248
|
+
padding: '6px 12px',
|
|
249
|
+
background: viewMode === 'all' ? theme.colors.primary : 'transparent',
|
|
250
|
+
border: `1px solid ${theme.colors.border}`,
|
|
251
|
+
borderRadius: '4px',
|
|
252
|
+
color: viewMode === 'all' ? '#ffffff' : theme.colors.text,
|
|
253
|
+
cursor: 'pointer',
|
|
254
|
+
fontSize: '12px',
|
|
255
|
+
fontWeight: 500,
|
|
256
|
+
}}
|
|
257
|
+
>
|
|
258
|
+
All ({timeline.length})
|
|
259
|
+
</button>
|
|
260
|
+
<button
|
|
261
|
+
onClick={() => setViewMode('events')}
|
|
262
|
+
style={{
|
|
263
|
+
padding: '6px 12px',
|
|
264
|
+
background: viewMode === 'events' ? theme.colors.primary : 'transparent',
|
|
265
|
+
border: `1px solid ${theme.colors.border}`,
|
|
266
|
+
borderRadius: '4px',
|
|
267
|
+
color: viewMode === 'events' ? '#ffffff' : theme.colors.text,
|
|
268
|
+
cursor: 'pointer',
|
|
269
|
+
fontSize: '12px',
|
|
270
|
+
fontWeight: 500,
|
|
271
|
+
}}
|
|
272
|
+
>
|
|
273
|
+
Events ({eventCount})
|
|
274
|
+
</button>
|
|
275
|
+
<button
|
|
276
|
+
onClick={() => setViewMode('logs')}
|
|
277
|
+
style={{
|
|
278
|
+
padding: '6px 12px',
|
|
279
|
+
background: viewMode === 'logs' ? theme.colors.primary : 'transparent',
|
|
280
|
+
border: `1px solid ${theme.colors.border}`,
|
|
281
|
+
borderRadius: '4px',
|
|
282
|
+
color: viewMode === 'logs' ? '#ffffff' : theme.colors.text,
|
|
283
|
+
cursor: 'pointer',
|
|
284
|
+
fontSize: '12px',
|
|
285
|
+
fontWeight: 500,
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
Logs ({logCount})
|
|
289
|
+
</button>
|
|
290
|
+
</div>
|
|
291
|
+
|
|
292
|
+
<div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
|
|
293
|
+
Test: {currentSpan?.name || 'Loading...'}
|
|
58
294
|
</div>
|
|
59
|
-
<button
|
|
60
|
-
onClick={() => setShowHelp(true)}
|
|
61
|
-
style={{
|
|
62
|
-
background: 'transparent',
|
|
63
|
-
border: 'none',
|
|
64
|
-
cursor: 'pointer',
|
|
65
|
-
padding: '4px',
|
|
66
|
-
display: 'flex',
|
|
67
|
-
alignItems: 'center',
|
|
68
|
-
color: theme.colors.textMuted,
|
|
69
|
-
}}
|
|
70
|
-
onMouseEnter={(e) => {
|
|
71
|
-
e.currentTarget.style.color = theme.colors.text;
|
|
72
|
-
}}
|
|
73
|
-
onMouseLeave={(e) => {
|
|
74
|
-
e.currentTarget.style.color = theme.colors.textMuted;
|
|
75
|
-
}}
|
|
76
|
-
>
|
|
77
|
-
<HelpCircle size={20} />
|
|
78
|
-
</button>
|
|
79
|
-
</div>
|
|
80
|
-
<div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
|
|
81
|
-
Test: {currentSpan?.name || 'Loading...'}
|
|
82
295
|
</div>
|
|
83
296
|
|
|
84
297
|
{/* Help Modal */}
|
|
@@ -114,9 +327,15 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
114
327
|
</div>
|
|
115
328
|
<div style={{ fontSize: '14px', marginBottom: '16px', lineHeight: '1.6' }}>
|
|
116
329
|
<p style={{ marginBottom: '12px' }}>
|
|
117
|
-
<strong>
|
|
330
|
+
<strong>Timeline shows both events and logs:</strong>
|
|
118
331
|
</p>
|
|
119
332
|
<ul style={{ marginLeft: '20px', marginBottom: '16px' }}>
|
|
333
|
+
<li style={{ marginBottom: '8px' }}>
|
|
334
|
+
<span style={{ color: '#f59e0b' }}>🟧 Events</span> - Structured lifecycle points
|
|
335
|
+
</li>
|
|
336
|
+
<li style={{ marginBottom: '8px' }}>
|
|
337
|
+
<span style={{ color: '#4ade80' }}>● Logs</span> - Standalone log records (color = severity)
|
|
338
|
+
</li>
|
|
120
339
|
<li style={{ marginBottom: '8px' }}>
|
|
121
340
|
<span style={{ color: '#60a5fa' }}>Blue = Test file</span>
|
|
122
341
|
</li>
|
|
@@ -125,30 +344,20 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
125
344
|
</li>
|
|
126
345
|
</ul>
|
|
127
346
|
<p style={{ marginBottom: '12px' }}>
|
|
128
|
-
<strong>
|
|
347
|
+
<strong>Use filter tabs to focus:</strong>
|
|
129
348
|
</p>
|
|
130
|
-
<
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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>
|
|
349
|
+
<ul style={{ marginLeft: '20px' }}>
|
|
350
|
+
<li>All - Interleaved timeline</li>
|
|
351
|
+
<li>Events - Span events only</li>
|
|
352
|
+
<li>Logs - OTEL logs only</li>
|
|
353
|
+
</ul>
|
|
145
354
|
</div>
|
|
146
355
|
<button
|
|
147
356
|
onClick={() => setShowHelp(false)}
|
|
148
357
|
style={{
|
|
149
358
|
padding: '8px 16px',
|
|
150
359
|
backgroundColor: theme.colors.primary,
|
|
151
|
-
color:
|
|
360
|
+
color: '#ffffff',
|
|
152
361
|
border: 'none',
|
|
153
362
|
borderRadius: '4px',
|
|
154
363
|
cursor: 'pointer',
|
|
@@ -162,27 +371,26 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
162
371
|
</div>
|
|
163
372
|
)}
|
|
164
373
|
|
|
165
|
-
{
|
|
166
|
-
|
|
167
|
-
|
|
374
|
+
{/* Scrollable Content */}
|
|
375
|
+
<div
|
|
376
|
+
style={{
|
|
377
|
+
flex: 1,
|
|
378
|
+
overflow: 'auto',
|
|
379
|
+
padding: '20px',
|
|
380
|
+
}}
|
|
381
|
+
>
|
|
382
|
+
{/* Timeline Items */}
|
|
383
|
+
{currentSpan && (
|
|
168
384
|
<div>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
385
|
+
{filteredTimeline.map((item, idx) => {
|
|
386
|
+
if (item.type === 'event') {
|
|
387
|
+
// SPAN EVENT RENDERING
|
|
388
|
+
const filepath = item.attributes?.['code.filepath'] as string;
|
|
389
|
+
const lineno = item.attributes?.['code.lineno'] as number;
|
|
182
390
|
const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
|
|
183
391
|
|
|
184
392
|
// Determine which phase this event belongs to
|
|
185
|
-
const eventPhase =
|
|
393
|
+
const eventPhase = item.name?.split('.')[0]; // 'setup', 'execution', 'assertion'
|
|
186
394
|
const isHighlighted = highlightedPhase === eventPhase;
|
|
187
395
|
|
|
188
396
|
return (
|
|
@@ -191,12 +399,14 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
191
399
|
style={{
|
|
192
400
|
marginBottom: '12px',
|
|
193
401
|
paddingBottom: '12px',
|
|
194
|
-
|
|
402
|
+
paddingLeft: '12px',
|
|
403
|
+
borderBottom: idx < filteredTimeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
|
|
404
|
+
borderLeft: '3px solid #f59e0b',
|
|
195
405
|
opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
|
|
196
406
|
transition: 'opacity 0.2s ease',
|
|
197
407
|
transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
|
|
198
408
|
backgroundColor: isHighlighted ? theme.colors.surface : 'transparent',
|
|
199
|
-
padding: isHighlighted ? '8px' : '0',
|
|
409
|
+
padding: isHighlighted ? '8px 8px 8px 12px' : '0 0 12px 12px',
|
|
200
410
|
borderRadius: '4px',
|
|
201
411
|
}}
|
|
202
412
|
>
|
|
@@ -209,8 +419,8 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
209
419
|
gap: '8px',
|
|
210
420
|
}}
|
|
211
421
|
>
|
|
212
|
-
<div style={{ color: '#f59e0b', fontSize: '13px', flexShrink: 0 }}>
|
|
213
|
-
|
|
422
|
+
<div style={{ color: '#f59e0b', fontSize: '13px', fontWeight: 'bold', flexShrink: 0 }}>
|
|
423
|
+
EVENT: {item.name}
|
|
214
424
|
</div>
|
|
215
425
|
{filepath && (
|
|
216
426
|
<div
|
|
@@ -247,7 +457,7 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
247
457
|
>
|
|
248
458
|
{JSON.stringify(
|
|
249
459
|
Object.fromEntries(
|
|
250
|
-
Object.entries(
|
|
460
|
+
Object.entries(item.attributes || {}).filter(
|
|
251
461
|
([key]) => key !== 'code.filepath' && key !== 'code.lineno'
|
|
252
462
|
)
|
|
253
463
|
),
|
|
@@ -257,30 +467,97 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
257
467
|
</pre>
|
|
258
468
|
</div>
|
|
259
469
|
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
470
|
+
} else {
|
|
471
|
+
// OTEL LOG RENDERING
|
|
472
|
+
const serviceName = item.resource?.['service.name'];
|
|
473
|
+
const severityColor = getSeverityColor(item.severity!);
|
|
264
474
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
475
|
+
return (
|
|
476
|
+
<div
|
|
477
|
+
key={idx}
|
|
478
|
+
style={{
|
|
479
|
+
marginBottom: '12px',
|
|
480
|
+
paddingBottom: '12px',
|
|
481
|
+
paddingLeft: '12px',
|
|
482
|
+
borderBottom: idx < filteredTimeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
|
|
483
|
+
borderLeft: `3px solid ${severityColor}`,
|
|
484
|
+
}}
|
|
485
|
+
>
|
|
486
|
+
<div
|
|
487
|
+
style={{
|
|
488
|
+
display: 'flex',
|
|
489
|
+
justifyContent: 'space-between',
|
|
490
|
+
alignItems: 'center',
|
|
491
|
+
marginBottom: '4px',
|
|
492
|
+
}}
|
|
493
|
+
>
|
|
494
|
+
<div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
|
|
495
|
+
<span style={{ fontSize: '16px' }}>{getSeverityIcon(item.severity!)}</span>
|
|
496
|
+
<span
|
|
497
|
+
style={{
|
|
498
|
+
color: severityColor,
|
|
499
|
+
fontSize: '13px',
|
|
500
|
+
fontWeight: 'bold',
|
|
501
|
+
}}
|
|
502
|
+
>
|
|
503
|
+
LOG: {item.severity}
|
|
504
|
+
</span>
|
|
505
|
+
</div>
|
|
506
|
+
{serviceName && (
|
|
507
|
+
<div
|
|
508
|
+
style={{
|
|
509
|
+
fontSize: '12px',
|
|
510
|
+
color: '#9ca3af',
|
|
511
|
+
background: '#1e293b',
|
|
512
|
+
padding: '2px 6px',
|
|
513
|
+
borderRadius: '3px',
|
|
514
|
+
}}
|
|
515
|
+
>
|
|
516
|
+
{serviceName}
|
|
517
|
+
</div>
|
|
518
|
+
)}
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
{/* Log body */}
|
|
522
|
+
<div
|
|
523
|
+
style={{
|
|
524
|
+
background: theme.colors.surface,
|
|
525
|
+
padding: '8px',
|
|
526
|
+
borderRadius: '4px',
|
|
527
|
+
marginBottom: item.attributes && Object.keys(item.attributes).length > 0 ? '8px' : '0',
|
|
528
|
+
fontSize: '13px',
|
|
529
|
+
}}
|
|
530
|
+
>
|
|
531
|
+
{typeof item.body === 'string' ? (
|
|
532
|
+
item.body
|
|
533
|
+
) : (
|
|
534
|
+
<pre style={{ margin: 0, fontSize: '12px' }}>
|
|
535
|
+
{JSON.stringify(item.body, null, 2)}
|
|
536
|
+
</pre>
|
|
537
|
+
)}
|
|
538
|
+
</div>
|
|
539
|
+
|
|
540
|
+
{/* Log attributes */}
|
|
541
|
+
{item.attributes && Object.keys(item.attributes).length > 0 && (
|
|
542
|
+
<pre
|
|
543
|
+
style={{
|
|
544
|
+
background: theme.colors.surface,
|
|
545
|
+
padding: '8px',
|
|
546
|
+
borderRadius: '4px',
|
|
547
|
+
margin: 0,
|
|
548
|
+
fontSize: '11px',
|
|
549
|
+
opacity: 0.8,
|
|
550
|
+
}}
|
|
551
|
+
>
|
|
552
|
+
{JSON.stringify(item.attributes, null, 2)}
|
|
553
|
+
</pre>
|
|
554
|
+
)}
|
|
555
|
+
</div>
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
})}
|
|
283
559
|
</div>
|
|
560
|
+
)}
|
|
284
561
|
</div>
|
|
285
562
|
</div>
|
|
286
563
|
);
|
|
@@ -188,16 +188,33 @@ const testExecutionCanvas: ExtendedCanvas = {
|
|
|
188
188
|
|
|
189
189
|
const AnimatedTestExecution = () => {
|
|
190
190
|
const [events] = useState<GraphEvent[]>([]);
|
|
191
|
-
const [currentSpanIndex] = useState(0);
|
|
191
|
+
const [currentSpanIndex, setCurrentSpanIndex] = useState(0);
|
|
192
192
|
// Show all events by default - set to a large number
|
|
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
|
-
{/*
|
|
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
|
+
onSpanIndexChange={setCurrentSpanIndex}
|
|
212
|
+
/>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Graph Visualization - Right Side */}
|
|
199
216
|
<div
|
|
200
|
-
style={{ flex: '0 0
|
|
217
|
+
style={{ flex: '0 0 50%', height: '100%', position: 'relative' }}
|
|
201
218
|
onMouseLeave={() => setHighlightedPhase(undefined)}
|
|
202
219
|
>
|
|
203
220
|
<div
|
|
@@ -218,16 +235,6 @@ const AnimatedTestExecution = () => {
|
|
|
218
235
|
/>
|
|
219
236
|
</div>
|
|
220
237
|
</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
238
|
</div>
|
|
232
239
|
);
|
|
233
240
|
};
|
|
@@ -263,18 +270,25 @@ export const StaticView: Story = {
|
|
|
263
270
|
/**
|
|
264
271
|
* Event panel component showing test execution narrative with file/line information.
|
|
265
272
|
*
|
|
266
|
-
* Shows how events
|
|
267
|
-
* capture from stack traces and
|
|
273
|
+
* Shows how events and logs are interleaved in chronological order, with automatic
|
|
274
|
+
* file/line capture from stack traces and severity-based color coding for logs.
|
|
268
275
|
*/
|
|
269
276
|
export const EventPanelOnly: StoryObj = {
|
|
270
|
-
render: () =>
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
277
|
+
render: () => {
|
|
278
|
+
const testData = testSpans as any;
|
|
279
|
+
const spans = Array.isArray(testData) ? testData : testData.spans || testData;
|
|
280
|
+
const logs = testData.logs || [];
|
|
281
|
+
|
|
282
|
+
return (
|
|
283
|
+
<div style={{ width: '600px', height: '100vh' }}>
|
|
284
|
+
<TestEventPanel
|
|
285
|
+
spans={spans}
|
|
286
|
+
logs={logs}
|
|
287
|
+
currentSpanIndex={0}
|
|
288
|
+
currentEventIndex={999} // Show all events
|
|
289
|
+
highlightedPhase={undefined}
|
|
290
|
+
/>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
},
|
|
280
294
|
};
|