@principal-ai/principal-view-react 0.7.10 → 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.
- package/dist/components/GraphRenderer.d.ts.map +1 -1
- package/dist/components/GraphRenderer.js +16 -3
- package/dist/components/GraphRenderer.js.map +1 -1
- package/dist/components/TestEventPanel.d.ts +11 -0
- package/dist/components/TestEventPanel.d.ts.map +1 -1
- package/dist/components/TestEventPanel.js +229 -68
- package/dist/components/TestEventPanel.js.map +1 -1
- package/dist/nodes/CustomNode.d.ts.map +1 -1
- package/dist/nodes/CustomNode.js +17 -10
- package/dist/nodes/CustomNode.js.map +1 -1
- package/package.json +1 -1
- package/src/components/GraphRenderer.tsx +15 -3
- package/src/components/TestEventPanel.tsx +370 -79
- package/src/nodes/CustomNode.tsx +18 -10
- package/src/stories/MultiConfig.stories.tsx +1 -1
- package/src/stories/MultiDirectionalConnections.stories.tsx +0 -1
- package/src/stories/RealTestExecution.stories.tsx +45 -172
- package/src/stories/ValidatedExecution.stories.tsx +3 -5
- package/src/stories/data/graph-converter-test-execution.json +326 -204
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import { useTheme } from '@principal-ade/industry-theme';
|
|
3
|
+
import { HelpCircle } from 'lucide-react';
|
|
2
4
|
|
|
3
5
|
interface SpanEvent {
|
|
4
6
|
time: number;
|
|
@@ -18,106 +20,298 @@ interface TestSpan {
|
|
|
18
20
|
errorMessage?: string;
|
|
19
21
|
}
|
|
20
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
|
+
|
|
21
49
|
export interface TestEventPanelProps {
|
|
22
50
|
spans: TestSpan[];
|
|
51
|
+
logs?: OtelLog[]; // Optional for backward compatibility
|
|
23
52
|
currentSpanIndex: number;
|
|
24
53
|
currentEventIndex: number;
|
|
25
54
|
highlightedPhase?: string; // 'setup' | 'execution' | 'assertion'
|
|
26
55
|
}
|
|
27
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
|
+
|
|
28
82
|
export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
29
83
|
spans,
|
|
84
|
+
logs = [],
|
|
30
85
|
currentSpanIndex,
|
|
31
86
|
currentEventIndex,
|
|
32
87
|
highlightedPhase,
|
|
33
88
|
}) => {
|
|
89
|
+
const { theme } = useTheme();
|
|
90
|
+
const [showHelp, setShowHelp] = useState(false);
|
|
91
|
+
const [viewMode, setViewMode] = useState<'all' | 'events' | 'logs'>('all');
|
|
92
|
+
|
|
34
93
|
const currentSpan = spans[currentSpanIndex];
|
|
35
|
-
|
|
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;
|
|
36
132
|
|
|
37
133
|
return (
|
|
38
134
|
<div
|
|
39
135
|
style={{
|
|
40
136
|
width: '100%',
|
|
41
137
|
height: '100%',
|
|
42
|
-
backgroundColor:
|
|
43
|
-
color:
|
|
138
|
+
backgroundColor: theme.colors.background,
|
|
139
|
+
color: theme.colors.text,
|
|
44
140
|
padding: '20px',
|
|
45
|
-
fontFamily:
|
|
46
|
-
fontSize: '
|
|
141
|
+
fontFamily: theme.fonts.monospace,
|
|
142
|
+
fontSize: '14px',
|
|
47
143
|
overflow: 'auto',
|
|
144
|
+
boxSizing: 'border-box',
|
|
48
145
|
}}
|
|
49
146
|
>
|
|
50
|
-
<div style={{
|
|
51
|
-
|
|
147
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '15px' }}>
|
|
148
|
+
<div style={{ fontWeight: 'bold', fontSize: '18px' }}>
|
|
149
|
+
Execution Timeline
|
|
150
|
+
</div>
|
|
151
|
+
<button
|
|
152
|
+
onClick={() => setShowHelp(true)}
|
|
153
|
+
style={{
|
|
154
|
+
background: 'transparent',
|
|
155
|
+
border: 'none',
|
|
156
|
+
cursor: 'pointer',
|
|
157
|
+
padding: '4px',
|
|
158
|
+
display: 'flex',
|
|
159
|
+
alignItems: 'center',
|
|
160
|
+
color: theme.colors.textMuted,
|
|
161
|
+
}}
|
|
162
|
+
onMouseEnter={(e) => {
|
|
163
|
+
e.currentTarget.style.color = theme.colors.text;
|
|
164
|
+
}}
|
|
165
|
+
onMouseLeave={(e) => {
|
|
166
|
+
e.currentTarget.style.color = theme.colors.textMuted;
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<HelpCircle size={20} />
|
|
170
|
+
</button>
|
|
52
171
|
</div>
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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>
|
|
57
220
|
</div>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
<span style={{ color: '#60a5fa' }}>Blue = Test file</span>
|
|
62
|
-
{' • '}
|
|
63
|
-
<span style={{ color: '#4ade80' }}>Green → Code under test</span>
|
|
221
|
+
|
|
222
|
+
<div style={{ fontSize: '13px', color: theme.colors.textMuted, marginBottom: '15px' }}>
|
|
223
|
+
Test: {currentSpan?.name || 'Loading...'}
|
|
64
224
|
</div>
|
|
65
225
|
|
|
66
|
-
{
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
226
|
+
{/* Help Modal */}
|
|
227
|
+
{showHelp && (
|
|
228
|
+
<div
|
|
229
|
+
style={{
|
|
230
|
+
position: 'fixed',
|
|
231
|
+
top: 0,
|
|
232
|
+
left: 0,
|
|
233
|
+
right: 0,
|
|
234
|
+
bottom: 0,
|
|
235
|
+
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
|
236
|
+
display: 'flex',
|
|
237
|
+
alignItems: 'center',
|
|
238
|
+
justifyContent: 'center',
|
|
239
|
+
zIndex: 9999,
|
|
240
|
+
}}
|
|
241
|
+
onClick={() => setShowHelp(false)}
|
|
242
|
+
>
|
|
243
|
+
<div
|
|
244
|
+
style={{
|
|
245
|
+
backgroundColor: theme.colors.background,
|
|
246
|
+
color: theme.colors.text,
|
|
247
|
+
padding: '24px',
|
|
248
|
+
borderRadius: '8px',
|
|
249
|
+
maxWidth: '600px',
|
|
250
|
+
border: `1px solid ${theme.colors.border}`,
|
|
251
|
+
}}
|
|
252
|
+
onClick={(e) => e.stopPropagation()}
|
|
253
|
+
>
|
|
254
|
+
<div style={{ fontWeight: 'bold', fontSize: '18px', marginBottom: '16px' }}>
|
|
255
|
+
How to Read This Panel
|
|
256
|
+
</div>
|
|
257
|
+
<div style={{ fontSize: '14px', marginBottom: '16px', lineHeight: '1.6' }}>
|
|
258
|
+
<p style={{ marginBottom: '12px' }}>
|
|
259
|
+
<strong>Timeline shows both events and logs:</strong>
|
|
260
|
+
</p>
|
|
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>
|
|
268
|
+
<li style={{ marginBottom: '8px' }}>
|
|
269
|
+
<span style={{ color: '#60a5fa' }}>Blue = Test file</span>
|
|
270
|
+
</li>
|
|
271
|
+
<li>
|
|
272
|
+
<span style={{ color: '#4ade80' }}>Green → Code under test</span>
|
|
273
|
+
</li>
|
|
274
|
+
</ul>
|
|
275
|
+
<p style={{ marginBottom: '12px' }}>
|
|
276
|
+
<strong>Use filter tabs to focus:</strong>
|
|
277
|
+
</p>
|
|
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>
|
|
79
283
|
</div>
|
|
80
|
-
<
|
|
284
|
+
<button
|
|
285
|
+
onClick={() => setShowHelp(false)}
|
|
81
286
|
style={{
|
|
82
|
-
|
|
83
|
-
|
|
287
|
+
padding: '8px 16px',
|
|
288
|
+
backgroundColor: theme.colors.primary,
|
|
289
|
+
color: '#ffffff',
|
|
290
|
+
border: 'none',
|
|
84
291
|
borderRadius: '4px',
|
|
85
|
-
|
|
86
|
-
fontSize: '
|
|
87
|
-
|
|
292
|
+
cursor: 'pointer',
|
|
293
|
+
fontSize: '14px',
|
|
294
|
+
fontWeight: 500,
|
|
88
295
|
}}
|
|
89
296
|
>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
'test.file': currentSpan.attributes['test.file'],
|
|
93
|
-
'test.suite': currentSpan.attributes['test.suite'],
|
|
94
|
-
'test.result': currentSpan.attributes['test.result'],
|
|
95
|
-
},
|
|
96
|
-
null,
|
|
97
|
-
2
|
|
98
|
-
)}
|
|
99
|
-
</pre>
|
|
297
|
+
Got it
|
|
298
|
+
</button>
|
|
100
299
|
</div>
|
|
300
|
+
</div>
|
|
301
|
+
)}
|
|
101
302
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}}
|
|
111
|
-
>
|
|
112
|
-
Event Timeline (Context Mutations)
|
|
113
|
-
</div>
|
|
114
|
-
{eventsUpToNow.map((event, idx) => {
|
|
115
|
-
const filepath = event.attributes['code.filepath'] as string;
|
|
116
|
-
const lineno = event.attributes['code.lineno'] as number;
|
|
303
|
+
{/* Timeline Items */}
|
|
304
|
+
{currentSpan && (
|
|
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;
|
|
117
311
|
const isCodeUnderTest = filepath && filepath !== 'GraphConverter.test.ts';
|
|
118
312
|
|
|
119
313
|
// Determine which phase this event belongs to
|
|
120
|
-
const eventPhase =
|
|
314
|
+
const eventPhase = item.name?.split('.')[0]; // 'setup', 'execution', 'assertion'
|
|
121
315
|
const isHighlighted = highlightedPhase === eventPhase;
|
|
122
316
|
|
|
123
317
|
return (
|
|
@@ -126,12 +320,14 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
126
320
|
style={{
|
|
127
321
|
marginBottom: '12px',
|
|
128
322
|
paddingBottom: '12px',
|
|
129
|
-
|
|
323
|
+
paddingLeft: '12px',
|
|
324
|
+
borderBottom: idx < filteredTimeline.length - 1 ? `1px solid ${theme.colors.border}` : 'none',
|
|
325
|
+
borderLeft: '3px solid #f59e0b',
|
|
130
326
|
opacity: highlightedPhase && !isHighlighted ? 0.4 : 1,
|
|
131
327
|
transition: 'opacity 0.2s ease',
|
|
132
328
|
transform: isHighlighted ? 'scale(1.02)' : 'scale(1)',
|
|
133
|
-
backgroundColor: isHighlighted ?
|
|
134
|
-
padding: isHighlighted ? '8px' : '0',
|
|
329
|
+
backgroundColor: isHighlighted ? theme.colors.surface : 'transparent',
|
|
330
|
+
padding: isHighlighted ? '8px 8px 8px 12px' : '0 0 12px 12px',
|
|
135
331
|
borderRadius: '4px',
|
|
136
332
|
}}
|
|
137
333
|
>
|
|
@@ -141,20 +337,26 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
141
337
|
justifyContent: 'space-between',
|
|
142
338
|
alignItems: 'center',
|
|
143
339
|
marginBottom: '4px',
|
|
340
|
+
gap: '8px',
|
|
144
341
|
}}
|
|
145
342
|
>
|
|
146
|
-
<div style={{ color: '#f59e0b', fontSize: '
|
|
147
|
-
|
|
343
|
+
<div style={{ color: '#f59e0b', fontSize: '13px', fontWeight: 'bold', flexShrink: 0 }}>
|
|
344
|
+
EVENT: {item.name}
|
|
148
345
|
</div>
|
|
149
346
|
{filepath && (
|
|
150
347
|
<div
|
|
151
348
|
style={{
|
|
152
|
-
fontSize: '
|
|
349
|
+
fontSize: '12px',
|
|
153
350
|
color: isCodeUnderTest ? '#4ade80' : '#60a5fa',
|
|
154
351
|
fontFamily: 'monospace',
|
|
155
352
|
background: isCodeUnderTest ? '#064e3b' : '#1e3a8a',
|
|
156
353
|
padding: '2px 6px',
|
|
157
354
|
borderRadius: '3px',
|
|
355
|
+
flexShrink: 1,
|
|
356
|
+
minWidth: 0,
|
|
357
|
+
overflow: 'hidden',
|
|
358
|
+
textOverflow: 'ellipsis',
|
|
359
|
+
whiteSpace: 'nowrap',
|
|
158
360
|
}}
|
|
159
361
|
>
|
|
160
362
|
{isCodeUnderTest && '→ '}
|
|
@@ -164,17 +366,19 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
164
366
|
</div>
|
|
165
367
|
<pre
|
|
166
368
|
style={{
|
|
167
|
-
background:
|
|
369
|
+
background: theme.colors.surface,
|
|
168
370
|
padding: '8px',
|
|
169
371
|
borderRadius: '4px',
|
|
170
372
|
margin: 0,
|
|
171
|
-
fontSize: '
|
|
373
|
+
fontSize: '12px',
|
|
172
374
|
lineHeight: '1.4',
|
|
375
|
+
overflow: 'auto',
|
|
376
|
+
maxWidth: '100%',
|
|
173
377
|
}}
|
|
174
378
|
>
|
|
175
379
|
{JSON.stringify(
|
|
176
380
|
Object.fromEntries(
|
|
177
|
-
Object.entries(
|
|
381
|
+
Object.entries(item.attributes || {}).filter(
|
|
178
382
|
([key]) => key !== 'code.filepath' && key !== 'code.lineno'
|
|
179
383
|
)
|
|
180
384
|
),
|
|
@@ -184,25 +388,112 @@ export const TestEventPanel: React.FC<TestEventPanelProps> = ({
|
|
|
184
388
|
</pre>
|
|
185
389
|
</div>
|
|
186
390
|
);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
|
|
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>
|
|
190
481
|
)}
|
|
191
482
|
|
|
192
483
|
<div
|
|
193
484
|
style={{
|
|
194
485
|
marginTop: '20px',
|
|
195
486
|
paddingTop: '15px',
|
|
196
|
-
borderTop:
|
|
197
|
-
fontSize: '
|
|
198
|
-
color:
|
|
487
|
+
borderTop: `1px solid ${theme.colors.border}`,
|
|
488
|
+
fontSize: '13px',
|
|
489
|
+
color: theme.colors.textMuted,
|
|
199
490
|
}}
|
|
200
491
|
>
|
|
201
492
|
<div style={{ marginBottom: '8px' }}>
|
|
202
493
|
<strong>Total tests:</strong> {spans.length}
|
|
203
494
|
</div>
|
|
204
495
|
<div style={{ marginBottom: '8px' }}>
|
|
205
|
-
<strong>
|
|
496
|
+
<strong>Timeline:</strong> {eventCount} events, {logCount} logs
|
|
206
497
|
</div>
|
|
207
498
|
<div>
|
|
208
499
|
<strong>Status:</strong>{' '}
|
package/src/nodes/CustomNode.tsx
CHANGED
|
@@ -7,12 +7,12 @@ import { NodeTooltip } from '../components/NodeTooltip';
|
|
|
7
7
|
import type { OtelInfo } from '../components/NodeTooltip';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Converts a hex color to a
|
|
10
|
+
* Converts a hex color to a lighter/tinted version (opaque, not transparent)
|
|
11
11
|
* @param hexColor - Hex color string (e.g., "#FF5733" or "#888")
|
|
12
|
-
* @param
|
|
13
|
-
* @returns
|
|
12
|
+
* @param lightness - How much to lighten (0-1), defaults to 0.88 (88% white mixed in)
|
|
13
|
+
* @returns hex color string
|
|
14
14
|
*/
|
|
15
|
-
function
|
|
15
|
+
function hexToLightColor(hexColor: string, lightness = 0.88): string {
|
|
16
16
|
// Remove # if present
|
|
17
17
|
const hex = hexColor.replace('#', '');
|
|
18
18
|
|
|
@@ -21,7 +21,15 @@ function hexToSoftColor(hexColor: string, alpha = 0.12): string {
|
|
|
21
21
|
const g = parseInt(hex.substring(2, 4), 16);
|
|
22
22
|
const b = parseInt(hex.substring(4, 6), 16);
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
// Mix with white based on lightness factor
|
|
25
|
+
// lightness of 0.88 means 88% white + 12% original color
|
|
26
|
+
const newR = Math.round(r + (255 - r) * lightness);
|
|
27
|
+
const newG = Math.round(g + (255 - g) * lightness);
|
|
28
|
+
const newB = Math.round(b + (255 - b) * lightness);
|
|
29
|
+
|
|
30
|
+
// Convert back to hex
|
|
31
|
+
const toHex = (n: number) => n.toString(16).padStart(2, '0');
|
|
32
|
+
return `#${toHex(newR)}${toHex(newG)}${toHex(newB)}`;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
export interface CustomNodeData extends Record<string, unknown> {
|
|
@@ -176,7 +184,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
176
184
|
const getShapeStyles = () => {
|
|
177
185
|
const baseStyles = {
|
|
178
186
|
padding: '12px 16px',
|
|
179
|
-
backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' :
|
|
187
|
+
backgroundColor: isGroup ? 'rgba(255, 255, 255, 0.7)' : hexToLightColor(fillColor),
|
|
180
188
|
color: '#000',
|
|
181
189
|
border: `2px solid ${hasViolations ? '#D0021B' : strokeColor}`,
|
|
182
190
|
fontSize: '12px',
|
|
@@ -278,7 +286,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
278
286
|
}
|
|
279
287
|
: {};
|
|
280
288
|
|
|
281
|
-
// Hexagon inner fill styles (
|
|
289
|
+
// Hexagon inner fill styles (light color background inset from border)
|
|
282
290
|
const hexagonInnerStyle: React.CSSProperties = isHexagon
|
|
283
291
|
? {
|
|
284
292
|
position: 'absolute',
|
|
@@ -287,7 +295,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
287
295
|
right: hexagonBorderWidth,
|
|
288
296
|
bottom: hexagonBorderWidth,
|
|
289
297
|
clipPath: hexagonClipPath,
|
|
290
|
-
backgroundColor:
|
|
298
|
+
backgroundColor: hexToLightColor(fillColor),
|
|
291
299
|
color: '#000',
|
|
292
300
|
display: 'flex',
|
|
293
301
|
flexDirection: 'column',
|
|
@@ -320,7 +328,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
320
328
|
}
|
|
321
329
|
: {};
|
|
322
330
|
|
|
323
|
-
// Diamond inner fill styles (
|
|
331
|
+
// Diamond inner fill styles (light color background inset from border)
|
|
324
332
|
const diamondInnerStyle: React.CSSProperties = isDiamond
|
|
325
333
|
? {
|
|
326
334
|
position: 'absolute',
|
|
@@ -329,7 +337,7 @@ export const CustomNode: React.FC<NodeProps<any>> = ({ data, selected, dragging
|
|
|
329
337
|
right: diamondBorderWidth,
|
|
330
338
|
bottom: diamondBorderWidth,
|
|
331
339
|
clipPath: diamondClipPath,
|
|
332
|
-
backgroundColor:
|
|
340
|
+
backgroundColor: hexToLightColor(fillColor),
|
|
333
341
|
color: '#000',
|
|
334
342
|
display: 'flex',
|
|
335
343
|
flexDirection: 'column',
|
|
@@ -411,7 +411,7 @@ function MultiConfigDemo() {
|
|
|
411
411
|
|
|
412
412
|
{/* Graph visualization */}
|
|
413
413
|
<div style={{ flex: 1 }}>
|
|
414
|
-
<GraphRenderer canvas={selectedConfig.canvas}
|
|
414
|
+
<GraphRenderer canvas={selectedConfig.canvas} showControls showBackground />
|
|
415
415
|
</div>
|
|
416
416
|
</div>
|
|
417
417
|
);
|