@kopai/ui 0.8.0 → 0.9.0
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/index.cjs +2427 -1139
- package/dist/index.d.cts +36 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +36 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2376 -1082
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
- package/src/components/observability/LogTimeline/index.tsx +6 -2
- package/src/components/observability/MetricHistogram/index.tsx +20 -19
- package/src/components/observability/MetricTimeSeries/index.tsx +8 -9
- package/src/components/observability/ServiceList/shortcuts.ts +1 -1
- package/src/components/observability/TraceComparison/index.tsx +332 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
- package/src/components/observability/TraceDetail/index.tsx +4 -3
- package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
- package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
- package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
- package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
- package/src/components/observability/TraceSearch/index.tsx +211 -218
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
- package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
- package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
- package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
- package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
- package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
- package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
- package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
- package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
- package/src/components/observability/TraceTimeline/index.tsx +254 -110
- package/src/components/observability/index.ts +15 -0
- package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
- package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
- package/src/components/observability/utils/flatten-tree.ts +15 -0
- package/src/components/observability/utils/time.ts +9 -0
- package/src/hooks/use-kopai-data.test.ts +3 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- package/src/hooks/use-live-logs.test.ts +3 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
- package/src/lib/component-catalog.ts +15 -0
- package/src/pages/observability.test.tsx +5 -0
- package/src/pages/observability.tsx +314 -235
- package/src/providers/kopai-provider.tsx +3 -0
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useMemo, useState, useRef, useEffect, useCallback } from "react";
|
|
7
|
-
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
8
7
|
import type { denormalizedSignals } from "@kopai/core";
|
|
9
8
|
type OtelTracesRow = denormalizedSignals.OtelTracesRow;
|
|
10
9
|
import type { SpanNode, ParsedTrace } from "../types.js";
|
|
11
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
flattenTree,
|
|
12
|
+
getAllSpanIds,
|
|
13
|
+
spanMatchesSearch,
|
|
14
|
+
} from "../utils/flatten-tree.js";
|
|
12
15
|
import {
|
|
13
16
|
calculateRelativeTime,
|
|
14
17
|
calculateRelativeDuration,
|
|
@@ -16,17 +19,32 @@ import {
|
|
|
16
19
|
} from "../utils/time.js";
|
|
17
20
|
import { TraceHeader } from "./TraceHeader.js";
|
|
18
21
|
import { SpanRow } from "./SpanRow.js";
|
|
19
|
-
import {
|
|
22
|
+
import { SpanDetailInline } from "./SpanDetailInline.js";
|
|
20
23
|
import { LoadingSkeleton } from "../shared/LoadingSkeleton.js";
|
|
21
24
|
import { useRegisterShortcuts } from "../../KeyboardShortcuts/index.js";
|
|
22
25
|
import { TRACE_VIEWER_SHORTCUTS } from "./shortcuts.js";
|
|
26
|
+
import { TimeRuler } from "./TimeRuler.js";
|
|
27
|
+
import { SpanSearch } from "./SpanSearch.js";
|
|
28
|
+
import { ViewTabs, type ViewName } from "./ViewTabs.js";
|
|
29
|
+
import { GraphView } from "./GraphView.js";
|
|
30
|
+
import { StatisticsView } from "./StatisticsView.js";
|
|
31
|
+
import { FlamegraphView } from "./FlamegraphView.js";
|
|
32
|
+
import { Minimap } from "./Minimap.js";
|
|
23
33
|
|
|
24
34
|
export interface TraceTimelineProps {
|
|
25
35
|
rows: OtelTracesRow[];
|
|
26
36
|
onSpanClick?: (span: SpanNode) => void;
|
|
37
|
+
onSpanDeselect?: () => void;
|
|
27
38
|
selectedSpanId?: string;
|
|
28
39
|
isLoading?: boolean;
|
|
29
40
|
error?: Error;
|
|
41
|
+
view?: ViewName;
|
|
42
|
+
onViewChange?: (view: ViewName) => void;
|
|
43
|
+
uiFind?: string;
|
|
44
|
+
onUiFindChange?: (value: string) => void;
|
|
45
|
+
viewStart?: number;
|
|
46
|
+
viewEnd?: number;
|
|
47
|
+
onViewRangeChange?: (viewStart: number, viewEnd: number) => void;
|
|
30
48
|
}
|
|
31
49
|
|
|
32
50
|
/** Transform OtelTracesRow[] to ParsedTrace */
|
|
@@ -150,12 +168,30 @@ function isSpanAncestorOf(
|
|
|
150
168
|
return false;
|
|
151
169
|
}
|
|
152
170
|
|
|
171
|
+
function collectServices(rootSpans: SpanNode[]): string[] {
|
|
172
|
+
const set = new Set<string>();
|
|
173
|
+
function walk(span: SpanNode) {
|
|
174
|
+
set.add(span.serviceName);
|
|
175
|
+
span.children.forEach(walk);
|
|
176
|
+
}
|
|
177
|
+
rootSpans.forEach(walk);
|
|
178
|
+
return Array.from(set).sort();
|
|
179
|
+
}
|
|
180
|
+
|
|
153
181
|
export function TraceTimeline({
|
|
154
182
|
rows,
|
|
155
183
|
onSpanClick,
|
|
184
|
+
onSpanDeselect,
|
|
156
185
|
selectedSpanId: externalSelectedSpanId,
|
|
157
186
|
isLoading,
|
|
158
187
|
error,
|
|
188
|
+
view: externalView,
|
|
189
|
+
onViewChange,
|
|
190
|
+
uiFind: externalUiFind,
|
|
191
|
+
onUiFindChange,
|
|
192
|
+
viewStart: externalViewStart,
|
|
193
|
+
viewEnd: externalViewEnd,
|
|
194
|
+
onViewRangeChange,
|
|
159
195
|
}: TraceTimelineProps) {
|
|
160
196
|
useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
|
|
161
197
|
|
|
@@ -164,23 +200,39 @@ export function TraceTimeline({
|
|
|
164
200
|
string | null
|
|
165
201
|
>(null);
|
|
166
202
|
const [hoveredSpanId, setHoveredSpanId] = useState<string | null>(null);
|
|
203
|
+
const [internalView, setInternalView] = useState<ViewName>("timeline");
|
|
204
|
+
const [internalUiFind, setInternalUiFind] = useState("");
|
|
205
|
+
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
|
206
|
+
const [headerCollapsed, setHeaderCollapsed] = useState(false);
|
|
207
|
+
const [internalViewStart, setInternalViewStart] = useState(0);
|
|
208
|
+
const [internalViewEnd, setInternalViewEnd] = useState(1);
|
|
209
|
+
|
|
167
210
|
const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
|
|
211
|
+
const viewStart = externalViewStart ?? internalViewStart;
|
|
212
|
+
const viewEnd = externalViewEnd ?? internalViewEnd;
|
|
213
|
+
const activeView = externalView ?? internalView;
|
|
214
|
+
const uiFind = externalUiFind ?? internalUiFind;
|
|
168
215
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
169
216
|
const announcementRef = useRef<HTMLDivElement>(null);
|
|
170
217
|
|
|
171
218
|
const parsedTrace = useMemo(() => buildTrace(rows), [rows]);
|
|
172
219
|
|
|
220
|
+
const services = useMemo(
|
|
221
|
+
() => (parsedTrace ? collectServices(parsedTrace.rootSpans) : []),
|
|
222
|
+
[parsedTrace]
|
|
223
|
+
);
|
|
224
|
+
|
|
173
225
|
const flattenedSpans = useMemo(() => {
|
|
174
226
|
if (!parsedTrace) return [];
|
|
175
227
|
return flattenTree(parsedTrace.rootSpans, collapsedIds);
|
|
176
228
|
}, [parsedTrace, collapsedIds]);
|
|
177
229
|
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
});
|
|
230
|
+
const matchingIndices = useMemo(() => {
|
|
231
|
+
if (!uiFind) return [];
|
|
232
|
+
return flattenedSpans
|
|
233
|
+
.map((item, idx) => (spanMatchesSearch(item.span, uiFind) ? idx : -1))
|
|
234
|
+
.filter((idx) => idx !== -1);
|
|
235
|
+
}, [flattenedSpans, uiFind]);
|
|
184
236
|
|
|
185
237
|
const handleToggleCollapse = (spanId: string) => {
|
|
186
238
|
setCollapsedIds((prev) => {
|
|
@@ -191,15 +243,25 @@ export function TraceTimeline({
|
|
|
191
243
|
});
|
|
192
244
|
};
|
|
193
245
|
|
|
246
|
+
const handleDeselect = useCallback(() => {
|
|
247
|
+
setInternalSelectedSpanId(null);
|
|
248
|
+
onSpanDeselect?.();
|
|
249
|
+
}, [onSpanDeselect]);
|
|
250
|
+
|
|
194
251
|
const handleSpanClick = useCallback(
|
|
195
252
|
(span: SpanNode) => {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
253
|
+
const isAlreadySelected = selectedSpanId === span.spanId;
|
|
254
|
+
if (isAlreadySelected) {
|
|
255
|
+
handleDeselect();
|
|
256
|
+
} else {
|
|
257
|
+
setInternalSelectedSpanId(span.spanId);
|
|
258
|
+
onSpanClick?.(span);
|
|
259
|
+
if (announcementRef.current) {
|
|
260
|
+
announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
|
|
261
|
+
}
|
|
200
262
|
}
|
|
201
263
|
},
|
|
202
|
-
[onSpanClick]
|
|
264
|
+
[onSpanClick, selectedSpanId, handleDeselect]
|
|
203
265
|
);
|
|
204
266
|
|
|
205
267
|
const handleExpandAll = useCallback(() => {
|
|
@@ -259,25 +321,94 @@ export function TraceTimeline({
|
|
|
259
321
|
[selectedSpanId, flattenedSpans]
|
|
260
322
|
);
|
|
261
323
|
|
|
262
|
-
const
|
|
263
|
-
|
|
324
|
+
const handleViewChange = useCallback(
|
|
325
|
+
(view: ViewName) => {
|
|
326
|
+
if (onViewChange) onViewChange(view);
|
|
327
|
+
else setInternalView(view);
|
|
328
|
+
},
|
|
329
|
+
[onViewChange]
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
const handleUiFindChange = useCallback(
|
|
333
|
+
(value: string) => {
|
|
334
|
+
if (onUiFindChange) onUiFindChange(value);
|
|
335
|
+
else setInternalUiFind(value);
|
|
336
|
+
setCurrentMatchIndex(0);
|
|
337
|
+
},
|
|
338
|
+
[onUiFindChange]
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
const handleViewRangeChange = useCallback(
|
|
342
|
+
(start: number, end: number) => {
|
|
343
|
+
if (onViewRangeChange) onViewRangeChange(start, end);
|
|
344
|
+
else {
|
|
345
|
+
setInternalViewStart(start);
|
|
346
|
+
setInternalViewEnd(end);
|
|
347
|
+
}
|
|
348
|
+
},
|
|
349
|
+
[onViewRangeChange]
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
const scrollToSpan = useCallback((spanId: string) => {
|
|
353
|
+
const el = scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`);
|
|
354
|
+
el?.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
264
355
|
}, []);
|
|
265
356
|
|
|
357
|
+
const handleSearchNext = useCallback(() => {
|
|
358
|
+
if (matchingIndices.length === 0) return;
|
|
359
|
+
const next = (currentMatchIndex + 1) % matchingIndices.length;
|
|
360
|
+
setCurrentMatchIndex(next);
|
|
361
|
+
const idx = matchingIndices[next];
|
|
362
|
+
if (idx !== undefined) {
|
|
363
|
+
const item = flattenedSpans[idx];
|
|
364
|
+
if (item) {
|
|
365
|
+
handleSpanClick(item.span);
|
|
366
|
+
scrollToSpan(item.span.spanId);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}, [
|
|
370
|
+
matchingIndices,
|
|
371
|
+
currentMatchIndex,
|
|
372
|
+
flattenedSpans,
|
|
373
|
+
handleSpanClick,
|
|
374
|
+
scrollToSpan,
|
|
375
|
+
]);
|
|
376
|
+
|
|
377
|
+
const handleSearchPrev = useCallback(() => {
|
|
378
|
+
if (matchingIndices.length === 0) return;
|
|
379
|
+
const prev =
|
|
380
|
+
(currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
|
|
381
|
+
setCurrentMatchIndex(prev);
|
|
382
|
+
const idx = matchingIndices[prev];
|
|
383
|
+
if (idx !== undefined) {
|
|
384
|
+
const item = flattenedSpans[idx];
|
|
385
|
+
if (item) {
|
|
386
|
+
handleSpanClick(item.span);
|
|
387
|
+
scrollToSpan(item.span.spanId);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}, [
|
|
391
|
+
matchingIndices,
|
|
392
|
+
currentMatchIndex,
|
|
393
|
+
flattenedSpans,
|
|
394
|
+
handleSpanClick,
|
|
395
|
+
scrollToSpan,
|
|
396
|
+
]);
|
|
397
|
+
|
|
266
398
|
useEffect(() => {
|
|
267
399
|
if (!selectedSpanId) return;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
);
|
|
271
|
-
if (selectedIndex !== -1) {
|
|
272
|
-
virtualizer.scrollToIndex(selectedIndex, {
|
|
273
|
-
align: "center",
|
|
274
|
-
behavior: "smooth",
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
}, [selectedSpanId, flattenedSpans, virtualizer]);
|
|
400
|
+
scrollToSpan(selectedSpanId);
|
|
401
|
+
}, [selectedSpanId, scrollToSpan]);
|
|
278
402
|
|
|
279
403
|
useEffect(() => {
|
|
280
404
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
405
|
+
// Escape always deselects when a span is selected
|
|
406
|
+
if (e.key === "Escape" && selectedSpanId) {
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
handleDeselect();
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
281
412
|
const timelineElement = scrollRef.current?.parentElement;
|
|
282
413
|
if (!timelineElement?.contains(document.activeElement)) return;
|
|
283
414
|
|
|
@@ -303,8 +434,7 @@ export function TraceTimeline({
|
|
|
303
434
|
handleCollapseExpand(false);
|
|
304
435
|
break;
|
|
305
436
|
case "Escape":
|
|
306
|
-
|
|
307
|
-
handleDeselect();
|
|
437
|
+
// handled above, before focus check
|
|
308
438
|
break;
|
|
309
439
|
case "Enter": {
|
|
310
440
|
if (selectedSpanId) {
|
|
@@ -378,10 +508,6 @@ export function TraceTimeline({
|
|
|
378
508
|
}
|
|
379
509
|
|
|
380
510
|
const totalDurationMs = parsedTrace.maxTimeMs - parsedTrace.minTimeMs;
|
|
381
|
-
const selectedSpan =
|
|
382
|
-
selectedSpanId && flattenedSpans.length > 0
|
|
383
|
-
? flattenedSpans.find((item) => item.span.spanId === selectedSpanId)?.span
|
|
384
|
-
: null;
|
|
385
511
|
|
|
386
512
|
return (
|
|
387
513
|
<div className="flex h-full bg-background">
|
|
@@ -393,86 +519,104 @@ export function TraceTimeline({
|
|
|
393
519
|
aria-live="polite"
|
|
394
520
|
aria-atomic="true"
|
|
395
521
|
/>
|
|
396
|
-
<TraceHeader
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
{virtualizer.getVirtualItems().map((virtualItem) => {
|
|
412
|
-
const item = flattenedSpans[virtualItem.index];
|
|
413
|
-
if (!item) return null;
|
|
414
|
-
|
|
415
|
-
const { span, level } = item;
|
|
416
|
-
const isCollapsed = collapsedIds.has(span.spanId);
|
|
417
|
-
const isSelected = span.spanId === selectedSpanId;
|
|
418
|
-
const isHovered = span.spanId === hoveredSpanId;
|
|
419
|
-
const isParentOfHovered = hoveredSpanId
|
|
420
|
-
? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans)
|
|
421
|
-
: false;
|
|
422
|
-
|
|
423
|
-
const relativeStart = calculateRelativeTime(
|
|
424
|
-
span.startTimeUnixMs,
|
|
425
|
-
parsedTrace.minTimeMs,
|
|
426
|
-
parsedTrace.maxTimeMs
|
|
427
|
-
);
|
|
428
|
-
const relativeDuration = calculateRelativeDuration(
|
|
429
|
-
span.durationMs,
|
|
430
|
-
totalDurationMs
|
|
431
|
-
);
|
|
432
|
-
|
|
433
|
-
return (
|
|
434
|
-
<div
|
|
435
|
-
key={span.spanId}
|
|
436
|
-
style={{
|
|
437
|
-
position: "absolute",
|
|
438
|
-
top: 0,
|
|
439
|
-
left: 0,
|
|
440
|
-
width: "100%",
|
|
441
|
-
height: `${virtualItem.size}px`,
|
|
442
|
-
transform: `translateY(${virtualItem.start}px)`,
|
|
443
|
-
}}
|
|
444
|
-
>
|
|
445
|
-
<SpanRow
|
|
446
|
-
span={span}
|
|
447
|
-
level={level}
|
|
448
|
-
isCollapsed={isCollapsed}
|
|
449
|
-
isSelected={isSelected}
|
|
450
|
-
isHovered={isHovered}
|
|
451
|
-
isParentOfHovered={isParentOfHovered}
|
|
452
|
-
relativeStart={relativeStart}
|
|
453
|
-
relativeDuration={relativeDuration}
|
|
454
|
-
onClick={() => handleSpanClick(span)}
|
|
455
|
-
onToggleCollapse={() => handleToggleCollapse(span.spanId)}
|
|
456
|
-
onMouseEnter={() => setHoveredSpanId(span.spanId)}
|
|
457
|
-
onMouseLeave={() => setHoveredSpanId(null)}
|
|
458
|
-
/>
|
|
459
|
-
</div>
|
|
460
|
-
);
|
|
461
|
-
})}
|
|
462
|
-
</div>
|
|
463
|
-
</div>
|
|
464
|
-
</div>
|
|
522
|
+
<TraceHeader
|
|
523
|
+
trace={parsedTrace}
|
|
524
|
+
services={services}
|
|
525
|
+
onHeaderToggle={() => setHeaderCollapsed((p) => !p)}
|
|
526
|
+
isCollapsed={headerCollapsed}
|
|
527
|
+
/>
|
|
528
|
+
<ViewTabs activeView={activeView} onChange={handleViewChange} />
|
|
529
|
+
<SpanSearch
|
|
530
|
+
value={uiFind}
|
|
531
|
+
onChange={handleUiFindChange}
|
|
532
|
+
matchCount={matchingIndices.length}
|
|
533
|
+
currentMatch={currentMatchIndex}
|
|
534
|
+
onPrev={handleSearchPrev}
|
|
535
|
+
onNext={handleSearchNext}
|
|
536
|
+
/>
|
|
465
537
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
538
|
+
{activeView === "graph" ? (
|
|
539
|
+
<GraphView trace={parsedTrace} />
|
|
540
|
+
) : activeView === "statistics" ? (
|
|
541
|
+
<StatisticsView trace={parsedTrace} />
|
|
542
|
+
) : activeView === "flamegraph" ? (
|
|
543
|
+
<FlamegraphView
|
|
544
|
+
trace={parsedTrace}
|
|
545
|
+
onSpanClick={handleSpanClick}
|
|
546
|
+
selectedSpanId={selectedSpanId ?? undefined}
|
|
473
547
|
/>
|
|
474
|
-
|
|
475
|
-
|
|
548
|
+
) : (
|
|
549
|
+
<>
|
|
550
|
+
<Minimap
|
|
551
|
+
trace={parsedTrace}
|
|
552
|
+
viewStart={viewStart}
|
|
553
|
+
viewEnd={viewEnd}
|
|
554
|
+
onViewChange={handleViewRangeChange}
|
|
555
|
+
/>
|
|
556
|
+
<TimeRuler
|
|
557
|
+
totalDurationMs={totalDurationMs * (viewEnd - viewStart)}
|
|
558
|
+
leftColumnWidth="24rem"
|
|
559
|
+
offsetMs={totalDurationMs * viewStart}
|
|
560
|
+
/>
|
|
561
|
+
<div
|
|
562
|
+
ref={scrollRef}
|
|
563
|
+
className="flex-1 overflow-auto outline-none"
|
|
564
|
+
role="tree"
|
|
565
|
+
aria-label="Trace timeline"
|
|
566
|
+
tabIndex={0}
|
|
567
|
+
>
|
|
568
|
+
{flattenedSpans.map((item) => {
|
|
569
|
+
const { span, level } = item;
|
|
570
|
+
const isCollapsed = collapsedIds.has(span.spanId);
|
|
571
|
+
const isSelected = span.spanId === selectedSpanId;
|
|
572
|
+
const isHovered = span.spanId === hoveredSpanId;
|
|
573
|
+
const isParentOfHovered = hoveredSpanId
|
|
574
|
+
? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans)
|
|
575
|
+
: false;
|
|
576
|
+
|
|
577
|
+
const viewRange = viewEnd - viewStart;
|
|
578
|
+
const relativeStart =
|
|
579
|
+
(calculateRelativeTime(
|
|
580
|
+
span.startTimeUnixMs,
|
|
581
|
+
parsedTrace.minTimeMs,
|
|
582
|
+
parsedTrace.maxTimeMs
|
|
583
|
+
) -
|
|
584
|
+
viewStart) /
|
|
585
|
+
viewRange;
|
|
586
|
+
const relativeDuration =
|
|
587
|
+
calculateRelativeDuration(span.durationMs, totalDurationMs) /
|
|
588
|
+
viewRange;
|
|
589
|
+
|
|
590
|
+
return (
|
|
591
|
+
<div key={span.spanId} data-span-id={span.spanId}>
|
|
592
|
+
<SpanRow
|
|
593
|
+
span={span}
|
|
594
|
+
level={level}
|
|
595
|
+
isCollapsed={isCollapsed}
|
|
596
|
+
isSelected={isSelected}
|
|
597
|
+
isHovered={isHovered}
|
|
598
|
+
isParentOfHovered={isParentOfHovered}
|
|
599
|
+
relativeStart={relativeStart}
|
|
600
|
+
relativeDuration={relativeDuration}
|
|
601
|
+
onClick={() => handleSpanClick(span)}
|
|
602
|
+
onToggleCollapse={() => handleToggleCollapse(span.spanId)}
|
|
603
|
+
onMouseEnter={() => setHoveredSpanId(span.spanId)}
|
|
604
|
+
onMouseLeave={() => setHoveredSpanId(null)}
|
|
605
|
+
uiFind={uiFind || undefined}
|
|
606
|
+
/>
|
|
607
|
+
{isSelected && (
|
|
608
|
+
<SpanDetailInline
|
|
609
|
+
span={span}
|
|
610
|
+
traceStartMs={parsedTrace.minTimeMs}
|
|
611
|
+
/>
|
|
612
|
+
)}
|
|
613
|
+
</div>
|
|
614
|
+
);
|
|
615
|
+
})}
|
|
616
|
+
</div>
|
|
617
|
+
</>
|
|
618
|
+
)}
|
|
619
|
+
</div>
|
|
476
620
|
</div>
|
|
477
621
|
);
|
|
478
622
|
}
|
|
@@ -12,6 +12,18 @@ export type {
|
|
|
12
12
|
TraceSummary,
|
|
13
13
|
} from "./TraceSearch/index.js";
|
|
14
14
|
|
|
15
|
+
export { SearchForm } from "./TraceSearch/SearchForm.js";
|
|
16
|
+
export type { SearchFormProps } from "./TraceSearch/SearchForm.js";
|
|
17
|
+
|
|
18
|
+
export { ScatterPlot } from "./TraceSearch/ScatterPlot.js";
|
|
19
|
+
export type { ScatterPlotProps } from "./TraceSearch/ScatterPlot.js";
|
|
20
|
+
|
|
21
|
+
export { SortDropdown } from "./TraceSearch/SortDropdown.js";
|
|
22
|
+
export type { SortDropdownProps } from "./TraceSearch/SortDropdown.js";
|
|
23
|
+
|
|
24
|
+
export { DurationBar } from "./TraceSearch/DurationBar.js";
|
|
25
|
+
export type { DurationBarProps } from "./TraceSearch/DurationBar.js";
|
|
26
|
+
|
|
15
27
|
export { TraceDetail } from "./TraceDetail/index.js";
|
|
16
28
|
export type { TraceDetailProps } from "./TraceDetail/index.js";
|
|
17
29
|
|
|
@@ -54,6 +66,9 @@ export type {
|
|
|
54
66
|
export { DynamicDashboard } from "./DynamicDashboard/index.js";
|
|
55
67
|
export type { DynamicDashboardProps } from "./DynamicDashboard/index.js";
|
|
56
68
|
|
|
69
|
+
export { TraceComparison } from "./TraceComparison/index.js";
|
|
70
|
+
export type { TraceComparisonProps } from "./TraceComparison/index.js";
|
|
71
|
+
|
|
57
72
|
// Types
|
|
58
73
|
export type {
|
|
59
74
|
SpanNode,
|
|
@@ -18,16 +18,13 @@ export function OtelTraceDetail(props: Props) {
|
|
|
18
18
|
|
|
19
19
|
const response = props.data as { data?: OtelTracesRow[] } | null;
|
|
20
20
|
const rows = response?.data ?? [];
|
|
21
|
-
const
|
|
22
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
23
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
21
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
24
22
|
|
|
25
23
|
return (
|
|
26
24
|
<TraceDetail
|
|
27
25
|
rows={rows}
|
|
28
26
|
isLoading={props.loading}
|
|
29
27
|
error={props.error ?? undefined}
|
|
30
|
-
service={service}
|
|
31
28
|
traceId={traceId}
|
|
32
29
|
onBack={() => {}}
|
|
33
30
|
/>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { TooltipPayload } from "recharts";
|
|
2
|
+
|
|
3
|
+
export function TooltipEntryList({
|
|
4
|
+
payload,
|
|
5
|
+
displayLabelMap,
|
|
6
|
+
formatValue,
|
|
7
|
+
}: {
|
|
8
|
+
payload: TooltipPayload;
|
|
9
|
+
displayLabelMap: Map<string, string>;
|
|
10
|
+
formatValue: (v: number) => string;
|
|
11
|
+
}) {
|
|
12
|
+
return payload.map((entry, i) => {
|
|
13
|
+
const dataKey = entry.dataKey;
|
|
14
|
+
const value = entry.value;
|
|
15
|
+
if (typeof dataKey !== "string" || typeof value !== "number") return null;
|
|
16
|
+
return (
|
|
17
|
+
<p key={i} className="text-sm" style={{ color: entry.color }}>
|
|
18
|
+
<span className="font-medium">
|
|
19
|
+
{displayLabelMap.get(dataKey) ?? dataKey}:
|
|
20
|
+
</span>{" "}
|
|
21
|
+
{formatValue(value)}
|
|
22
|
+
</p>
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -40,6 +40,21 @@ export function getAllDescendantIds(span: SpanNode): string[] {
|
|
|
40
40
|
return ids;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** Flatten all spans (ignoring collapse state) with depth. */
|
|
44
|
+
export function flattenAllSpans(rootSpans: SpanNode[]): FlattenedSpan[] {
|
|
45
|
+
return flattenTree(rootSpans, new Set());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function spanMatchesSearch(span: SpanNode, query: string): boolean {
|
|
49
|
+
const q = query.toLowerCase();
|
|
50
|
+
if (span.name.toLowerCase().includes(q)) return true;
|
|
51
|
+
if (span.serviceName.toLowerCase().includes(q)) return true;
|
|
52
|
+
for (const val of Object.values(span.attributes)) {
|
|
53
|
+
if (String(val).toLowerCase().includes(q)) return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
43
58
|
export function getAllSpanIds(rootSpans: SpanNode[]): string[] {
|
|
44
59
|
const ids: string[] = [];
|
|
45
60
|
|
|
@@ -27,6 +27,15 @@ export function formatTimestamp(timestampMs: number): string {
|
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
export function formatRelativeTime(
|
|
31
|
+
eventTimeMs: number,
|
|
32
|
+
spanStartMs: number
|
|
33
|
+
): string {
|
|
34
|
+
const relativeMs = eventTimeMs - spanStartMs;
|
|
35
|
+
const prefix = relativeMs < 0 ? "-" : "+";
|
|
36
|
+
return `${prefix}${formatDuration(Math.abs(relativeMs))}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
30
39
|
export function calculateRelativeTime(
|
|
31
40
|
timeMs: number,
|
|
32
41
|
minTimeMs: number,
|
|
@@ -19,6 +19,9 @@ const createMockClient = () => ({
|
|
|
19
19
|
getTrace: vi.fn(),
|
|
20
20
|
discoverMetrics: vi.fn(),
|
|
21
21
|
getDashboard: vi.fn(),
|
|
22
|
+
getServices: vi.fn(),
|
|
23
|
+
getOperations: vi.fn(),
|
|
24
|
+
searchTraceSummariesPage: vi.fn(),
|
|
22
25
|
});
|
|
23
26
|
|
|
24
27
|
type MockClient = ReturnType<typeof createMockClient>;
|
|
@@ -34,6 +34,17 @@ function fetchForDataSource(
|
|
|
34
34
|
return client.getTrace(dataSource.params.traceId, { signal });
|
|
35
35
|
case "discoverMetrics":
|
|
36
36
|
return client.discoverMetrics({ signal });
|
|
37
|
+
case "getServices":
|
|
38
|
+
return client.getServices({ signal });
|
|
39
|
+
case "getOperations":
|
|
40
|
+
return client.getOperations(dataSource.params.serviceName, { signal });
|
|
41
|
+
case "searchTraceSummariesPage":
|
|
42
|
+
return client.searchTraceSummariesPage(
|
|
43
|
+
dataSource.params as Parameters<
|
|
44
|
+
typeof client.searchTraceSummariesPage
|
|
45
|
+
>[0],
|
|
46
|
+
{ signal }
|
|
47
|
+
);
|
|
37
48
|
default: {
|
|
38
49
|
const exhaustiveCheck: never = dataSource;
|
|
39
50
|
throw new Error(
|