flock-core 0.5.0b52__py3-none-any.whl → 0.5.0b54__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

@@ -0,0 +1,1971 @@
1
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
+ import { ModuleContext } from './ModuleRegistry';
3
+ import JsonAttributeRenderer from './JsonAttributeRenderer';
4
+ import TracingSettings from '../settings/TracingSettings';
5
+
6
+ interface Span {
7
+ name: string;
8
+ context: {
9
+ trace_id: string;
10
+ span_id: string;
11
+ };
12
+ parent_id?: string;
13
+ start_time: number;
14
+ end_time: number;
15
+ status: {
16
+ status_code: string;
17
+ description?: string;
18
+ };
19
+ attributes: Record<string, string>;
20
+ kind: string;
21
+ resource: {
22
+ 'service.name'?: string;
23
+ };
24
+ }
25
+
26
+ interface SpanNode extends Span {
27
+ children: SpanNode[];
28
+ depth: number;
29
+ }
30
+
31
+ interface TraceModuleJaegerProps {
32
+ context: ModuleContext;
33
+ }
34
+
35
+ interface TraceGroup {
36
+ traceId: string;
37
+ spans: Span[];
38
+ startTime: number;
39
+ endTime: number;
40
+ duration: number;
41
+ spanCount: number;
42
+ hasError: boolean;
43
+ services: Set<string>;
44
+ }
45
+
46
+ interface ServiceMetrics {
47
+ service: string;
48
+ totalSpans: number;
49
+ errorSpans: number;
50
+ avgDuration: number;
51
+ p95Duration: number;
52
+ p99Duration: number;
53
+ rate: number;
54
+ }
55
+
56
+ interface OperationMetrics {
57
+ operation: string;
58
+ service: string;
59
+ totalCalls: number;
60
+ errorCalls: number;
61
+ avgDuration: number;
62
+ p95Duration: number;
63
+ }
64
+
65
+ interface DependencyEdge {
66
+ from: string; // parent service
67
+ to: string; // child service
68
+ operations: Map<string, OperationMetrics>; // parent.operation -> child.operation
69
+ }
70
+
71
+ type ViewMode = 'timeline' | 'statistics' | 'metrics' | 'dependencies' | 'sql' | 'configuration' | 'guide';
72
+
73
+ const TraceModuleJaeger: React.FC<TraceModuleJaegerProps> = () => {
74
+ const [traces, setTraces] = useState<Span[]>([]);
75
+ const [loading, setLoading] = useState(true);
76
+ const [error, setError] = useState<string | null>(null);
77
+ const [selectedTraceIds, setSelectedTraceIds] = useState<Set<string>>(new Set());
78
+ const [expandedSpans, setExpandedSpans] = useState<Set<string>>(new Set());
79
+ const [collapsedSpans, setCollapsedSpans] = useState<Set<string>>(new Set());
80
+ const [searchQuery, setSearchQuery] = useState('');
81
+ const [viewMode, setViewMode] = useState<ViewMode>('timeline');
82
+ const [focusedSpanId, setFocusedSpanId] = useState<string | null>(null);
83
+ const [expandedDeps, setExpandedDeps] = useState<Set<string>>(new Set());
84
+
85
+ // SQL query state
86
+ const [sqlQuery, setSqlQuery] = useState('SELECT * FROM spans LIMIT 10');
87
+ const [sqlResults, setSqlResults] = useState<any[] | null>(null);
88
+ const [sqlColumns, setSqlColumns] = useState<string[]>([]);
89
+ const [sqlLoading, setSqlLoading] = useState(false);
90
+ const [sqlError, setSqlError] = useState<string | null>(null);
91
+
92
+ // Sort state for timeline/statistics
93
+ type SortField = 'date' | 'spans' | 'duration';
94
+ type SortOrder = 'asc' | 'desc';
95
+ const [sortField, setSortField] = useState<SortField>('date');
96
+ const [sortOrder, setSortOrder] = useState<SortOrder>('desc'); // newest first by default
97
+
98
+ const scrollContainerRef = useRef<HTMLDivElement>(null);
99
+ const lastTraceCountRef = useRef<number>(0);
100
+
101
+ // Execute SQL query
102
+ const executeSqlQuery = async () => {
103
+ setSqlLoading(true);
104
+ setSqlError(null);
105
+ try {
106
+ const response = await fetch('/api/traces/query', {
107
+ method: 'POST',
108
+ headers: { 'Content-Type': 'application/json' },
109
+ body: JSON.stringify({ query: sqlQuery }),
110
+ });
111
+
112
+ if (!response.ok) {
113
+ const error = await response.json();
114
+ throw new Error(error.error || 'Query failed');
115
+ }
116
+
117
+ const data = await response.json();
118
+ setSqlResults(data.results);
119
+ setSqlColumns(data.columns);
120
+ } catch (err) {
121
+ setSqlError(err instanceof Error ? err.message : 'Unknown error');
122
+ setSqlResults(null);
123
+ setSqlColumns([]);
124
+ } finally {
125
+ setSqlLoading(false);
126
+ }
127
+ };
128
+
129
+ // Service colors - assign consistent colors per service or span type
130
+ const serviceColors = useMemo(() => {
131
+ const colors = [
132
+ '#3b82f6', // blue
133
+ '#10b981', // green
134
+ '#f59e0b', // amber
135
+ '#ef4444', // red
136
+ '#8b5cf6', // purple
137
+ '#ec4899', // pink
138
+ '#06b6d4', // cyan
139
+ '#f97316', // orange
140
+ ];
141
+
142
+ const colorMap = new Map<string, string>();
143
+ const services: string[] = [];
144
+ const spanTypes: string[] = [];
145
+
146
+ traces.forEach(span => {
147
+ // Extract service from span name (e.g., "Flock.publish" -> service: "Flock")
148
+ const serviceName = span.name.split('.')[0] || span.resource['service.name'] || 'unknown';
149
+ if (serviceName && !services.includes(serviceName)) {
150
+ services.push(serviceName);
151
+ }
152
+
153
+ // Also track span types for color coding (use full span name for more granular types)
154
+ const spanType = span.name.split('.')[0] || span.name; // Get class name
155
+ if (spanType && !spanTypes.includes(spanType)) {
156
+ spanTypes.push(spanType);
157
+ }
158
+ });
159
+
160
+ // If all spans have the same service, color by span type instead
161
+ if (services.length === 1) {
162
+ spanTypes.forEach((type, idx) => {
163
+ const color = colors[idx % colors.length] || '#6366f1';
164
+ colorMap.set(type, color);
165
+ });
166
+ } else {
167
+ services.forEach((service, idx) => {
168
+ const color = colors[idx % colors.length] || '#6366f1';
169
+ colorMap.set(service!, color);
170
+ });
171
+ }
172
+
173
+ return { colorMap, useSpanType: services.length === 1 };
174
+ }, [traces]);
175
+
176
+ useEffect(() => {
177
+ const fetchTraces = async () => {
178
+ try {
179
+ if (traces.length === 0) {
180
+ setLoading(true);
181
+ }
182
+
183
+ const response = await fetch('/api/traces');
184
+ if (!response.ok) {
185
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
186
+ }
187
+ const data = await response.json();
188
+
189
+ if (JSON.stringify(data) !== JSON.stringify(traces)) {
190
+ const scrollTop = scrollContainerRef.current?.scrollTop || 0;
191
+ setTraces(data);
192
+ setError(null);
193
+
194
+ requestAnimationFrame(() => {
195
+ if (scrollContainerRef.current && data.length === lastTraceCountRef.current) {
196
+ scrollContainerRef.current.scrollTop = scrollTop;
197
+ }
198
+ });
199
+
200
+ lastTraceCountRef.current = data.length;
201
+ }
202
+ } catch (err) {
203
+ setError(err instanceof Error ? err.message : 'Failed to load traces');
204
+ } finally {
205
+ setLoading(false);
206
+ }
207
+ };
208
+
209
+ fetchTraces();
210
+ const interval = setInterval(fetchTraces, 5000);
211
+ return () => clearInterval(interval);
212
+ }, [traces]);
213
+
214
+ const traceGroups = useMemo((): TraceGroup[] => {
215
+ const grouped = new Map<string, Span[]>();
216
+
217
+ traces.forEach(span => {
218
+ const traceId = span.context.trace_id;
219
+ if (!grouped.has(traceId)) {
220
+ grouped.set(traceId, []);
221
+ }
222
+ grouped.get(traceId)!.push(span);
223
+ });
224
+
225
+ return Array.from(grouped.entries()).map(([traceId, spans]) => {
226
+ const startTime = Math.min(...spans.map(s => s.start_time));
227
+ const endTime = Math.max(...spans.map(s => s.end_time));
228
+ const duration = (endTime - startTime) / 1_000_000;
229
+ const hasError = spans.some(s => s.status.status_code === 'ERROR');
230
+ const services = new Set(spans.map(s => s.name.split('.')[0] || s.resource['service.name'] || 'unknown'));
231
+
232
+ return {
233
+ traceId,
234
+ spans: spans.sort((a, b) => a.start_time - b.start_time),
235
+ startTime,
236
+ endTime,
237
+ duration,
238
+ spanCount: spans.length,
239
+ hasError,
240
+ services,
241
+ };
242
+ });
243
+ }, [traces]);
244
+
245
+ const filteredTraces = useMemo(() => {
246
+ let result = traceGroups;
247
+
248
+ if (searchQuery) {
249
+ const query = searchQuery.toLowerCase();
250
+ result = result.filter(trace =>
251
+ trace.traceId.toLowerCase().includes(query) ||
252
+ trace.spans.some(span =>
253
+ span.name.toLowerCase().includes(query) ||
254
+ Object.values(span.attributes).some(val =>
255
+ typeof val === 'string' && val.toLowerCase().includes(query)
256
+ )
257
+ )
258
+ );
259
+ }
260
+
261
+ // Apply sorting
262
+ return [...result].sort((a, b) => {
263
+ let comparison = 0;
264
+
265
+ switch (sortField) {
266
+ case 'date':
267
+ comparison = a.startTime - b.startTime;
268
+ break;
269
+ case 'spans':
270
+ comparison = a.spanCount - b.spanCount;
271
+ break;
272
+ case 'duration':
273
+ comparison = a.duration - b.duration;
274
+ break;
275
+ }
276
+
277
+ return sortOrder === 'asc' ? comparison : -comparison;
278
+ });
279
+ }, [traceGroups, searchQuery, sortField, sortOrder]);
280
+
281
+ // Calculate RED metrics per service
282
+ const serviceMetrics = useMemo<ServiceMetrics[]>(() => {
283
+ const metricsMap = new Map<string, ServiceMetrics>();
284
+
285
+ traces.forEach(span => {
286
+ const service = span.name.split('.')[0] || 'unknown';
287
+ if (!metricsMap.has(service)) {
288
+ metricsMap.set(service, {
289
+ service,
290
+ totalSpans: 0,
291
+ errorSpans: 0,
292
+ avgDuration: 0,
293
+ p95Duration: 0,
294
+ p99Duration: 0,
295
+ rate: 0,
296
+ });
297
+ }
298
+
299
+ const metrics = metricsMap.get(service)!;
300
+ metrics.totalSpans++;
301
+ if (span.status.status_code === 'ERROR') {
302
+ metrics.errorSpans++;
303
+ }
304
+ });
305
+
306
+ // Calculate durations and rate
307
+ metricsMap.forEach((metrics, service) => {
308
+ const serviceSpans = traces.filter(s => (s.name.split('.')[0] || 'unknown') === service);
309
+ const durations = serviceSpans.map(s => (s.end_time - s.start_time) / 1_000_000).sort((a, b) => a - b);
310
+
311
+ metrics.avgDuration = durations.reduce((sum, d) => sum + d, 0) / durations.length || 0;
312
+ metrics.p95Duration = durations[Math.floor(durations.length * 0.95)] || 0;
313
+ metrics.p99Duration = durations[Math.floor(durations.length * 0.99)] || 0;
314
+
315
+ // Calculate rate (spans per second)
316
+ if (serviceSpans.length > 1) {
317
+ const timeSpan = (Math.max(...serviceSpans.map(s => s.end_time)) -
318
+ Math.min(...serviceSpans.map(s => s.start_time))) / 1_000_000_000;
319
+ metrics.rate = serviceSpans.length / Math.max(timeSpan, 1);
320
+ }
321
+ });
322
+
323
+ return Array.from(metricsMap.values()).sort((a, b) => b.totalSpans - a.totalSpans);
324
+ }, [traces]);
325
+
326
+ // Build service dependency graph with operation-level drill-down
327
+ const serviceDependencies = useMemo(() => {
328
+ const deps = new Map<string, DependencyEdge>();
329
+
330
+ traceGroups.forEach(trace => {
331
+ trace.spans.forEach(span => {
332
+ if (span.parent_id) {
333
+ const parent = trace.spans.find(s => s.context.span_id === span.parent_id);
334
+ if (parent) {
335
+ const parentService = parent.name.split('.')[0] || 'unknown';
336
+ const childService = span.name.split('.')[0] || 'unknown';
337
+
338
+ // Only track cross-service dependencies
339
+ if (parentService !== childService) {
340
+ const key = `${parentService}->${childService}`;
341
+
342
+ if (!deps.has(key)) {
343
+ deps.set(key, {
344
+ from: parentService,
345
+ to: childService,
346
+ operations: new Map(),
347
+ });
348
+ }
349
+
350
+ const edge = deps.get(key)!;
351
+ const opKey = `${parent.name} → ${span.name}`;
352
+
353
+ if (!edge.operations.has(opKey)) {
354
+ edge.operations.set(opKey, {
355
+ operation: opKey,
356
+ service: childService,
357
+ totalCalls: 0,
358
+ errorCalls: 0,
359
+ avgDuration: 0,
360
+ p95Duration: 0,
361
+ });
362
+ }
363
+
364
+ const opMetrics = edge.operations.get(opKey)!;
365
+ opMetrics.totalCalls++;
366
+ if (span.status.status_code === 'ERROR') {
367
+ opMetrics.errorCalls++;
368
+ }
369
+ }
370
+ }
371
+ }
372
+ });
373
+ });
374
+
375
+ // Calculate operation metrics
376
+ deps.forEach(edge => {
377
+ edge.operations.forEach((opMetrics, opKey) => {
378
+ const childOp = opKey.split(' → ')[1];
379
+ const relevantSpans = traces.filter(s => s.name === childOp);
380
+ const durations = relevantSpans.map(s => (s.end_time - s.start_time) / 1_000_000).sort((a, b) => a - b);
381
+
382
+ opMetrics.avgDuration = durations.reduce((sum, d) => sum + d, 0) / durations.length || 0;
383
+ opMetrics.p95Duration = durations[Math.floor(durations.length * 0.95)] || 0;
384
+ });
385
+ });
386
+
387
+ return Array.from(deps.values());
388
+ }, [traceGroups, traces]);
389
+
390
+ const buildSpanTree = (spans: Span[]): SpanNode[] => {
391
+ const spanMap = new Map<string, SpanNode>();
392
+ const roots: SpanNode[] = [];
393
+
394
+ spans.forEach(span => {
395
+ spanMap.set(span.context.span_id, {
396
+ ...span,
397
+ children: [],
398
+ depth: 0,
399
+ });
400
+ });
401
+
402
+ spans.forEach(span => {
403
+ const node = spanMap.get(span.context.span_id)!;
404
+
405
+ if (span.parent_id && spanMap.has(span.parent_id)) {
406
+ const parent = spanMap.get(span.parent_id)!;
407
+ parent.children.push(node);
408
+ node.depth = parent.depth + 1;
409
+ } else {
410
+ roots.push(node);
411
+ }
412
+ });
413
+
414
+ const sortChildren = (node: SpanNode) => {
415
+ node.children.sort((a, b) => a.start_time - b.start_time);
416
+ node.children.forEach(sortChildren);
417
+ };
418
+ roots.forEach(sortChildren);
419
+
420
+ return roots;
421
+ };
422
+
423
+ const toggleSpanExpand = (spanId: string) => {
424
+ setExpandedSpans(prev => {
425
+ const newSet = new Set(prev);
426
+ if (newSet.has(spanId)) {
427
+ newSet.delete(spanId);
428
+ } else {
429
+ newSet.add(spanId);
430
+ }
431
+ return newSet;
432
+ });
433
+ };
434
+
435
+ const toggleSpanCollapse = (spanId: string) => {
436
+ setCollapsedSpans(prev => {
437
+ const newSet = new Set(prev);
438
+ if (newSet.has(spanId)) {
439
+ newSet.delete(spanId);
440
+ } else {
441
+ newSet.add(spanId);
442
+ }
443
+ return newSet;
444
+ });
445
+ };
446
+
447
+ const getServiceColor = (serviceName: string | undefined, spanName: string): string => {
448
+ if (serviceColors.useSpanType) {
449
+ // Color by span type if all services are the same
450
+ const spanType = spanName.split('.')[0] || spanName;
451
+ return serviceColors.colorMap.get(spanType) || '#6366f1';
452
+ }
453
+ if (!serviceName) return '#6366f1';
454
+ return serviceColors.colorMap.get(serviceName) || '#6366f1';
455
+ };
456
+
457
+ const renderStatisticsView = (trace: TraceGroup) => {
458
+ return (
459
+ <div style={{
460
+ marginTop: 'var(--space-component-md)',
461
+ background: 'var(--color-bg-base)',
462
+ borderRadius: 'var(--radius-lg)',
463
+ overflow: 'hidden',
464
+ }}>
465
+ <table style={{
466
+ width: '100%',
467
+ fontSize: 'var(--font-size-body-sm)',
468
+ fontFamily: 'var(--font-family-mono)',
469
+ borderCollapse: 'collapse',
470
+ }}>
471
+ <thead>
472
+ <tr style={{
473
+ background: 'var(--color-bg-surface)',
474
+ borderBottom: '2px solid var(--color-border-subtle)',
475
+ }}>
476
+ <th style={{ padding: '12px', textAlign: 'left', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Service</th>
477
+ <th style={{ padding: '12px', textAlign: 'left', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Operation</th>
478
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Duration</th>
479
+ <th style={{ padding: '12px', textAlign: 'right', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Start Time</th>
480
+ <th style={{ padding: '12px', textAlign: 'center', color: 'var(--color-text-secondary)', fontWeight: 'var(--font-weight-bold)' }}>Status</th>
481
+ </tr>
482
+ </thead>
483
+ <tbody>
484
+ {trace.spans.map((span, idx) => {
485
+ const duration = (span.end_time - span.start_time) / 1_000_000;
486
+ const startOffset = (span.start_time - trace.startTime) / 1_000_000;
487
+ const serviceName = span.name.split('.')[0] || span.resource['service.name'] || 'unknown';
488
+
489
+ return (
490
+ <tr
491
+ key={span.context.span_id}
492
+ style={{
493
+ background: idx % 2 === 0 ? 'transparent' : 'var(--color-bg-surface)',
494
+ borderBottom: '1px solid var(--color-border-subtle)',
495
+ }}
496
+ >
497
+ <td style={{ padding: '10px' }}>
498
+ <span style={{
499
+ display: 'inline-block',
500
+ width: '8px',
501
+ height: '8px',
502
+ borderRadius: '50%',
503
+ background: getServiceColor(serviceName, span.name),
504
+ marginRight: '8px',
505
+ }} />
506
+ {serviceName}
507
+ </td>
508
+ <td style={{ padding: '10px', color: 'var(--color-text-primary)' }}>{span.name}</td>
509
+ <td style={{ padding: '10px', textAlign: 'right', color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
510
+ {duration.toFixed(2)}ms
511
+ </td>
512
+ <td style={{ padding: '10px', textAlign: 'right', color: 'var(--color-text-tertiary)' }}>
513
+ +{startOffset.toFixed(2)}ms
514
+ </td>
515
+ <td style={{ padding: '10px', textAlign: 'center' }}>
516
+ <span style={{
517
+ padding: '2px 8px',
518
+ borderRadius: 'var(--radius-sm)',
519
+ fontSize: 'var(--font-size-body-xs)',
520
+ fontWeight: 'var(--font-weight-medium)',
521
+ background: span.status.status_code === 'ERROR' ? 'rgba(239, 68, 68, 0.2)' : 'rgba(16, 185, 129, 0.2)',
522
+ color: span.status.status_code === 'ERROR' ? '#ef4444' : '#10b981',
523
+ }}>
524
+ {span.status.status_code}
525
+ </span>
526
+ </td>
527
+ </tr>
528
+ );
529
+ })}
530
+ </tbody>
531
+ </table>
532
+ </div>
533
+ );
534
+ };
535
+
536
+ const renderSpanNode = (
537
+ node: SpanNode,
538
+ traceStartTime: number,
539
+ scaleDuration: number
540
+ ): React.ReactElement => {
541
+ const spanStartOffset = (node.start_time - traceStartTime) / 1_000_000;
542
+ const spanDuration = (node.end_time - node.start_time) / 1_000_000;
543
+
544
+ const leftPercent = Math.min((spanStartOffset / scaleDuration) * 100, 100);
545
+ const widthPercent = Math.min((spanDuration / scaleDuration) * 100, 100 - leftPercent);
546
+ const displayWidthPercent = Math.max(widthPercent, 0.5);
547
+
548
+ const isExpanded = expandedSpans.has(node.context.span_id);
549
+ const isCollapsed = collapsedSpans.has(node.context.span_id);
550
+ const hasChildren = node.children.length > 0;
551
+ const serviceName = node.name.split('.')[0] || node.resource['service.name'] || 'unknown';
552
+ const serviceColor = getServiceColor(serviceName, node.name);
553
+ const isFocused = focusedSpanId === node.context.span_id;
554
+
555
+ return (
556
+ <div key={node.context.span_id} style={{ marginBottom: '1px', opacity: isFocused ? 1 : (focusedSpanId ? 0.4 : 1) }}>
557
+ <div
558
+ style={{
559
+ display: 'grid',
560
+ gridTemplateColumns: '400px 1fr',
561
+ alignItems: 'center',
562
+ background: isExpanded ? 'var(--color-bg-elevated)' : 'transparent',
563
+ borderBottom: '1px solid rgba(255, 255, 255, 0.03)',
564
+ }}
565
+ >
566
+ {/* Left side: Hierarchy */}
567
+ <div style={{
568
+ padding: '8px 12px',
569
+ fontSize: 'var(--font-size-body-sm)',
570
+ fontFamily: 'var(--font-family-mono)',
571
+ color: 'var(--color-text-primary)',
572
+ display: 'flex',
573
+ alignItems: 'center',
574
+ gap: '8px',
575
+ paddingLeft: `${12 + node.depth * 20}px`,
576
+ borderRight: '1px solid var(--color-border-subtle)',
577
+ }}>
578
+ {hasChildren && (
579
+ <span
580
+ onClick={() => toggleSpanCollapse(node.context.span_id)}
581
+ style={{
582
+ cursor: 'pointer',
583
+ userSelect: 'none',
584
+ width: '12px',
585
+ opacity: 0.6,
586
+ fontSize: '10px',
587
+ }}
588
+ >
589
+ {isCollapsed ? '►' : '▼'}
590
+ </span>
591
+ )}
592
+ {!hasChildren && <span style={{ width: '12px' }} />}
593
+
594
+ <span
595
+ style={{
596
+ display: 'inline-block',
597
+ width: '6px',
598
+ height: '6px',
599
+ borderRadius: '50%',
600
+ background: serviceColor,
601
+ flexShrink: 0,
602
+ }}
603
+ />
604
+
605
+ <span
606
+ onClick={(e) => {
607
+ if (e.shiftKey) {
608
+ setFocusedSpanId(isFocused ? null : node.context.span_id);
609
+ } else {
610
+ toggleSpanExpand(node.context.span_id);
611
+ }
612
+ }}
613
+ style={{
614
+ flex: 1,
615
+ overflow: 'hidden',
616
+ textOverflow: 'ellipsis',
617
+ whiteSpace: 'nowrap',
618
+ cursor: 'pointer',
619
+ display: 'flex',
620
+ alignItems: 'center',
621
+ gap: '4px',
622
+ }}
623
+ title={`${node.name}\nShift+click to focus`}
624
+ >
625
+ <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>{node.name}</span>
626
+ </span>
627
+
628
+ <span style={{
629
+ fontSize: 'var(--font-size-body-xs)',
630
+ color: 'var(--color-text-tertiary)',
631
+ flexShrink: 0,
632
+ }}>
633
+ {spanDuration.toFixed(1)}ms
634
+ </span>
635
+ </div>
636
+
637
+ {/* Right side: Gantt chart */}
638
+ <div style={{
639
+ padding: '8px 12px',
640
+ height: '32px',
641
+ }}>
642
+ <div style={{
643
+ position: 'relative',
644
+ height: '18px',
645
+ width: '100%',
646
+ }}>
647
+ <div
648
+ style={{
649
+ position: 'absolute',
650
+ left: `${leftPercent}%`,
651
+ width: `${displayWidthPercent}%`,
652
+ height: '100%',
653
+ background: serviceColor,
654
+ border: node.status.status_code === 'ERROR' ? '2px solid #ef4444' : 'none',
655
+ borderRadius: '2px',
656
+ display: 'flex',
657
+ alignItems: 'center',
658
+ paddingLeft: '4px',
659
+ fontSize: '10px',
660
+ color: 'white',
661
+ fontWeight: 'var(--font-weight-medium)',
662
+ cursor: 'pointer',
663
+ boxSizing: 'border-box',
664
+ }}
665
+ onClick={() => toggleSpanExpand(node.context.span_id)}
666
+ title={`${node.name}\nService: ${serviceName || 'unknown'}\n${spanDuration.toFixed(2)}ms\nStart: +${spanStartOffset.toFixed(2)}ms`}
667
+ />
668
+ </div>
669
+ </div>
670
+ </div>
671
+
672
+ {isExpanded && (
673
+ <div style={{
674
+ background: 'var(--color-bg-surface)',
675
+ border: '1px solid var(--color-border-subtle)',
676
+ borderLeft: `4px solid ${serviceColor}`,
677
+ margin: '0 12px 8px 12px',
678
+ padding: 'var(--space-component-sm)',
679
+ borderRadius: 'var(--radius-md)',
680
+ fontSize: 'var(--font-size-body-xs)',
681
+ fontFamily: 'var(--font-family-mono)',
682
+ }}>
683
+ <div style={{ display: 'grid', gridTemplateColumns: '120px 1fr', gap: '6px', color: 'var(--color-text-secondary)' }}>
684
+ <div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Service:</div>
685
+ <div>{serviceName}</div>
686
+
687
+ <div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Span ID:</div>
688
+ <div style={{ wordBreak: 'break-all' }}>{node.context.span_id}</div>
689
+
690
+ {node.parent_id && (
691
+ <>
692
+ <div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Parent ID:</div>
693
+ <div style={{ wordBreak: 'break-all' }}>{node.parent_id}</div>
694
+ </>
695
+ )}
696
+
697
+ <div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Duration:</div>
698
+ <div>{spanDuration.toFixed(3)}ms</div>
699
+
700
+ <div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Start Time:</div>
701
+ <div>+{spanStartOffset.toFixed(3)}ms</div>
702
+
703
+ <div style={{ color: 'var(--color-text-tertiary)', fontWeight: 'var(--font-weight-medium)' }}>Status:</div>
704
+ <div style={{
705
+ color: node.status.status_code === 'ERROR' ? '#ef4444' : '#10b981',
706
+ fontWeight: 'var(--font-weight-bold)',
707
+ }}>
708
+ {node.status.status_code}
709
+ </div>
710
+
711
+ {Object.entries(node.attributes).length > 0 && (
712
+ <>
713
+ <div style={{
714
+ gridColumn: '1 / -1',
715
+ borderTop: '1px solid var(--color-border-subtle)',
716
+ margin: '8px 0 4px 0',
717
+ paddingTop: '8px',
718
+ color: 'var(--color-text-secondary)',
719
+ fontWeight: 'var(--font-weight-medium)',
720
+ }}>
721
+ Tags:
722
+ </div>
723
+ {Object.entries(node.attributes).map(([key, value]) => (
724
+ <React.Fragment key={key}>
725
+ <div style={{ color: 'var(--color-text-tertiary)', alignSelf: 'start' }}>{key}:</div>
726
+ <div>
727
+ <JsonAttributeRenderer value={value} />
728
+ </div>
729
+ </React.Fragment>
730
+ ))}
731
+ </>
732
+ )}
733
+ </div>
734
+ </div>
735
+ )}
736
+
737
+ {hasChildren && !isCollapsed && (
738
+ <div>
739
+ {node.children.map(child => renderSpanNode(child, traceStartTime, scaleDuration))}
740
+ </div>
741
+ )}
742
+ </div>
743
+ );
744
+ };
745
+
746
+ const renderTimelineView = (trace: TraceGroup) => {
747
+ const maxEndOffset = Math.max(...trace.spans.map(s => (s.end_time - trace.startTime) / 1_000_000));
748
+ const scaleDuration = Math.max(maxEndOffset, trace.duration);
749
+ const spanTree = buildSpanTree(trace.spans);
750
+
751
+ return (
752
+ <div style={{
753
+ marginTop: 'var(--space-component-md)',
754
+ background: 'var(--color-bg-base)',
755
+ borderRadius: 'var(--radius-lg)',
756
+ overflow: 'hidden',
757
+ }}>
758
+ {/* Header with timeline scale */}
759
+ <div style={{
760
+ display: 'grid',
761
+ gridTemplateColumns: '400px 1fr',
762
+ background: 'var(--color-bg-surface)',
763
+ borderBottom: '2px solid var(--color-border-subtle)',
764
+ fontSize: 'var(--font-size-body-xs)',
765
+ color: 'var(--color-text-tertiary)',
766
+ fontWeight: 'var(--font-weight-medium)',
767
+ }}>
768
+ <div style={{ padding: '10px 12px', borderRight: '1px solid var(--color-border-subtle)' }}>
769
+ Service & Operation
770
+ </div>
771
+ <div style={{ padding: '10px 12px' }}>
772
+ Timeline (0ms - {scaleDuration.toFixed(0)}ms)
773
+ </div>
774
+ </div>
775
+
776
+ {spanTree.map(node => renderSpanNode(node, trace.startTime, scaleDuration))}
777
+ </div>
778
+ );
779
+ };
780
+
781
+ if (loading) {
782
+ return (
783
+ <div style={{
784
+ display: 'flex',
785
+ alignItems: 'center',
786
+ justifyContent: 'center',
787
+ height: '100%',
788
+ color: 'var(--color-text-secondary)',
789
+ }}>
790
+ <div style={{ textAlign: 'center' }}>
791
+ <div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)', opacity: 0.5 }}>🔎</div>
792
+ <div>Loading traces...</div>
793
+ </div>
794
+ </div>
795
+ );
796
+ }
797
+
798
+ if (error) {
799
+ return (
800
+ <div style={{
801
+ display: 'flex',
802
+ alignItems: 'center',
803
+ justifyContent: 'center',
804
+ height: '100%',
805
+ color: 'var(--color-error)',
806
+ }}>
807
+ <div style={{ textAlign: 'center' }}>
808
+ <div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)' }}>⚠️</div>
809
+ <div>{error}</div>
810
+ </div>
811
+ </div>
812
+ );
813
+ }
814
+
815
+ return (
816
+ <div style={{
817
+ height: '100%',
818
+ display: 'flex',
819
+ flexDirection: 'column',
820
+ background: 'var(--color-bg-surface)',
821
+ }}>
822
+ {/* Two-row header */}
823
+ <div style={{
824
+ padding: 'var(--space-component-md)',
825
+ borderBottom: '1px solid var(--color-border-subtle)',
826
+ display: 'flex',
827
+ flexDirection: 'column',
828
+ gap: 'var(--gap-sm)',
829
+ }}>
830
+ {/* Row 1: View mode buttons */}
831
+ <div style={{ display: 'flex', gap: '8px', alignItems: 'center' }}>
832
+ <button
833
+ onClick={() => setViewMode('timeline')}
834
+ style={{
835
+ padding: '6px 12px',
836
+ border: '1px solid var(--color-border-subtle)',
837
+ borderRadius: 'var(--radius-md)',
838
+ background: viewMode === 'timeline' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
839
+ color: viewMode === 'timeline' ? 'white' : 'var(--color-text-primary)',
840
+ fontSize: 'var(--font-size-body-sm)',
841
+ cursor: 'pointer',
842
+ fontWeight: 'var(--font-weight-medium)',
843
+ }}
844
+ >
845
+ 📅 Timeline
846
+ </button>
847
+ <button
848
+ onClick={() => setViewMode('statistics')}
849
+ style={{
850
+ padding: '6px 12px',
851
+ border: '1px solid var(--color-border-subtle)',
852
+ borderRadius: 'var(--radius-md)',
853
+ background: viewMode === 'statistics' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
854
+ color: viewMode === 'statistics' ? 'white' : 'var(--color-text-primary)',
855
+ fontSize: 'var(--font-size-body-sm)',
856
+ cursor: 'pointer',
857
+ fontWeight: 'var(--font-weight-medium)',
858
+ }}
859
+ >
860
+ 📊 Statistics
861
+ </button>
862
+ <button
863
+ onClick={() => setViewMode('metrics')}
864
+ style={{
865
+ padding: '6px 12px',
866
+ border: '1px solid var(--color-border-subtle)',
867
+ borderRadius: 'var(--radius-md)',
868
+ background: viewMode === 'metrics' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
869
+ color: viewMode === 'metrics' ? 'white' : 'var(--color-text-primary)',
870
+ fontSize: 'var(--font-size-body-sm)',
871
+ cursor: 'pointer',
872
+ fontWeight: 'var(--font-weight-medium)',
873
+ }}
874
+ >
875
+ 🔴 RED Metrics
876
+ </button>
877
+ <button
878
+ onClick={() => setViewMode('dependencies')}
879
+ style={{
880
+ padding: '6px 12px',
881
+ border: '1px solid var(--color-border-subtle)',
882
+ borderRadius: 'var(--radius-md)',
883
+ background: viewMode === 'dependencies' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
884
+ color: viewMode === 'dependencies' ? 'white' : 'var(--color-text-primary)',
885
+ fontSize: 'var(--font-size-body-sm)',
886
+ cursor: 'pointer',
887
+ fontWeight: 'var(--font-weight-medium)',
888
+ }}
889
+ >
890
+ 🔗 Dependencies
891
+ </button>
892
+ <button
893
+ onClick={() => setViewMode('sql')}
894
+ style={{
895
+ padding: '6px 12px',
896
+ border: '1px solid var(--color-border-subtle)',
897
+ borderRadius: 'var(--radius-md)',
898
+ background: viewMode === 'sql' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
899
+ color: viewMode === 'sql' ? 'white' : 'var(--color-text-primary)',
900
+ fontSize: 'var(--font-size-body-sm)',
901
+ cursor: 'pointer',
902
+ fontWeight: 'var(--font-weight-medium)',
903
+ }}
904
+ >
905
+ 🗄️ DuckDB SQL
906
+ </button>
907
+ <button
908
+ onClick={() => setViewMode('configuration')}
909
+ style={{
910
+ padding: '6px 12px',
911
+ border: '1px solid var(--color-border-subtle)',
912
+ borderRadius: 'var(--radius-md)',
913
+ background: viewMode === 'configuration' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
914
+ color: viewMode === 'configuration' ? 'white' : 'var(--color-text-primary)',
915
+ fontSize: 'var(--font-size-body-sm)',
916
+ cursor: 'pointer',
917
+ fontWeight: 'var(--font-weight-medium)',
918
+ }}
919
+ >
920
+ ⚙️ Configuration
921
+ </button>
922
+ <button
923
+ onClick={() => setViewMode('guide')}
924
+ style={{
925
+ padding: '6px 12px',
926
+ border: '1px solid var(--color-border-subtle)',
927
+ borderRadius: 'var(--radius-md)',
928
+ background: viewMode === 'guide' ? 'var(--color-primary-500)' : 'var(--color-bg-base)',
929
+ color: viewMode === 'guide' ? 'white' : 'var(--color-text-primary)',
930
+ fontSize: 'var(--font-size-body-sm)',
931
+ cursor: 'pointer',
932
+ fontWeight: 'var(--font-weight-medium)',
933
+ }}
934
+ >
935
+ 📚 Guide
936
+ </button>
937
+
938
+ {/* Trace count */}
939
+ {(viewMode === 'timeline' || viewMode === 'statistics') && (
940
+ <div style={{
941
+ fontSize: 'var(--font-size-body-xs)',
942
+ color: 'var(--color-text-secondary)',
943
+ marginLeft: 'auto',
944
+ }}>
945
+ {filteredTraces.length} trace{filteredTraces.length !== 1 ? 's' : ''}
946
+ </div>
947
+ )}
948
+ </div>
949
+
950
+ {/* Row 2: Search box */}
951
+ <input
952
+ type="text"
953
+ placeholder="🔎 Find traces (Jaeger style) - Search by service, operation, trace ID, correlation ID, or error"
954
+ value={searchQuery}
955
+ onChange={(e) => setSearchQuery(e.target.value)}
956
+ style={{
957
+ width: '100%',
958
+ padding: 'var(--space-component-sm)',
959
+ border: '1px solid var(--color-border-subtle)',
960
+ borderRadius: 'var(--radius-md)',
961
+ background: 'var(--color-bg-base)',
962
+ color: 'var(--color-text-primary)',
963
+ fontSize: 'var(--font-size-body-sm)',
964
+ }}
965
+ />
966
+
967
+ {/* Sort buttons for Timeline and Statistics */}
968
+ {(viewMode === 'timeline' || viewMode === 'statistics') && (
969
+ <div style={{
970
+ display: 'flex',
971
+ gap: 'var(--gap-xs)',
972
+ alignItems: 'center',
973
+ fontSize: 'var(--font-size-body-xs)',
974
+ }}>
975
+ <span style={{ color: 'var(--color-text-tertiary)', flexShrink: 0 }}>Sort:</span>
976
+ {(['date', 'spans', 'duration'] as SortField[]).map((field) => {
977
+ const isActive = sortField === field;
978
+ const labels = { date: 'Date', spans: 'Spans', duration: 'Duration' };
979
+
980
+ return (
981
+ <button
982
+ key={field}
983
+ onClick={() => {
984
+ if (sortField === field) {
985
+ // Toggle order if same field
986
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
987
+ } else {
988
+ // Switch field, use default order
989
+ setSortField(field);
990
+ setSortOrder(field === 'date' ? 'desc' : 'asc');
991
+ }
992
+ }}
993
+ style={{
994
+ padding: '4px 10px',
995
+ background: isActive ? 'var(--color-accent)' : 'var(--color-bg-elevated)',
996
+ color: isActive ? 'white' : 'var(--color-text-secondary)',
997
+ border: isActive ? 'none' : '1px solid var(--color-border-subtle)',
998
+ borderRadius: 'var(--radius-sm)',
999
+ fontSize: 'var(--font-size-body-xs)',
1000
+ cursor: 'pointer',
1001
+ display: 'flex',
1002
+ alignItems: 'center',
1003
+ gap: '4px',
1004
+ fontWeight: isActive ? 'var(--font-weight-medium)' : 'normal',
1005
+ }}
1006
+ >
1007
+ <span>{labels[field]}</span>
1008
+ {isActive && (
1009
+ <span style={{ fontSize: '10px' }}>
1010
+ {sortOrder === 'asc' ? '↑' : '↓'}
1011
+ </span>
1012
+ )}
1013
+ </button>
1014
+ );
1015
+ })}
1016
+ </div>
1017
+ )}
1018
+ </div>
1019
+
1020
+ <div
1021
+ ref={scrollContainerRef}
1022
+ style={{ flex: 1, overflow: 'auto', padding: 'var(--space-component-md)' }}
1023
+ >
1024
+ {/* RED Metrics View */}
1025
+ {viewMode === 'metrics' && (
1026
+ <div>
1027
+ <div style={{
1028
+ fontSize: 'var(--font-size-body-md)',
1029
+ fontWeight: 'var(--font-weight-medium)',
1030
+ color: 'var(--color-text-primary)',
1031
+ marginBottom: 'var(--space-component-md)',
1032
+ }}>
1033
+ RED Metrics by Service
1034
+ </div>
1035
+ {serviceMetrics.length === 0 ? (
1036
+ <div style={{ textAlign: 'center', padding: 'var(--space-component-xl)', color: 'var(--color-text-tertiary)' }}>
1037
+ No metrics data available
1038
+ </div>
1039
+ ) : (
1040
+ <div style={{ display: 'grid', gap: 'var(--gap-md)' }}>
1041
+ {serviceMetrics.map(metrics => {
1042
+ const serviceColor = serviceColors.colorMap.get(metrics.service) || '#6366f1';
1043
+ const errorRate = metrics.totalSpans > 0 ? (metrics.errorSpans / metrics.totalSpans) * 100 : 0;
1044
+
1045
+ return (
1046
+ <div
1047
+ key={metrics.service}
1048
+ style={{
1049
+ padding: 'var(--space-component-md)',
1050
+ background: 'var(--color-bg-elevated)',
1051
+ borderRadius: 'var(--radius-lg)',
1052
+ border: '1px solid var(--color-border-subtle)',
1053
+ borderLeft: `4px solid ${serviceColor}`,
1054
+ }}
1055
+ >
1056
+ <div style={{
1057
+ fontSize: 'var(--font-size-body-lg)',
1058
+ fontWeight: 'var(--font-weight-bold)',
1059
+ color: serviceColor,
1060
+ marginBottom: 'var(--space-component-sm)',
1061
+ }}>
1062
+ {metrics.service}
1063
+ </div>
1064
+
1065
+ <div style={{
1066
+ display: 'grid',
1067
+ gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))',
1068
+ gap: 'var(--gap-md)',
1069
+ }}>
1070
+ <div>
1071
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
1072
+ Rate
1073
+ </div>
1074
+ <div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
1075
+ {metrics.rate.toFixed(2)} req/s
1076
+ </div>
1077
+ </div>
1078
+
1079
+ <div>
1080
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
1081
+ Error Rate
1082
+ </div>
1083
+ <div style={{
1084
+ fontSize: 'var(--font-size-body-lg)',
1085
+ fontWeight: 'var(--font-weight-medium)',
1086
+ color: errorRate > 0 ? '#ef4444' : '#10b981',
1087
+ }}>
1088
+ {errorRate.toFixed(1)}%
1089
+ </div>
1090
+ </div>
1091
+
1092
+ <div>
1093
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
1094
+ Avg Duration
1095
+ </div>
1096
+ <div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
1097
+ {metrics.avgDuration.toFixed(2)}ms
1098
+ </div>
1099
+ </div>
1100
+
1101
+ <div>
1102
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
1103
+ P95 Duration
1104
+ </div>
1105
+ <div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
1106
+ {metrics.p95Duration.toFixed(2)}ms
1107
+ </div>
1108
+ </div>
1109
+
1110
+ <div>
1111
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
1112
+ P99 Duration
1113
+ </div>
1114
+ <div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
1115
+ {metrics.p99Duration.toFixed(2)}ms
1116
+ </div>
1117
+ </div>
1118
+
1119
+ <div>
1120
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)', marginBottom: '4px' }}>
1121
+ Total Spans
1122
+ </div>
1123
+ <div style={{ fontSize: 'var(--font-size-body-lg)', fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)' }}>
1124
+ {metrics.totalSpans}
1125
+ </div>
1126
+ </div>
1127
+ </div>
1128
+ </div>
1129
+ );
1130
+ })}
1131
+ </div>
1132
+ )}
1133
+ </div>
1134
+ )}
1135
+
1136
+ {/* Dependencies View */}
1137
+ {viewMode === 'dependencies' && (
1138
+ <div>
1139
+ <div style={{
1140
+ fontSize: 'var(--font-size-body-md)',
1141
+ fontWeight: 'var(--font-weight-medium)',
1142
+ color: 'var(--color-text-primary)',
1143
+ marginBottom: 'var(--space-component-md)',
1144
+ }}>
1145
+ Service Dependencies with Operation Drill-down
1146
+ </div>
1147
+ {serviceDependencies.length === 0 ? (
1148
+ <div style={{ textAlign: 'center', padding: 'var(--space-component-xl)', color: 'var(--color-text-tertiary)' }}>
1149
+ No service dependencies detected
1150
+ </div>
1151
+ ) : (
1152
+ <div style={{ display: 'grid', gap: 'var(--gap-md)' }}>
1153
+ {serviceDependencies.map(dep => {
1154
+ const depKey = `${dep.from}->${dep.to}`;
1155
+ const isExpanded = expandedDeps.has(depKey);
1156
+ const fromColor = serviceColors.colorMap.get(dep.from) || '#6366f1';
1157
+ const toColor = serviceColors.colorMap.get(dep.to) || '#6366f1';
1158
+
1159
+ return (
1160
+ <div
1161
+ key={depKey}
1162
+ style={{
1163
+ padding: 'var(--space-component-md)',
1164
+ background: 'var(--color-bg-elevated)',
1165
+ borderRadius: 'var(--radius-lg)',
1166
+ border: '1px solid var(--color-border-subtle)',
1167
+ }}
1168
+ >
1169
+ <div
1170
+ onClick={() => {
1171
+ setExpandedDeps(prev => {
1172
+ const newSet = new Set(prev);
1173
+ if (newSet.has(depKey)) {
1174
+ newSet.delete(depKey);
1175
+ } else {
1176
+ newSet.add(depKey);
1177
+ }
1178
+ return newSet;
1179
+ });
1180
+ }}
1181
+ style={{
1182
+ display: 'flex',
1183
+ alignItems: 'center',
1184
+ gap: 'var(--gap-md)',
1185
+ cursor: 'pointer',
1186
+ marginBottom: isExpanded ? 'var(--space-component-md)' : 0,
1187
+ }}
1188
+ >
1189
+ <div style={{ fontSize: '14px', opacity: 0.5 }}>
1190
+ {isExpanded ? '▼' : '►'}
1191
+ </div>
1192
+
1193
+ <div
1194
+ style={{
1195
+ padding: '6px 16px',
1196
+ borderRadius: 'var(--radius-md)',
1197
+ background: fromColor,
1198
+ color: 'white',
1199
+ fontSize: 'var(--font-size-body-sm)',
1200
+ fontWeight: 'var(--font-weight-bold)',
1201
+ }}
1202
+ >
1203
+ {dep.from}
1204
+ </div>
1205
+
1206
+ <div style={{ fontSize: 'var(--font-size-body-lg)', color: 'var(--color-text-tertiary)' }}>
1207
+
1208
+ </div>
1209
+
1210
+ <div
1211
+ style={{
1212
+ padding: '6px 16px',
1213
+ borderRadius: 'var(--radius-md)',
1214
+ background: toColor,
1215
+ color: 'white',
1216
+ fontSize: 'var(--font-size-body-sm)',
1217
+ fontWeight: 'var(--font-weight-bold)',
1218
+ }}
1219
+ >
1220
+ {dep.to}
1221
+ </div>
1222
+
1223
+ <div style={{
1224
+ fontSize: 'var(--font-size-body-xs)',
1225
+ color: 'var(--color-text-tertiary)',
1226
+ marginLeft: 'auto',
1227
+ }}>
1228
+ {dep.operations.size} operation{dep.operations.size !== 1 ? 's' : ''}
1229
+ </div>
1230
+ </div>
1231
+
1232
+ {/* Operation-level drill-down */}
1233
+ {isExpanded && (
1234
+ <div style={{
1235
+ display: 'grid',
1236
+ gap: 'var(--gap-sm)',
1237
+ paddingLeft: '30px',
1238
+ }}>
1239
+ {Array.from(dep.operations.values()).map(opMetrics => {
1240
+ const errorRate = opMetrics.totalCalls > 0 ? (opMetrics.errorCalls / opMetrics.totalCalls) * 100 : 0;
1241
+
1242
+ return (
1243
+ <div
1244
+ key={opMetrics.operation}
1245
+ style={{
1246
+ padding: 'var(--space-component-sm)',
1247
+ background: 'var(--color-bg-surface)',
1248
+ borderRadius: 'var(--radius-md)',
1249
+ border: '1px solid var(--color-border-subtle)',
1250
+ }}
1251
+ >
1252
+ <div style={{
1253
+ fontSize: 'var(--font-size-body-xs)',
1254
+ fontFamily: 'var(--font-family-mono)',
1255
+ color: 'var(--color-text-secondary)',
1256
+ marginBottom: 'var(--gap-xs)',
1257
+ }}>
1258
+ {opMetrics.operation}
1259
+ </div>
1260
+
1261
+ <div style={{
1262
+ display: 'flex',
1263
+ gap: 'var(--gap-lg)',
1264
+ fontSize: 'var(--font-size-body-xs)',
1265
+ }}>
1266
+ <div>
1267
+ <span style={{ color: 'var(--color-text-tertiary)' }}>Calls: </span>
1268
+ <span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
1269
+ {opMetrics.totalCalls}
1270
+ </span>
1271
+ </div>
1272
+
1273
+ <div>
1274
+ <span style={{ color: 'var(--color-text-tertiary)' }}>Errors: </span>
1275
+ <span style={{ color: errorRate > 0 ? '#ef4444' : '#10b981', fontWeight: 'var(--font-weight-medium)' }}>
1276
+ {errorRate.toFixed(1)}%
1277
+ </span>
1278
+ </div>
1279
+
1280
+ <div>
1281
+ <span style={{ color: 'var(--color-text-tertiary)' }}>Avg: </span>
1282
+ <span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
1283
+ {opMetrics.avgDuration.toFixed(2)}ms
1284
+ </span>
1285
+ </div>
1286
+
1287
+ <div>
1288
+ <span style={{ color: 'var(--color-text-tertiary)' }}>P95: </span>
1289
+ <span style={{ color: 'var(--color-text-primary)', fontWeight: 'var(--font-weight-medium)' }}>
1290
+ {opMetrics.p95Duration.toFixed(2)}ms
1291
+ </span>
1292
+ </div>
1293
+ </div>
1294
+ </div>
1295
+ );
1296
+ })}
1297
+ </div>
1298
+ )}
1299
+ </div>
1300
+ );
1301
+ })}
1302
+ </div>
1303
+ )}
1304
+ </div>
1305
+ )}
1306
+
1307
+ {/* Configuration View */}
1308
+ {viewMode === 'configuration' && (
1309
+ <div>
1310
+ <TracingSettings />
1311
+ </div>
1312
+ )}
1313
+
1314
+ {/* Guide View */}
1315
+ {viewMode === 'guide' && (
1316
+ <div style={{
1317
+ display: 'flex',
1318
+ flexDirection: 'column',
1319
+ gap: 'var(--gap-lg)',
1320
+ padding: 'var(--space-component-md)',
1321
+ }}>
1322
+ <div>
1323
+ <h3 style={{
1324
+ fontSize: 'var(--font-size-heading-sm)',
1325
+ fontWeight: 'var(--font-weight-bold)',
1326
+ color: 'var(--color-text-primary)',
1327
+ marginBottom: 'var(--gap-md)',
1328
+ }}>
1329
+ 🚀 Quick Start Guide
1330
+ </h3>
1331
+ <div style={{
1332
+ display: 'grid',
1333
+ gap: 'var(--gap-md)',
1334
+ color: 'var(--color-text-secondary)',
1335
+ fontSize: 'var(--font-size-body-sm)',
1336
+ }}>
1337
+ <p>
1338
+ The Trace Viewer provides comprehensive observability for your Flock applications using OpenTelemetry distributed tracing.
1339
+ </p>
1340
+ </div>
1341
+ </div>
1342
+
1343
+ <div>
1344
+ <h4 style={{
1345
+ fontSize: 'var(--font-size-body-md)',
1346
+ fontWeight: 'var(--font-weight-bold)',
1347
+ color: 'var(--color-text-primary)',
1348
+ marginBottom: 'var(--gap-sm)',
1349
+ }}>
1350
+ 📊 View Modes
1351
+ </h4>
1352
+ <div style={{
1353
+ display: 'grid',
1354
+ gap: 'var(--gap-sm)',
1355
+ }}>
1356
+ {[
1357
+ { icon: '📅', name: 'Timeline', desc: 'Waterfall view of spans showing execution flow and dependencies' },
1358
+ { icon: '📈', name: 'Statistics', desc: 'Aggregated metrics including duration, error rates, and call counts' },
1359
+ { icon: '🔴', name: 'RED Metrics', desc: 'Rate, Errors, Duration metrics for service health monitoring' },
1360
+ { icon: '🔗', name: 'Dependencies', desc: 'Service-to-service communication patterns and operation drill-down' },
1361
+ { icon: '🗄️', name: 'DuckDB SQL', desc: 'Write custom SQL queries against the trace database for advanced analysis' },
1362
+ { icon: '⚙️', name: 'Configuration', desc: 'Configure tracing settings, service filters, and operation blacklists' },
1363
+ ].map((mode) => (
1364
+ <div
1365
+ key={mode.name}
1366
+ style={{
1367
+ padding: 'var(--space-component-sm)',
1368
+ background: 'var(--color-bg-surface)',
1369
+ borderRadius: 'var(--radius-md)',
1370
+ border: '1px solid var(--color-border-subtle)',
1371
+ }}
1372
+ >
1373
+ <div style={{ fontWeight: 'var(--font-weight-medium)', color: 'var(--color-text-primary)', marginBottom: '4px' }}>
1374
+ {mode.icon} {mode.name}
1375
+ </div>
1376
+ <div style={{ fontSize: 'var(--font-size-body-xs)', color: 'var(--color-text-tertiary)' }}>
1377
+ {mode.desc}
1378
+ </div>
1379
+ </div>
1380
+ ))}
1381
+ </div>
1382
+ </div>
1383
+
1384
+ <div>
1385
+ <h4 style={{
1386
+ fontSize: 'var(--font-size-body-md)',
1387
+ fontWeight: 'var(--font-weight-bold)',
1388
+ color: 'var(--color-text-primary)',
1389
+ marginBottom: 'var(--gap-sm)',
1390
+ }}>
1391
+ 🔍 Search Tips
1392
+ </h4>
1393
+ <div style={{
1394
+ padding: 'var(--space-component-sm)',
1395
+ background: 'var(--color-bg-surface)',
1396
+ borderRadius: 'var(--radius-md)',
1397
+ border: '1px solid var(--color-border-subtle)',
1398
+ fontSize: 'var(--font-size-body-xs)',
1399
+ color: 'var(--color-text-secondary)',
1400
+ }}>
1401
+ <p style={{ marginBottom: 'var(--gap-xs)' }}>
1402
+ The search box performs text matching across:
1403
+ </p>
1404
+ <ul style={{ marginLeft: '20px', marginBottom: 'var(--gap-xs)' }}>
1405
+ <li>Trace IDs</li>
1406
+ <li>Span names and operation names</li>
1407
+ <li>Span attributes (key-value pairs)</li>
1408
+ </ul>
1409
+ <p style={{ fontStyle: 'italic', color: 'var(--color-text-tertiary)' }}>
1410
+ For advanced queries, use the DuckDB SQL tab to write custom queries.
1411
+ </p>
1412
+ </div>
1413
+ </div>
1414
+
1415
+ <div>
1416
+ <h4 style={{
1417
+ fontSize: 'var(--font-size-body-md)',
1418
+ fontWeight: 'var(--font-weight-bold)',
1419
+ color: 'var(--color-text-primary)',
1420
+ marginBottom: 'var(--gap-sm)',
1421
+ }}>
1422
+ 💡 DuckDB SQL Examples
1423
+ </h4>
1424
+ <div style={{
1425
+ display: 'grid',
1426
+ gap: 'var(--gap-sm)',
1427
+ }}>
1428
+ {[
1429
+ { title: 'Find slow operations', query: 'SELECT service_name, name, duration_ms FROM spans WHERE duration_ms > 1000 ORDER BY duration_ms DESC LIMIT 10' },
1430
+ { title: 'Error rate by service', query: 'SELECT service_name, COUNT(*) as total, SUM(CASE WHEN status_code = \'ERROR\' THEN 1 ELSE 0 END) as errors FROM spans GROUP BY service_name' },
1431
+ { title: 'Recent traces', query: 'SELECT DISTINCT trace_id, MIN(timestamp) as start_time FROM spans GROUP BY trace_id ORDER BY start_time DESC LIMIT 20' },
1432
+ { title: 'Operation hotspots', query: 'SELECT name, COUNT(*) as call_count, AVG(duration_ms) as avg_duration FROM spans GROUP BY name ORDER BY call_count DESC LIMIT 10' },
1433
+ ].map((example) => (
1434
+ <div
1435
+ key={example.title}
1436
+ style={{
1437
+ padding: 'var(--space-component-sm)',
1438
+ background: 'var(--color-bg-surface)',
1439
+ borderRadius: 'var(--radius-md)',
1440
+ border: '1px solid var(--color-border-subtle)',
1441
+ }}
1442
+ >
1443
+ <div style={{
1444
+ fontWeight: 'var(--font-weight-medium)',
1445
+ color: 'var(--color-text-primary)',
1446
+ marginBottom: '4px',
1447
+ fontSize: 'var(--font-size-body-xs)',
1448
+ }}>
1449
+ {example.title}
1450
+ </div>
1451
+ <code style={{
1452
+ display: 'block',
1453
+ padding: '8px',
1454
+ background: 'var(--color-bg-elevated)',
1455
+ borderRadius: 'var(--radius-sm)',
1456
+ fontSize: 'var(--font-size-body-xs)',
1457
+ fontFamily: 'var(--font-family-mono)',
1458
+ color: 'var(--color-text-secondary)',
1459
+ overflowX: 'auto',
1460
+ whiteSpace: 'pre-wrap',
1461
+ wordBreak: 'break-all',
1462
+ }}>
1463
+ {example.query}
1464
+ </code>
1465
+ </div>
1466
+ ))}
1467
+ </div>
1468
+ </div>
1469
+
1470
+ <div>
1471
+ <h4 style={{
1472
+ fontSize: 'var(--font-size-body-md)',
1473
+ fontWeight: 'var(--font-weight-bold)',
1474
+ color: 'var(--color-text-primary)',
1475
+ marginBottom: 'var(--gap-sm)',
1476
+ }}>
1477
+ ⚡ Best Practices
1478
+ </h4>
1479
+ <div style={{
1480
+ display: 'grid',
1481
+ gap: 'var(--gap-xs)',
1482
+ fontSize: 'var(--font-size-body-xs)',
1483
+ color: 'var(--color-text-secondary)',
1484
+ }}>
1485
+ {[
1486
+ 'Use service filters to focus on specific components',
1487
+ 'Blacklist noisy operations to reduce clutter',
1488
+ 'Check RED metrics for quick health overview',
1489
+ 'Use Dependencies view to understand service communication',
1490
+ 'Write SQL queries for custom analysis and reporting',
1491
+ 'Monitor error traces to identify failure patterns',
1492
+ ].map((tip, idx) => (
1493
+ <div
1494
+ key={idx}
1495
+ style={{
1496
+ padding: '8px 12px',
1497
+ background: 'var(--color-bg-surface)',
1498
+ borderRadius: 'var(--radius-sm)',
1499
+ border: '1px solid var(--color-border-subtle)',
1500
+ }}
1501
+ >
1502
+ ✓ {tip}
1503
+ </div>
1504
+ ))}
1505
+ </div>
1506
+ </div>
1507
+
1508
+ <div style={{
1509
+ padding: 'var(--space-component-sm)',
1510
+ background: 'rgba(59, 130, 246, 0.1)',
1511
+ border: '1px solid #3b82f6',
1512
+ borderRadius: 'var(--radius-md)',
1513
+ fontSize: 'var(--font-size-body-xs)',
1514
+ color: 'var(--color-text-secondary)',
1515
+ }}>
1516
+ <div style={{ fontWeight: 'var(--font-weight-bold)', color: '#3b82f6', marginBottom: '4px' }}>
1517
+ 📚 Full Documentation
1518
+ </div>
1519
+ For comprehensive tracing guides, see <code style={{ fontFamily: 'var(--font-family-mono)' }}>docs/how_to_use_tracing_effectively.md</code>
1520
+ </div>
1521
+ </div>
1522
+ )}
1523
+
1524
+ {/* SQL Query View */}
1525
+ {viewMode === 'sql' && (
1526
+ <div style={{
1527
+ display: 'flex',
1528
+ flexDirection: 'column',
1529
+ gap: 'var(--gap-sm)',
1530
+ height: '100%',
1531
+ }}>
1532
+ {/* Compact SQL Editor */}
1533
+ <div style={{
1534
+ display: 'flex',
1535
+ flexDirection: 'column',
1536
+ gap: 'var(--gap-xs)',
1537
+ flexShrink: 0,
1538
+ }}>
1539
+ <div style={{
1540
+ display: 'flex',
1541
+ justifyContent: 'space-between',
1542
+ alignItems: 'center',
1543
+ gap: 'var(--gap-sm)',
1544
+ }}>
1545
+ <div style={{
1546
+ display: 'flex',
1547
+ gap: 'var(--gap-xs)',
1548
+ flexWrap: 'wrap',
1549
+ flex: 1,
1550
+ alignItems: 'center',
1551
+ }}>
1552
+ <span style={{
1553
+ fontSize: 'var(--font-size-body-xs)',
1554
+ color: 'var(--color-text-tertiary)',
1555
+ flexShrink: 0,
1556
+ }}>
1557
+ Quick:
1558
+ </span>
1559
+ {[
1560
+ { label: 'All', query: 'SELECT * FROM spans LIMIT 10' },
1561
+ { label: 'By Service', query: 'SELECT service_name, COUNT(*) FROM spans GROUP BY service_name' },
1562
+ { label: 'Errors', query: 'SELECT * FROM spans WHERE status_code = \'ERROR\'' },
1563
+ { label: 'Avg Duration', query: 'SELECT service_name, AVG(duration_ms) FROM spans GROUP BY service_name' },
1564
+ ].map((example) => (
1565
+ <button
1566
+ key={example.label}
1567
+ onClick={() => setSqlQuery(example.query)}
1568
+ style={{
1569
+ padding: '4px 8px',
1570
+ background: 'var(--color-bg-elevated)',
1571
+ color: 'var(--color-text-secondary)',
1572
+ border: '1px solid var(--color-border-subtle)',
1573
+ borderRadius: 'var(--radius-sm)',
1574
+ fontSize: 'var(--font-size-body-xs)',
1575
+ cursor: 'pointer',
1576
+ whiteSpace: 'nowrap',
1577
+ }}
1578
+ >
1579
+ {example.label}
1580
+ </button>
1581
+ ))}
1582
+ </div>
1583
+ <button
1584
+ onClick={executeSqlQuery}
1585
+ disabled={sqlLoading}
1586
+ style={{
1587
+ padding: '6px 12px',
1588
+ background: sqlLoading ? 'var(--color-bg-surface)' : 'var(--color-accent)',
1589
+ color: sqlLoading ? 'var(--color-text-tertiary)' : 'white',
1590
+ border: 'none',
1591
+ borderRadius: 'var(--radius-md)',
1592
+ fontSize: 'var(--font-size-body-xs)',
1593
+ fontWeight: 'var(--font-weight-medium)',
1594
+ cursor: sqlLoading ? 'not-allowed' : 'pointer',
1595
+ display: 'flex',
1596
+ alignItems: 'center',
1597
+ gap: '6px',
1598
+ flexShrink: 0,
1599
+ }}
1600
+ >
1601
+ {sqlLoading ? (
1602
+ <>
1603
+ <span>⏳</span>
1604
+ <span>Running...</span>
1605
+ </>
1606
+ ) : (
1607
+ <>
1608
+ <span>▶️</span>
1609
+ <span>Run (Cmd+Enter)</span>
1610
+ </>
1611
+ )}
1612
+ </button>
1613
+ </div>
1614
+
1615
+ <textarea
1616
+ value={sqlQuery}
1617
+ onChange={(e) => setSqlQuery(e.target.value)}
1618
+ onKeyDown={(e) => {
1619
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
1620
+ e.preventDefault();
1621
+ executeSqlQuery();
1622
+ }
1623
+ }}
1624
+ placeholder="SELECT * FROM spans WHERE service_name = 'my-service' LIMIT 100"
1625
+ style={{
1626
+ width: '100%',
1627
+ minHeight: '60px',
1628
+ maxHeight: '120px',
1629
+ padding: '8px 12px',
1630
+ fontFamily: 'var(--font-family-mono)',
1631
+ fontSize: 'var(--font-size-body-xs)',
1632
+ background: 'var(--color-bg-surface)',
1633
+ color: 'var(--color-text-primary)',
1634
+ border: '1px solid var(--color-border-subtle)',
1635
+ borderRadius: 'var(--radius-md)',
1636
+ resize: 'vertical',
1637
+ lineHeight: '1.5',
1638
+ }}
1639
+ />
1640
+ </div>
1641
+
1642
+ {/* Error Display */}
1643
+ {sqlError && (
1644
+ <div style={{
1645
+ padding: '8px 12px',
1646
+ background: 'rgba(239, 68, 68, 0.1)',
1647
+ border: '1px solid #ef4444',
1648
+ borderRadius: 'var(--radius-md)',
1649
+ color: '#ef4444',
1650
+ fontSize: 'var(--font-size-body-xs)',
1651
+ fontFamily: 'var(--font-family-mono)',
1652
+ display: 'flex',
1653
+ gap: '8px',
1654
+ alignItems: 'center',
1655
+ flexShrink: 0,
1656
+ }}>
1657
+ <span>❌</span>
1658
+ <span>{sqlError}</span>
1659
+ </div>
1660
+ )}
1661
+
1662
+ {/* Results Table */}
1663
+ {sqlResults && sqlResults.length > 0 && (
1664
+ <div style={{
1665
+ flex: 1,
1666
+ display: 'flex',
1667
+ flexDirection: 'column',
1668
+ gap: 'var(--gap-xs)',
1669
+ minHeight: 0,
1670
+ }}>
1671
+ <div style={{
1672
+ display: 'flex',
1673
+ justifyContent: 'space-between',
1674
+ alignItems: 'center',
1675
+ }}>
1676
+ <div style={{
1677
+ fontSize: 'var(--font-size-body-sm)',
1678
+ fontWeight: 'var(--font-weight-medium)',
1679
+ color: 'var(--color-text-secondary)',
1680
+ }}>
1681
+ Results ({sqlResults.length} row{sqlResults.length !== 1 ? 's' : ''}, {sqlColumns.length} column{sqlColumns.length !== 1 ? 's' : ''})
1682
+ </div>
1683
+ <button
1684
+ onClick={() => {
1685
+ // Generate CSV
1686
+ const csv = [
1687
+ sqlColumns.join(','), // Header
1688
+ ...sqlResults.map(row =>
1689
+ sqlColumns.map(col => {
1690
+ const val = row[col];
1691
+ if (val === null || val === undefined) return '';
1692
+ const str = String(val);
1693
+ // Escape quotes and wrap in quotes if contains comma, quote, or newline
1694
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
1695
+ return `"${str.replace(/"/g, '""')}"`;
1696
+ }
1697
+ return str;
1698
+ }).join(',')
1699
+ )
1700
+ ].join('\n');
1701
+
1702
+ // Download CSV
1703
+ const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
1704
+ const link = document.createElement('a');
1705
+ const url = URL.createObjectURL(blob);
1706
+ link.setAttribute('href', url);
1707
+ link.setAttribute('download', `trace-query-${new Date().toISOString().split('T')[0]}.csv`);
1708
+ link.style.visibility = 'hidden';
1709
+ document.body.appendChild(link);
1710
+ link.click();
1711
+ document.body.removeChild(link);
1712
+ }}
1713
+ style={{
1714
+ padding: '4px 10px',
1715
+ background: 'var(--color-bg-elevated)',
1716
+ color: 'var(--color-text-secondary)',
1717
+ border: '1px solid var(--color-border-subtle)',
1718
+ borderRadius: 'var(--radius-sm)',
1719
+ fontSize: 'var(--font-size-body-xs)',
1720
+ fontWeight: 'var(--font-weight-medium)',
1721
+ cursor: 'pointer',
1722
+ display: 'flex',
1723
+ alignItems: 'center',
1724
+ gap: '4px',
1725
+ }}
1726
+ >
1727
+ <span>📥</span>
1728
+ <span>Export CSV</span>
1729
+ </button>
1730
+ </div>
1731
+
1732
+ <div style={{
1733
+ flex: 1,
1734
+ overflow: 'auto',
1735
+ background: 'var(--color-bg-surface)',
1736
+ border: '1px solid var(--color-border-subtle)',
1737
+ borderRadius: 'var(--radius-md)',
1738
+ }}>
1739
+ <table style={{
1740
+ width: '100%',
1741
+ borderCollapse: 'collapse',
1742
+ fontSize: 'var(--font-size-body-xs)',
1743
+ }}>
1744
+ <thead style={{
1745
+ position: 'sticky',
1746
+ top: 0,
1747
+ background: 'var(--color-bg-elevated)',
1748
+ borderBottom: '2px solid var(--color-border-subtle)',
1749
+ zIndex: 1,
1750
+ }}>
1751
+ <tr>
1752
+ {sqlColumns.map((col) => (
1753
+ <th
1754
+ key={col}
1755
+ style={{
1756
+ padding: '10px 12px',
1757
+ textAlign: 'left',
1758
+ fontWeight: 'var(--font-weight-bold)',
1759
+ color: 'var(--color-text-secondary)',
1760
+ whiteSpace: 'nowrap',
1761
+ borderRight: '1px solid var(--color-border-subtle)',
1762
+ }}
1763
+ >
1764
+ {col}
1765
+ </th>
1766
+ ))}
1767
+ </tr>
1768
+ </thead>
1769
+ <tbody>
1770
+ {sqlResults.map((row, idx) => (
1771
+ <tr
1772
+ key={idx}
1773
+ style={{
1774
+ borderBottom: '1px solid var(--color-border-subtle)',
1775
+ background: idx % 2 === 0 ? 'transparent' : 'rgba(255, 255, 255, 0.02)',
1776
+ }}
1777
+ >
1778
+ {sqlColumns.map((col) => (
1779
+ <td
1780
+ key={col}
1781
+ style={{
1782
+ padding: '8px 12px',
1783
+ color: 'var(--color-text-primary)',
1784
+ fontFamily: typeof row[col] === 'string' && row[col].length > 50 ? 'var(--font-family-mono)' : 'inherit',
1785
+ maxWidth: '400px',
1786
+ overflow: 'hidden',
1787
+ textOverflow: 'ellipsis',
1788
+ whiteSpace: 'nowrap',
1789
+ borderRight: '1px solid var(--color-border-subtle)',
1790
+ }}
1791
+ title={String(row[col])}
1792
+ >
1793
+ {row[col] === null || row[col] === undefined ? (
1794
+ <span style={{ color: 'var(--color-text-tertiary)', fontStyle: 'italic' }}>null</span>
1795
+ ) : (
1796
+ String(row[col])
1797
+ )}
1798
+ </td>
1799
+ ))}
1800
+ </tr>
1801
+ ))}
1802
+ </tbody>
1803
+ </table>
1804
+ </div>
1805
+ </div>
1806
+ )}
1807
+
1808
+ {/* Empty State */}
1809
+ {!sqlLoading && !sqlError && (!sqlResults || sqlResults.length === 0) && (
1810
+ <div style={{
1811
+ textAlign: 'center',
1812
+ padding: 'var(--space-component-xl)',
1813
+ color: 'var(--color-text-secondary)',
1814
+ }}>
1815
+ <div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)', opacity: 0.5 }}>📊</div>
1816
+ <div>Run a query to see results</div>
1817
+ <div style={{ fontSize: 'var(--font-size-body-xs)', marginTop: 'var(--gap-xs)', opacity: 0.7 }}>
1818
+ Try one of the example queries above
1819
+ </div>
1820
+ </div>
1821
+ )}
1822
+ </div>
1823
+ )}
1824
+
1825
+ {/* Traces View (Timeline & Statistics) */}
1826
+ {(viewMode === 'timeline' || viewMode === 'statistics') && (
1827
+ <>
1828
+ {filteredTraces.length === 0 ? (
1829
+ <div style={{
1830
+ textAlign: 'center',
1831
+ padding: 'var(--space-component-xl)',
1832
+ color: 'var(--color-text-secondary)',
1833
+ }}>
1834
+ <div style={{ fontSize: '32px', marginBottom: 'var(--gap-md)', opacity: 0.5 }}>🔎</div>
1835
+ <div>No traces found</div>
1836
+ </div>
1837
+ ) : (
1838
+ filteredTraces.map((trace) => (
1839
+ <div
1840
+ key={trace.traceId}
1841
+ style={{
1842
+ marginBottom: 'var(--space-component-lg)',
1843
+ background: 'var(--color-bg-elevated)',
1844
+ borderRadius: 'var(--radius-lg)',
1845
+ border: `1px solid ${trace.hasError ? '#ef4444' : 'var(--color-border-subtle)'}`,
1846
+ overflow: 'hidden',
1847
+ }}
1848
+ >
1849
+ <div
1850
+ onClick={() => {
1851
+ setSelectedTraceIds(prev => {
1852
+ const newSet = new Set(prev);
1853
+ if (newSet.has(trace.traceId)) {
1854
+ newSet.delete(trace.traceId);
1855
+ } else {
1856
+ newSet.add(trace.traceId);
1857
+ }
1858
+ return newSet;
1859
+ });
1860
+ }}
1861
+ style={{
1862
+ padding: 'var(--space-component-md)',
1863
+ background: trace.hasError ? 'rgba(239, 68, 68, 0.1)' : 'var(--color-bg-surface)',
1864
+ cursor: 'pointer',
1865
+ display: 'flex',
1866
+ flexDirection: 'column',
1867
+ gap: 'var(--gap-sm)',
1868
+ }}
1869
+ >
1870
+ {/* Top row: Status, Duration, Services */}
1871
+ <div style={{
1872
+ display: 'flex',
1873
+ alignItems: 'center',
1874
+ gap: 'var(--gap-md)',
1875
+ }}>
1876
+ <div style={{ fontSize: '14px', opacity: 0.5 }}>
1877
+ {selectedTraceIds.has(trace.traceId) ? '▼' : '►'}
1878
+ </div>
1879
+
1880
+ {/* Status indicator */}
1881
+ <div style={{
1882
+ display: 'flex',
1883
+ alignItems: 'center',
1884
+ gap: '6px',
1885
+ fontSize: 'var(--font-size-body-sm)',
1886
+ fontWeight: 'var(--font-weight-medium)',
1887
+ color: trace.hasError ? '#ef4444' : '#10b981',
1888
+ }}>
1889
+ <span>{trace.hasError ? '✗' : '✓'}</span>
1890
+ <span>{trace.hasError ? 'ERROR' : 'OK'}</span>
1891
+ </div>
1892
+
1893
+ {/* Duration */}
1894
+ <div style={{
1895
+ fontSize: 'var(--font-size-body-sm)',
1896
+ fontWeight: 'var(--font-weight-medium)',
1897
+ color: 'var(--color-text-primary)',
1898
+ }}>
1899
+ {trace.duration.toFixed(2)}ms
1900
+ </div>
1901
+
1902
+ {/* Service badges */}
1903
+ <div style={{
1904
+ display: 'flex',
1905
+ gap: '6px',
1906
+ flex: 1,
1907
+ flexWrap: 'wrap',
1908
+ }}>
1909
+ {Array.from(trace.services).map(service => {
1910
+ const serviceColor = serviceColors.colorMap.get(service) || '#6366f1';
1911
+ return (
1912
+ <div
1913
+ key={service}
1914
+ style={{
1915
+ padding: '4px 12px',
1916
+ borderRadius: 'var(--radius-sm)',
1917
+ background: serviceColor,
1918
+ color: 'white',
1919
+ fontSize: 'var(--font-size-body-xs)',
1920
+ fontWeight: 'var(--font-weight-medium)',
1921
+ whiteSpace: 'nowrap',
1922
+ }}
1923
+ >
1924
+ {service}
1925
+ </div>
1926
+ );
1927
+ })}
1928
+ </div>
1929
+ </div>
1930
+
1931
+ {/* Bottom row: Trace ID, Span count, Timestamp */}
1932
+ <div style={{
1933
+ display: 'flex',
1934
+ alignItems: 'center',
1935
+ gap: 'var(--gap-md)',
1936
+ paddingLeft: '30px',
1937
+ fontSize: 'var(--font-size-body-xs)',
1938
+ color: 'var(--color-text-tertiary)',
1939
+ }}>
1940
+ <div style={{
1941
+ fontFamily: 'var(--font-family-mono)',
1942
+ color: 'var(--color-text-secondary)',
1943
+ }}>
1944
+ {trace.traceId.slice(0, 16)}...
1945
+ </div>
1946
+ <span>•</span>
1947
+ <span>{trace.spanCount} spans</span>
1948
+ <span>•</span>
1949
+ <span>{trace.services.size} service{trace.services.size !== 1 ? 's' : ''}</span>
1950
+ <span>•</span>
1951
+ <span>{new Date(trace.startTime / 1_000_000).toLocaleTimeString()}</span>
1952
+ </div>
1953
+ </div>
1954
+
1955
+ {selectedTraceIds.has(trace.traceId) && (
1956
+ <div style={{ padding: 'var(--space-component-md)', paddingTop: 0 }}>
1957
+ {viewMode === 'timeline' && renderTimelineView(trace)}
1958
+ {viewMode === 'statistics' && renderStatisticsView(trace)}
1959
+ </div>
1960
+ )}
1961
+ </div>
1962
+ ))
1963
+ )}
1964
+ </>
1965
+ )}
1966
+ </div>
1967
+ </div>
1968
+ );
1969
+ };
1970
+
1971
+ export default TraceModuleJaeger;