@kopai/ui 0.8.0 → 0.10.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 +2704 -1288
- package/dist/index.d.cts +38 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +38 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2722 -1300
- 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 +8 -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/MetricStat/index.tsx +12 -4
- 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/OtelMetricStat.tsx +40 -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 +34 -0
- package/src/hooks/use-kopai-data.ts +23 -5
- package/src/hooks/use-live-logs.test.ts +4 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
- package/src/lib/component-catalog.ts +15 -0
- package/src/lib/renderer.test.tsx +2 -0
- package/src/pages/observability.test.tsx +8 -0
- package/src/pages/observability.tsx +397 -236
- package/src/providers/kopai-provider.tsx +4 -0
package/dist/index.mjs
CHANGED
|
@@ -2,12 +2,11 @@ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRe
|
|
|
2
2
|
import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
|
|
3
3
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
4
|
import { KopaiClient } from "@kopai/sdk";
|
|
5
|
-
import
|
|
5
|
+
import { z } from "zod";
|
|
6
6
|
import { dataFilterSchemas } from "@kopai/core";
|
|
7
|
-
import {
|
|
7
|
+
import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from "recharts";
|
|
8
8
|
import { createPortal } from "react-dom";
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
11
10
|
//#region src/providers/kopai-provider.tsx
|
|
12
11
|
const KopaiSDKContext = createContext(null);
|
|
13
12
|
const queryClient = new QueryClient({ defaultOptions: { queries: {
|
|
@@ -29,16 +28,25 @@ function useKopaiSDK() {
|
|
|
29
28
|
if (!ctx) throw new Error("useKopaiSDK must be used within KopaiSDKProvider");
|
|
30
29
|
return ctx.client;
|
|
31
30
|
}
|
|
32
|
-
|
|
33
31
|
//#endregion
|
|
34
32
|
//#region src/hooks/use-kopai-data.ts
|
|
35
33
|
function fetchForDataSource(client, dataSource, signal) {
|
|
36
34
|
switch (dataSource.method) {
|
|
37
35
|
case "searchTracesPage": return client.searchTracesPage(dataSource.params, { signal });
|
|
38
36
|
case "searchLogsPage": return client.searchLogsPage(dataSource.params, { signal });
|
|
39
|
-
case "searchMetricsPage":
|
|
37
|
+
case "searchMetricsPage": {
|
|
38
|
+
const params = dataSource.params;
|
|
39
|
+
if (params.aggregate) return client.searchAggregatedMetrics({
|
|
40
|
+
...params,
|
|
41
|
+
aggregate: params.aggregate
|
|
42
|
+
}, { signal });
|
|
43
|
+
return client.searchMetricsPage(params, { signal });
|
|
44
|
+
}
|
|
40
45
|
case "getTrace": return client.getTrace(dataSource.params.traceId, { signal });
|
|
41
46
|
case "discoverMetrics": return client.discoverMetrics({ signal });
|
|
47
|
+
case "getServices": return client.getServices({ signal });
|
|
48
|
+
case "getOperations": return client.getOperations(dataSource.params.serviceName, { signal });
|
|
49
|
+
case "searchTraceSummariesPage": return client.searchTraceSummariesPage(dataSource.params, { signal });
|
|
42
50
|
default: {
|
|
43
51
|
const exhaustiveCheck = dataSource;
|
|
44
52
|
throw new Error(`Unknown method: ${exhaustiveCheck.method}`);
|
|
@@ -64,7 +72,6 @@ function useKopaiData(dataSource) {
|
|
|
64
72
|
refetch
|
|
65
73
|
};
|
|
66
74
|
}
|
|
67
|
-
|
|
68
75
|
//#endregion
|
|
69
76
|
//#region src/lib/log-buffer.ts
|
|
70
77
|
function logKey(row) {
|
|
@@ -116,7 +123,6 @@ var LogBuffer = class {
|
|
|
116
123
|
this.keys.clear();
|
|
117
124
|
}
|
|
118
125
|
};
|
|
119
|
-
|
|
120
126
|
//#endregion
|
|
121
127
|
//#region src/hooks/use-live-logs.ts
|
|
122
128
|
function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = true }) {
|
|
@@ -171,7 +177,6 @@ function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = tr
|
|
|
171
177
|
setLive
|
|
172
178
|
};
|
|
173
179
|
}
|
|
174
|
-
|
|
175
180
|
//#endregion
|
|
176
181
|
//#region src/lib/component-catalog.ts
|
|
177
182
|
const dataSourceSchema = z.discriminatedUnion("method", [
|
|
@@ -199,6 +204,21 @@ const dataSourceSchema = z.discriminatedUnion("method", [
|
|
|
199
204
|
method: z.literal("discoverMetrics"),
|
|
200
205
|
params: z.object({}).optional(),
|
|
201
206
|
refetchIntervalMs: z.number().optional()
|
|
207
|
+
}),
|
|
208
|
+
z.object({
|
|
209
|
+
method: z.literal("getServices"),
|
|
210
|
+
params: z.object({}).optional(),
|
|
211
|
+
refetchIntervalMs: z.number().optional()
|
|
212
|
+
}),
|
|
213
|
+
z.object({
|
|
214
|
+
method: z.literal("getOperations"),
|
|
215
|
+
params: z.object({ serviceName: z.string() }),
|
|
216
|
+
refetchIntervalMs: z.number().optional()
|
|
217
|
+
}),
|
|
218
|
+
z.object({
|
|
219
|
+
method: z.literal("searchTraceSummariesPage"),
|
|
220
|
+
params: dataFilterSchemas.traceSummariesFilterSchema,
|
|
221
|
+
refetchIntervalMs: z.number().optional()
|
|
202
222
|
})
|
|
203
223
|
]);
|
|
204
224
|
const componentDefinitionSchema = z.object({
|
|
@@ -206,7 +226,7 @@ const componentDefinitionSchema = z.object({
|
|
|
206
226
|
description: z.string().describe("Component description to be displayed by the prompt generator"),
|
|
207
227
|
props: z.unknown()
|
|
208
228
|
}).describe("All options and properties necessary to render the React component with renderer");
|
|
209
|
-
|
|
229
|
+
z.object({
|
|
210
230
|
name: z.string().describe("catalog name"),
|
|
211
231
|
components: z.record(z.string().describe("React component name"), componentDefinitionSchema)
|
|
212
232
|
});
|
|
@@ -253,7 +273,6 @@ function createCatalog(catalogConfig) {
|
|
|
253
273
|
uiTreeSchema
|
|
254
274
|
};
|
|
255
275
|
}
|
|
256
|
-
|
|
257
276
|
//#endregion
|
|
258
277
|
//#region src/lib/observability-catalog.ts
|
|
259
278
|
const observabilityCatalog = createCatalog({
|
|
@@ -412,7 +431,6 @@ const observabilityCatalog = createCatalog({
|
|
|
412
431
|
}
|
|
413
432
|
}
|
|
414
433
|
});
|
|
415
|
-
|
|
416
434
|
//#endregion
|
|
417
435
|
//#region src/components/observability/TabBar/index.tsx
|
|
418
436
|
function renderLabel(label, shortcutKey) {
|
|
@@ -441,7 +459,6 @@ function TabBar({ tabs, active, onChange }) {
|
|
|
441
459
|
}, t.key))
|
|
442
460
|
});
|
|
443
461
|
}
|
|
444
|
-
|
|
445
462
|
//#endregion
|
|
446
463
|
//#region src/components/observability/utils/colors.ts
|
|
447
464
|
/**
|
|
@@ -461,38 +478,6 @@ function getSpanBarColor(serviceName, isError) {
|
|
|
461
478
|
if (isError) return ERROR_COLOR;
|
|
462
479
|
return getServiceColor(serviceName);
|
|
463
480
|
}
|
|
464
|
-
|
|
465
|
-
//#endregion
|
|
466
|
-
//#region src/components/observability/ServiceList/index.tsx
|
|
467
|
-
function ServiceList({ services, isLoading, error, onSelect }) {
|
|
468
|
-
if (isLoading) return /* @__PURE__ */ jsxs("div", {
|
|
469
|
-
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
470
|
-
children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading services..."]
|
|
471
|
-
});
|
|
472
|
-
if (error) return /* @__PURE__ */ jsxs("div", {
|
|
473
|
-
className: "text-red-400 py-4",
|
|
474
|
-
children: ["Error loading services: ", error.message]
|
|
475
|
-
});
|
|
476
|
-
if (services.length === 0) return /* @__PURE__ */ jsx("div", {
|
|
477
|
-
className: "text-muted-foreground py-8",
|
|
478
|
-
children: "No services found"
|
|
479
|
-
});
|
|
480
|
-
return /* @__PURE__ */ jsx("div", {
|
|
481
|
-
className: "space-y-1",
|
|
482
|
-
children: services.map((svc) => /* @__PURE__ */ jsx("button", {
|
|
483
|
-
onClick: () => onSelect(svc.name),
|
|
484
|
-
className: "w-full text-left px-4 py-3 rounded-lg border border-border hover:border-foreground/30 hover:bg-muted/50 transition-colors group",
|
|
485
|
-
children: /* @__PURE__ */ jsxs("span", {
|
|
486
|
-
className: "flex items-center gap-2 font-medium text-foreground",
|
|
487
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
488
|
-
className: "inline-block w-2.5 h-2.5 rounded-full shrink-0",
|
|
489
|
-
style: { backgroundColor: getServiceColor(svc.name) }
|
|
490
|
-
}), svc.name]
|
|
491
|
-
})
|
|
492
|
-
}, svc.name))
|
|
493
|
-
});
|
|
494
|
-
}
|
|
495
|
-
|
|
496
481
|
//#endregion
|
|
497
482
|
//#region src/components/observability/utils/time.ts
|
|
498
483
|
/**
|
|
@@ -514,6 +499,10 @@ function formatTimestamp$1(timestampMs) {
|
|
|
514
499
|
timeZoneName: "short"
|
|
515
500
|
});
|
|
516
501
|
}
|
|
502
|
+
function formatRelativeTime$1(eventTimeMs, spanStartMs) {
|
|
503
|
+
const relativeMs = eventTimeMs - spanStartMs;
|
|
504
|
+
return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
|
|
505
|
+
}
|
|
517
506
|
function calculateRelativeTime(timeMs, minTimeMs, maxTimeMs) {
|
|
518
507
|
const totalDuration = maxTimeMs - minTimeMs;
|
|
519
508
|
if (totalDuration === 0) return 0;
|
|
@@ -523,259 +512,556 @@ function calculateRelativeDuration(durationMs, totalDurationMs) {
|
|
|
523
512
|
if (totalDurationMs === 0) return 0;
|
|
524
513
|
return durationMs / totalDurationMs;
|
|
525
514
|
}
|
|
526
|
-
|
|
527
515
|
//#endregion
|
|
528
|
-
//#region src/components/observability/TraceSearch/
|
|
516
|
+
//#region src/components/observability/TraceSearch/SearchForm.tsx
|
|
517
|
+
/**
|
|
518
|
+
* SearchForm - Jaeger-style sidebar search form for trace filtering.
|
|
519
|
+
* Owns its own form state; parent only receives values on submit.
|
|
520
|
+
*/
|
|
529
521
|
const LOOKBACK_OPTIONS$1 = [
|
|
530
522
|
{
|
|
531
523
|
label: "Last 5 Minutes",
|
|
532
|
-
|
|
524
|
+
value: "5m"
|
|
533
525
|
},
|
|
534
526
|
{
|
|
535
527
|
label: "Last 15 Minutes",
|
|
536
|
-
|
|
528
|
+
value: "15m"
|
|
537
529
|
},
|
|
538
530
|
{
|
|
539
531
|
label: "Last 30 Minutes",
|
|
540
|
-
|
|
532
|
+
value: "30m"
|
|
541
533
|
},
|
|
542
534
|
{
|
|
543
535
|
label: "Last 1 Hour",
|
|
544
|
-
|
|
536
|
+
value: "1h"
|
|
545
537
|
},
|
|
546
538
|
{
|
|
547
539
|
label: "Last 2 Hours",
|
|
548
|
-
|
|
540
|
+
value: "2h"
|
|
549
541
|
},
|
|
550
542
|
{
|
|
551
543
|
label: "Last 6 Hours",
|
|
552
|
-
|
|
544
|
+
value: "6h"
|
|
553
545
|
},
|
|
554
546
|
{
|
|
555
547
|
label: "Last 12 Hours",
|
|
556
|
-
|
|
548
|
+
value: "12h"
|
|
557
549
|
},
|
|
558
550
|
{
|
|
559
551
|
label: "Last 24 Hours",
|
|
560
|
-
|
|
552
|
+
value: "24h"
|
|
561
553
|
}
|
|
562
554
|
];
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
const [
|
|
566
|
-
const [
|
|
567
|
-
const [
|
|
568
|
-
const [
|
|
569
|
-
const [
|
|
570
|
-
const
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
555
|
+
const inputClass = "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground";
|
|
556
|
+
function SearchForm({ services, operations, initialValues, onSubmit, isLoading }) {
|
|
557
|
+
const [service, setService] = useState(initialValues?.service ?? "");
|
|
558
|
+
const [operation, setOperation] = useState(initialValues?.operation ?? "");
|
|
559
|
+
const [tags, setTags] = useState(initialValues?.tags ?? "");
|
|
560
|
+
const [lookback, setLookback] = useState(initialValues?.lookback ?? "");
|
|
561
|
+
const [minDuration, setMinDuration] = useState(initialValues?.minDuration ?? "");
|
|
562
|
+
const [maxDuration, setMaxDuration] = useState(initialValues?.maxDuration ?? "");
|
|
563
|
+
const [limit, setLimit] = useState(initialValues?.limit ?? 20);
|
|
564
|
+
useEffect(() => {
|
|
565
|
+
if (initialValues?.service != null) setService(initialValues.service);
|
|
566
|
+
}, [initialValues?.service]);
|
|
567
|
+
const handleSubmit = () => {
|
|
568
|
+
onSubmit({
|
|
569
|
+
service,
|
|
570
|
+
operation,
|
|
571
|
+
tags,
|
|
572
|
+
lookback,
|
|
573
|
+
minDuration,
|
|
574
|
+
maxDuration,
|
|
576
575
|
limit
|
|
577
576
|
});
|
|
578
577
|
};
|
|
579
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
/* @__PURE__ */ jsx("span", {
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
children: filtersOpen ? "▲" : "▼"
|
|
578
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
579
|
+
className: "space-y-4",
|
|
580
|
+
children: [
|
|
581
|
+
/* @__PURE__ */ jsx("h3", {
|
|
582
|
+
className: "text-sm font-semibold text-foreground uppercase tracking-wider",
|
|
583
|
+
children: "Search"
|
|
584
|
+
}),
|
|
585
|
+
/* @__PURE__ */ jsxs("label", {
|
|
586
|
+
className: "block space-y-1",
|
|
587
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
588
|
+
className: "text-xs text-muted-foreground",
|
|
589
|
+
children: "Service"
|
|
590
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
591
|
+
value: service,
|
|
592
|
+
onChange: (e) => setService(e.target.value),
|
|
593
|
+
className: inputClass,
|
|
594
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
595
|
+
value: "",
|
|
596
|
+
children: "All Services"
|
|
597
|
+
}), services.map((s) => /* @__PURE__ */ jsx("option", {
|
|
598
|
+
value: s,
|
|
599
|
+
children: s
|
|
600
|
+
}, s))]
|
|
603
601
|
})]
|
|
604
|
-
}),
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
}), operations.map((op) => /* @__PURE__ */ jsx("option", {
|
|
622
|
-
value: op,
|
|
623
|
-
children: op
|
|
624
|
-
}, op))]
|
|
625
|
-
})]
|
|
626
|
-
}),
|
|
627
|
-
/* @__PURE__ */ jsxs("label", {
|
|
628
|
-
className: "space-y-1",
|
|
629
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
630
|
-
className: "text-xs text-muted-foreground",
|
|
631
|
-
children: "Lookback"
|
|
632
|
-
}), /* @__PURE__ */ jsxs("select", {
|
|
633
|
-
value: lookbackIdx,
|
|
634
|
-
onChange: (e) => setLookbackIdx(Number(e.target.value)),
|
|
635
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
636
|
-
children: [/* @__PURE__ */ jsx("option", {
|
|
637
|
-
value: -1,
|
|
638
|
-
children: "All time"
|
|
639
|
-
}), LOOKBACK_OPTIONS$1.map((opt, i) => /* @__PURE__ */ jsx("option", {
|
|
640
|
-
value: i,
|
|
641
|
-
children: opt.label
|
|
642
|
-
}, i))]
|
|
643
|
-
})]
|
|
644
|
-
}),
|
|
645
|
-
/* @__PURE__ */ jsxs("label", {
|
|
646
|
-
className: "space-y-1",
|
|
647
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
648
|
-
className: "text-xs text-muted-foreground",
|
|
649
|
-
children: "Limit"
|
|
650
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
651
|
-
type: "number",
|
|
652
|
-
min: 1,
|
|
653
|
-
max: 1e3,
|
|
654
|
-
value: limit,
|
|
655
|
-
onChange: (e) => {
|
|
656
|
-
const n = Number(e.target.value);
|
|
657
|
-
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
658
|
-
},
|
|
659
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
|
|
660
|
-
})]
|
|
661
|
-
}),
|
|
662
|
-
/* @__PURE__ */ jsxs("label", {
|
|
663
|
-
className: "space-y-1",
|
|
664
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
665
|
-
className: "text-xs text-muted-foreground",
|
|
666
|
-
children: "Min Duration"
|
|
667
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
668
|
-
type: "text",
|
|
669
|
-
placeholder: "e.g. 100ms",
|
|
670
|
-
value: minDuration,
|
|
671
|
-
onChange: (e) => setMinDuration(e.target.value),
|
|
672
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
673
|
-
})]
|
|
674
|
-
}),
|
|
675
|
-
/* @__PURE__ */ jsxs("label", {
|
|
676
|
-
className: "space-y-1",
|
|
677
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
678
|
-
className: "text-xs text-muted-foreground",
|
|
679
|
-
children: "Max Duration"
|
|
680
|
-
}), /* @__PURE__ */ jsx("input", {
|
|
681
|
-
type: "text",
|
|
682
|
-
placeholder: "e.g. 5s",
|
|
683
|
-
value: maxDuration,
|
|
684
|
-
onChange: (e) => setMaxDuration(e.target.value),
|
|
685
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
686
|
-
})]
|
|
687
|
-
})
|
|
688
|
-
]
|
|
689
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
690
|
-
onClick: handleFindTraces,
|
|
691
|
-
className: "px-4 py-1.5 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
692
|
-
children: "Find Traces"
|
|
602
|
+
}),
|
|
603
|
+
/* @__PURE__ */ jsxs("label", {
|
|
604
|
+
className: "block space-y-1",
|
|
605
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
606
|
+
className: "text-xs text-muted-foreground",
|
|
607
|
+
children: "Operation"
|
|
608
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
609
|
+
value: operation,
|
|
610
|
+
onChange: (e) => setOperation(e.target.value),
|
|
611
|
+
className: inputClass,
|
|
612
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
613
|
+
value: "",
|
|
614
|
+
children: "All Operations"
|
|
615
|
+
}), operations.map((op) => /* @__PURE__ */ jsx("option", {
|
|
616
|
+
value: op,
|
|
617
|
+
children: op
|
|
618
|
+
}, op))]
|
|
693
619
|
})]
|
|
694
|
-
})
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
620
|
+
}),
|
|
621
|
+
/* @__PURE__ */ jsxs("label", {
|
|
622
|
+
className: "block space-y-1",
|
|
623
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
624
|
+
className: "text-xs text-muted-foreground",
|
|
625
|
+
children: "Tags"
|
|
626
|
+
}), /* @__PURE__ */ jsx("textarea", {
|
|
627
|
+
value: tags,
|
|
628
|
+
onChange: (e) => setTags(e.target.value),
|
|
629
|
+
placeholder: "key=value key2=\"quoted value\"",
|
|
630
|
+
rows: 3,
|
|
631
|
+
className: `${inputClass} placeholder:text-muted-foreground/50 resize-y`
|
|
632
|
+
})]
|
|
633
|
+
}),
|
|
634
|
+
/* @__PURE__ */ jsxs("label", {
|
|
635
|
+
className: "block space-y-1",
|
|
636
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
637
|
+
className: "text-xs text-muted-foreground",
|
|
638
|
+
children: "Lookback"
|
|
639
|
+
}), /* @__PURE__ */ jsxs("select", {
|
|
640
|
+
value: lookback,
|
|
641
|
+
onChange: (e) => setLookback(e.target.value),
|
|
642
|
+
className: inputClass,
|
|
643
|
+
children: [/* @__PURE__ */ jsx("option", {
|
|
644
|
+
value: "",
|
|
645
|
+
children: "All time"
|
|
646
|
+
}), LOOKBACK_OPTIONS$1.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
647
|
+
value: opt.value,
|
|
648
|
+
children: opt.label
|
|
649
|
+
}, opt.value))]
|
|
650
|
+
})]
|
|
651
|
+
}),
|
|
652
|
+
/* @__PURE__ */ jsxs("div", {
|
|
653
|
+
className: "grid grid-cols-2 gap-2",
|
|
654
|
+
children: [/* @__PURE__ */ jsxs("label", {
|
|
655
|
+
className: "block space-y-1",
|
|
656
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
657
|
+
className: "text-xs text-muted-foreground",
|
|
658
|
+
children: "Min Duration"
|
|
659
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
660
|
+
type: "text",
|
|
661
|
+
placeholder: "e.g. 100ms",
|
|
662
|
+
value: minDuration,
|
|
663
|
+
onChange: (e) => setMinDuration(e.target.value),
|
|
664
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
665
|
+
})]
|
|
666
|
+
}), /* @__PURE__ */ jsxs("label", {
|
|
667
|
+
className: "block space-y-1",
|
|
668
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
669
|
+
className: "text-xs text-muted-foreground",
|
|
670
|
+
children: "Max Duration"
|
|
671
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
672
|
+
type: "text",
|
|
673
|
+
placeholder: "e.g. 5s",
|
|
674
|
+
value: maxDuration,
|
|
675
|
+
onChange: (e) => setMaxDuration(e.target.value),
|
|
676
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
677
|
+
})]
|
|
678
|
+
})]
|
|
679
|
+
}),
|
|
680
|
+
/* @__PURE__ */ jsxs("label", {
|
|
681
|
+
className: "block space-y-1",
|
|
682
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
683
|
+
className: "text-xs text-muted-foreground",
|
|
684
|
+
children: "Limit"
|
|
685
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
686
|
+
type: "number",
|
|
687
|
+
min: 1,
|
|
688
|
+
max: 1e3,
|
|
689
|
+
value: limit,
|
|
690
|
+
onChange: (e) => {
|
|
691
|
+
const n = Number(e.target.value);
|
|
692
|
+
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
693
|
+
},
|
|
694
|
+
className: inputClass
|
|
695
|
+
})]
|
|
696
|
+
}),
|
|
697
|
+
/* @__PURE__ */ jsx("button", {
|
|
698
|
+
onClick: handleSubmit,
|
|
699
|
+
disabled: isLoading,
|
|
700
|
+
className: "w-full px-4 py-2 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors disabled:opacity-50",
|
|
701
|
+
children: isLoading ? "Searching..." : "Find Traces"
|
|
702
|
+
})
|
|
703
|
+
]
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
//#endregion
|
|
707
|
+
//#region src/components/observability/TraceSearch/ScatterPlot.tsx
|
|
708
|
+
/**
|
|
709
|
+
* ScatterPlot - Scatter chart showing trace duration vs timestamp.
|
|
710
|
+
*/
|
|
711
|
+
function CustomTooltip$1({ active, payload }) {
|
|
712
|
+
if (!active || !payload?.[0]) return null;
|
|
713
|
+
const d = payload[0].payload;
|
|
714
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
715
|
+
className: "bg-background border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
716
|
+
children: [
|
|
717
|
+
/* @__PURE__ */ jsxs("div", {
|
|
718
|
+
className: "font-medium text-foreground",
|
|
713
719
|
children: [
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
720
|
+
d.serviceName,
|
|
721
|
+
": ",
|
|
722
|
+
d.rootSpanName
|
|
723
|
+
]
|
|
724
|
+
}),
|
|
725
|
+
/* @__PURE__ */ jsxs("div", {
|
|
726
|
+
className: "text-muted-foreground mt-1",
|
|
727
|
+
children: [
|
|
728
|
+
d.spanCount,
|
|
729
|
+
" span",
|
|
730
|
+
d.spanCount !== 1 ? "s" : "",
|
|
731
|
+
" ·",
|
|
732
|
+
" ",
|
|
733
|
+
formatDuration(d.y)
|
|
734
|
+
]
|
|
735
|
+
}),
|
|
736
|
+
/* @__PURE__ */ jsx("div", {
|
|
737
|
+
className: "text-muted-foreground",
|
|
738
|
+
children: formatTimestamp$1(d.x)
|
|
739
|
+
})
|
|
740
|
+
]
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function ScatterPlot({ traces, onSelectTrace }) {
|
|
744
|
+
const data = useMemo(() => traces.map((t) => ({
|
|
745
|
+
x: t.timestampMs,
|
|
746
|
+
y: t.durationMs,
|
|
747
|
+
traceId: t.traceId,
|
|
748
|
+
serviceName: t.serviceName,
|
|
749
|
+
rootSpanName: t.rootSpanName,
|
|
750
|
+
spanCount: t.spanCount,
|
|
751
|
+
hasError: t.errorCount > 0
|
|
752
|
+
})), [traces]);
|
|
753
|
+
const handleClick = useCallback((entry) => {
|
|
754
|
+
const payload = entry?.payload;
|
|
755
|
+
if (payload?.traceId) onSelectTrace(payload.traceId);
|
|
756
|
+
}, [onSelectTrace]);
|
|
757
|
+
if (traces.length === 0) return null;
|
|
758
|
+
return /* @__PURE__ */ jsx("div", {
|
|
759
|
+
className: "border border-border rounded-lg p-4 bg-background",
|
|
760
|
+
children: /* @__PURE__ */ jsx(ResponsiveContainer, {
|
|
761
|
+
width: "100%",
|
|
762
|
+
height: 200,
|
|
763
|
+
children: /* @__PURE__ */ jsxs(ScatterChart, {
|
|
764
|
+
margin: {
|
|
765
|
+
top: 8,
|
|
766
|
+
right: 8,
|
|
767
|
+
bottom: 4,
|
|
768
|
+
left: 0
|
|
769
|
+
},
|
|
770
|
+
children: [
|
|
771
|
+
/* @__PURE__ */ jsx(CartesianGrid, {
|
|
772
|
+
strokeDasharray: "3 3",
|
|
773
|
+
stroke: "hsl(var(--border))",
|
|
774
|
+
opacity: .4
|
|
733
775
|
}),
|
|
734
|
-
/* @__PURE__ */
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
t.errorCount,
|
|
749
|
-
" Error",
|
|
750
|
-
t.errorCount !== 1 ? "s" : ""
|
|
751
|
-
]
|
|
752
|
-
}),
|
|
753
|
-
t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
|
|
754
|
-
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
755
|
-
style: {
|
|
756
|
-
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
757
|
-
color: getServiceColor(svc.name)
|
|
758
|
-
},
|
|
759
|
-
children: [
|
|
760
|
-
svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
761
|
-
svc.name,
|
|
762
|
-
" (",
|
|
763
|
-
svc.count,
|
|
764
|
-
")"
|
|
765
|
-
]
|
|
766
|
-
}, svc.name))
|
|
767
|
-
]
|
|
776
|
+
/* @__PURE__ */ jsx(XAxis, {
|
|
777
|
+
dataKey: "x",
|
|
778
|
+
type: "number",
|
|
779
|
+
domain: ["dataMin", "dataMax"],
|
|
780
|
+
tickFormatter: (v) => {
|
|
781
|
+
const d = new Date(v);
|
|
782
|
+
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
783
|
+
},
|
|
784
|
+
tick: {
|
|
785
|
+
fontSize: 11,
|
|
786
|
+
fill: "hsl(var(--muted-foreground))"
|
|
787
|
+
},
|
|
788
|
+
stroke: "hsl(var(--border))",
|
|
789
|
+
name: "Time"
|
|
768
790
|
}),
|
|
769
|
-
/* @__PURE__ */ jsx(
|
|
770
|
-
|
|
771
|
-
|
|
791
|
+
/* @__PURE__ */ jsx(YAxis, {
|
|
792
|
+
dataKey: "y",
|
|
793
|
+
type: "number",
|
|
794
|
+
tickFormatter: (v) => formatDuration(v),
|
|
795
|
+
tick: {
|
|
796
|
+
fontSize: 11,
|
|
797
|
+
fill: "hsl(var(--muted-foreground))"
|
|
798
|
+
},
|
|
799
|
+
stroke: "hsl(var(--border))",
|
|
800
|
+
name: "Duration",
|
|
801
|
+
width: 70
|
|
802
|
+
}),
|
|
803
|
+
/* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip$1, {}) }),
|
|
804
|
+
/* @__PURE__ */ jsx(Scatter, {
|
|
805
|
+
data,
|
|
806
|
+
onClick: handleClick,
|
|
807
|
+
cursor: "pointer",
|
|
808
|
+
children: data.map((point, i) => /* @__PURE__ */ jsx(Cell, {
|
|
809
|
+
fill: point.hasError ? "#ef4444" : getServiceColor(point.serviceName),
|
|
810
|
+
stroke: point.hasError ? "#ef4444" : "none",
|
|
811
|
+
strokeWidth: point.hasError ? 2 : 0
|
|
812
|
+
}, i))
|
|
772
813
|
})
|
|
773
814
|
]
|
|
774
|
-
}
|
|
815
|
+
})
|
|
775
816
|
})
|
|
776
|
-
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
//#endregion
|
|
820
|
+
//#region src/components/observability/TraceSearch/SortDropdown.tsx
|
|
821
|
+
const SORT_OPTIONS = [
|
|
822
|
+
{
|
|
823
|
+
value: "recent",
|
|
824
|
+
label: "Most Recent"
|
|
825
|
+
},
|
|
826
|
+
{
|
|
827
|
+
value: "longest",
|
|
828
|
+
label: "Longest First"
|
|
829
|
+
},
|
|
830
|
+
{
|
|
831
|
+
value: "shortest",
|
|
832
|
+
label: "Shortest First"
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
value: "mostSpans",
|
|
836
|
+
label: "Most Spans"
|
|
837
|
+
},
|
|
838
|
+
{
|
|
839
|
+
value: "leastSpans",
|
|
840
|
+
label: "Least Spans"
|
|
841
|
+
}
|
|
842
|
+
];
|
|
843
|
+
function SortDropdown({ value, onChange }) {
|
|
844
|
+
return /* @__PURE__ */ jsx("select", {
|
|
845
|
+
value,
|
|
846
|
+
onChange: (e) => onChange(e.target.value),
|
|
847
|
+
className: "bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
848
|
+
children: SORT_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", {
|
|
849
|
+
value: opt.value,
|
|
850
|
+
children: opt.label
|
|
851
|
+
}, opt.value))
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
//#endregion
|
|
855
|
+
//#region src/components/observability/TraceSearch/DurationBar.tsx
|
|
856
|
+
/**
|
|
857
|
+
* DurationBar - Horizontal bar showing relative trace duration.
|
|
858
|
+
*/
|
|
859
|
+
function DurationBar({ durationMs, maxDurationMs, color }) {
|
|
860
|
+
const rawPct = maxDurationMs > 0 ? durationMs / maxDurationMs * 100 : 0;
|
|
861
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
862
|
+
className: "flex items-center gap-2",
|
|
863
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
864
|
+
className: "flex-1 h-2 bg-muted/30 rounded overflow-hidden",
|
|
865
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
866
|
+
className: "h-full rounded",
|
|
867
|
+
style: {
|
|
868
|
+
width: `${durationMs <= 0 ? 0 : Math.min(Math.max(rawPct, 1), 100)}%`,
|
|
869
|
+
backgroundColor: color,
|
|
870
|
+
opacity: .7
|
|
871
|
+
}
|
|
872
|
+
})
|
|
873
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
874
|
+
className: "text-xs text-foreground/80 shrink-0 w-16 text-right font-mono",
|
|
875
|
+
children: formatDuration(durationMs)
|
|
876
|
+
})]
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/components/observability/TraceSearch/index.tsx
|
|
881
|
+
function sortTraces(traces, sort) {
|
|
882
|
+
const sorted = [...traces];
|
|
883
|
+
switch (sort) {
|
|
884
|
+
case "longest": return sorted.sort((a, b) => b.durationMs - a.durationMs);
|
|
885
|
+
case "shortest": return sorted.sort((a, b) => a.durationMs - b.durationMs);
|
|
886
|
+
case "mostSpans": return sorted.sort((a, b) => b.spanCount - a.spanCount);
|
|
887
|
+
case "leastSpans": return sorted.sort((a, b) => a.spanCount - b.spanCount);
|
|
888
|
+
default: return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function TraceSearch({ services = [], service, operations = [], traces, isLoading, error, onSelectTrace, onSearch, onCompare, sort: controlledSort, onSortChange }) {
|
|
892
|
+
const [internalSort, setInternalSort] = useState("recent");
|
|
893
|
+
const currentSort = controlledSort ?? internalSort;
|
|
894
|
+
const handleSortChange = (s) => {
|
|
895
|
+
if (onSortChange) onSortChange(s);
|
|
896
|
+
else setInternalSort(s);
|
|
897
|
+
};
|
|
898
|
+
const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
|
|
899
|
+
const toggleSelected = (traceId) => {
|
|
900
|
+
setSelected((prev) => {
|
|
901
|
+
const next = new Set(prev);
|
|
902
|
+
if (next.has(traceId)) next.delete(traceId);
|
|
903
|
+
else {
|
|
904
|
+
if (next.size >= 2) return prev;
|
|
905
|
+
next.add(traceId);
|
|
906
|
+
}
|
|
907
|
+
return next;
|
|
908
|
+
});
|
|
909
|
+
};
|
|
910
|
+
const handleFormSubmit = (values) => {
|
|
911
|
+
onSearch?.({
|
|
912
|
+
service: values.service || void 0,
|
|
913
|
+
operation: values.operation || void 0,
|
|
914
|
+
tags: values.tags || void 0,
|
|
915
|
+
lookback: values.lookback || void 0,
|
|
916
|
+
minDuration: values.minDuration || void 0,
|
|
917
|
+
maxDuration: values.maxDuration || void 0,
|
|
918
|
+
limit: values.limit
|
|
919
|
+
});
|
|
920
|
+
};
|
|
921
|
+
const sortedTraces = useMemo(() => sortTraces(traces, currentSort), [traces, currentSort]);
|
|
922
|
+
const maxDurationMs = useMemo(() => Math.max(...traces.map((t) => t.durationMs), 0), [traces]);
|
|
923
|
+
const selectedArr = Array.from(selected);
|
|
924
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
925
|
+
className: "flex gap-6 min-h-0",
|
|
926
|
+
children: [onSearch && /* @__PURE__ */ jsx("div", {
|
|
927
|
+
className: "w-72 shrink-0 border border-border rounded-lg p-4 self-start",
|
|
928
|
+
children: /* @__PURE__ */ jsx(SearchForm, {
|
|
929
|
+
services,
|
|
930
|
+
operations,
|
|
931
|
+
initialValues: { service },
|
|
932
|
+
onSubmit: handleFormSubmit,
|
|
933
|
+
isLoading
|
|
934
|
+
})
|
|
935
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
936
|
+
className: "flex-1 min-w-0 space-y-4",
|
|
937
|
+
children: [
|
|
938
|
+
traces.length > 0 && /* @__PURE__ */ jsx(ScatterPlot, {
|
|
939
|
+
traces,
|
|
940
|
+
onSelectTrace
|
|
941
|
+
}),
|
|
942
|
+
/* @__PURE__ */ jsxs("div", {
|
|
943
|
+
className: "flex items-center justify-between gap-2",
|
|
944
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
945
|
+
className: "text-sm text-muted-foreground",
|
|
946
|
+
children: [
|
|
947
|
+
traces.length,
|
|
948
|
+
" Trace",
|
|
949
|
+
traces.length !== 1 ? "s" : ""
|
|
950
|
+
]
|
|
951
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
952
|
+
className: "flex items-center gap-2",
|
|
953
|
+
children: [onCompare && selected.size === 2 && /* @__PURE__ */ jsx("button", {
|
|
954
|
+
onClick: () => onCompare(selectedArr),
|
|
955
|
+
className: "px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
956
|
+
children: "Compare"
|
|
957
|
+
}), /* @__PURE__ */ jsx(SortDropdown, {
|
|
958
|
+
value: currentSort,
|
|
959
|
+
onChange: handleSortChange
|
|
960
|
+
})]
|
|
961
|
+
})]
|
|
962
|
+
}),
|
|
963
|
+
isLoading && /* @__PURE__ */ jsxs("div", {
|
|
964
|
+
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
965
|
+
children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading traces..."]
|
|
966
|
+
}),
|
|
967
|
+
error && /* @__PURE__ */ jsxs("div", {
|
|
968
|
+
className: "text-red-400 py-4",
|
|
969
|
+
children: ["Error loading traces: ", error.message]
|
|
970
|
+
}),
|
|
971
|
+
!isLoading && !error && traces.length === 0 && /* @__PURE__ */ jsx("div", {
|
|
972
|
+
className: "text-muted-foreground py-8",
|
|
973
|
+
children: "No traces found"
|
|
974
|
+
}),
|
|
975
|
+
sortedTraces.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
976
|
+
className: "space-y-2",
|
|
977
|
+
children: sortedTraces.map((t) => /* @__PURE__ */ jsx("div", {
|
|
978
|
+
className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
|
|
979
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
980
|
+
className: "flex items-center gap-2",
|
|
981
|
+
children: [onCompare && /* @__PURE__ */ jsx("input", {
|
|
982
|
+
type: "checkbox",
|
|
983
|
+
checked: selected.has(t.traceId),
|
|
984
|
+
onChange: () => toggleSelected(t.traceId),
|
|
985
|
+
onClick: (e) => e.stopPropagation(),
|
|
986
|
+
className: "shrink-0",
|
|
987
|
+
disabled: !selected.has(t.traceId) && selected.size >= 2
|
|
988
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
989
|
+
className: "flex-1 min-w-0",
|
|
990
|
+
onClick: () => onSelectTrace(t.traceId),
|
|
991
|
+
children: [
|
|
992
|
+
/* @__PURE__ */ jsx("div", {
|
|
993
|
+
className: "flex items-baseline justify-between gap-2",
|
|
994
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
995
|
+
className: "flex items-baseline gap-1.5 min-w-0",
|
|
996
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
997
|
+
className: "font-medium text-foreground truncate",
|
|
998
|
+
children: [
|
|
999
|
+
t.serviceName,
|
|
1000
|
+
": ",
|
|
1001
|
+
t.rootSpanName
|
|
1002
|
+
]
|
|
1003
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1004
|
+
className: "text-xs font-mono text-muted-foreground shrink-0",
|
|
1005
|
+
children: t.traceId.slice(0, 7)
|
|
1006
|
+
})]
|
|
1007
|
+
})
|
|
1008
|
+
}),
|
|
1009
|
+
/* @__PURE__ */ jsx("div", {
|
|
1010
|
+
className: "mt-1.5",
|
|
1011
|
+
children: /* @__PURE__ */ jsx(DurationBar, {
|
|
1012
|
+
durationMs: t.durationMs,
|
|
1013
|
+
maxDurationMs,
|
|
1014
|
+
color: getServiceColor(t.serviceName)
|
|
1015
|
+
})
|
|
1016
|
+
}),
|
|
1017
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1018
|
+
className: "flex items-center flex-wrap gap-1.5 mt-1.5",
|
|
1019
|
+
children: [
|
|
1020
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1021
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
|
|
1022
|
+
children: [
|
|
1023
|
+
t.spanCount,
|
|
1024
|
+
" Span",
|
|
1025
|
+
t.spanCount !== 1 ? "s" : ""
|
|
1026
|
+
]
|
|
1027
|
+
}),
|
|
1028
|
+
t.errorCount > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1029
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
|
|
1030
|
+
children: [
|
|
1031
|
+
t.errorCount,
|
|
1032
|
+
" Error",
|
|
1033
|
+
t.errorCount !== 1 ? "s" : ""
|
|
1034
|
+
]
|
|
1035
|
+
}),
|
|
1036
|
+
t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
|
|
1037
|
+
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
1038
|
+
style: {
|
|
1039
|
+
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
1040
|
+
color: getServiceColor(svc.name)
|
|
1041
|
+
},
|
|
1042
|
+
children: [
|
|
1043
|
+
svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
1044
|
+
svc.name,
|
|
1045
|
+
" (",
|
|
1046
|
+
svc.count,
|
|
1047
|
+
")"
|
|
1048
|
+
]
|
|
1049
|
+
}, svc.name))
|
|
1050
|
+
]
|
|
1051
|
+
}),
|
|
1052
|
+
/* @__PURE__ */ jsx("div", {
|
|
1053
|
+
className: "text-xs text-muted-foreground mt-1 text-right",
|
|
1054
|
+
children: formatTimestamp$1(t.timestampMs)
|
|
1055
|
+
})
|
|
1056
|
+
]
|
|
1057
|
+
})]
|
|
1058
|
+
})
|
|
1059
|
+
}, t.traceId))
|
|
1060
|
+
})
|
|
1061
|
+
]
|
|
1062
|
+
})]
|
|
1063
|
+
});
|
|
777
1064
|
}
|
|
778
|
-
|
|
779
1065
|
//#endregion
|
|
780
1066
|
//#region src/components/observability/utils/flatten-tree.ts
|
|
781
1067
|
function flattenTree(rootSpans, collapsedIds) {
|
|
@@ -790,6 +1076,17 @@ function flattenTree(rootSpans, collapsedIds) {
|
|
|
790
1076
|
rootSpans.forEach((root) => traverse(root, 0));
|
|
791
1077
|
return result;
|
|
792
1078
|
}
|
|
1079
|
+
/** Flatten all spans (ignoring collapse state) with depth. */
|
|
1080
|
+
function flattenAllSpans(rootSpans) {
|
|
1081
|
+
return flattenTree(rootSpans, /* @__PURE__ */ new Set());
|
|
1082
|
+
}
|
|
1083
|
+
function spanMatchesSearch(span, query) {
|
|
1084
|
+
const q = query.toLowerCase();
|
|
1085
|
+
if (span.name.toLowerCase().includes(q)) return true;
|
|
1086
|
+
if (span.serviceName.toLowerCase().includes(q)) return true;
|
|
1087
|
+
for (const val of Object.values(span.attributes)) if (String(val).toLowerCase().includes(q)) return true;
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
793
1090
|
function getAllSpanIds(rootSpans) {
|
|
794
1091
|
const ids = [];
|
|
795
1092
|
function traverse(span) {
|
|
@@ -799,15 +1096,26 @@ function getAllSpanIds(rootSpans) {
|
|
|
799
1096
|
rootSpans.forEach((root) => traverse(root));
|
|
800
1097
|
return ids;
|
|
801
1098
|
}
|
|
802
|
-
|
|
803
1099
|
//#endregion
|
|
804
1100
|
//#region src/components/observability/TraceTimeline/TraceHeader.tsx
|
|
805
|
-
function
|
|
1101
|
+
function computeMaxDepth(spans) {
|
|
1102
|
+
let max = 0;
|
|
1103
|
+
function walk(nodes, depth) {
|
|
1104
|
+
for (const node of nodes) {
|
|
1105
|
+
if (depth > max) max = depth;
|
|
1106
|
+
walk(node.children, depth + 1);
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
walk(spans, 1);
|
|
1110
|
+
return max;
|
|
1111
|
+
}
|
|
1112
|
+
function TraceHeader({ trace, services = [], onHeaderToggle, isCollapsed = false }) {
|
|
806
1113
|
const [copied, setCopied] = useState(false);
|
|
807
1114
|
const rootSpan = trace.rootSpans[0];
|
|
808
1115
|
const rootServiceName = rootSpan?.serviceName ?? "unknown";
|
|
809
1116
|
const rootSpanName = rootSpan?.name ?? "unknown";
|
|
810
1117
|
const totalDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
1118
|
+
const maxDepth = computeMaxDepth(trace.rootSpans);
|
|
811
1119
|
const handleCopyTraceId = async () => {
|
|
812
1120
|
try {
|
|
813
1121
|
await navigator.clipboard.writeText(trace.traceId);
|
|
@@ -817,9 +1125,35 @@ function TraceHeader({ trace }) {
|
|
|
817
1125
|
console.error("Failed to copy trace ID:", err);
|
|
818
1126
|
}
|
|
819
1127
|
};
|
|
820
|
-
return /* @__PURE__ */
|
|
1128
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
821
1129
|
className: "bg-background border-b border-border px-4 py-3",
|
|
822
|
-
children: /* @__PURE__ */ jsxs("div", {
|
|
1130
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1131
|
+
className: "flex items-center gap-2 mb-1",
|
|
1132
|
+
children: [onHeaderToggle && /* @__PURE__ */ jsx("button", {
|
|
1133
|
+
onClick: onHeaderToggle,
|
|
1134
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground",
|
|
1135
|
+
"aria-label": isCollapsed ? "Expand header" : "Collapse header",
|
|
1136
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1137
|
+
className: `w-4 h-4 transition-transform ${isCollapsed ? "-rotate-90" : ""}`,
|
|
1138
|
+
fill: "none",
|
|
1139
|
+
stroke: "currentColor",
|
|
1140
|
+
viewBox: "0 0 24 24",
|
|
1141
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1142
|
+
strokeLinecap: "round",
|
|
1143
|
+
strokeLinejoin: "round",
|
|
1144
|
+
strokeWidth: 2,
|
|
1145
|
+
d: "M19 9l-7 7-7-7"
|
|
1146
|
+
})
|
|
1147
|
+
})
|
|
1148
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
1149
|
+
className: "text-sm font-semibold text-foreground",
|
|
1150
|
+
children: [
|
|
1151
|
+
rootServiceName,
|
|
1152
|
+
": ",
|
|
1153
|
+
rootSpanName
|
|
1154
|
+
]
|
|
1155
|
+
})]
|
|
1156
|
+
}), !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
|
|
823
1157
|
className: "flex items-center gap-6 flex-wrap",
|
|
824
1158
|
children: [
|
|
825
1159
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -845,43 +1179,30 @@ function TraceHeader({ trace }) {
|
|
|
845
1179
|
className: "flex items-center gap-2",
|
|
846
1180
|
children: [/* @__PURE__ */ jsx("span", {
|
|
847
1181
|
className: "text-xs font-semibold text-muted-foreground",
|
|
848
|
-
children: "
|
|
849
|
-
}), /* @__PURE__ */
|
|
850
|
-
className: "text-sm",
|
|
851
|
-
children:
|
|
852
|
-
/* @__PURE__ */ jsx("span", {
|
|
853
|
-
className: "text-muted-foreground",
|
|
854
|
-
children: rootServiceName
|
|
855
|
-
}),
|
|
856
|
-
/* @__PURE__ */ jsx("span", {
|
|
857
|
-
className: "mx-1 text-muted-foreground/70",
|
|
858
|
-
children: "/"
|
|
859
|
-
}),
|
|
860
|
-
/* @__PURE__ */ jsx("span", {
|
|
861
|
-
className: "font-medium text-foreground",
|
|
862
|
-
children: rootSpanName
|
|
863
|
-
})
|
|
864
|
-
]
|
|
1182
|
+
children: "Duration:"
|
|
1183
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1184
|
+
className: "text-sm font-medium text-foreground",
|
|
1185
|
+
children: formatDuration(totalDuration)
|
|
865
1186
|
})]
|
|
866
1187
|
}),
|
|
867
1188
|
/* @__PURE__ */ jsxs("div", {
|
|
868
1189
|
className: "flex items-center gap-2",
|
|
869
1190
|
children: [/* @__PURE__ */ jsx("span", {
|
|
870
1191
|
className: "text-xs font-semibold text-muted-foreground",
|
|
871
|
-
children: "
|
|
1192
|
+
children: "Spans:"
|
|
872
1193
|
}), /* @__PURE__ */ jsx("span", {
|
|
873
1194
|
className: "text-sm font-medium text-foreground",
|
|
874
|
-
children:
|
|
1195
|
+
children: trace.totalSpanCount
|
|
875
1196
|
})]
|
|
876
1197
|
}),
|
|
877
1198
|
/* @__PURE__ */ jsxs("div", {
|
|
878
1199
|
className: "flex items-center gap-2",
|
|
879
1200
|
children: [/* @__PURE__ */ jsx("span", {
|
|
880
1201
|
className: "text-xs font-semibold text-muted-foreground",
|
|
881
|
-
children: "
|
|
1202
|
+
children: "Depth:"
|
|
882
1203
|
}), /* @__PURE__ */ jsx("span", {
|
|
883
1204
|
className: "text-sm font-medium text-foreground",
|
|
884
|
-
children:
|
|
1205
|
+
children: maxDepth
|
|
885
1206
|
})]
|
|
886
1207
|
}),
|
|
887
1208
|
/* @__PURE__ */ jsxs("div", {
|
|
@@ -895,10 +1216,21 @@ function TraceHeader({ trace }) {
|
|
|
895
1216
|
})]
|
|
896
1217
|
})
|
|
897
1218
|
]
|
|
898
|
-
})
|
|
1219
|
+
}), services.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
1220
|
+
className: "flex items-center gap-3 mt-2 flex-wrap",
|
|
1221
|
+
children: services.map((svc) => /* @__PURE__ */ jsxs("div", {
|
|
1222
|
+
className: "flex items-center gap-1.5",
|
|
1223
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1224
|
+
className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
|
|
1225
|
+
style: { backgroundColor: getServiceColor(svc) }
|
|
1226
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1227
|
+
className: "text-xs text-muted-foreground",
|
|
1228
|
+
children: svc
|
|
1229
|
+
})]
|
|
1230
|
+
}, svc))
|
|
1231
|
+
})] })]
|
|
899
1232
|
});
|
|
900
1233
|
}
|
|
901
|
-
|
|
902
1234
|
//#endregion
|
|
903
1235
|
//#region src/components/observability/TraceTimeline/Tooltip.tsx
|
|
904
1236
|
function Tooltip$1({ content, children }) {
|
|
@@ -928,7 +1260,6 @@ function Tooltip$1({ content, children }) {
|
|
|
928
1260
|
children: content
|
|
929
1261
|
}), document.body)] });
|
|
930
1262
|
}
|
|
931
|
-
|
|
932
1263
|
//#endregion
|
|
933
1264
|
//#region src/components/observability/TraceTimeline/TimelineBar.tsx
|
|
934
1265
|
function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
@@ -936,45 +1267,48 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
|
936
1267
|
const barColor = getSpanBarColor(span.serviceName, isError);
|
|
937
1268
|
const leftPercent = relativeStart * 100;
|
|
938
1269
|
const widthPercent = Math.max(.2, relativeDuration * 100);
|
|
1270
|
+
const isWide = widthPercent > 8;
|
|
1271
|
+
const tooltipText = `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`;
|
|
1272
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
939
1273
|
return /* @__PURE__ */ jsx("div", {
|
|
940
1274
|
className: "relative h-full",
|
|
941
1275
|
children: /* @__PURE__ */ jsx(Tooltip$1, {
|
|
942
|
-
content:
|
|
943
|
-
children: /* @__PURE__ */
|
|
1276
|
+
content: tooltipText,
|
|
1277
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
944
1278
|
className: "absolute inset-0",
|
|
945
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
946
|
-
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity",
|
|
1279
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1280
|
+
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity flex items-center",
|
|
947
1281
|
style: {
|
|
948
1282
|
left: `${leftPercent}%`,
|
|
949
1283
|
width: `max(2px, ${widthPercent}%)`,
|
|
950
1284
|
backgroundColor: barColor
|
|
951
|
-
}
|
|
952
|
-
|
|
1285
|
+
},
|
|
1286
|
+
children: isWide && /* @__PURE__ */ jsx("span", {
|
|
1287
|
+
className: "text-[10px] font-mono text-white px-1 truncate",
|
|
1288
|
+
children: durationLabel
|
|
1289
|
+
})
|
|
1290
|
+
}), !isWide && /* @__PURE__ */ jsx("span", {
|
|
1291
|
+
className: "absolute top-1/2 -translate-y-1/2 text-[10px] font-mono text-muted-foreground whitespace-nowrap",
|
|
1292
|
+
style: { left: `calc(${leftPercent + widthPercent}% + 4px)` },
|
|
1293
|
+
children: durationLabel
|
|
1294
|
+
})]
|
|
953
1295
|
})
|
|
954
1296
|
})
|
|
955
1297
|
});
|
|
956
1298
|
}
|
|
957
|
-
|
|
958
1299
|
//#endregion
|
|
959
1300
|
//#region src/components/observability/TraceTimeline/SpanRow.tsx
|
|
960
|
-
function
|
|
961
|
-
const attrs = span.attributes;
|
|
962
|
-
const method = attrs["http.method"];
|
|
963
|
-
const url = attrs["http.url"] || attrs["http.target"];
|
|
964
|
-
const statusCode = attrs["http.status_code"];
|
|
965
|
-
if (!method && !url) return null;
|
|
966
|
-
const parts = [];
|
|
967
|
-
if (method) parts.push(String(method));
|
|
968
|
-
if (url) parts.push(String(url));
|
|
969
|
-
if (statusCode) parts.push(`[${statusCode}]`);
|
|
970
|
-
return parts.join(" ");
|
|
971
|
-
}
|
|
972
|
-
const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave }) {
|
|
1301
|
+
const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave, uiFind }) {
|
|
973
1302
|
const hasChildren = span.children.length > 0;
|
|
974
1303
|
const isError = span.status === "ERROR";
|
|
975
|
-
const
|
|
1304
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1305
|
+
const isDimmed = uiFind ? !spanMatchesSearch(span, uiFind) : false;
|
|
976
1306
|
return /* @__PURE__ */ jsxs("div", {
|
|
977
1307
|
className: `flex h-8 border-b border-border hover:bg-muted cursor-pointer ${isSelected ? "bg-blue-100 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/30" : ""}`,
|
|
1308
|
+
style: {
|
|
1309
|
+
borderLeft: `3px solid ${serviceColor}`,
|
|
1310
|
+
opacity: isDimmed ? .4 : 1
|
|
1311
|
+
},
|
|
978
1312
|
onClick,
|
|
979
1313
|
onMouseEnter,
|
|
980
1314
|
onMouseLeave,
|
|
@@ -1029,7 +1363,8 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
|
|
|
1029
1363
|
})
|
|
1030
1364
|
}),
|
|
1031
1365
|
/* @__PURE__ */ jsx("span", {
|
|
1032
|
-
className: "text-xs
|
|
1366
|
+
className: "text-xs flex-shrink-0 mr-2 font-medium",
|
|
1367
|
+
style: { color: serviceColor },
|
|
1033
1368
|
children: span.serviceName
|
|
1034
1369
|
}),
|
|
1035
1370
|
/* @__PURE__ */ jsx("span", {
|
|
@@ -1044,10 +1379,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
|
|
|
1044
1379
|
")"
|
|
1045
1380
|
]
|
|
1046
1381
|
}),
|
|
1047
|
-
httpContext && /* @__PURE__ */ jsx("span", {
|
|
1048
|
-
className: "text-xs text-muted-foreground truncate ml-2 flex-shrink-0 max-w-xs",
|
|
1049
|
-
children: httpContext
|
|
1050
|
-
}),
|
|
1051
1382
|
/* @__PURE__ */ jsx("span", {
|
|
1052
1383
|
className: "text-xs text-muted-foreground flex-shrink-0 ml-2",
|
|
1053
1384
|
children: formatDuration(span.durationMs)
|
|
@@ -1063,7 +1394,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
|
|
|
1063
1394
|
})]
|
|
1064
1395
|
});
|
|
1065
1396
|
});
|
|
1066
|
-
|
|
1067
1397
|
//#endregion
|
|
1068
1398
|
//#region src/components/observability/utils/attributes.ts
|
|
1069
1399
|
function formatAttributeValue(value) {
|
|
@@ -1082,570 +1412,271 @@ function formatSeriesLabel(labels) {
|
|
|
1082
1412
|
function isComplexValue(value) {
|
|
1083
1413
|
return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
|
|
1084
1414
|
}
|
|
1085
|
-
|
|
1086
1415
|
//#endregion
|
|
1087
|
-
//#region src/components/observability/TraceTimeline/
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
"
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
"http.request_content_length",
|
|
1098
|
-
"http.response_content_length"
|
|
1099
|
-
]);
|
|
1100
|
-
function AttributesTab$1({ span }) {
|
|
1101
|
-
const { httpAttributes, otherAttributes, resourceAttributes } = useMemo(() => {
|
|
1102
|
-
const http = [];
|
|
1103
|
-
const other = [];
|
|
1104
|
-
const resource = [];
|
|
1105
|
-
if (span.attributes) Object.entries(span.attributes).forEach(([key, value]) => {
|
|
1106
|
-
if (HTTP_SEMANTIC_CONVENTIONS.has(key)) http.push([key, value]);
|
|
1107
|
-
else other.push([key, value]);
|
|
1108
|
-
});
|
|
1109
|
-
if (span.resourceAttributes) Object.entries(span.resourceAttributes).forEach(([key, value]) => {
|
|
1110
|
-
resource.push([key, value]);
|
|
1111
|
-
});
|
|
1112
|
-
http.sort(([a], [b]) => a.localeCompare(b));
|
|
1113
|
-
other.sort(([a], [b]) => a.localeCompare(b));
|
|
1114
|
-
resource.sort(([a], [b]) => a.localeCompare(b));
|
|
1115
|
-
return {
|
|
1116
|
-
httpAttributes: http,
|
|
1117
|
-
otherAttributes: other,
|
|
1118
|
-
resourceAttributes: resource
|
|
1119
|
-
};
|
|
1120
|
-
}, [span]);
|
|
1121
|
-
if (!(httpAttributes.length > 0 || otherAttributes.length > 0 || resourceAttributes.length > 0)) return /* @__PURE__ */ jsx("div", {
|
|
1122
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1123
|
-
children: "No attributes available"
|
|
1124
|
-
});
|
|
1125
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1126
|
-
className: "space-y-6",
|
|
1416
|
+
//#region src/components/observability/TraceTimeline/SpanDetailInline.tsx
|
|
1417
|
+
function CollapsibleSection({ title, count, children }) {
|
|
1418
|
+
const [open, setOpen] = useState(false);
|
|
1419
|
+
if (count === 0) return null;
|
|
1420
|
+
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
|
|
1421
|
+
className: "flex items-center gap-1 text-xs font-medium text-foreground hover:text-blue-600 dark:hover:text-blue-400 py-1",
|
|
1422
|
+
onClick: (e) => {
|
|
1423
|
+
e.stopPropagation();
|
|
1424
|
+
setOpen((p) => !p);
|
|
1425
|
+
},
|
|
1127
1426
|
children: [
|
|
1128
|
-
|
|
1129
|
-
className: "
|
|
1130
|
-
children:
|
|
1131
|
-
}),
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
children: "Span Attributes"
|
|
1142
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1143
|
-
className: "space-y-2",
|
|
1144
|
-
children: otherAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
|
|
1145
|
-
attrKey: key,
|
|
1146
|
-
value
|
|
1147
|
-
}, key))
|
|
1148
|
-
})] }),
|
|
1149
|
-
resourceAttributes.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
|
|
1150
|
-
className: "text-sm font-semibold text-foreground mb-3",
|
|
1151
|
-
children: "Resource Attributes"
|
|
1152
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1153
|
-
className: "space-y-2",
|
|
1154
|
-
children: resourceAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
|
|
1155
|
-
attrKey: key,
|
|
1156
|
-
value
|
|
1157
|
-
}, key))
|
|
1158
|
-
})] })
|
|
1427
|
+
/* @__PURE__ */ jsx("span", {
|
|
1428
|
+
className: "w-3 text-center",
|
|
1429
|
+
children: open ? "▾" : "▸"
|
|
1430
|
+
}),
|
|
1431
|
+
title,
|
|
1432
|
+
/* @__PURE__ */ jsxs("span", {
|
|
1433
|
+
className: "text-muted-foreground",
|
|
1434
|
+
children: [
|
|
1435
|
+
"(",
|
|
1436
|
+
count,
|
|
1437
|
+
")"
|
|
1438
|
+
]
|
|
1439
|
+
})
|
|
1159
1440
|
]
|
|
1160
|
-
})
|
|
1441
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
1442
|
+
className: "ml-4 mt-1 space-y-1",
|
|
1443
|
+
children
|
|
1444
|
+
})] });
|
|
1161
1445
|
}
|
|
1162
|
-
function
|
|
1163
|
-
const
|
|
1164
|
-
const formattedValue = formatAttributeValue(value);
|
|
1446
|
+
function KeyValueRow({ k, v }) {
|
|
1447
|
+
const formatted = formatAttributeValue(v);
|
|
1165
1448
|
return /* @__PURE__ */ jsxs("div", {
|
|
1166
|
-
className:
|
|
1167
|
-
children: [
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
children: isComplex ? /* @__PURE__ */ jsx("pre", {
|
|
1174
|
-
className: "text-xs text-foreground bg-background p-2 rounded border border-border overflow-x-auto",
|
|
1175
|
-
children: formattedValue
|
|
1176
|
-
}) : /* @__PURE__ */ jsx("span", {
|
|
1449
|
+
className: "flex gap-2 text-xs font-mono py-0.5",
|
|
1450
|
+
children: [
|
|
1451
|
+
/* @__PURE__ */ jsx("span", {
|
|
1452
|
+
className: "text-muted-foreground flex-shrink-0",
|
|
1453
|
+
children: k
|
|
1454
|
+
}),
|
|
1455
|
+
/* @__PURE__ */ jsx("span", {
|
|
1177
1456
|
className: "text-foreground",
|
|
1178
|
-
children:
|
|
1457
|
+
children: "="
|
|
1458
|
+
}),
|
|
1459
|
+
/* @__PURE__ */ jsx("span", {
|
|
1460
|
+
className: "text-foreground break-all",
|
|
1461
|
+
children: formatted
|
|
1179
1462
|
})
|
|
1180
|
-
|
|
1463
|
+
]
|
|
1181
1464
|
});
|
|
1182
1465
|
}
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1466
|
+
function SpanDetailInline({ span, traceStartMs }) {
|
|
1467
|
+
const [copiedId, setCopiedId] = useState(false);
|
|
1468
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1469
|
+
const relativeStartMs = span.startTimeUnixMs - traceStartMs;
|
|
1470
|
+
const handleCopy = useCallback(async () => {
|
|
1471
|
+
try {
|
|
1472
|
+
await navigator.clipboard.writeText(span.spanId);
|
|
1473
|
+
setCopiedId(true);
|
|
1474
|
+
setTimeout(() => setCopiedId(false), 2e3);
|
|
1475
|
+
} catch {}
|
|
1476
|
+
}, [span.spanId]);
|
|
1477
|
+
const spanAttrs = Object.entries(span.attributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1478
|
+
const resourceAttrs = Object.entries(span.resourceAttributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1479
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1480
|
+
className: "border-b border-border bg-muted/50 px-4 py-3",
|
|
1481
|
+
style: { borderLeft: `3px solid ${serviceColor}` },
|
|
1482
|
+
onClick: (e) => e.stopPropagation(),
|
|
1483
|
+
children: [
|
|
1484
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1485
|
+
className: "mb-2",
|
|
1486
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1487
|
+
className: "text-sm font-medium text-foreground",
|
|
1488
|
+
children: span.name
|
|
1489
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
1490
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground mt-1",
|
|
1491
|
+
children: [
|
|
1492
|
+
/* @__PURE__ */ jsxs("span", { children: ["Service: ", /* @__PURE__ */ jsx("span", {
|
|
1493
|
+
className: "text-foreground",
|
|
1494
|
+
children: span.serviceName
|
|
1495
|
+
})] }),
|
|
1496
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1497
|
+
"Duration:",
|
|
1498
|
+
" ",
|
|
1499
|
+
/* @__PURE__ */ jsx("span", {
|
|
1500
|
+
className: "text-foreground",
|
|
1501
|
+
children: formatDuration(span.durationMs)
|
|
1502
|
+
})
|
|
1503
|
+
] }),
|
|
1504
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
1505
|
+
"Start:",
|
|
1506
|
+
" ",
|
|
1507
|
+
/* @__PURE__ */ jsx("span", {
|
|
1508
|
+
className: "text-foreground",
|
|
1509
|
+
children: formatDuration(relativeStartMs)
|
|
1510
|
+
})
|
|
1511
|
+
] }),
|
|
1512
|
+
/* @__PURE__ */ jsxs("span", { children: ["Kind: ", /* @__PURE__ */ jsx("span", {
|
|
1513
|
+
className: "text-foreground",
|
|
1514
|
+
children: span.kind
|
|
1515
|
+
})] }),
|
|
1516
|
+
span.status !== "UNSET" && /* @__PURE__ */ jsxs("span", { children: [
|
|
1517
|
+
"Status:",
|
|
1518
|
+
" ",
|
|
1519
|
+
/* @__PURE__ */ jsx("span", {
|
|
1520
|
+
className: span.status === "ERROR" ? "text-red-500" : "text-foreground",
|
|
1521
|
+
children: span.status
|
|
1522
|
+
})
|
|
1523
|
+
] })
|
|
1524
|
+
]
|
|
1525
|
+
})]
|
|
1526
|
+
}),
|
|
1527
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1528
|
+
className: "space-y-1",
|
|
1212
1529
|
children: [
|
|
1213
|
-
/* @__PURE__ */ jsx(
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1530
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1531
|
+
title: "Tags",
|
|
1532
|
+
count: spanAttrs.length,
|
|
1533
|
+
children: spanAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
|
|
1534
|
+
k,
|
|
1535
|
+
v
|
|
1536
|
+
}, k))
|
|
1537
|
+
}),
|
|
1538
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1539
|
+
title: "Process",
|
|
1540
|
+
count: resourceAttrs.length,
|
|
1541
|
+
children: resourceAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
|
|
1542
|
+
k,
|
|
1543
|
+
v
|
|
1544
|
+
}, k))
|
|
1545
|
+
}),
|
|
1546
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1547
|
+
title: "Events",
|
|
1548
|
+
count: span.events.length,
|
|
1549
|
+
children: span.events.map((event, i) => /* @__PURE__ */ jsxs("div", {
|
|
1550
|
+
className: "text-xs border-l-2 border-border pl-2 py-1.5 space-y-0.5",
|
|
1217
1551
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
1218
|
-
className: "flex-
|
|
1219
|
-
children: [/* @__PURE__ */ jsx("
|
|
1220
|
-
className: "font-
|
|
1552
|
+
className: "flex items-center gap-2",
|
|
1553
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
1554
|
+
className: "font-mono text-muted-foreground flex-shrink-0",
|
|
1555
|
+
children: formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs)
|
|
1556
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
1557
|
+
className: "font-medium text-foreground",
|
|
1221
1558
|
children: event.name
|
|
1222
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1223
|
-
className: "text-xs text-muted-foreground mt-1 font-mono",
|
|
1224
|
-
children: [relativeTime, " from span start"]
|
|
1225
1559
|
})]
|
|
1226
|
-
}),
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1232
|
-
className: `w-4 h-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-180" : ""}`,
|
|
1233
|
-
fill: "none",
|
|
1234
|
-
stroke: "currentColor",
|
|
1235
|
-
viewBox: "0 0 24 24",
|
|
1236
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1237
|
-
strokeLinecap: "round",
|
|
1238
|
-
strokeLinejoin: "round",
|
|
1239
|
-
strokeWidth: 2,
|
|
1240
|
-
d: "M19 9l-7 7-7-7"
|
|
1241
|
-
})
|
|
1242
|
-
})
|
|
1243
|
-
})]
|
|
1244
|
-
})
|
|
1560
|
+
}), Object.entries(event.attributes).map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
|
|
1561
|
+
k,
|
|
1562
|
+
v
|
|
1563
|
+
}, k))]
|
|
1564
|
+
}, i))
|
|
1245
1565
|
}),
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
})
|
|
1566
|
+
/* @__PURE__ */ jsx(CollapsibleSection, {
|
|
1567
|
+
title: "Links",
|
|
1568
|
+
count: span.links.length,
|
|
1569
|
+
children: span.links.map((link, i) => /* @__PURE__ */ jsxs("div", {
|
|
1570
|
+
className: "text-xs font-mono py-0.5",
|
|
1571
|
+
children: [
|
|
1572
|
+
/* @__PURE__ */ jsx("span", {
|
|
1573
|
+
className: "text-muted-foreground",
|
|
1574
|
+
children: "trace:"
|
|
1575
|
+
}),
|
|
1576
|
+
" ",
|
|
1577
|
+
link.traceId,
|
|
1578
|
+
" ",
|
|
1579
|
+
/* @__PURE__ */ jsx("span", {
|
|
1580
|
+
className: "text-muted-foreground",
|
|
1581
|
+
children: "span:"
|
|
1582
|
+
}),
|
|
1583
|
+
" ",
|
|
1584
|
+
link.spanId
|
|
1585
|
+
]
|
|
1586
|
+
}, i))
|
|
1587
|
+
})
|
|
1588
|
+
]
|
|
1589
|
+
}),
|
|
1590
|
+
/* @__PURE__ */ jsxs("div", {
|
|
1591
|
+
className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
|
|
1592
|
+
children: [
|
|
1593
|
+
/* @__PURE__ */ jsx("span", {
|
|
1594
|
+
className: "text-xs text-muted-foreground",
|
|
1595
|
+
children: "SpanID:"
|
|
1267
1596
|
}),
|
|
1268
|
-
|
|
1269
|
-
className: "
|
|
1270
|
-
children:
|
|
1597
|
+
/* @__PURE__ */ jsx("code", {
|
|
1598
|
+
className: "text-xs font-mono text-foreground",
|
|
1599
|
+
children: span.spanId
|
|
1600
|
+
}),
|
|
1601
|
+
/* @__PURE__ */ jsx("button", {
|
|
1602
|
+
onClick: handleCopy,
|
|
1603
|
+
className: "text-xs text-muted-foreground hover:text-foreground",
|
|
1604
|
+
"aria-label": "Copy span ID",
|
|
1605
|
+
children: copiedId ? "✓" : "Copy"
|
|
1271
1606
|
})
|
|
1272
1607
|
]
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1608
|
+
})
|
|
1609
|
+
]
|
|
1275
1610
|
});
|
|
1276
1611
|
}
|
|
1277
|
-
|
|
1278
1612
|
//#endregion
|
|
1279
|
-
//#region src/components/observability/
|
|
1280
|
-
function
|
|
1281
|
-
return
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
1347
|
-
})
|
|
1348
|
-
})
|
|
1349
|
-
})]
|
|
1350
|
-
})]
|
|
1351
|
-
}),
|
|
1352
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1353
|
-
className: "mb-2",
|
|
1354
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1355
|
-
className: "text-xs font-semibold text-muted-foreground mb-1",
|
|
1356
|
-
children: "Span ID"
|
|
1357
|
-
}), /* @__PURE__ */ jsxs("div", {
|
|
1358
|
-
className: "flex items-center gap-2",
|
|
1359
|
-
children: [/* @__PURE__ */ jsx("code", {
|
|
1360
|
-
className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
|
|
1361
|
-
title: link.spanId,
|
|
1362
|
-
children: truncateId(link.spanId)
|
|
1363
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1364
|
-
onClick: () => copyToClipboard(link.spanId, "span", index),
|
|
1365
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1366
|
-
"aria-label": "Copy span ID",
|
|
1367
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1368
|
-
className: `w-4 h-4 ${copiedId === `span-${index}-${link.spanId}` ? "text-green-600" : "text-muted-foreground"}`,
|
|
1369
|
-
fill: "none",
|
|
1370
|
-
stroke: "currentColor",
|
|
1371
|
-
viewBox: "0 0 24 24",
|
|
1372
|
-
children: copiedId === `span-${index}-${link.spanId}` ? /* @__PURE__ */ jsx("path", {
|
|
1373
|
-
strokeLinecap: "round",
|
|
1374
|
-
strokeLinejoin: "round",
|
|
1375
|
-
strokeWidth: 2,
|
|
1376
|
-
d: "M5 13l4 4L19 7"
|
|
1377
|
-
}) : /* @__PURE__ */ jsx("path", {
|
|
1378
|
-
strokeLinecap: "round",
|
|
1379
|
-
strokeLinejoin: "round",
|
|
1380
|
-
strokeWidth: 2,
|
|
1381
|
-
d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
1382
|
-
})
|
|
1383
|
-
})
|
|
1384
|
-
})]
|
|
1385
|
-
})]
|
|
1386
|
-
}),
|
|
1387
|
-
onLinkClick && /* @__PURE__ */ jsx("button", {
|
|
1388
|
-
onClick: () => onLinkClick(link.traceId, link.spanId),
|
|
1389
|
-
className: "w-full mt-2 px-3 py-2 bg-primary text-primary-foreground text-sm font-medium rounded hover:bg-primary/90 transition-colors",
|
|
1390
|
-
children: "Navigate to Span"
|
|
1391
|
-
}),
|
|
1392
|
-
hasAttributes && /* @__PURE__ */ jsxs("button", {
|
|
1393
|
-
onClick: () => toggleLinkExpanded(index),
|
|
1394
|
-
className: "w-full mt-2 px-3 py-1.5 text-xs text-foreground bg-background border border-border rounded hover:bg-muted transition-colors flex items-center justify-center gap-1",
|
|
1395
|
-
"aria-expanded": isExpanded,
|
|
1396
|
-
children: [/* @__PURE__ */ jsxs("span", { children: [
|
|
1397
|
-
isExpanded ? "Hide" : "Show",
|
|
1398
|
-
" Attributes (",
|
|
1399
|
-
Object.keys(link.attributes).length,
|
|
1400
|
-
")"
|
|
1401
|
-
] }), /* @__PURE__ */ jsx("svg", {
|
|
1402
|
-
className: `w-3 h-3 transition-transform ${isExpanded ? "rotate-180" : ""}`,
|
|
1403
|
-
fill: "none",
|
|
1404
|
-
stroke: "currentColor",
|
|
1405
|
-
viewBox: "0 0 24 24",
|
|
1406
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1407
|
-
strokeLinecap: "round",
|
|
1408
|
-
strokeLinejoin: "round",
|
|
1409
|
-
strokeWidth: 2,
|
|
1410
|
-
d: "M19 9l-7 7-7-7"
|
|
1411
|
-
})
|
|
1412
|
-
})]
|
|
1413
|
-
})
|
|
1414
|
-
]
|
|
1415
|
-
}), hasAttributes && isExpanded && /* @__PURE__ */ jsx("div", {
|
|
1416
|
-
className: "p-3 bg-background border-t border-border",
|
|
1417
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1418
|
-
className: "space-y-2",
|
|
1419
|
-
children: Object.entries(link.attributes).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
|
|
1420
|
-
className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
|
|
1421
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1422
|
-
className: "font-mono font-medium text-foreground break-words",
|
|
1423
|
-
children: key
|
|
1424
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1425
|
-
className: "text-foreground break-words",
|
|
1426
|
-
children: typeof value === "object" ? /* @__PURE__ */ jsx("pre", {
|
|
1427
|
-
className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
|
|
1428
|
-
children: formatAttributeValue(value)
|
|
1429
|
-
}) : /* @__PURE__ */ jsx("span", { children: formatAttributeValue(value) })
|
|
1430
|
-
})]
|
|
1431
|
-
}, key))
|
|
1432
|
-
})
|
|
1433
|
-
})]
|
|
1434
|
-
}, index);
|
|
1435
|
-
})
|
|
1436
|
-
});
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
//#endregion
|
|
1440
|
-
//#region src/components/observability/TraceTimeline/DetailPane/index.tsx
|
|
1441
|
-
function DetailPane({ span, onClose, onLinkClick, initialTab = "attributes" }) {
|
|
1442
|
-
const [activeTab, setActiveTab] = useState(initialTab);
|
|
1443
|
-
const [copiedId, setCopiedId] = useState(false);
|
|
1444
|
-
const handleTabChange = useCallback((tab) => {
|
|
1445
|
-
setActiveTab(tab);
|
|
1446
|
-
}, []);
|
|
1447
|
-
const handleCopySpanId = useCallback(async () => {
|
|
1448
|
-
try {
|
|
1449
|
-
await navigator.clipboard.writeText(span.spanId);
|
|
1450
|
-
setCopiedId(true);
|
|
1451
|
-
setTimeout(() => setCopiedId(false), 2e3);
|
|
1452
|
-
} catch (err) {
|
|
1453
|
-
console.error("Failed to copy span ID:", err);
|
|
1454
|
-
}
|
|
1455
|
-
}, [span.spanId]);
|
|
1456
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1457
|
-
className: "flex flex-col h-full bg-background border-l border-border",
|
|
1458
|
-
onKeyDown: useCallback((e) => {
|
|
1459
|
-
if (e.key === "Escape") onClose();
|
|
1460
|
-
}, [onClose]),
|
|
1461
|
-
tabIndex: -1,
|
|
1462
|
-
role: "complementary",
|
|
1463
|
-
"aria-label": "Span details",
|
|
1464
|
-
children: [
|
|
1465
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1466
|
-
className: "p-4 border-b border-border",
|
|
1467
|
-
children: [
|
|
1468
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1469
|
-
className: "flex items-center justify-between mb-3",
|
|
1470
|
-
children: [/* @__PURE__ */ jsx("h2", {
|
|
1471
|
-
className: "text-lg font-semibold text-foreground truncate",
|
|
1472
|
-
children: "Span Details"
|
|
1473
|
-
}), /* @__PURE__ */ jsx("button", {
|
|
1474
|
-
onClick: onClose,
|
|
1475
|
-
className: "p-1 hover:bg-muted rounded transition-colors",
|
|
1476
|
-
"aria-label": "Close detail pane",
|
|
1477
|
-
title: "Close (Esc)",
|
|
1478
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1479
|
-
className: "w-5 h-5 text-muted-foreground",
|
|
1480
|
-
fill: "none",
|
|
1481
|
-
stroke: "currentColor",
|
|
1482
|
-
viewBox: "0 0 24 24",
|
|
1483
|
-
children: /* @__PURE__ */ jsx("path", {
|
|
1484
|
-
strokeLinecap: "round",
|
|
1485
|
-
strokeLinejoin: "round",
|
|
1486
|
-
strokeWidth: 2,
|
|
1487
|
-
d: "M6 18L18 6M6 6l12 12"
|
|
1488
|
-
})
|
|
1489
|
-
})
|
|
1490
|
-
})]
|
|
1491
|
-
}),
|
|
1492
|
-
/* @__PURE__ */ jsx("div", {
|
|
1493
|
-
className: "mb-2",
|
|
1494
|
-
children: /* @__PURE__ */ jsx("div", {
|
|
1495
|
-
className: "text-sm font-medium text-foreground truncate",
|
|
1496
|
-
title: span.name,
|
|
1497
|
-
children: span.name
|
|
1498
|
-
})
|
|
1499
|
-
}),
|
|
1500
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1501
|
-
className: "flex items-center gap-2",
|
|
1502
|
-
children: [
|
|
1503
|
-
/* @__PURE__ */ jsx("span", {
|
|
1504
|
-
className: "text-xs text-muted-foreground",
|
|
1505
|
-
children: "Span ID:"
|
|
1506
|
-
}),
|
|
1507
|
-
/* @__PURE__ */ jsx("code", {
|
|
1508
|
-
className: "text-xs font-mono text-foreground bg-muted px-2 py-1 rounded flex-1 truncate",
|
|
1509
|
-
title: span.spanId,
|
|
1510
|
-
children: span.spanId
|
|
1511
|
-
}),
|
|
1512
|
-
/* @__PURE__ */ jsx("button", {
|
|
1513
|
-
onClick: handleCopySpanId,
|
|
1514
|
-
className: "p-1 hover:bg-muted rounded transition-colors",
|
|
1515
|
-
"aria-label": "Copy span ID",
|
|
1516
|
-
children: /* @__PURE__ */ jsx("svg", {
|
|
1517
|
-
className: `w-4 h-4 ${copiedId ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`,
|
|
1518
|
-
fill: "none",
|
|
1519
|
-
stroke: "currentColor",
|
|
1520
|
-
viewBox: "0 0 24 24",
|
|
1521
|
-
children: copiedId ? /* @__PURE__ */ jsx("path", {
|
|
1522
|
-
strokeLinecap: "round",
|
|
1523
|
-
strokeLinejoin: "round",
|
|
1524
|
-
strokeWidth: 2,
|
|
1525
|
-
d: "M5 13l4 4L19 7"
|
|
1526
|
-
}) : /* @__PURE__ */ jsx("path", {
|
|
1527
|
-
strokeLinecap: "round",
|
|
1528
|
-
strokeLinejoin: "round",
|
|
1529
|
-
strokeWidth: 2,
|
|
1530
|
-
d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
|
|
1531
|
-
})
|
|
1532
|
-
})
|
|
1533
|
-
})
|
|
1534
|
-
]
|
|
1535
|
-
})
|
|
1536
|
-
]
|
|
1537
|
-
}),
|
|
1538
|
-
/* @__PURE__ */ jsx("div", {
|
|
1539
|
-
className: "flex border-b border-border",
|
|
1540
|
-
role: "tablist",
|
|
1541
|
-
"aria-label": "Span detail tabs",
|
|
1542
|
-
children: [
|
|
1543
|
-
"attributes",
|
|
1544
|
-
"events",
|
|
1545
|
-
"links"
|
|
1546
|
-
].map((tab) => {
|
|
1547
|
-
const count = tab === "attributes" ? Object.keys(span.attributes).length : tab === "events" ? span.events.length : span.links.length;
|
|
1548
|
-
return /* @__PURE__ */ jsxs("button", {
|
|
1549
|
-
role: "tab",
|
|
1550
|
-
"aria-selected": activeTab === tab,
|
|
1551
|
-
onClick: () => handleTabChange(tab),
|
|
1552
|
-
className: `px-4 py-2 text-sm font-medium transition-colors ${activeTab === tab ? "text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400" : "text-muted-foreground hover:text-foreground"}`,
|
|
1553
|
-
children: [tab.charAt(0).toUpperCase() + tab.slice(1), count > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1554
|
-
className: "ml-1 text-xs text-muted-foreground",
|
|
1555
|
-
children: [
|
|
1556
|
-
"(",
|
|
1557
|
-
count,
|
|
1558
|
-
")"
|
|
1559
|
-
]
|
|
1560
|
-
})]
|
|
1561
|
-
}, tab);
|
|
1562
|
-
})
|
|
1563
|
-
}),
|
|
1564
|
-
/* @__PURE__ */ jsxs("div", {
|
|
1565
|
-
className: "flex-1 overflow-auto p-4",
|
|
1566
|
-
children: [
|
|
1567
|
-
activeTab === "attributes" && /* @__PURE__ */ jsx(AttributesTab$1, { span }),
|
|
1568
|
-
activeTab === "events" && /* @__PURE__ */ jsx(EventsTab, { span }),
|
|
1569
|
-
activeTab === "links" && /* @__PURE__ */ jsx(LinksTab, {
|
|
1570
|
-
span,
|
|
1571
|
-
onLinkClick
|
|
1572
|
-
})
|
|
1573
|
-
]
|
|
1574
|
-
})
|
|
1575
|
-
]
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
|
|
1579
|
-
//#endregion
|
|
1580
|
-
//#region src/components/observability/shared/LoadingSkeleton.tsx
|
|
1581
|
-
function LoadingSkeleton() {
|
|
1582
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
1583
|
-
className: "flex flex-col h-full bg-background animate-pulse",
|
|
1584
|
-
children: [/* @__PURE__ */ jsxs("div", {
|
|
1585
|
-
className: "border-b border-border p-4",
|
|
1586
|
-
children: [/* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-1/4 mb-3" }), /* @__PURE__ */ jsxs("div", {
|
|
1587
|
-
className: "flex gap-4",
|
|
1588
|
-
children: [
|
|
1589
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-32" }),
|
|
1590
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-24" }),
|
|
1591
|
-
/* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-20" })
|
|
1592
|
-
]
|
|
1593
|
-
})]
|
|
1594
|
-
}), /* @__PURE__ */ jsx("div", {
|
|
1595
|
-
className: "flex-1 p-4 space-y-2",
|
|
1596
|
-
children: Array.from({ length: 15 }).map((_, i) => /* @__PURE__ */ jsxs("div", {
|
|
1597
|
-
className: "flex items-start gap-3",
|
|
1598
|
-
children: [
|
|
1599
|
-
/* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-32" }),
|
|
1600
|
-
/* @__PURE__ */ jsx("div", {
|
|
1601
|
-
className: "h-4 rounded w-16",
|
|
1602
|
-
style: {
|
|
1603
|
-
backgroundColor: i % 4 === 0 ? "#ef4444" : i % 4 === 1 ? "#f97316" : i % 4 === 2 ? "#3b82f6" : "#6b7280",
|
|
1604
|
-
opacity: .3
|
|
1605
|
-
}
|
|
1606
|
-
}),
|
|
1607
|
-
/* @__PURE__ */ jsx("div", {
|
|
1608
|
-
className: "h-4 bg-muted rounded",
|
|
1609
|
-
style: { width: `${80 + i * 7 % 40}px` }
|
|
1610
|
-
}),
|
|
1611
|
-
/* @__PURE__ */ jsx("div", {
|
|
1612
|
-
className: "h-4 bg-muted/80 rounded flex-1",
|
|
1613
|
-
style: { maxWidth: `${300 + i * 13 % 200}px` }
|
|
1614
|
-
})
|
|
1615
|
-
]
|
|
1616
|
-
}, i))
|
|
1617
|
-
})]
|
|
1618
|
-
});
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
|
-
//#endregion
|
|
1622
|
-
//#region src/components/KeyboardShortcuts/context.ts
|
|
1623
|
-
const noop = () => {};
|
|
1624
|
-
const KeyboardShortcutsContext = createContext({
|
|
1625
|
-
register: noop,
|
|
1626
|
-
unregister: noop
|
|
1627
|
-
});
|
|
1628
|
-
function useRegisterShortcuts(id, group) {
|
|
1629
|
-
const { register, unregister } = useContext(KeyboardShortcutsContext);
|
|
1630
|
-
useEffect(() => {
|
|
1631
|
-
register(id, group);
|
|
1632
|
-
return () => unregister(id);
|
|
1633
|
-
}, [
|
|
1634
|
-
id,
|
|
1635
|
-
group,
|
|
1636
|
-
register,
|
|
1637
|
-
unregister
|
|
1638
|
-
]);
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
//#endregion
|
|
1642
|
-
//#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
|
|
1643
|
-
function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
1644
|
-
if (!open) return null;
|
|
1645
|
-
const handleKeyDown = (e) => {
|
|
1646
|
-
if (e.key === "Escape") {
|
|
1647
|
-
e.stopPropagation();
|
|
1648
|
-
onClose();
|
|
1613
|
+
//#region src/components/observability/shared/LoadingSkeleton.tsx
|
|
1614
|
+
function LoadingSkeleton() {
|
|
1615
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1616
|
+
className: "flex flex-col h-full bg-background animate-pulse",
|
|
1617
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
1618
|
+
className: "border-b border-border p-4",
|
|
1619
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-1/4 mb-3" }), /* @__PURE__ */ jsxs("div", {
|
|
1620
|
+
className: "flex gap-4",
|
|
1621
|
+
children: [
|
|
1622
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-32" }),
|
|
1623
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-24" }),
|
|
1624
|
+
/* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-20" })
|
|
1625
|
+
]
|
|
1626
|
+
})]
|
|
1627
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1628
|
+
className: "flex-1 p-4 space-y-2",
|
|
1629
|
+
children: Array.from({ length: 15 }).map((_, i) => /* @__PURE__ */ jsxs("div", {
|
|
1630
|
+
className: "flex items-start gap-3",
|
|
1631
|
+
children: [
|
|
1632
|
+
/* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-32" }),
|
|
1633
|
+
/* @__PURE__ */ jsx("div", {
|
|
1634
|
+
className: "h-4 rounded w-16",
|
|
1635
|
+
style: {
|
|
1636
|
+
backgroundColor: i % 4 === 0 ? "#ef4444" : i % 4 === 1 ? "#f97316" : i % 4 === 2 ? "#3b82f6" : "#6b7280",
|
|
1637
|
+
opacity: .3
|
|
1638
|
+
}
|
|
1639
|
+
}),
|
|
1640
|
+
/* @__PURE__ */ jsx("div", {
|
|
1641
|
+
className: "h-4 bg-muted rounded",
|
|
1642
|
+
style: { width: `${80 + i * 7 % 40}px` }
|
|
1643
|
+
}),
|
|
1644
|
+
/* @__PURE__ */ jsx("div", {
|
|
1645
|
+
className: "h-4 bg-muted/80 rounded flex-1",
|
|
1646
|
+
style: { maxWidth: `${300 + i * 13 % 200}px` }
|
|
1647
|
+
})
|
|
1648
|
+
]
|
|
1649
|
+
}, i))
|
|
1650
|
+
})]
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
//#endregion
|
|
1654
|
+
//#region src/components/KeyboardShortcuts/context.ts
|
|
1655
|
+
const noop = () => {};
|
|
1656
|
+
const KeyboardShortcutsContext = createContext({
|
|
1657
|
+
register: noop,
|
|
1658
|
+
unregister: noop
|
|
1659
|
+
});
|
|
1660
|
+
function useRegisterShortcuts(id, group) {
|
|
1661
|
+
const { register, unregister } = useContext(KeyboardShortcutsContext);
|
|
1662
|
+
useEffect(() => {
|
|
1663
|
+
register(id, group);
|
|
1664
|
+
return () => unregister(id);
|
|
1665
|
+
}, [
|
|
1666
|
+
id,
|
|
1667
|
+
group,
|
|
1668
|
+
register,
|
|
1669
|
+
unregister
|
|
1670
|
+
]);
|
|
1671
|
+
}
|
|
1672
|
+
//#endregion
|
|
1673
|
+
//#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
|
|
1674
|
+
function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
1675
|
+
if (!open) return null;
|
|
1676
|
+
const handleKeyDown = (e) => {
|
|
1677
|
+
if (e.key === "Escape") {
|
|
1678
|
+
e.stopPropagation();
|
|
1679
|
+
onClose();
|
|
1649
1680
|
}
|
|
1650
1681
|
};
|
|
1651
1682
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -1694,154 +1725,1059 @@ function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
|
1694
1725
|
})
|
|
1695
1726
|
});
|
|
1696
1727
|
}
|
|
1697
|
-
|
|
1698
1728
|
//#endregion
|
|
1699
|
-
//#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
|
|
1700
|
-
const GENERAL_GROUP = {
|
|
1701
|
-
name: "General",
|
|
1702
|
-
shortcuts: [
|
|
1703
|
-
{
|
|
1704
|
-
keys: ["Shift", "?"],
|
|
1705
|
-
description: "Toggle shortcuts help"
|
|
1706
|
-
},
|
|
1707
|
-
{
|
|
1708
|
-
keys: ["Shift", "
|
|
1709
|
-
description: "
|
|
1710
|
-
},
|
|
1711
|
-
{
|
|
1712
|
-
keys: ["Shift", "L"],
|
|
1713
|
-
description: "Logs tab"
|
|
1714
|
-
},
|
|
1715
|
-
{
|
|
1716
|
-
keys: ["Shift", "M"],
|
|
1717
|
-
description: "Metrics tab"
|
|
1718
|
-
}
|
|
1719
|
-
]
|
|
1720
|
-
};
|
|
1721
|
-
function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLogs, onNavigateMetrics }) {
|
|
1722
|
-
const [registry, setRegistry] = useState(() => /* @__PURE__ */ new Map());
|
|
1723
|
-
const [isOpen, setIsOpen] = useState(false);
|
|
1724
|
-
const register = useCallback((id, group) => {
|
|
1725
|
-
setRegistry((prev) => {
|
|
1726
|
-
const next = new Map(prev);
|
|
1727
|
-
next.set(id, group);
|
|
1728
|
-
return next;
|
|
1729
|
-
});
|
|
1729
|
+
//#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
|
|
1730
|
+
const GENERAL_GROUP = {
|
|
1731
|
+
name: "General",
|
|
1732
|
+
shortcuts: [
|
|
1733
|
+
{
|
|
1734
|
+
keys: ["Shift", "?"],
|
|
1735
|
+
description: "Toggle shortcuts help"
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
keys: ["Shift", "T"],
|
|
1739
|
+
description: "Traces tab"
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
keys: ["Shift", "L"],
|
|
1743
|
+
description: "Logs tab"
|
|
1744
|
+
},
|
|
1745
|
+
{
|
|
1746
|
+
keys: ["Shift", "M"],
|
|
1747
|
+
description: "Metrics tab"
|
|
1748
|
+
}
|
|
1749
|
+
]
|
|
1750
|
+
};
|
|
1751
|
+
function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLogs, onNavigateMetrics }) {
|
|
1752
|
+
const [registry, setRegistry] = useState(() => /* @__PURE__ */ new Map());
|
|
1753
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
1754
|
+
const register = useCallback((id, group) => {
|
|
1755
|
+
setRegistry((prev) => {
|
|
1756
|
+
const next = new Map(prev);
|
|
1757
|
+
next.set(id, group);
|
|
1758
|
+
return next;
|
|
1759
|
+
});
|
|
1760
|
+
}, []);
|
|
1761
|
+
const unregister = useCallback((id) => {
|
|
1762
|
+
setRegistry((prev) => {
|
|
1763
|
+
const next = new Map(prev);
|
|
1764
|
+
next.delete(id);
|
|
1765
|
+
return next;
|
|
1766
|
+
});
|
|
1767
|
+
}, []);
|
|
1768
|
+
useEffect(() => {
|
|
1769
|
+
function handleKeyDown(e) {
|
|
1770
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
1771
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || e.target.isContentEditable) return;
|
|
1772
|
+
if (e.shiftKey && e.key === "?") {
|
|
1773
|
+
e.preventDefault();
|
|
1774
|
+
setIsOpen((v) => !v);
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
if (e.key === "Escape" && isOpen) {
|
|
1778
|
+
e.preventDefault();
|
|
1779
|
+
setIsOpen(false);
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
if (e.shiftKey && e.key === "T") {
|
|
1783
|
+
e.preventDefault();
|
|
1784
|
+
onNavigateServices();
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (e.shiftKey && e.key === "L") {
|
|
1788
|
+
e.preventDefault();
|
|
1789
|
+
onNavigateLogs();
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
if (e.shiftKey && e.key === "M") {
|
|
1793
|
+
e.preventDefault();
|
|
1794
|
+
onNavigateMetrics();
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1799
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1800
|
+
}, [
|
|
1801
|
+
isOpen,
|
|
1802
|
+
onNavigateServices,
|
|
1803
|
+
onNavigateLogs,
|
|
1804
|
+
onNavigateMetrics
|
|
1805
|
+
]);
|
|
1806
|
+
const groups = useMemo(() => {
|
|
1807
|
+
return [GENERAL_GROUP, ...registry.values()];
|
|
1808
|
+
}, [registry]);
|
|
1809
|
+
const contextValue = useMemo(() => ({
|
|
1810
|
+
register,
|
|
1811
|
+
unregister
|
|
1812
|
+
}), [register, unregister]);
|
|
1813
|
+
return /* @__PURE__ */ jsxs(KeyboardShortcutsContext.Provider, {
|
|
1814
|
+
value: contextValue,
|
|
1815
|
+
children: [children, /* @__PURE__ */ jsx(ShortcutsHelpDialog, {
|
|
1816
|
+
open: isOpen,
|
|
1817
|
+
onClose: () => setIsOpen(false),
|
|
1818
|
+
groups
|
|
1819
|
+
})]
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
//#endregion
|
|
1823
|
+
//#region src/components/observability/TraceTimeline/shortcuts.ts
|
|
1824
|
+
const TRACE_VIEWER_SHORTCUTS = {
|
|
1825
|
+
name: "Trace Viewer",
|
|
1826
|
+
shortcuts: [
|
|
1827
|
+
{
|
|
1828
|
+
keys: ["↑/K"],
|
|
1829
|
+
description: "Previous span"
|
|
1830
|
+
},
|
|
1831
|
+
{
|
|
1832
|
+
keys: ["↓/J"],
|
|
1833
|
+
description: "Next span"
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
keys: ["←"],
|
|
1837
|
+
description: "Collapse span"
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
keys: ["→"],
|
|
1841
|
+
description: "Expand span"
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
keys: ["Enter"],
|
|
1845
|
+
description: "Focus detail pane"
|
|
1846
|
+
},
|
|
1847
|
+
{
|
|
1848
|
+
keys: ["C"],
|
|
1849
|
+
description: "Copy span name"
|
|
1850
|
+
},
|
|
1851
|
+
{
|
|
1852
|
+
keys: ["Esc"],
|
|
1853
|
+
description: "Deselect span"
|
|
1854
|
+
},
|
|
1855
|
+
{
|
|
1856
|
+
keys: [
|
|
1857
|
+
"Ctrl",
|
|
1858
|
+
"Shift",
|
|
1859
|
+
"E"
|
|
1860
|
+
],
|
|
1861
|
+
description: "Expand all"
|
|
1862
|
+
},
|
|
1863
|
+
{
|
|
1864
|
+
keys: [
|
|
1865
|
+
"Ctrl",
|
|
1866
|
+
"Shift",
|
|
1867
|
+
"C"
|
|
1868
|
+
],
|
|
1869
|
+
description: "Collapse all"
|
|
1870
|
+
}
|
|
1871
|
+
]
|
|
1872
|
+
};
|
|
1873
|
+
//#endregion
|
|
1874
|
+
//#region src/components/observability/TraceTimeline/TimeRuler.tsx
|
|
1875
|
+
const TICK_COUNT = 5;
|
|
1876
|
+
function TimeRuler({ totalDurationMs, leftColumnWidth, offsetMs = 0 }) {
|
|
1877
|
+
const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => {
|
|
1878
|
+
const fraction = i / TICK_COUNT;
|
|
1879
|
+
return {
|
|
1880
|
+
label: formatDuration(offsetMs + totalDurationMs * fraction),
|
|
1881
|
+
percent: fraction * 100
|
|
1882
|
+
};
|
|
1883
|
+
});
|
|
1884
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1885
|
+
className: "flex border-b border-border bg-background",
|
|
1886
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
1887
|
+
className: "flex-shrink-0",
|
|
1888
|
+
style: { width: leftColumnWidth }
|
|
1889
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
1890
|
+
className: "flex-1 relative h-6 px-2",
|
|
1891
|
+
children: ticks.map((tick) => /* @__PURE__ */ jsxs("div", {
|
|
1892
|
+
className: "absolute top-0 h-full flex flex-col justify-end",
|
|
1893
|
+
style: { left: `${tick.percent}%` },
|
|
1894
|
+
children: [/* @__PURE__ */ jsx("div", { className: "h-2 border-l border-muted-foreground/40" }), /* @__PURE__ */ jsx("span", {
|
|
1895
|
+
className: "text-[10px] text-muted-foreground font-mono -translate-x-1/2 absolute bottom-0 whitespace-nowrap",
|
|
1896
|
+
style: {
|
|
1897
|
+
left: 0,
|
|
1898
|
+
transform: tick.percent === 100 ? "translateX(-100%)" : tick.percent === 0 ? "none" : "translateX(-50%)"
|
|
1899
|
+
},
|
|
1900
|
+
children: tick.label
|
|
1901
|
+
})]
|
|
1902
|
+
}, tick.percent))
|
|
1903
|
+
})]
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
//#endregion
|
|
1907
|
+
//#region src/components/observability/TraceTimeline/SpanSearch.tsx
|
|
1908
|
+
function SpanSearch({ value, onChange, matchCount, currentMatch, onPrev, onNext }) {
|
|
1909
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1910
|
+
className: "flex items-center gap-1 px-2 py-1 border-b border-border bg-background",
|
|
1911
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
1912
|
+
type: "text",
|
|
1913
|
+
placeholder: "Find...",
|
|
1914
|
+
value,
|
|
1915
|
+
onChange: (e) => onChange(e.target.value),
|
|
1916
|
+
className: "bg-muted text-foreground text-sm px-2 py-0.5 rounded border border-border outline-none focus:border-blue-500 w-48"
|
|
1917
|
+
}), value && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1918
|
+
/* @__PURE__ */ jsx("span", {
|
|
1919
|
+
className: "text-xs text-muted-foreground whitespace-nowrap",
|
|
1920
|
+
children: matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : "0 matches"
|
|
1921
|
+
}),
|
|
1922
|
+
/* @__PURE__ */ jsx("button", {
|
|
1923
|
+
onClick: onPrev,
|
|
1924
|
+
disabled: matchCount === 0,
|
|
1925
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1926
|
+
"aria-label": "Previous match",
|
|
1927
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1928
|
+
className: "w-3.5 h-3.5",
|
|
1929
|
+
fill: "none",
|
|
1930
|
+
stroke: "currentColor",
|
|
1931
|
+
viewBox: "0 0 24 24",
|
|
1932
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1933
|
+
strokeLinecap: "round",
|
|
1934
|
+
strokeLinejoin: "round",
|
|
1935
|
+
strokeWidth: 2,
|
|
1936
|
+
d: "M5 15l7-7 7 7"
|
|
1937
|
+
})
|
|
1938
|
+
})
|
|
1939
|
+
}),
|
|
1940
|
+
/* @__PURE__ */ jsx("button", {
|
|
1941
|
+
onClick: onNext,
|
|
1942
|
+
disabled: matchCount === 0,
|
|
1943
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1944
|
+
"aria-label": "Next match",
|
|
1945
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
1946
|
+
className: "w-3.5 h-3.5",
|
|
1947
|
+
fill: "none",
|
|
1948
|
+
stroke: "currentColor",
|
|
1949
|
+
viewBox: "0 0 24 24",
|
|
1950
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
1951
|
+
strokeLinecap: "round",
|
|
1952
|
+
strokeLinejoin: "round",
|
|
1953
|
+
strokeWidth: 2,
|
|
1954
|
+
d: "M19 9l-7 7-7-7"
|
|
1955
|
+
})
|
|
1956
|
+
})
|
|
1957
|
+
})
|
|
1958
|
+
] })]
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
//#endregion
|
|
1962
|
+
//#region src/components/observability/TraceTimeline/ViewTabs.tsx
|
|
1963
|
+
const VIEWS = [
|
|
1964
|
+
"timeline",
|
|
1965
|
+
"graph",
|
|
1966
|
+
"statistics",
|
|
1967
|
+
"flamegraph"
|
|
1968
|
+
];
|
|
1969
|
+
const VIEW_LABELS = {
|
|
1970
|
+
timeline: "Timeline",
|
|
1971
|
+
graph: "Graph",
|
|
1972
|
+
statistics: "Statistics",
|
|
1973
|
+
flamegraph: "Flamegraph"
|
|
1974
|
+
};
|
|
1975
|
+
function ViewTabs({ activeView, onChange }) {
|
|
1976
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1977
|
+
className: "flex border-b border-border bg-background",
|
|
1978
|
+
children: VIEWS.map((view) => /* @__PURE__ */ jsx("button", {
|
|
1979
|
+
onClick: () => onChange(view),
|
|
1980
|
+
className: `px-4 py-1.5 text-sm font-medium transition-colors ${activeView === view ? "text-foreground border-b-2 border-blue-500" : "text-muted-foreground hover:text-foreground"}`,
|
|
1981
|
+
children: VIEW_LABELS[view]
|
|
1982
|
+
}, view))
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
//#endregion
|
|
1986
|
+
//#region src/components/observability/TraceTimeline/GraphView.tsx
|
|
1987
|
+
/**
|
|
1988
|
+
* GraphView - SVG-based DAG showing service dependencies within a trace.
|
|
1989
|
+
*/
|
|
1990
|
+
function buildDAG(trace) {
|
|
1991
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
1992
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
1993
|
+
const childServices = /* @__PURE__ */ new Map();
|
|
1994
|
+
function walk(span, parentService) {
|
|
1995
|
+
const svc = span.serviceName;
|
|
1996
|
+
const existing = nodeMap.get(svc);
|
|
1997
|
+
if (existing) {
|
|
1998
|
+
existing.spanCount++;
|
|
1999
|
+
if (span.status === "ERROR") existing.errorCount++;
|
|
2000
|
+
} else nodeMap.set(svc, {
|
|
2001
|
+
spanCount: 1,
|
|
2002
|
+
errorCount: span.status === "ERROR" ? 1 : 0
|
|
2003
|
+
});
|
|
2004
|
+
if (parentService && parentService !== svc) {
|
|
2005
|
+
const key = `${parentService}→${svc}`;
|
|
2006
|
+
const edge = edgeMap.get(key);
|
|
2007
|
+
if (edge) {
|
|
2008
|
+
edge.callCount++;
|
|
2009
|
+
edge.totalDurationMs += span.durationMs;
|
|
2010
|
+
} else edgeMap.set(key, {
|
|
2011
|
+
callCount: 1,
|
|
2012
|
+
totalDurationMs: span.durationMs
|
|
2013
|
+
});
|
|
2014
|
+
if (!childServices.has(parentService)) childServices.set(parentService, /* @__PURE__ */ new Set());
|
|
2015
|
+
const parentChildren = childServices.get(parentService);
|
|
2016
|
+
if (parentChildren) parentChildren.add(svc);
|
|
2017
|
+
}
|
|
2018
|
+
for (const child of span.children) walk(child, svc);
|
|
2019
|
+
}
|
|
2020
|
+
for (const root of trace.rootSpans) walk(root);
|
|
2021
|
+
const edges = [];
|
|
2022
|
+
for (const [key, meta] of edgeMap) {
|
|
2023
|
+
const [from, to] = key.split("→");
|
|
2024
|
+
if (from && to) edges.push({
|
|
2025
|
+
from,
|
|
2026
|
+
to,
|
|
2027
|
+
...meta
|
|
2028
|
+
});
|
|
2029
|
+
}
|
|
2030
|
+
return {
|
|
2031
|
+
nodeMap,
|
|
2032
|
+
edges,
|
|
2033
|
+
childServices
|
|
2034
|
+
};
|
|
2035
|
+
}
|
|
2036
|
+
const NODE_W = 160;
|
|
2037
|
+
const NODE_H = 60;
|
|
2038
|
+
const LAYER_GAP_Y = 100;
|
|
2039
|
+
const NODE_GAP_X = 40;
|
|
2040
|
+
function layoutNodes(nodeMap, edges) {
|
|
2041
|
+
const children = /* @__PURE__ */ new Map();
|
|
2042
|
+
const hasParent = /* @__PURE__ */ new Set();
|
|
2043
|
+
for (const e of edges) {
|
|
2044
|
+
if (!children.has(e.from)) children.set(e.from, /* @__PURE__ */ new Set());
|
|
2045
|
+
const fromChildren = children.get(e.from);
|
|
2046
|
+
if (fromChildren) fromChildren.add(e.to);
|
|
2047
|
+
hasParent.add(e.to);
|
|
2048
|
+
}
|
|
2049
|
+
const roots = [...nodeMap.keys()].filter((s) => !hasParent.has(s));
|
|
2050
|
+
if (roots.length === 0 && nodeMap.size > 0) {
|
|
2051
|
+
const firstKey = nodeMap.keys().next().value;
|
|
2052
|
+
if (firstKey !== void 0) roots.push(firstKey);
|
|
2053
|
+
}
|
|
2054
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
2055
|
+
const enqueueCount = /* @__PURE__ */ new Map();
|
|
2056
|
+
const maxEnqueue = nodeMap.size * 2;
|
|
2057
|
+
const queue = [];
|
|
2058
|
+
for (const r of roots) {
|
|
2059
|
+
layerOf.set(r, 0);
|
|
2060
|
+
queue.push(r);
|
|
2061
|
+
}
|
|
2062
|
+
while (queue.length > 0) {
|
|
2063
|
+
const cur = queue.shift();
|
|
2064
|
+
if (!cur) continue;
|
|
2065
|
+
const curLayer = layerOf.get(cur);
|
|
2066
|
+
if (curLayer === void 0) continue;
|
|
2067
|
+
const kids = children.get(cur);
|
|
2068
|
+
if (!kids) continue;
|
|
2069
|
+
for (const kid of kids) {
|
|
2070
|
+
const prev = layerOf.get(kid);
|
|
2071
|
+
const count = enqueueCount.get(kid) ?? 0;
|
|
2072
|
+
if (prev === void 0 && count < maxEnqueue) {
|
|
2073
|
+
layerOf.set(kid, curLayer + 1);
|
|
2074
|
+
enqueueCount.set(kid, count + 1);
|
|
2075
|
+
queue.push(kid);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
for (const name of nodeMap.keys()) if (!layerOf.has(name)) layerOf.set(name, 0);
|
|
2080
|
+
const layers = /* @__PURE__ */ new Map();
|
|
2081
|
+
for (const [name, layer] of layerOf) {
|
|
2082
|
+
if (!layers.has(layer)) layers.set(layer, []);
|
|
2083
|
+
const layerNames = layers.get(layer);
|
|
2084
|
+
if (layerNames) layerNames.push(name);
|
|
2085
|
+
}
|
|
2086
|
+
const nodes = [];
|
|
2087
|
+
const totalWidth = Math.max(...Array.from(layers.values()).map((l) => l.length), 1) * (NODE_W + NODE_GAP_X) - NODE_GAP_X;
|
|
2088
|
+
for (const [layer, names] of layers) {
|
|
2089
|
+
const offsetX = (totalWidth - (names.length * (NODE_W + NODE_GAP_X) - NODE_GAP_X)) / 2;
|
|
2090
|
+
names.forEach((name, i) => {
|
|
2091
|
+
const meta = nodeMap.get(name);
|
|
2092
|
+
if (!meta) return;
|
|
2093
|
+
nodes.push({
|
|
2094
|
+
name,
|
|
2095
|
+
spanCount: meta.spanCount,
|
|
2096
|
+
errorCount: meta.errorCount,
|
|
2097
|
+
layer,
|
|
2098
|
+
x: offsetX + i * (NODE_W + NODE_GAP_X),
|
|
2099
|
+
y: layer * (NODE_H + LAYER_GAP_Y)
|
|
2100
|
+
});
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
return nodes;
|
|
2104
|
+
}
|
|
2105
|
+
function GraphView({ trace }) {
|
|
2106
|
+
const { nodes, edges, svgWidth, svgHeight } = useMemo(() => {
|
|
2107
|
+
const { nodeMap, edges } = buildDAG(trace);
|
|
2108
|
+
const nodes = layoutNodes(nodeMap, edges);
|
|
2109
|
+
const maxX = Math.max(...nodes.map((n) => n.x + NODE_W), NODE_W);
|
|
2110
|
+
const maxY = Math.max(...nodes.map((n) => n.y + NODE_H), NODE_H);
|
|
2111
|
+
const padding = 40;
|
|
2112
|
+
return {
|
|
2113
|
+
nodes,
|
|
2114
|
+
edges,
|
|
2115
|
+
svgWidth: maxX + padding * 2,
|
|
2116
|
+
svgHeight: maxY + padding * 2
|
|
2117
|
+
};
|
|
2118
|
+
}, [trace]);
|
|
2119
|
+
const nodeByName = useMemo(() => {
|
|
2120
|
+
const m = /* @__PURE__ */ new Map();
|
|
2121
|
+
for (const n of nodes) m.set(n.name, n);
|
|
2122
|
+
return m;
|
|
2123
|
+
}, [nodes]);
|
|
2124
|
+
const padding = 40;
|
|
2125
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2126
|
+
className: "flex-1 overflow-auto bg-background p-4 flex justify-center",
|
|
2127
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
2128
|
+
viewBox: `0 0 ${svgWidth} ${svgHeight}`,
|
|
2129
|
+
width: svgWidth,
|
|
2130
|
+
height: svgHeight,
|
|
2131
|
+
role: "img",
|
|
2132
|
+
"aria-label": "Service dependency graph",
|
|
2133
|
+
children: [
|
|
2134
|
+
/* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
|
|
2135
|
+
id: "arrowhead",
|
|
2136
|
+
markerWidth: "10",
|
|
2137
|
+
markerHeight: "7",
|
|
2138
|
+
refX: "9",
|
|
2139
|
+
refY: "3.5",
|
|
2140
|
+
orient: "auto",
|
|
2141
|
+
children: /* @__PURE__ */ jsx("polygon", {
|
|
2142
|
+
points: "0 0, 10 3.5, 0 7",
|
|
2143
|
+
fill: "#94a3b8"
|
|
2144
|
+
})
|
|
2145
|
+
}) }),
|
|
2146
|
+
edges.map((edge) => {
|
|
2147
|
+
const from = nodeByName.get(edge.from);
|
|
2148
|
+
const to = nodeByName.get(edge.to);
|
|
2149
|
+
if (!from || !to) return null;
|
|
2150
|
+
const x1 = padding + from.x + NODE_W / 2;
|
|
2151
|
+
const y1 = padding + from.y + NODE_H;
|
|
2152
|
+
const x2 = padding + to.x + NODE_W / 2;
|
|
2153
|
+
const y2 = padding + to.y;
|
|
2154
|
+
const midY = (y1 + y2) / 2;
|
|
2155
|
+
return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
|
|
2156
|
+
d: `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`,
|
|
2157
|
+
fill: "none",
|
|
2158
|
+
stroke: "#475569",
|
|
2159
|
+
strokeWidth: 1.5,
|
|
2160
|
+
markerEnd: "url(#arrowhead)"
|
|
2161
|
+
}), edge.callCount > 1 && /* @__PURE__ */ jsxs("text", {
|
|
2162
|
+
x: (x1 + x2) / 2,
|
|
2163
|
+
y: midY - 6,
|
|
2164
|
+
textAnchor: "middle",
|
|
2165
|
+
fontSize: 11,
|
|
2166
|
+
fill: "#94a3b8",
|
|
2167
|
+
children: [edge.callCount, "x"]
|
|
2168
|
+
})] }, `${edge.from}→${edge.to}`);
|
|
2169
|
+
}),
|
|
2170
|
+
nodes.map((node) => {
|
|
2171
|
+
const color = getServiceColor(node.name);
|
|
2172
|
+
const hasError = node.errorCount > 0;
|
|
2173
|
+
const textColor = "#f8fafc";
|
|
2174
|
+
const nx = padding + node.x;
|
|
2175
|
+
const ny = padding + node.y;
|
|
2176
|
+
return /* @__PURE__ */ jsxs("g", { children: [
|
|
2177
|
+
/* @__PURE__ */ jsx("rect", {
|
|
2178
|
+
x: nx,
|
|
2179
|
+
y: ny,
|
|
2180
|
+
width: NODE_W,
|
|
2181
|
+
height: NODE_H,
|
|
2182
|
+
rx: 8,
|
|
2183
|
+
ry: 8,
|
|
2184
|
+
fill: color,
|
|
2185
|
+
stroke: hasError ? "#ef4444" : "none",
|
|
2186
|
+
strokeWidth: hasError ? 2 : 0
|
|
2187
|
+
}),
|
|
2188
|
+
/* @__PURE__ */ jsx("text", {
|
|
2189
|
+
x: nx + NODE_W / 2,
|
|
2190
|
+
y: ny + 24,
|
|
2191
|
+
textAnchor: "middle",
|
|
2192
|
+
fontSize: 13,
|
|
2193
|
+
fontWeight: 600,
|
|
2194
|
+
fill: textColor,
|
|
2195
|
+
children: node.name.length > 18 ? node.name.slice(0, 16) + "..." : node.name
|
|
2196
|
+
}),
|
|
2197
|
+
/* @__PURE__ */ jsxs("text", {
|
|
2198
|
+
x: nx + NODE_W / 2,
|
|
2199
|
+
y: ny + 44,
|
|
2200
|
+
textAnchor: "middle",
|
|
2201
|
+
fontSize: 11,
|
|
2202
|
+
fill: textColor,
|
|
2203
|
+
opacity: .85,
|
|
2204
|
+
children: [
|
|
2205
|
+
node.spanCount,
|
|
2206
|
+
" span",
|
|
2207
|
+
node.spanCount !== 1 ? "s" : "",
|
|
2208
|
+
node.errorCount > 0 && ` · ${node.errorCount} err`
|
|
2209
|
+
]
|
|
2210
|
+
})
|
|
2211
|
+
] }, node.name);
|
|
2212
|
+
})
|
|
2213
|
+
]
|
|
2214
|
+
})
|
|
2215
|
+
});
|
|
2216
|
+
}
|
|
2217
|
+
//#endregion
|
|
2218
|
+
//#region src/components/observability/TraceTimeline/StatisticsView.tsx
|
|
2219
|
+
function computeSelfTime(span) {
|
|
2220
|
+
const childrenTotal = span.children.reduce((sum, child) => sum + child.durationMs, 0);
|
|
2221
|
+
return Math.max(0, span.durationMs - childrenTotal);
|
|
2222
|
+
}
|
|
2223
|
+
function computeStats(trace) {
|
|
2224
|
+
const allFlattened = flattenAllSpans(trace.rootSpans);
|
|
2225
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2226
|
+
for (const { span } of allFlattened) {
|
|
2227
|
+
const key = `${span.serviceName}:${span.name}`;
|
|
2228
|
+
let group = groups.get(key);
|
|
2229
|
+
if (!group) {
|
|
2230
|
+
group = {
|
|
2231
|
+
spans: [],
|
|
2232
|
+
selfTimes: []
|
|
2233
|
+
};
|
|
2234
|
+
groups.set(key, group);
|
|
2235
|
+
}
|
|
2236
|
+
group.spans.push(span);
|
|
2237
|
+
group.selfTimes.push(computeSelfTime(span));
|
|
2238
|
+
}
|
|
2239
|
+
const stats = [];
|
|
2240
|
+
for (const [key, { spans, selfTimes }] of groups) {
|
|
2241
|
+
const durations = spans.map((s) => s.durationMs);
|
|
2242
|
+
const count = spans.length;
|
|
2243
|
+
const totalDuration = durations.reduce((a, b) => a + b, 0);
|
|
2244
|
+
const selfTimeTotal = selfTimes.reduce((a, b) => a + b, 0);
|
|
2245
|
+
const firstSpan = spans[0];
|
|
2246
|
+
if (!firstSpan) continue;
|
|
2247
|
+
stats.push({
|
|
2248
|
+
key,
|
|
2249
|
+
serviceName: firstSpan.serviceName,
|
|
2250
|
+
spanName: firstSpan.name,
|
|
2251
|
+
count,
|
|
2252
|
+
totalDuration,
|
|
2253
|
+
avgDuration: totalDuration / count,
|
|
2254
|
+
minDuration: Math.min(...durations),
|
|
2255
|
+
maxDuration: Math.max(...durations),
|
|
2256
|
+
selfTimeTotal,
|
|
2257
|
+
selfTimeAvg: selfTimeTotal / count,
|
|
2258
|
+
selfTimeMin: Math.min(...selfTimes),
|
|
2259
|
+
selfTimeMax: Math.max(...selfTimes)
|
|
2260
|
+
});
|
|
2261
|
+
}
|
|
2262
|
+
return stats;
|
|
2263
|
+
}
|
|
2264
|
+
function getSortValue(stat, field) {
|
|
2265
|
+
switch (field) {
|
|
2266
|
+
case "name": return stat.key.toLowerCase();
|
|
2267
|
+
case "count": return stat.count;
|
|
2268
|
+
case "total": return stat.totalDuration;
|
|
2269
|
+
case "avg": return stat.avgDuration;
|
|
2270
|
+
case "min": return stat.minDuration;
|
|
2271
|
+
case "max": return stat.maxDuration;
|
|
2272
|
+
case "selfTotal": return stat.selfTimeTotal;
|
|
2273
|
+
case "selfAvg": return stat.selfTimeAvg;
|
|
2274
|
+
case "selfMin": return stat.selfTimeMin;
|
|
2275
|
+
case "selfMax": return stat.selfTimeMax;
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
const COLUMNS = [
|
|
2279
|
+
{
|
|
2280
|
+
label: "Name",
|
|
2281
|
+
field: "name"
|
|
2282
|
+
},
|
|
2283
|
+
{
|
|
2284
|
+
label: "Count",
|
|
2285
|
+
field: "count"
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
label: "Total",
|
|
2289
|
+
field: "total"
|
|
2290
|
+
},
|
|
2291
|
+
{
|
|
2292
|
+
label: "Avg",
|
|
2293
|
+
field: "avg"
|
|
2294
|
+
},
|
|
2295
|
+
{
|
|
2296
|
+
label: "Min",
|
|
2297
|
+
field: "min"
|
|
2298
|
+
},
|
|
2299
|
+
{
|
|
2300
|
+
label: "Max",
|
|
2301
|
+
field: "max"
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
label: "ST Total",
|
|
2305
|
+
field: "selfTotal"
|
|
2306
|
+
},
|
|
2307
|
+
{
|
|
2308
|
+
label: "ST Avg",
|
|
2309
|
+
field: "selfAvg"
|
|
2310
|
+
},
|
|
2311
|
+
{
|
|
2312
|
+
label: "ST Min",
|
|
2313
|
+
field: "selfMin"
|
|
2314
|
+
},
|
|
2315
|
+
{
|
|
2316
|
+
label: "ST Max",
|
|
2317
|
+
field: "selfMax"
|
|
2318
|
+
}
|
|
2319
|
+
];
|
|
2320
|
+
function StatisticsView({ trace }) {
|
|
2321
|
+
const [sortField, setSortField] = useState("total");
|
|
2322
|
+
const [sortAsc, setSortAsc] = useState(false);
|
|
2323
|
+
const stats = useMemo(() => computeStats(trace), [trace]);
|
|
2324
|
+
const sorted = useMemo(() => {
|
|
2325
|
+
const copy = [...stats];
|
|
2326
|
+
copy.sort((a, b) => {
|
|
2327
|
+
const aVal = getSortValue(a, sortField);
|
|
2328
|
+
const bVal = getSortValue(b, sortField);
|
|
2329
|
+
let cmp;
|
|
2330
|
+
if (typeof aVal === "string" && typeof bVal === "string") cmp = aVal.localeCompare(bVal);
|
|
2331
|
+
else if (typeof aVal === "number" && typeof bVal === "number") cmp = aVal - bVal;
|
|
2332
|
+
else cmp = 0;
|
|
2333
|
+
return sortAsc ? cmp : -cmp;
|
|
2334
|
+
});
|
|
2335
|
+
return copy;
|
|
2336
|
+
}, [
|
|
2337
|
+
stats,
|
|
2338
|
+
sortField,
|
|
2339
|
+
sortAsc
|
|
2340
|
+
]);
|
|
2341
|
+
const handleSort = (field) => {
|
|
2342
|
+
if (sortField === field) setSortAsc((p) => !p);
|
|
2343
|
+
else {
|
|
2344
|
+
setSortField(field);
|
|
2345
|
+
setSortAsc(false);
|
|
2346
|
+
}
|
|
2347
|
+
};
|
|
2348
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2349
|
+
className: "flex-1 overflow-auto p-2",
|
|
2350
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
2351
|
+
className: "w-full text-sm border-collapse",
|
|
2352
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
|
|
2353
|
+
className: "border-b border-border",
|
|
2354
|
+
children: COLUMNS.map((col) => /* @__PURE__ */ jsxs("th", {
|
|
2355
|
+
className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground cursor-pointer select-none hover:text-foreground whitespace-nowrap",
|
|
2356
|
+
onClick: () => handleSort(col.field),
|
|
2357
|
+
children: [
|
|
2358
|
+
col.label,
|
|
2359
|
+
" ",
|
|
2360
|
+
sortField === col.field ? sortAsc ? "▲" : "▼" : ""
|
|
2361
|
+
]
|
|
2362
|
+
}, col.field))
|
|
2363
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((stat, i) => /* @__PURE__ */ jsxs("tr", {
|
|
2364
|
+
className: `border-b border-border/50 ${i % 2 === 0 ? "bg-background" : "bg-muted/30"}`,
|
|
2365
|
+
children: [
|
|
2366
|
+
/* @__PURE__ */ jsxs("td", {
|
|
2367
|
+
className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
|
|
2368
|
+
children: [
|
|
2369
|
+
/* @__PURE__ */ jsx("span", {
|
|
2370
|
+
className: "text-muted-foreground",
|
|
2371
|
+
children: stat.serviceName
|
|
2372
|
+
}),
|
|
2373
|
+
/* @__PURE__ */ jsx("span", {
|
|
2374
|
+
className: "text-muted-foreground/50",
|
|
2375
|
+
children: ":"
|
|
2376
|
+
}),
|
|
2377
|
+
" ",
|
|
2378
|
+
stat.spanName
|
|
2379
|
+
]
|
|
2380
|
+
}),
|
|
2381
|
+
/* @__PURE__ */ jsx("td", {
|
|
2382
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2383
|
+
children: stat.count
|
|
2384
|
+
}),
|
|
2385
|
+
/* @__PURE__ */ jsx("td", {
|
|
2386
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2387
|
+
children: formatDuration(stat.totalDuration)
|
|
2388
|
+
}),
|
|
2389
|
+
/* @__PURE__ */ jsx("td", {
|
|
2390
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2391
|
+
children: formatDuration(stat.avgDuration)
|
|
2392
|
+
}),
|
|
2393
|
+
/* @__PURE__ */ jsx("td", {
|
|
2394
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2395
|
+
children: formatDuration(stat.minDuration)
|
|
2396
|
+
}),
|
|
2397
|
+
/* @__PURE__ */ jsx("td", {
|
|
2398
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2399
|
+
children: formatDuration(stat.maxDuration)
|
|
2400
|
+
}),
|
|
2401
|
+
/* @__PURE__ */ jsx("td", {
|
|
2402
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2403
|
+
children: formatDuration(stat.selfTimeTotal)
|
|
2404
|
+
}),
|
|
2405
|
+
/* @__PURE__ */ jsx("td", {
|
|
2406
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2407
|
+
children: formatDuration(stat.selfTimeAvg)
|
|
2408
|
+
}),
|
|
2409
|
+
/* @__PURE__ */ jsx("td", {
|
|
2410
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2411
|
+
children: formatDuration(stat.selfTimeMin)
|
|
2412
|
+
}),
|
|
2413
|
+
/* @__PURE__ */ jsx("td", {
|
|
2414
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2415
|
+
children: formatDuration(stat.selfTimeMax)
|
|
2416
|
+
})
|
|
2417
|
+
]
|
|
2418
|
+
}, stat.key)) })]
|
|
2419
|
+
})
|
|
2420
|
+
});
|
|
2421
|
+
}
|
|
2422
|
+
//#endregion
|
|
2423
|
+
//#region src/components/observability/TraceTimeline/FlamegraphView.tsx
|
|
2424
|
+
const ROW_HEIGHT = 24;
|
|
2425
|
+
const MIN_WIDTH = 1;
|
|
2426
|
+
const LABEL_MIN_WIDTH = 40;
|
|
2427
|
+
function findSpanById(rootSpans, spanId) {
|
|
2428
|
+
for (const root of rootSpans) {
|
|
2429
|
+
if (root.spanId === spanId) return root;
|
|
2430
|
+
const found = findSpanById(root.children, spanId);
|
|
2431
|
+
if (found) return found;
|
|
2432
|
+
}
|
|
2433
|
+
return null;
|
|
2434
|
+
}
|
|
2435
|
+
function getAncestorPath(rootSpans, targetId) {
|
|
2436
|
+
const path = [];
|
|
2437
|
+
function walk(span, ancestors) {
|
|
2438
|
+
if (span.spanId === targetId) {
|
|
2439
|
+
path.push(...ancestors, span);
|
|
2440
|
+
return true;
|
|
2441
|
+
}
|
|
2442
|
+
for (const child of span.children) if (walk(child, [...ancestors, span])) return true;
|
|
2443
|
+
return false;
|
|
2444
|
+
}
|
|
2445
|
+
for (const root of rootSpans) if (walk(root, [])) break;
|
|
2446
|
+
return path;
|
|
2447
|
+
}
|
|
2448
|
+
function FlamegraphView({ trace, onSpanClick, selectedSpanId }) {
|
|
2449
|
+
const [zoomSpanId, setZoomSpanId] = useState(null);
|
|
2450
|
+
const [tooltip, setTooltip] = useState(null);
|
|
2451
|
+
const zoomRoot = useMemo(() => {
|
|
2452
|
+
if (!zoomSpanId) return null;
|
|
2453
|
+
return findSpanById(trace.rootSpans, zoomSpanId);
|
|
2454
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2455
|
+
const breadcrumbs = useMemo(() => {
|
|
2456
|
+
if (!zoomSpanId) return [];
|
|
2457
|
+
return getAncestorPath(trace.rootSpans, zoomSpanId);
|
|
2458
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2459
|
+
const viewRoots = zoomRoot ? [zoomRoot] : trace.rootSpans;
|
|
2460
|
+
const viewMinTime = zoomRoot ? zoomRoot.startTimeUnixMs : trace.minTimeMs;
|
|
2461
|
+
const viewDuration = (zoomRoot ? zoomRoot.endTimeUnixMs : trace.maxTimeMs) - viewMinTime;
|
|
2462
|
+
const flatSpans = useMemo(() => flattenAllSpans(viewRoots).map((fs) => ({
|
|
2463
|
+
span: fs.span,
|
|
2464
|
+
depth: fs.level
|
|
2465
|
+
})), [viewRoots]);
|
|
2466
|
+
const maxDepth = useMemo(() => flatSpans.reduce((max, fs) => Math.max(max, fs.depth), 0) + 1, [flatSpans]);
|
|
2467
|
+
const svgWidth = 1200;
|
|
2468
|
+
const svgHeight = maxDepth * ROW_HEIGHT;
|
|
2469
|
+
const handleClick = useCallback((span) => {
|
|
2470
|
+
onSpanClick?.(span);
|
|
2471
|
+
setZoomSpanId(span.spanId);
|
|
2472
|
+
}, [onSpanClick]);
|
|
2473
|
+
const handleZoomOut = useCallback((spanId) => {
|
|
2474
|
+
setZoomSpanId(spanId);
|
|
2475
|
+
}, []);
|
|
2476
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2477
|
+
className: "flex-1 overflow-auto p-2",
|
|
2478
|
+
children: [
|
|
2479
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
2480
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground mb-2 flex-wrap",
|
|
2481
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
2482
|
+
className: "hover:text-foreground underline",
|
|
2483
|
+
onClick: () => handleZoomOut(null),
|
|
2484
|
+
children: "root"
|
|
2485
|
+
}), breadcrumbs.map((bc, i) => /* @__PURE__ */ jsxs("span", {
|
|
2486
|
+
className: "flex items-center gap-1",
|
|
2487
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2488
|
+
className: "text-muted-foreground/50",
|
|
2489
|
+
children: ">"
|
|
2490
|
+
}), i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxs("button", {
|
|
2491
|
+
className: "hover:text-foreground underline",
|
|
2492
|
+
onClick: () => handleZoomOut(bc.spanId),
|
|
2493
|
+
children: [
|
|
2494
|
+
bc.serviceName,
|
|
2495
|
+
": ",
|
|
2496
|
+
bc.name
|
|
2497
|
+
]
|
|
2498
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
2499
|
+
className: "text-foreground",
|
|
2500
|
+
children: [
|
|
2501
|
+
bc.serviceName,
|
|
2502
|
+
": ",
|
|
2503
|
+
bc.name
|
|
2504
|
+
]
|
|
2505
|
+
})]
|
|
2506
|
+
}, bc.spanId))]
|
|
2507
|
+
}),
|
|
2508
|
+
/* @__PURE__ */ jsx("div", {
|
|
2509
|
+
className: "overflow-x-auto",
|
|
2510
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
2511
|
+
width: svgWidth,
|
|
2512
|
+
height: svgHeight,
|
|
2513
|
+
className: "block",
|
|
2514
|
+
onMouseLeave: () => setTooltip(null),
|
|
2515
|
+
children: flatSpans.map(({ span, depth }) => {
|
|
2516
|
+
const x = viewDuration > 0 ? (span.startTimeUnixMs - viewMinTime) / viewDuration * svgWidth : 0;
|
|
2517
|
+
const w = viewDuration > 0 ? Math.max(MIN_WIDTH, span.durationMs / viewDuration * svgWidth) : svgWidth;
|
|
2518
|
+
const y = depth * ROW_HEIGHT;
|
|
2519
|
+
const color = getServiceColor(span.serviceName);
|
|
2520
|
+
const isSelected = span.spanId === selectedSpanId;
|
|
2521
|
+
const showLabel = w >= LABEL_MIN_WIDTH;
|
|
2522
|
+
const label = `${span.serviceName}: ${span.name}`;
|
|
2523
|
+
return /* @__PURE__ */ jsxs("g", {
|
|
2524
|
+
className: "cursor-pointer",
|
|
2525
|
+
onClick: () => handleClick(span),
|
|
2526
|
+
onMouseEnter: (e) => setTooltip({
|
|
2527
|
+
span,
|
|
2528
|
+
x: e.clientX,
|
|
2529
|
+
y: e.clientY
|
|
2530
|
+
}),
|
|
2531
|
+
onMouseMove: (e) => setTooltip((prev) => prev ? {
|
|
2532
|
+
...prev,
|
|
2533
|
+
x: e.clientX,
|
|
2534
|
+
y: e.clientY
|
|
2535
|
+
} : null),
|
|
2536
|
+
onMouseLeave: () => setTooltip(null),
|
|
2537
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
2538
|
+
x,
|
|
2539
|
+
y,
|
|
2540
|
+
width: w,
|
|
2541
|
+
height: ROW_HEIGHT - 1,
|
|
2542
|
+
fill: color,
|
|
2543
|
+
opacity: .85,
|
|
2544
|
+
rx: 2,
|
|
2545
|
+
stroke: isSelected ? "#ffffff" : "transparent",
|
|
2546
|
+
strokeWidth: isSelected ? 2 : 0,
|
|
2547
|
+
className: "hover:opacity-100"
|
|
2548
|
+
}), showLabel && /* @__PURE__ */ jsx("text", {
|
|
2549
|
+
x: x + 4,
|
|
2550
|
+
y: y + ROW_HEIGHT / 2 + 1,
|
|
2551
|
+
dominantBaseline: "middle",
|
|
2552
|
+
fill: "#ffffff",
|
|
2553
|
+
fontSize: 11,
|
|
2554
|
+
fontFamily: "monospace",
|
|
2555
|
+
clipPath: `inset(0 0 0 0)`,
|
|
2556
|
+
children: /* @__PURE__ */ jsx("tspan", { children: label.length > w / 7 ? label.slice(0, Math.floor(w / 7) - 1) + "…" : label })
|
|
2557
|
+
})]
|
|
2558
|
+
}, span.spanId);
|
|
2559
|
+
})
|
|
2560
|
+
})
|
|
2561
|
+
}),
|
|
2562
|
+
tooltip && /* @__PURE__ */ jsxs("div", {
|
|
2563
|
+
className: "fixed z-50 pointer-events-none bg-popover border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
2564
|
+
style: {
|
|
2565
|
+
left: tooltip.x + 12,
|
|
2566
|
+
top: tooltip.y + 12
|
|
2567
|
+
},
|
|
2568
|
+
children: [
|
|
2569
|
+
/* @__PURE__ */ jsx("div", {
|
|
2570
|
+
className: "font-medium text-foreground",
|
|
2571
|
+
children: tooltip.span.name
|
|
2572
|
+
}),
|
|
2573
|
+
/* @__PURE__ */ jsx("div", {
|
|
2574
|
+
className: "text-muted-foreground",
|
|
2575
|
+
children: tooltip.span.serviceName
|
|
2576
|
+
}),
|
|
2577
|
+
/* @__PURE__ */ jsx("div", {
|
|
2578
|
+
className: "text-foreground mt-1",
|
|
2579
|
+
children: formatDuration(tooltip.span.durationMs)
|
|
2580
|
+
})
|
|
2581
|
+
]
|
|
2582
|
+
})
|
|
2583
|
+
]
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
//#endregion
|
|
2587
|
+
//#region src/components/observability/TraceTimeline/Minimap.tsx
|
|
2588
|
+
/**
|
|
2589
|
+
* Minimap - Compressed overview of all spans with a draggable viewport.
|
|
2590
|
+
*/
|
|
2591
|
+
const MINIMAP_HEIGHT = 40;
|
|
2592
|
+
const SPAN_HEIGHT = 2;
|
|
2593
|
+
const SPAN_GAP = 1;
|
|
2594
|
+
const MIN_VIEWPORT_WIDTH = .02;
|
|
2595
|
+
const HANDLE_WIDTH = 6;
|
|
2596
|
+
function Minimap({ trace, viewStart, viewEnd, onViewChange }) {
|
|
2597
|
+
const containerRef = useRef(null);
|
|
2598
|
+
const dragRef = useRef(null);
|
|
2599
|
+
const cleanupRef = useRef(null);
|
|
2600
|
+
useEffect(() => {
|
|
2601
|
+
return () => {
|
|
2602
|
+
cleanupRef.current?.();
|
|
2603
|
+
};
|
|
1730
2604
|
}, []);
|
|
1731
|
-
const
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2605
|
+
const allSpans = useMemo(() => flattenAllSpans(trace.rootSpans), [trace.rootSpans]);
|
|
2606
|
+
const traceDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
2607
|
+
const getFraction = useCallback((clientX) => {
|
|
2608
|
+
const el = containerRef.current;
|
|
2609
|
+
if (!el) return 0;
|
|
2610
|
+
const rect = el.getBoundingClientRect();
|
|
2611
|
+
if (!rect.width) return 0;
|
|
2612
|
+
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1737
2613
|
}, []);
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
setIsOpen((v) => !v);
|
|
1745
|
-
return;
|
|
1746
|
-
}
|
|
1747
|
-
if (e.key === "Escape" && isOpen) {
|
|
1748
|
-
e.preventDefault();
|
|
1749
|
-
setIsOpen(false);
|
|
1750
|
-
return;
|
|
1751
|
-
}
|
|
1752
|
-
if (e.shiftKey && e.key === "S") {
|
|
1753
|
-
e.preventDefault();
|
|
1754
|
-
onNavigateServices();
|
|
1755
|
-
return;
|
|
1756
|
-
}
|
|
1757
|
-
if (e.shiftKey && e.key === "L") {
|
|
1758
|
-
e.preventDefault();
|
|
1759
|
-
onNavigateLogs();
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
if (e.shiftKey && e.key === "M") {
|
|
1763
|
-
e.preventDefault();
|
|
1764
|
-
onNavigateMetrics();
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
2614
|
+
const clampView = useCallback((start, end) => {
|
|
2615
|
+
let s = Math.max(0, Math.min(1 - MIN_VIEWPORT_WIDTH, start));
|
|
2616
|
+
let e = Math.max(s + MIN_VIEWPORT_WIDTH, Math.min(1, end));
|
|
2617
|
+
if (e > 1) {
|
|
2618
|
+
e = 1;
|
|
2619
|
+
s = Math.max(0, e - Math.max(MIN_VIEWPORT_WIDTH, end - start));
|
|
1767
2620
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
2621
|
+
return [s, e];
|
|
2622
|
+
}, []);
|
|
2623
|
+
const handleMouseDown = useCallback((e, mode) => {
|
|
2624
|
+
e.preventDefault();
|
|
2625
|
+
e.stopPropagation();
|
|
2626
|
+
cleanupRef.current?.();
|
|
2627
|
+
dragRef.current = {
|
|
2628
|
+
mode,
|
|
2629
|
+
startX: e.clientX,
|
|
2630
|
+
origViewStart: viewStart,
|
|
2631
|
+
origViewEnd: viewEnd
|
|
2632
|
+
};
|
|
2633
|
+
const handleMouseMove = (ev) => {
|
|
2634
|
+
const drag = dragRef.current;
|
|
2635
|
+
if (!drag || !containerRef.current) return;
|
|
2636
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
2637
|
+
if (!rect.width) return;
|
|
2638
|
+
const deltaFrac = (ev.clientX - drag.startX) / rect.width;
|
|
2639
|
+
let newStart;
|
|
2640
|
+
let newEnd;
|
|
2641
|
+
if (drag.mode === "pan") {
|
|
2642
|
+
const width = drag.origViewEnd - drag.origViewStart;
|
|
2643
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2644
|
+
newEnd = newStart + width;
|
|
2645
|
+
if (newStart < 0) {
|
|
2646
|
+
newStart = 0;
|
|
2647
|
+
newEnd = width;
|
|
2648
|
+
}
|
|
2649
|
+
if (newEnd > 1) {
|
|
2650
|
+
newEnd = 1;
|
|
2651
|
+
newStart = 1 - width;
|
|
2652
|
+
}
|
|
2653
|
+
} else if (drag.mode === "resize-left") {
|
|
2654
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2655
|
+
newEnd = drag.origViewEnd;
|
|
2656
|
+
} else {
|
|
2657
|
+
newStart = drag.origViewStart;
|
|
2658
|
+
newEnd = drag.origViewEnd + deltaFrac;
|
|
2659
|
+
}
|
|
2660
|
+
const [s, e] = clampView(newStart, newEnd);
|
|
2661
|
+
onViewChange(s, e);
|
|
2662
|
+
};
|
|
2663
|
+
const handleMouseUp = () => {
|
|
2664
|
+
dragRef.current = null;
|
|
2665
|
+
cleanupRef.current = null;
|
|
2666
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
2667
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
2668
|
+
};
|
|
2669
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
2670
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
2671
|
+
cleanupRef.current = handleMouseUp;
|
|
1770
2672
|
}, [
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
2673
|
+
viewStart,
|
|
2674
|
+
viewEnd,
|
|
2675
|
+
onViewChange,
|
|
2676
|
+
clampView
|
|
1775
2677
|
]);
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
2678
|
+
const handleBackgroundClick = useCallback((e) => {
|
|
2679
|
+
if (dragRef.current) return;
|
|
2680
|
+
if (e.target !== e.currentTarget) return;
|
|
2681
|
+
const frac = getFraction(e.clientX);
|
|
2682
|
+
const half = (viewEnd - viewStart) / 2;
|
|
2683
|
+
const [s, eVal] = clampView(frac - half, frac + half);
|
|
2684
|
+
onViewChange(s, eVal);
|
|
2685
|
+
}, [
|
|
2686
|
+
viewStart,
|
|
2687
|
+
viewEnd,
|
|
2688
|
+
onViewChange,
|
|
2689
|
+
getFraction,
|
|
2690
|
+
clampView
|
|
2691
|
+
]);
|
|
2692
|
+
const handleKeyDown = useCallback((e) => {
|
|
2693
|
+
const step = .05;
|
|
2694
|
+
const width = viewEnd - viewStart;
|
|
2695
|
+
let newStart;
|
|
2696
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowUp") newStart = viewStart - step;
|
|
2697
|
+
else if (e.key === "ArrowRight" || e.key === "ArrowDown") newStart = viewStart + step;
|
|
2698
|
+
else return;
|
|
2699
|
+
e.preventDefault();
|
|
2700
|
+
const [s, eVal] = clampView(newStart, newStart + width);
|
|
2701
|
+
onViewChange(s, eVal);
|
|
2702
|
+
}, [
|
|
2703
|
+
viewStart,
|
|
2704
|
+
viewEnd,
|
|
2705
|
+
onViewChange,
|
|
2706
|
+
clampView
|
|
2707
|
+
]);
|
|
2708
|
+
const viewStartPct = viewStart * 100;
|
|
2709
|
+
const viewEndPct = viewEnd * 100;
|
|
2710
|
+
const viewWidthPct = viewEndPct - viewStartPct;
|
|
2711
|
+
const totalRows = allSpans.length;
|
|
2712
|
+
const availableHeight = MINIMAP_HEIGHT - 4;
|
|
2713
|
+
const rowHeight = totalRows > 0 ? Math.min(SPAN_HEIGHT + SPAN_GAP, availableHeight / totalRows) : SPAN_HEIGHT;
|
|
2714
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2715
|
+
ref: containerRef,
|
|
2716
|
+
className: "relative w-full border-b border-border bg-muted/30 select-none",
|
|
2717
|
+
style: { height: MINIMAP_HEIGHT },
|
|
2718
|
+
onClick: handleBackgroundClick,
|
|
2719
|
+
onKeyDown: handleKeyDown,
|
|
2720
|
+
role: "slider",
|
|
2721
|
+
tabIndex: 0,
|
|
2722
|
+
"aria-label": "Trace minimap viewport",
|
|
2723
|
+
"aria-valuemin": 0,
|
|
2724
|
+
"aria-valuemax": 100,
|
|
2725
|
+
"aria-valuenow": Math.round(viewStartPct),
|
|
2726
|
+
children: [
|
|
2727
|
+
traceDuration > 0 && allSpans.map(({ span }, i) => {
|
|
2728
|
+
const left = (span.startTimeUnixMs - trace.minTimeMs) / traceDuration * 100;
|
|
2729
|
+
const width = Math.max(.2, span.durationMs / traceDuration * 100);
|
|
2730
|
+
const color = getSpanBarColor(span.serviceName, span.status === "ERROR");
|
|
2731
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2732
|
+
className: "absolute pointer-events-none",
|
|
2733
|
+
style: {
|
|
2734
|
+
left: `${left}%`,
|
|
2735
|
+
width: `${width}%`,
|
|
2736
|
+
top: 2 + i * rowHeight,
|
|
2737
|
+
height: Math.max(1, rowHeight - SPAN_GAP),
|
|
2738
|
+
backgroundColor: color,
|
|
2739
|
+
opacity: .8,
|
|
2740
|
+
borderRadius: 1
|
|
2741
|
+
}
|
|
2742
|
+
}, span.spanId);
|
|
2743
|
+
}),
|
|
2744
|
+
viewStartPct > 0 && /* @__PURE__ */ jsx("div", {
|
|
2745
|
+
className: "absolute top-0 left-0 h-full bg-black/30 pointer-events-none",
|
|
2746
|
+
style: { width: `${viewStartPct}%` }
|
|
2747
|
+
}),
|
|
2748
|
+
viewEndPct < 100 && /* @__PURE__ */ jsx("div", {
|
|
2749
|
+
className: "absolute top-0 h-full bg-black/30 pointer-events-none",
|
|
2750
|
+
style: {
|
|
2751
|
+
left: `${viewEndPct}%`,
|
|
2752
|
+
right: 0
|
|
2753
|
+
}
|
|
2754
|
+
}),
|
|
2755
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2756
|
+
className: "absolute top-0 h-full border border-blue-500/50 bg-blue-500/10 cursor-grab active:cursor-grabbing",
|
|
2757
|
+
style: {
|
|
2758
|
+
left: `${viewStartPct}%`,
|
|
2759
|
+
width: `${viewWidthPct}%`
|
|
2760
|
+
},
|
|
2761
|
+
onMouseDown: (e) => handleMouseDown(e, "pan"),
|
|
2762
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2763
|
+
className: "absolute top-0 left-0 h-full cursor-ew-resize z-10",
|
|
2764
|
+
style: {
|
|
2765
|
+
width: HANDLE_WIDTH,
|
|
2766
|
+
marginLeft: -HANDLE_WIDTH / 2
|
|
2767
|
+
},
|
|
2768
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-left")
|
|
2769
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2770
|
+
className: "absolute top-0 right-0 h-full cursor-ew-resize z-10",
|
|
2771
|
+
style: {
|
|
2772
|
+
width: HANDLE_WIDTH,
|
|
2773
|
+
marginRight: -HANDLE_WIDTH / 2
|
|
2774
|
+
},
|
|
2775
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-right")
|
|
2776
|
+
})]
|
|
2777
|
+
})
|
|
2778
|
+
]
|
|
1790
2779
|
});
|
|
1791
2780
|
}
|
|
1792
|
-
|
|
1793
|
-
//#endregion
|
|
1794
|
-
//#region src/components/observability/TraceTimeline/shortcuts.ts
|
|
1795
|
-
const TRACE_VIEWER_SHORTCUTS = {
|
|
1796
|
-
name: "Trace Viewer",
|
|
1797
|
-
shortcuts: [
|
|
1798
|
-
{
|
|
1799
|
-
keys: ["↑/K"],
|
|
1800
|
-
description: "Previous span"
|
|
1801
|
-
},
|
|
1802
|
-
{
|
|
1803
|
-
keys: ["↓/J"],
|
|
1804
|
-
description: "Next span"
|
|
1805
|
-
},
|
|
1806
|
-
{
|
|
1807
|
-
keys: ["←"],
|
|
1808
|
-
description: "Collapse span"
|
|
1809
|
-
},
|
|
1810
|
-
{
|
|
1811
|
-
keys: ["→"],
|
|
1812
|
-
description: "Expand span"
|
|
1813
|
-
},
|
|
1814
|
-
{
|
|
1815
|
-
keys: ["Enter"],
|
|
1816
|
-
description: "Focus detail pane"
|
|
1817
|
-
},
|
|
1818
|
-
{
|
|
1819
|
-
keys: ["C"],
|
|
1820
|
-
description: "Copy span name"
|
|
1821
|
-
},
|
|
1822
|
-
{
|
|
1823
|
-
keys: ["Esc"],
|
|
1824
|
-
description: "Deselect span"
|
|
1825
|
-
},
|
|
1826
|
-
{
|
|
1827
|
-
keys: [
|
|
1828
|
-
"Ctrl",
|
|
1829
|
-
"Shift",
|
|
1830
|
-
"E"
|
|
1831
|
-
],
|
|
1832
|
-
description: "Expand all"
|
|
1833
|
-
},
|
|
1834
|
-
{
|
|
1835
|
-
keys: [
|
|
1836
|
-
"Ctrl",
|
|
1837
|
-
"Shift",
|
|
1838
|
-
"C"
|
|
1839
|
-
],
|
|
1840
|
-
description: "Collapse all"
|
|
1841
|
-
}
|
|
1842
|
-
]
|
|
1843
|
-
};
|
|
1844
|
-
|
|
1845
2781
|
//#endregion
|
|
1846
2782
|
//#region src/components/observability/TraceTimeline/index.tsx
|
|
1847
2783
|
/**
|
|
@@ -1930,25 +2866,43 @@ function isSpanAncestorOf(potentialAncestor, descendantId, flattenedSpans) {
|
|
|
1930
2866
|
}
|
|
1931
2867
|
return false;
|
|
1932
2868
|
}
|
|
1933
|
-
function
|
|
2869
|
+
function collectServices(rootSpans) {
|
|
2870
|
+
const set = /* @__PURE__ */ new Set();
|
|
2871
|
+
function walk(span) {
|
|
2872
|
+
set.add(span.serviceName);
|
|
2873
|
+
span.children.forEach(walk);
|
|
2874
|
+
}
|
|
2875
|
+
rootSpans.forEach(walk);
|
|
2876
|
+
return Array.from(set).sort();
|
|
2877
|
+
}
|
|
2878
|
+
function TraceTimeline({ rows, onSpanClick, onSpanDeselect, selectedSpanId: externalSelectedSpanId, isLoading, error, view: externalView, onViewChange, uiFind: externalUiFind, onUiFindChange, viewStart: externalViewStart, viewEnd: externalViewEnd, onViewRangeChange }) {
|
|
1934
2879
|
useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
|
|
1935
2880
|
const [collapsedIds, setCollapsedIds] = useState(/* @__PURE__ */ new Set());
|
|
1936
2881
|
const [internalSelectedSpanId, setInternalSelectedSpanId] = useState(null);
|
|
1937
2882
|
const [hoveredSpanId, setHoveredSpanId] = useState(null);
|
|
2883
|
+
const [internalView, setInternalView] = useState("timeline");
|
|
2884
|
+
const [internalUiFind, setInternalUiFind] = useState("");
|
|
2885
|
+
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
|
|
2886
|
+
const [headerCollapsed, setHeaderCollapsed] = useState(false);
|
|
2887
|
+
const [internalViewStart, setInternalViewStart] = useState(0);
|
|
2888
|
+
const [internalViewEnd, setInternalViewEnd] = useState(1);
|
|
1938
2889
|
const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
|
|
2890
|
+
const viewStart = externalViewStart ?? internalViewStart;
|
|
2891
|
+
const viewEnd = externalViewEnd ?? internalViewEnd;
|
|
2892
|
+
const activeView = externalView ?? internalView;
|
|
2893
|
+
const uiFind = externalUiFind ?? internalUiFind;
|
|
1939
2894
|
const scrollRef = useRef(null);
|
|
1940
2895
|
const announcementRef = useRef(null);
|
|
1941
2896
|
const parsedTrace = useMemo(() => buildTrace(rows), [rows]);
|
|
2897
|
+
const services = useMemo(() => parsedTrace ? collectServices(parsedTrace.rootSpans) : [], [parsedTrace]);
|
|
1942
2898
|
const flattenedSpans = useMemo(() => {
|
|
1943
2899
|
if (!parsedTrace) return [];
|
|
1944
2900
|
return flattenTree(parsedTrace.rootSpans, collapsedIds);
|
|
1945
2901
|
}, [parsedTrace, collapsedIds]);
|
|
1946
|
-
const
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
overscan: 5
|
|
1951
|
-
});
|
|
2902
|
+
const matchingIndices = useMemo(() => {
|
|
2903
|
+
if (!uiFind) return [];
|
|
2904
|
+
return flattenedSpans.map((item, idx) => spanMatchesSearch(item.span, uiFind) ? idx : -1).filter((idx) => idx !== -1);
|
|
2905
|
+
}, [flattenedSpans, uiFind]);
|
|
1952
2906
|
const handleToggleCollapse = (spanId) => {
|
|
1953
2907
|
setCollapsedIds((prev) => {
|
|
1954
2908
|
const next = new Set(prev);
|
|
@@ -1957,11 +2911,22 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
1957
2911
|
return next;
|
|
1958
2912
|
});
|
|
1959
2913
|
};
|
|
2914
|
+
const handleDeselect = useCallback(() => {
|
|
2915
|
+
setInternalSelectedSpanId(null);
|
|
2916
|
+
onSpanDeselect?.();
|
|
2917
|
+
}, [onSpanDeselect]);
|
|
1960
2918
|
const handleSpanClick = useCallback((span) => {
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
2919
|
+
if (selectedSpanId === span.spanId) handleDeselect();
|
|
2920
|
+
else {
|
|
2921
|
+
setInternalSelectedSpanId(span.spanId);
|
|
2922
|
+
onSpanClick?.(span);
|
|
2923
|
+
if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
|
|
2924
|
+
}
|
|
2925
|
+
}, [
|
|
2926
|
+
onSpanClick,
|
|
2927
|
+
selectedSpanId,
|
|
2928
|
+
handleDeselect
|
|
2929
|
+
]);
|
|
1965
2930
|
const handleExpandAll = useCallback(() => {
|
|
1966
2931
|
setCollapsedIds(/* @__PURE__ */ new Set());
|
|
1967
2932
|
}, []);
|
|
@@ -2010,23 +2975,77 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2010
2975
|
return next;
|
|
2011
2976
|
});
|
|
2012
2977
|
}, [selectedSpanId, flattenedSpans]);
|
|
2013
|
-
const
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2978
|
+
const handleViewChange = useCallback((view) => {
|
|
2979
|
+
if (onViewChange) onViewChange(view);
|
|
2980
|
+
else setInternalView(view);
|
|
2981
|
+
}, [onViewChange]);
|
|
2982
|
+
const handleUiFindChange = useCallback((value) => {
|
|
2983
|
+
if (onUiFindChange) onUiFindChange(value);
|
|
2984
|
+
else setInternalUiFind(value);
|
|
2985
|
+
setCurrentMatchIndex(0);
|
|
2986
|
+
}, [onUiFindChange]);
|
|
2987
|
+
const handleViewRangeChange = useCallback((start, end) => {
|
|
2988
|
+
if (onViewRangeChange) onViewRangeChange(start, end);
|
|
2989
|
+
else {
|
|
2990
|
+
setInternalViewStart(start);
|
|
2991
|
+
setInternalViewEnd(end);
|
|
2992
|
+
}
|
|
2993
|
+
}, [onViewRangeChange]);
|
|
2994
|
+
const scrollToSpan = useCallback((spanId) => {
|
|
2995
|
+
(scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`))?.scrollIntoView({
|
|
2996
|
+
block: "center",
|
|
2021
2997
|
behavior: "smooth"
|
|
2022
2998
|
});
|
|
2999
|
+
}, []);
|
|
3000
|
+
const handleSearchNext = useCallback(() => {
|
|
3001
|
+
if (matchingIndices.length === 0) return;
|
|
3002
|
+
const next = (currentMatchIndex + 1) % matchingIndices.length;
|
|
3003
|
+
setCurrentMatchIndex(next);
|
|
3004
|
+
const idx = matchingIndices[next];
|
|
3005
|
+
if (idx !== void 0) {
|
|
3006
|
+
const item = flattenedSpans[idx];
|
|
3007
|
+
if (item) {
|
|
3008
|
+
handleSpanClick(item.span);
|
|
3009
|
+
scrollToSpan(item.span.spanId);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
2023
3012
|
}, [
|
|
2024
|
-
|
|
3013
|
+
matchingIndices,
|
|
3014
|
+
currentMatchIndex,
|
|
2025
3015
|
flattenedSpans,
|
|
2026
|
-
|
|
3016
|
+
handleSpanClick,
|
|
3017
|
+
scrollToSpan
|
|
2027
3018
|
]);
|
|
3019
|
+
const handleSearchPrev = useCallback(() => {
|
|
3020
|
+
if (matchingIndices.length === 0) return;
|
|
3021
|
+
const prev = (currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
|
|
3022
|
+
setCurrentMatchIndex(prev);
|
|
3023
|
+
const idx = matchingIndices[prev];
|
|
3024
|
+
if (idx !== void 0) {
|
|
3025
|
+
const item = flattenedSpans[idx];
|
|
3026
|
+
if (item) {
|
|
3027
|
+
handleSpanClick(item.span);
|
|
3028
|
+
scrollToSpan(item.span.spanId);
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
}, [
|
|
3032
|
+
matchingIndices,
|
|
3033
|
+
currentMatchIndex,
|
|
3034
|
+
flattenedSpans,
|
|
3035
|
+
handleSpanClick,
|
|
3036
|
+
scrollToSpan
|
|
3037
|
+
]);
|
|
3038
|
+
useEffect(() => {
|
|
3039
|
+
if (!selectedSpanId) return;
|
|
3040
|
+
scrollToSpan(selectedSpanId);
|
|
3041
|
+
}, [selectedSpanId, scrollToSpan]);
|
|
2028
3042
|
useEffect(() => {
|
|
2029
3043
|
const handleKeyDown = (e) => {
|
|
3044
|
+
if (e.key === "Escape" && selectedSpanId) {
|
|
3045
|
+
e.preventDefault();
|
|
3046
|
+
handleDeselect();
|
|
3047
|
+
return;
|
|
3048
|
+
}
|
|
2030
3049
|
if (!(scrollRef.current?.parentElement)?.contains(document.activeElement)) return;
|
|
2031
3050
|
switch (e.key) {
|
|
2032
3051
|
case "ArrowUp":
|
|
@@ -2049,10 +3068,7 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2049
3068
|
e.preventDefault();
|
|
2050
3069
|
handleCollapseExpand(false);
|
|
2051
3070
|
break;
|
|
2052
|
-
case "Escape":
|
|
2053
|
-
e.preventDefault();
|
|
2054
|
-
handleDeselect();
|
|
2055
|
-
break;
|
|
3071
|
+
case "Escape": break;
|
|
2056
3072
|
case "Enter":
|
|
2057
3073
|
if (selectedSpanId) {
|
|
2058
3074
|
e.preventDefault();
|
|
@@ -2120,10 +3136,9 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2120
3136
|
})
|
|
2121
3137
|
});
|
|
2122
3138
|
const totalDurationMs = parsedTrace.maxTimeMs - parsedTrace.minTimeMs;
|
|
2123
|
-
|
|
2124
|
-
return /* @__PURE__ */ jsxs("div", {
|
|
3139
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2125
3140
|
className: "flex h-full bg-background",
|
|
2126
|
-
children:
|
|
3141
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
2127
3142
|
className: "flex flex-col flex-1 min-w-0",
|
|
2128
3143
|
children: [
|
|
2129
3144
|
/* @__PURE__ */ jsx("div", {
|
|
@@ -2133,39 +3148,58 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2133
3148
|
"aria-live": "polite",
|
|
2134
3149
|
"aria-atomic": "true"
|
|
2135
3150
|
}),
|
|
2136
|
-
/* @__PURE__ */ jsx(TraceHeader, {
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
3151
|
+
/* @__PURE__ */ jsx(TraceHeader, {
|
|
3152
|
+
trace: parsedTrace,
|
|
3153
|
+
services,
|
|
3154
|
+
onHeaderToggle: () => setHeaderCollapsed((p) => !p),
|
|
3155
|
+
isCollapsed: headerCollapsed
|
|
3156
|
+
}),
|
|
3157
|
+
/* @__PURE__ */ jsx(ViewTabs, {
|
|
3158
|
+
activeView,
|
|
3159
|
+
onChange: handleViewChange
|
|
3160
|
+
}),
|
|
3161
|
+
/* @__PURE__ */ jsx(SpanSearch, {
|
|
3162
|
+
value: uiFind,
|
|
3163
|
+
onChange: handleUiFindChange,
|
|
3164
|
+
matchCount: matchingIndices.length,
|
|
3165
|
+
currentMatch: currentMatchIndex,
|
|
3166
|
+
onPrev: handleSearchPrev,
|
|
3167
|
+
onNext: handleSearchNext
|
|
3168
|
+
}),
|
|
3169
|
+
activeView === "graph" ? /* @__PURE__ */ jsx(GraphView, { trace: parsedTrace }) : activeView === "statistics" ? /* @__PURE__ */ jsx(StatisticsView, { trace: parsedTrace }) : activeView === "flamegraph" ? /* @__PURE__ */ jsx(FlamegraphView, {
|
|
3170
|
+
trace: parsedTrace,
|
|
3171
|
+
onSpanClick: handleSpanClick,
|
|
3172
|
+
selectedSpanId: selectedSpanId ?? void 0
|
|
3173
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3174
|
+
/* @__PURE__ */ jsx(Minimap, {
|
|
3175
|
+
trace: parsedTrace,
|
|
3176
|
+
viewStart,
|
|
3177
|
+
viewEnd,
|
|
3178
|
+
onViewChange: handleViewRangeChange
|
|
3179
|
+
}),
|
|
3180
|
+
/* @__PURE__ */ jsx(TimeRuler, {
|
|
3181
|
+
totalDurationMs: totalDurationMs * (viewEnd - viewStart),
|
|
3182
|
+
leftColumnWidth: "24rem",
|
|
3183
|
+
offsetMs: totalDurationMs * viewStart
|
|
3184
|
+
}),
|
|
3185
|
+
/* @__PURE__ */ jsx("div", {
|
|
3186
|
+
ref: scrollRef,
|
|
3187
|
+
className: "flex-1 overflow-auto outline-none",
|
|
3188
|
+
role: "tree",
|
|
3189
|
+
"aria-label": "Trace timeline",
|
|
3190
|
+
tabIndex: 0,
|
|
3191
|
+
children: flattenedSpans.map((item) => {
|
|
2152
3192
|
const { span, level } = item;
|
|
2153
3193
|
const isCollapsed = collapsedIds.has(span.spanId);
|
|
2154
3194
|
const isSelected = span.spanId === selectedSpanId;
|
|
2155
3195
|
const isHovered = span.spanId === hoveredSpanId;
|
|
2156
|
-
const isParentOfHovered = hoveredSpanId ? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans) : false;
|
|
2157
|
-
const
|
|
2158
|
-
const
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
left: 0,
|
|
2164
|
-
width: "100%",
|
|
2165
|
-
height: `${virtualItem.size}px`,
|
|
2166
|
-
transform: `translateY(${virtualItem.start}px)`
|
|
2167
|
-
},
|
|
2168
|
-
children: /* @__PURE__ */ jsx(SpanRow, {
|
|
3196
|
+
const isParentOfHovered = hoveredSpanId ? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans) : false;
|
|
3197
|
+
const viewRange = viewEnd - viewStart;
|
|
3198
|
+
const relativeStart = (calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs) - viewStart) / viewRange;
|
|
3199
|
+
const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs) / viewRange;
|
|
3200
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3201
|
+
"data-span-id": span.spanId,
|
|
3202
|
+
children: [/* @__PURE__ */ jsx(SpanRow, {
|
|
2169
3203
|
span,
|
|
2170
3204
|
level,
|
|
2171
3205
|
isCollapsed,
|
|
@@ -2177,34 +3211,30 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2177
3211
|
onClick: () => handleSpanClick(span),
|
|
2178
3212
|
onToggleCollapse: () => handleToggleCollapse(span.spanId),
|
|
2179
3213
|
onMouseEnter: () => setHoveredSpanId(span.spanId),
|
|
2180
|
-
onMouseLeave: () => setHoveredSpanId(null)
|
|
2181
|
-
|
|
3214
|
+
onMouseLeave: () => setHoveredSpanId(null),
|
|
3215
|
+
uiFind: uiFind || void 0
|
|
3216
|
+
}), isSelected && /* @__PURE__ */ jsx(SpanDetailInline, {
|
|
3217
|
+
span,
|
|
3218
|
+
traceStartMs: parsedTrace.minTimeMs
|
|
3219
|
+
})]
|
|
2182
3220
|
}, span.spanId);
|
|
2183
3221
|
})
|
|
2184
3222
|
})
|
|
2185
|
-
})
|
|
3223
|
+
] })
|
|
2186
3224
|
]
|
|
2187
|
-
})
|
|
2188
|
-
className: "w-96 h-full flex-shrink-0",
|
|
2189
|
-
children: /* @__PURE__ */ jsx(DetailPane, {
|
|
2190
|
-
span: selectedSpan,
|
|
2191
|
-
onClose: handleDeselect,
|
|
2192
|
-
onLinkClick: void 0
|
|
2193
|
-
})
|
|
2194
|
-
})]
|
|
3225
|
+
})
|
|
2195
3226
|
});
|
|
2196
3227
|
}
|
|
2197
|
-
|
|
2198
3228
|
//#endregion
|
|
2199
3229
|
//#region src/components/observability/TraceDetail/index.tsx
|
|
2200
|
-
function TraceDetail({
|
|
3230
|
+
function TraceDetail({ traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onSpanDeselect, onBack }) {
|
|
2201
3231
|
return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
|
|
2202
3232
|
className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
|
|
2203
3233
|
children: [
|
|
2204
|
-
/* @__PURE__ */
|
|
3234
|
+
/* @__PURE__ */ jsx("button", {
|
|
2205
3235
|
onClick: onBack,
|
|
2206
3236
|
className: "hover:text-foreground transition-colors",
|
|
2207
|
-
children:
|
|
3237
|
+
children: "Traces"
|
|
2208
3238
|
}),
|
|
2209
3239
|
/* @__PURE__ */ jsx("span", { children: "/" }),
|
|
2210
3240
|
/* @__PURE__ */ jsxs("span", {
|
|
@@ -2217,10 +3247,10 @@ function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId,
|
|
|
2217
3247
|
isLoading,
|
|
2218
3248
|
error,
|
|
2219
3249
|
selectedSpanId,
|
|
2220
|
-
onSpanClick
|
|
3250
|
+
onSpanClick,
|
|
3251
|
+
onSpanDeselect
|
|
2221
3252
|
})] });
|
|
2222
3253
|
}
|
|
2223
|
-
|
|
2224
3254
|
//#endregion
|
|
2225
3255
|
//#region src/components/observability/LogTimeline/LogRow.tsx
|
|
2226
3256
|
function formatTimestamp(timeMs) {
|
|
@@ -2346,7 +3376,6 @@ const LogRow = memo(function LogRow({ log, isSelected, onClick, searchText, rela
|
|
|
2346
3376
|
]
|
|
2347
3377
|
});
|
|
2348
3378
|
});
|
|
2349
|
-
|
|
2350
3379
|
//#endregion
|
|
2351
3380
|
//#region src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx
|
|
2352
3381
|
function AttributesTab({ log }) {
|
|
@@ -2379,7 +3408,6 @@ function AttributesTab({ log }) {
|
|
|
2379
3408
|
})
|
|
2380
3409
|
});
|
|
2381
3410
|
}
|
|
2382
|
-
|
|
2383
3411
|
//#endregion
|
|
2384
3412
|
//#region src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx
|
|
2385
3413
|
function JsonTreeView({ data, level = 0 }) {
|
|
@@ -2467,7 +3495,6 @@ function formatPrimitiveValue(value) {
|
|
|
2467
3495
|
if (typeof value === "number") return String(value);
|
|
2468
3496
|
return String(value);
|
|
2469
3497
|
}
|
|
2470
|
-
|
|
2471
3498
|
//#endregion
|
|
2472
3499
|
//#region src/components/observability/LogTimeline/LogDetailPane/index.tsx
|
|
2473
3500
|
function LogDetailPane({ log, onClose, onTraceLinkClick, initialTab = "message", wordWrap = true }) {
|
|
@@ -2679,7 +3706,6 @@ function getSeverityColor(severity) {
|
|
|
2679
3706
|
bg: "bg-gray-50 dark:bg-gray-800/20"
|
|
2680
3707
|
};
|
|
2681
3708
|
}
|
|
2682
|
-
|
|
2683
3709
|
//#endregion
|
|
2684
3710
|
//#region src/components/observability/LogTimeline/shortcuts.ts
|
|
2685
3711
|
const LOG_VIEWER_SHORTCUTS = {
|
|
@@ -2731,7 +3757,6 @@ const LOG_VIEWER_SHORTCUTS = {
|
|
|
2731
3757
|
}
|
|
2732
3758
|
]
|
|
2733
3759
|
};
|
|
2734
|
-
|
|
2735
3760
|
//#endregion
|
|
2736
3761
|
//#region src/components/observability/LogTimeline/index.tsx
|
|
2737
3762
|
/**
|
|
@@ -2937,7 +3962,7 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
2937
3962
|
useEffect(() => {
|
|
2938
3963
|
const handleKeyDown = (e) => {
|
|
2939
3964
|
const isFormField = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement;
|
|
2940
|
-
if (isFormField && e.key === "Escape") {
|
|
3965
|
+
if (isFormField && e.key === "Escape" && e.target instanceof HTMLElement) {
|
|
2941
3966
|
e.target.blur();
|
|
2942
3967
|
return;
|
|
2943
3968
|
}
|
|
@@ -3178,7 +4203,6 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
3178
4203
|
})]
|
|
3179
4204
|
});
|
|
3180
4205
|
}
|
|
3181
|
-
|
|
3182
4206
|
//#endregion
|
|
3183
4207
|
//#region src/components/observability/LogTimeline/LogFilter.tsx
|
|
3184
4208
|
/**
|
|
@@ -3279,7 +4303,7 @@ function MultiSelect({ options, selected, onChange, testId }) {
|
|
|
3279
4303
|
useEffect(() => {
|
|
3280
4304
|
if (!dropOpen) return;
|
|
3281
4305
|
const handler = (e) => {
|
|
3282
|
-
if (ref.current && !ref.current.contains(e.target)) setDropOpen(false);
|
|
4306
|
+
if (ref.current && e.target instanceof Node && !ref.current.contains(e.target)) setDropOpen(false);
|
|
3283
4307
|
};
|
|
3284
4308
|
document.addEventListener("mousedown", handler);
|
|
3285
4309
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -3730,7 +4754,6 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
|
|
|
3730
4754
|
})]
|
|
3731
4755
|
});
|
|
3732
4756
|
}
|
|
3733
|
-
|
|
3734
4757
|
//#endregion
|
|
3735
4758
|
//#region src/components/observability/utils/lttb.ts
|
|
3736
4759
|
function triangleArea(p1, p2, p3) {
|
|
@@ -3796,7 +4819,27 @@ function downsampleLTTB(data, targetPoints) {
|
|
|
3796
4819
|
if (lastPoint) sampled.push(lastPoint);
|
|
3797
4820
|
return sampled;
|
|
3798
4821
|
}
|
|
3799
|
-
|
|
4822
|
+
//#endregion
|
|
4823
|
+
//#region src/components/observability/shared/TooltipEntryList.tsx
|
|
4824
|
+
function TooltipEntryList({ payload, displayLabelMap, formatValue }) {
|
|
4825
|
+
return payload.map((entry, i) => {
|
|
4826
|
+
const dataKey = entry.dataKey;
|
|
4827
|
+
const value = entry.value;
|
|
4828
|
+
if (typeof dataKey !== "string" || typeof value !== "number") return null;
|
|
4829
|
+
return /* @__PURE__ */ jsxs("p", {
|
|
4830
|
+
className: "text-sm",
|
|
4831
|
+
style: { color: entry.color },
|
|
4832
|
+
children: [
|
|
4833
|
+
/* @__PURE__ */ jsxs("span", {
|
|
4834
|
+
className: "font-medium",
|
|
4835
|
+
children: [displayLabelMap.get(dataKey) ?? dataKey, ":"]
|
|
4836
|
+
}),
|
|
4837
|
+
" ",
|
|
4838
|
+
formatValue(value)
|
|
4839
|
+
]
|
|
4840
|
+
}, i);
|
|
4841
|
+
});
|
|
4842
|
+
}
|
|
3800
4843
|
//#endregion
|
|
3801
4844
|
//#region src/components/observability/utils/units.ts
|
|
3802
4845
|
const BYTE_SCALES = [
|
|
@@ -3971,7 +5014,6 @@ function formatDisplayValue(value, scale) {
|
|
|
3971
5014
|
function formatOtelValue(value, unit) {
|
|
3972
5015
|
return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
|
|
3973
5016
|
}
|
|
3974
|
-
|
|
3975
5017
|
//#endregion
|
|
3976
5018
|
//#region src/components/observability/MetricTimeSeries/index.tsx
|
|
3977
5019
|
/**
|
|
@@ -4280,18 +5322,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
|
|
|
4280
5322
|
children: [/* @__PURE__ */ jsx("p", {
|
|
4281
5323
|
className: "text-gray-400 text-xs mb-2",
|
|
4282
5324
|
children: formatTime(typeof label === "number" ? label : Number(label))
|
|
4283
|
-
}),
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
className: "font-medium",
|
|
4289
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4290
|
-
}),
|
|
4291
|
-
" ",
|
|
4292
|
-
formatValue(entry.value)
|
|
4293
|
-
]
|
|
4294
|
-
}, i))]
|
|
5325
|
+
}), /* @__PURE__ */ jsx(TooltipEntryList, {
|
|
5326
|
+
payload,
|
|
5327
|
+
displayLabelMap,
|
|
5328
|
+
formatValue
|
|
5329
|
+
})]
|
|
4295
5330
|
});
|
|
4296
5331
|
}
|
|
4297
5332
|
function MetricLoadingSkeleton({ height = 400 }) {
|
|
@@ -4338,7 +5373,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
|
|
|
4338
5373
|
})
|
|
4339
5374
|
});
|
|
4340
5375
|
}
|
|
4341
|
-
|
|
4342
5376
|
//#endregion
|
|
4343
5377
|
//#region src/components/observability/MetricHistogram/index.tsx
|
|
4344
5378
|
/**
|
|
@@ -4352,6 +5386,9 @@ const COLORS = [
|
|
|
4352
5386
|
"#00C49F",
|
|
4353
5387
|
"#0088FE"
|
|
4354
5388
|
];
|
|
5389
|
+
function isBucketData(v) {
|
|
5390
|
+
return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
|
|
5391
|
+
}
|
|
4355
5392
|
const defaultFormatBucketLabel = (bound, index, bounds) => {
|
|
4356
5393
|
if (index === 0) return `≤${bound}`;
|
|
4357
5394
|
if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
|
|
@@ -4394,7 +5431,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
|
|
|
4394
5431
|
};
|
|
4395
5432
|
buckets.push(bucket);
|
|
4396
5433
|
}
|
|
4397
|
-
|
|
5434
|
+
const prev = bucket[seriesName];
|
|
5435
|
+
bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
|
|
4398
5436
|
}
|
|
4399
5437
|
}
|
|
4400
5438
|
buckets.sort((a, b) => a.lowerBound - b.lowerBound);
|
|
@@ -4531,25 +5569,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
|
|
|
4531
5569
|
}
|
|
4532
5570
|
function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
|
|
4533
5571
|
if (!active || !payload?.length) return null;
|
|
4534
|
-
const
|
|
4535
|
-
if (!
|
|
5572
|
+
const raw = payload[0]?.payload;
|
|
5573
|
+
if (!isBucketData(raw)) return null;
|
|
4536
5574
|
return /* @__PURE__ */ jsxs("div", {
|
|
4537
5575
|
className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
|
|
4538
5576
|
children: [/* @__PURE__ */ jsxs("p", {
|
|
4539
5577
|
className: "text-gray-300 text-sm font-medium mb-2",
|
|
4540
|
-
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(
|
|
4541
|
-
}),
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
className: "font-medium",
|
|
4547
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4548
|
-
}),
|
|
4549
|
-
" ",
|
|
4550
|
-
formatValue(entry.value)
|
|
4551
|
-
]
|
|
4552
|
-
}, i))]
|
|
5578
|
+
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(raw.lowerBound, boundsScale)} – ${raw.upperBound === Infinity ? "∞" : formatDisplayValue(raw.upperBound, boundsScale)}` : raw.bucket]
|
|
5579
|
+
}), /* @__PURE__ */ jsx(TooltipEntryList, {
|
|
5580
|
+
payload,
|
|
5581
|
+
displayLabelMap,
|
|
5582
|
+
formatValue
|
|
5583
|
+
})]
|
|
4553
5584
|
});
|
|
4554
5585
|
}
|
|
4555
5586
|
function HistogramLoadingSkeleton({ height = 400 }) {
|
|
@@ -4589,7 +5620,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
|
|
|
4589
5620
|
})
|
|
4590
5621
|
});
|
|
4591
5622
|
}
|
|
4592
|
-
|
|
4593
5623
|
//#endregion
|
|
4594
5624
|
//#region src/components/observability/MetricStat/index.tsx
|
|
4595
5625
|
/**
|
|
@@ -4677,8 +5707,11 @@ function buildStatData(rows) {
|
|
|
4677
5707
|
metricName
|
|
4678
5708
|
};
|
|
4679
5709
|
}
|
|
4680
|
-
function MetricStat({ rows, isLoading = false, error, label, formatValue = defaultFormatValue$1, showTimestamp = false, trend, trendValue, className = "", showSparkline = false, sparklinePoints = 20, sparklineHeight = 40, thresholds, colorBackground, colorValue = false }) {
|
|
4681
|
-
const
|
|
5710
|
+
function MetricStat({ rows, value: directValue, unit: directUnit, isLoading = false, error, label, formatValue = defaultFormatValue$1, showTimestamp = false, trend, trendValue, className = "", showSparkline = false, sparklinePoints = 20, sparklineHeight = 40, thresholds, colorBackground, colorValue = false }) {
|
|
5711
|
+
const statData = useMemo(() => buildStatData(rows), [rows]);
|
|
5712
|
+
const latestValue = directValue ?? statData.latestValue;
|
|
5713
|
+
const unit = directUnit ?? statData.unit;
|
|
5714
|
+
const { timestamp, dataPoints, metricName } = statData;
|
|
4682
5715
|
const sparklineData = useMemo(() => {
|
|
4683
5716
|
if (!showSparkline || dataPoints.length === 0) return [];
|
|
4684
5717
|
return dataPoints.slice(-sparklinePoints).map((dp) => ({ value: dp.value }));
|
|
@@ -4782,7 +5815,6 @@ function TrendIndicator({ direction, value }) {
|
|
|
4782
5815
|
children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
|
|
4783
5816
|
});
|
|
4784
5817
|
}
|
|
4785
|
-
|
|
4786
5818
|
//#endregion
|
|
4787
5819
|
//#region src/components/observability/MetricTable/index.tsx
|
|
4788
5820
|
/**
|
|
@@ -4922,7 +5954,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
|
|
|
4922
5954
|
})]
|
|
4923
5955
|
});
|
|
4924
5956
|
}
|
|
4925
|
-
|
|
4926
5957
|
//#endregion
|
|
4927
5958
|
//#region src/lib/renderer.tsx
|
|
4928
5959
|
/**
|
|
@@ -5027,7 +6058,6 @@ function Renderer({ tree, registry, fallback }) {
|
|
|
5027
6058
|
fallback
|
|
5028
6059
|
});
|
|
5029
6060
|
}
|
|
5030
|
-
|
|
5031
6061
|
//#endregion
|
|
5032
6062
|
//#region src/lib/catalog.ts
|
|
5033
6063
|
const dashboardCatalog = createCatalog({
|
|
@@ -5227,8 +6257,7 @@ const dashboardCatalog = createCatalog({
|
|
|
5227
6257
|
}
|
|
5228
6258
|
}
|
|
5229
6259
|
});
|
|
5230
|
-
|
|
5231
|
-
|
|
6260
|
+
Object.keys(dashboardCatalog.components);
|
|
5232
6261
|
//#endregion
|
|
5233
6262
|
//#region src/components/dashboard/Badge/index.tsx
|
|
5234
6263
|
function Badge({ element }) {
|
|
@@ -5252,7 +6281,6 @@ function Badge({ element }) {
|
|
|
5252
6281
|
children: text
|
|
5253
6282
|
});
|
|
5254
6283
|
}
|
|
5255
|
-
|
|
5256
6284
|
//#endregion
|
|
5257
6285
|
//#region src/components/dashboard/Card/index.tsx
|
|
5258
6286
|
function Card({ element, children }) {
|
|
@@ -5293,7 +6321,6 @@ function Card({ element, children }) {
|
|
|
5293
6321
|
})]
|
|
5294
6322
|
});
|
|
5295
6323
|
}
|
|
5296
|
-
|
|
5297
6324
|
//#endregion
|
|
5298
6325
|
//#region src/components/dashboard/Divider/index.tsx
|
|
5299
6326
|
function Divider({ element }) {
|
|
@@ -5331,7 +6358,6 @@ function Divider({ element }) {
|
|
|
5331
6358
|
margin: "16px 0"
|
|
5332
6359
|
} });
|
|
5333
6360
|
}
|
|
5334
|
-
|
|
5335
6361
|
//#endregion
|
|
5336
6362
|
//#region src/components/dashboard/Empty/index.tsx
|
|
5337
6363
|
function Empty({ element, onAction }) {
|
|
@@ -5374,7 +6400,6 @@ function Empty({ element, onAction }) {
|
|
|
5374
6400
|
]
|
|
5375
6401
|
});
|
|
5376
6402
|
}
|
|
5377
|
-
|
|
5378
6403
|
//#endregion
|
|
5379
6404
|
//#region src/components/dashboard/Grid/index.tsx
|
|
5380
6405
|
function Grid({ element, children }) {
|
|
@@ -5392,7 +6417,6 @@ function Grid({ element, children }) {
|
|
|
5392
6417
|
children
|
|
5393
6418
|
});
|
|
5394
6419
|
}
|
|
5395
|
-
|
|
5396
6420
|
//#endregion
|
|
5397
6421
|
//#region src/components/dashboard/Heading/index.tsx
|
|
5398
6422
|
function Heading({ element }) {
|
|
@@ -5411,7 +6435,6 @@ function Heading({ element }) {
|
|
|
5411
6435
|
children: text
|
|
5412
6436
|
});
|
|
5413
6437
|
}
|
|
5414
|
-
|
|
5415
6438
|
//#endregion
|
|
5416
6439
|
//#region src/components/dashboard/Stack/index.tsx
|
|
5417
6440
|
function Stack({ element, children }) {
|
|
@@ -5435,7 +6458,6 @@ function Stack({ element, children }) {
|
|
|
5435
6458
|
children
|
|
5436
6459
|
});
|
|
5437
6460
|
}
|
|
5438
|
-
|
|
5439
6461
|
//#endregion
|
|
5440
6462
|
//#region src/components/dashboard/Text/index.tsx
|
|
5441
6463
|
function Text({ element }) {
|
|
@@ -5454,7 +6476,6 @@ function Text({ element }) {
|
|
|
5454
6476
|
children: content
|
|
5455
6477
|
});
|
|
5456
6478
|
}
|
|
5457
|
-
|
|
5458
6479
|
//#endregion
|
|
5459
6480
|
//#region src/components/observability/renderers/OtelLogTimeline.tsx
|
|
5460
6481
|
function OtelLogTimeline(props) {
|
|
@@ -5476,7 +6497,6 @@ function OtelLogTimeline(props) {
|
|
|
5476
6497
|
})
|
|
5477
6498
|
});
|
|
5478
6499
|
}
|
|
5479
|
-
|
|
5480
6500
|
//#endregion
|
|
5481
6501
|
//#region src/components/observability/renderers/OtelMetricDiscovery.tsx
|
|
5482
6502
|
const TYPE_ORDER = {
|
|
@@ -5554,7 +6574,6 @@ function OtelMetricDiscovery(props) {
|
|
|
5554
6574
|
})
|
|
5555
6575
|
});
|
|
5556
6576
|
}
|
|
5557
|
-
|
|
5558
6577
|
//#endregion
|
|
5559
6578
|
//#region src/components/observability/renderers/OtelMetricHistogram.tsx
|
|
5560
6579
|
function OtelMetricHistogram(props) {
|
|
@@ -5575,9 +6594,15 @@ function OtelMetricHistogram(props) {
|
|
|
5575
6594
|
unit: props.element.props.unit ?? void 0
|
|
5576
6595
|
});
|
|
5577
6596
|
}
|
|
5578
|
-
|
|
5579
6597
|
//#endregion
|
|
5580
6598
|
//#region src/components/observability/renderers/OtelMetricStat.tsx
|
|
6599
|
+
const EMPTY_ROWS = [];
|
|
6600
|
+
const GROUPED_AGGREGATE_ERROR = /* @__PURE__ */ new Error("MetricStat cannot display grouped aggregates. Remove groupBy or use MetricTable.");
|
|
6601
|
+
function isAggregatedRequest(props) {
|
|
6602
|
+
const ds = props.element.dataSource;
|
|
6603
|
+
if (!ds || ds.method !== "searchMetricsPage" || !ds.params) return false;
|
|
6604
|
+
return !!ds.params.aggregate;
|
|
6605
|
+
}
|
|
5581
6606
|
function OtelMetricStat(props) {
|
|
5582
6607
|
if (!props.hasData) return /* @__PURE__ */ jsx("div", {
|
|
5583
6608
|
style: {
|
|
@@ -5586,6 +6611,24 @@ function OtelMetricStat(props) {
|
|
|
5586
6611
|
},
|
|
5587
6612
|
children: "No data source"
|
|
5588
6613
|
});
|
|
6614
|
+
if (isAggregatedRequest(props)) {
|
|
6615
|
+
const rows = props.data?.data ?? [];
|
|
6616
|
+
if (rows.length > 1) return /* @__PURE__ */ jsx(MetricStat, {
|
|
6617
|
+
rows: EMPTY_ROWS,
|
|
6618
|
+
error: GROUPED_AGGREGATE_ERROR,
|
|
6619
|
+
label: props.element.props.label ?? void 0,
|
|
6620
|
+
formatValue: formatOtelValue
|
|
6621
|
+
});
|
|
6622
|
+
return /* @__PURE__ */ jsx(MetricStat, {
|
|
6623
|
+
rows: EMPTY_ROWS,
|
|
6624
|
+
value: rows[0]?.value,
|
|
6625
|
+
isLoading: props.loading,
|
|
6626
|
+
error: props.error ?? void 0,
|
|
6627
|
+
label: props.element.props.label ?? void 0,
|
|
6628
|
+
showSparkline: false,
|
|
6629
|
+
formatValue: formatOtelValue
|
|
6630
|
+
});
|
|
6631
|
+
}
|
|
5589
6632
|
const response = props.data;
|
|
5590
6633
|
return /* @__PURE__ */ jsx(MetricStat, {
|
|
5591
6634
|
rows: response?.data ?? [],
|
|
@@ -5596,7 +6639,6 @@ function OtelMetricStat(props) {
|
|
|
5596
6639
|
formatValue: formatOtelValue
|
|
5597
6640
|
});
|
|
5598
6641
|
}
|
|
5599
|
-
|
|
5600
6642
|
//#endregion
|
|
5601
6643
|
//#region src/components/observability/renderers/OtelMetricTable.tsx
|
|
5602
6644
|
function OtelMetricTable(props) {
|
|
@@ -5615,7 +6657,6 @@ function OtelMetricTable(props) {
|
|
|
5615
6657
|
maxRows: props.element.props.maxRows ?? 100
|
|
5616
6658
|
});
|
|
5617
6659
|
}
|
|
5618
|
-
|
|
5619
6660
|
//#endregion
|
|
5620
6661
|
//#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
|
|
5621
6662
|
function OtelMetricTimeSeries(props) {
|
|
@@ -5637,7 +6678,6 @@ function OtelMetricTimeSeries(props) {
|
|
|
5637
6678
|
unit: props.element.props.unit ?? void 0
|
|
5638
6679
|
});
|
|
5639
6680
|
}
|
|
5640
|
-
|
|
5641
6681
|
//#endregion
|
|
5642
6682
|
//#region src/components/observability/renderers/OtelTraceDetail.tsx
|
|
5643
6683
|
function OtelTraceDetail(props) {
|
|
@@ -5649,19 +6689,15 @@ function OtelTraceDetail(props) {
|
|
|
5649
6689
|
children: "No data source"
|
|
5650
6690
|
});
|
|
5651
6691
|
const rows = props.data?.data ?? [];
|
|
5652
|
-
const
|
|
5653
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
5654
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
6692
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
5655
6693
|
return /* @__PURE__ */ jsx(TraceDetail, {
|
|
5656
6694
|
rows,
|
|
5657
6695
|
isLoading: props.loading,
|
|
5658
6696
|
error: props.error ?? void 0,
|
|
5659
|
-
service,
|
|
5660
6697
|
traceId,
|
|
5661
6698
|
onBack: () => {}
|
|
5662
6699
|
});
|
|
5663
6700
|
}
|
|
5664
|
-
|
|
5665
6701
|
//#endregion
|
|
5666
6702
|
//#region src/components/observability/DynamicDashboard/index.tsx
|
|
5667
6703
|
const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
|
|
@@ -5687,24 +6723,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
|
|
|
5687
6723
|
children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
|
|
5688
6724
|
});
|
|
5689
6725
|
}
|
|
5690
|
-
|
|
6726
|
+
//#endregion
|
|
6727
|
+
//#region src/components/observability/TraceComparison/index.tsx
|
|
6728
|
+
function computeTraceStats(rows) {
|
|
6729
|
+
if (rows.length === 0) return {
|
|
6730
|
+
durationMs: 0,
|
|
6731
|
+
spanCount: 0
|
|
6732
|
+
};
|
|
6733
|
+
let minTs = Infinity;
|
|
6734
|
+
let maxEnd = -Infinity;
|
|
6735
|
+
for (const row of rows) {
|
|
6736
|
+
const startMs = parseInt(row.Timestamp, 10) / 1e6;
|
|
6737
|
+
const endMs = startMs + (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6738
|
+
minTs = Math.min(minTs, startMs);
|
|
6739
|
+
maxEnd = Math.max(maxEnd, endMs);
|
|
6740
|
+
}
|
|
6741
|
+
return {
|
|
6742
|
+
durationMs: maxEnd - minTs,
|
|
6743
|
+
spanCount: rows.length
|
|
6744
|
+
};
|
|
6745
|
+
}
|
|
6746
|
+
function collectSignatures(rows) {
|
|
6747
|
+
const map = /* @__PURE__ */ new Map();
|
|
6748
|
+
for (const row of rows) {
|
|
6749
|
+
const key = `${row.ServiceName ?? "unknown"}::${row.SpanName ?? ""}`;
|
|
6750
|
+
const durMs = (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6751
|
+
const existing = map.get(key);
|
|
6752
|
+
if (existing) {
|
|
6753
|
+
existing.count++;
|
|
6754
|
+
existing.totalDurationMs += durMs;
|
|
6755
|
+
} else map.set(key, {
|
|
6756
|
+
count: 1,
|
|
6757
|
+
totalDurationMs: durMs
|
|
6758
|
+
});
|
|
6759
|
+
}
|
|
6760
|
+
return map;
|
|
6761
|
+
}
|
|
6762
|
+
function computeDiff(rowsA, rowsB) {
|
|
6763
|
+
const sigA = collectSignatures(rowsA);
|
|
6764
|
+
const sigB = collectSignatures(rowsB);
|
|
6765
|
+
const allKeys = new Set([...sigA.keys(), ...sigB.keys()]);
|
|
6766
|
+
const result = [];
|
|
6767
|
+
for (const key of allKeys) {
|
|
6768
|
+
const [serviceName = "unknown", spanName = ""] = key.split("::");
|
|
6769
|
+
const a = sigA.get(key);
|
|
6770
|
+
const b = sigB.get(key);
|
|
6771
|
+
const countA = a?.count ?? 0;
|
|
6772
|
+
const countB = b?.count ?? 0;
|
|
6773
|
+
const avgA = a ? a.totalDurationMs / a.count : 0;
|
|
6774
|
+
const avgB = b ? b.totalDurationMs / b.count : 0;
|
|
6775
|
+
result.push({
|
|
6776
|
+
serviceName,
|
|
6777
|
+
spanName,
|
|
6778
|
+
countA,
|
|
6779
|
+
countB,
|
|
6780
|
+
avgDurationA: avgA,
|
|
6781
|
+
avgDurationB: avgB,
|
|
6782
|
+
deltaMs: avgB - avgA
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6785
|
+
return result.sort((a, b) => {
|
|
6786
|
+
const aShared = a.countA > 0 && a.countB > 0;
|
|
6787
|
+
if (aShared !== (b.countA > 0 && b.countB > 0)) return aShared ? 1 : -1;
|
|
6788
|
+
return Math.abs(b.deltaMs) - Math.abs(a.deltaMs);
|
|
6789
|
+
});
|
|
6790
|
+
}
|
|
6791
|
+
function formatDelta(ms) {
|
|
6792
|
+
return `${ms > 0 ? "+" : ""}${formatDuration(ms)}`;
|
|
6793
|
+
}
|
|
6794
|
+
function TraceComparison({ traceIdA, traceIdB, onBack }) {
|
|
6795
|
+
const dsA = useMemo(() => ({
|
|
6796
|
+
method: "getTrace",
|
|
6797
|
+
params: { traceId: traceIdA }
|
|
6798
|
+
}), [traceIdA]);
|
|
6799
|
+
const dsB = useMemo(() => ({
|
|
6800
|
+
method: "getTrace",
|
|
6801
|
+
params: { traceId: traceIdB }
|
|
6802
|
+
}), [traceIdB]);
|
|
6803
|
+
const { data: rowsA, loading: loadingA, error: errorA } = useKopaiData(dsA);
|
|
6804
|
+
const { data: rowsB, loading: loadingB, error: errorB } = useKopaiData(dsB);
|
|
6805
|
+
const statsA = useMemo(() => computeTraceStats(rowsA ?? []), [rowsA]);
|
|
6806
|
+
const statsB = useMemo(() => computeTraceStats(rowsB ?? []), [rowsB]);
|
|
6807
|
+
const diff = useMemo(() => computeDiff(rowsA ?? [], rowsB ?? []), [rowsA, rowsB]);
|
|
6808
|
+
const durationDelta = statsB.durationMs - statsA.durationMs;
|
|
6809
|
+
const spanDelta = statsB.spanCount - statsA.spanCount;
|
|
6810
|
+
const isLoading = loadingA || loadingB;
|
|
6811
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
6812
|
+
className: "flex flex-col gap-4",
|
|
6813
|
+
children: [
|
|
6814
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6815
|
+
className: "flex items-center justify-between bg-background border border-border rounded-lg p-4",
|
|
6816
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
6817
|
+
className: "flex items-center gap-4",
|
|
6818
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
6819
|
+
onClick: onBack,
|
|
6820
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
6821
|
+
children: "← Back"
|
|
6822
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
6823
|
+
className: "flex items-center gap-6 text-sm",
|
|
6824
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6825
|
+
className: "text-muted-foreground mr-1",
|
|
6826
|
+
children: "A:"
|
|
6827
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
6828
|
+
className: "font-mono text-xs text-foreground",
|
|
6829
|
+
children: [traceIdA.slice(0, 16), "..."]
|
|
6830
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6831
|
+
className: "text-muted-foreground mr-1",
|
|
6832
|
+
children: "B:"
|
|
6833
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
6834
|
+
className: "font-mono text-xs text-foreground",
|
|
6835
|
+
children: [traceIdB.slice(0, 16), "..."]
|
|
6836
|
+
})] })]
|
|
6837
|
+
})]
|
|
6838
|
+
}), !isLoading && /* @__PURE__ */ jsxs("div", {
|
|
6839
|
+
className: "flex items-center gap-6 text-sm",
|
|
6840
|
+
children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6841
|
+
className: "text-muted-foreground mr-1",
|
|
6842
|
+
children: "Duration delta:"
|
|
6843
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
6844
|
+
className: durationDelta > 0 ? "text-red-400" : durationDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6845
|
+
children: formatDelta(durationDelta)
|
|
6846
|
+
})] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
|
|
6847
|
+
className: "text-muted-foreground mr-1",
|
|
6848
|
+
children: "Span count delta:"
|
|
6849
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
6850
|
+
className: spanDelta > 0 ? "text-red-400" : spanDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6851
|
+
children: spanDelta > 0 ? `+${spanDelta}` : String(spanDelta)
|
|
6852
|
+
})] })]
|
|
6853
|
+
})]
|
|
6854
|
+
}),
|
|
6855
|
+
/* @__PURE__ */ jsxs("div", {
|
|
6856
|
+
className: "grid grid-cols-2 gap-4",
|
|
6857
|
+
style: { height: "50vh" },
|
|
6858
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
6859
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6860
|
+
children: /* @__PURE__ */ jsx(TraceTimeline, {
|
|
6861
|
+
rows: rowsA ?? [],
|
|
6862
|
+
isLoading: loadingA,
|
|
6863
|
+
error: errorA ?? void 0
|
|
6864
|
+
})
|
|
6865
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
6866
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6867
|
+
children: /* @__PURE__ */ jsx(TraceTimeline, {
|
|
6868
|
+
rows: rowsB ?? [],
|
|
6869
|
+
isLoading: loadingB,
|
|
6870
|
+
error: errorB ?? void 0
|
|
6871
|
+
})
|
|
6872
|
+
})]
|
|
6873
|
+
}),
|
|
6874
|
+
!isLoading && diff.length > 0 && /* @__PURE__ */ jsxs("div", {
|
|
6875
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6876
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
6877
|
+
className: "px-4 py-3 border-b border-border bg-background",
|
|
6878
|
+
children: /* @__PURE__ */ jsx("h3", {
|
|
6879
|
+
className: "text-sm font-medium text-foreground",
|
|
6880
|
+
children: "Structural Diff"
|
|
6881
|
+
})
|
|
6882
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
6883
|
+
className: "overflow-x-auto",
|
|
6884
|
+
children: /* @__PURE__ */ jsxs("table", {
|
|
6885
|
+
className: "w-full text-sm",
|
|
6886
|
+
children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
|
|
6887
|
+
className: "border-b border-border bg-muted/30",
|
|
6888
|
+
children: [
|
|
6889
|
+
/* @__PURE__ */ jsx("th", {
|
|
6890
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6891
|
+
children: "Service"
|
|
6892
|
+
}),
|
|
6893
|
+
/* @__PURE__ */ jsx("th", {
|
|
6894
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6895
|
+
children: "Span"
|
|
6896
|
+
}),
|
|
6897
|
+
/* @__PURE__ */ jsx("th", {
|
|
6898
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6899
|
+
children: "Count A"
|
|
6900
|
+
}),
|
|
6901
|
+
/* @__PURE__ */ jsx("th", {
|
|
6902
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6903
|
+
children: "Count B"
|
|
6904
|
+
}),
|
|
6905
|
+
/* @__PURE__ */ jsx("th", {
|
|
6906
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6907
|
+
children: "Avg Dur A"
|
|
6908
|
+
}),
|
|
6909
|
+
/* @__PURE__ */ jsx("th", {
|
|
6910
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6911
|
+
children: "Avg Dur B"
|
|
6912
|
+
}),
|
|
6913
|
+
/* @__PURE__ */ jsx("th", {
|
|
6914
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6915
|
+
children: "Delta"
|
|
6916
|
+
})
|
|
6917
|
+
]
|
|
6918
|
+
}) }), /* @__PURE__ */ jsx("tbody", { children: diff.map((row) => {
|
|
6919
|
+
const onlyA = row.countA > 0 && row.countB === 0;
|
|
6920
|
+
const onlyB = row.countA === 0 && row.countB > 0;
|
|
6921
|
+
return /* @__PURE__ */ jsxs("tr", {
|
|
6922
|
+
className: `border-b border-border/50 ${onlyA ? "bg-red-500/5" : onlyB ? "bg-green-500/5" : ""}`,
|
|
6923
|
+
children: [
|
|
6924
|
+
/* @__PURE__ */ jsx("td", {
|
|
6925
|
+
className: "px-4 py-1.5 text-foreground",
|
|
6926
|
+
children: row.serviceName
|
|
6927
|
+
}),
|
|
6928
|
+
/* @__PURE__ */ jsx("td", {
|
|
6929
|
+
className: "px-4 py-1.5 font-mono text-xs text-foreground",
|
|
6930
|
+
children: row.spanName
|
|
6931
|
+
}),
|
|
6932
|
+
/* @__PURE__ */ jsx("td", {
|
|
6933
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6934
|
+
children: row.countA || /* @__PURE__ */ jsx("span", {
|
|
6935
|
+
className: "text-muted-foreground",
|
|
6936
|
+
children: "-"
|
|
6937
|
+
})
|
|
6938
|
+
}),
|
|
6939
|
+
/* @__PURE__ */ jsx("td", {
|
|
6940
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6941
|
+
children: row.countB || /* @__PURE__ */ jsx("span", {
|
|
6942
|
+
className: "text-muted-foreground",
|
|
6943
|
+
children: "-"
|
|
6944
|
+
})
|
|
6945
|
+
}),
|
|
6946
|
+
/* @__PURE__ */ jsx("td", {
|
|
6947
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6948
|
+
children: row.countA > 0 ? formatDuration(row.avgDurationA) : /* @__PURE__ */ jsx("span", {
|
|
6949
|
+
className: "text-muted-foreground",
|
|
6950
|
+
children: "-"
|
|
6951
|
+
})
|
|
6952
|
+
}),
|
|
6953
|
+
/* @__PURE__ */ jsx("td", {
|
|
6954
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6955
|
+
children: row.countB > 0 ? formatDuration(row.avgDurationB) : /* @__PURE__ */ jsx("span", {
|
|
6956
|
+
className: "text-muted-foreground",
|
|
6957
|
+
children: "-"
|
|
6958
|
+
})
|
|
6959
|
+
}),
|
|
6960
|
+
/* @__PURE__ */ jsx("td", {
|
|
6961
|
+
className: "px-4 py-1.5 text-right",
|
|
6962
|
+
children: row.countA > 0 && row.countB > 0 ? /* @__PURE__ */ jsx("span", {
|
|
6963
|
+
className: row.deltaMs > 0 ? "text-red-400" : row.deltaMs < 0 ? "text-green-400" : "text-foreground",
|
|
6964
|
+
children: formatDelta(row.deltaMs)
|
|
6965
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
6966
|
+
className: onlyA ? "text-red-400" : "text-green-400",
|
|
6967
|
+
children: onlyA ? "removed" : "added"
|
|
6968
|
+
})
|
|
6969
|
+
})
|
|
6970
|
+
]
|
|
6971
|
+
}, `${row.serviceName}::${row.spanName}`);
|
|
6972
|
+
}) })]
|
|
6973
|
+
})
|
|
6974
|
+
})]
|
|
6975
|
+
})
|
|
6976
|
+
]
|
|
6977
|
+
});
|
|
6978
|
+
}
|
|
5691
6979
|
//#endregion
|
|
5692
6980
|
//#region src/components/observability/ServiceList/shortcuts.ts
|
|
5693
6981
|
const SERVICES_SHORTCUTS = {
|
|
5694
|
-
name: "
|
|
6982
|
+
name: "Traces",
|
|
5695
6983
|
shortcuts: [{
|
|
5696
6984
|
keys: ["Backspace"],
|
|
5697
6985
|
description: "Go back"
|
|
5698
6986
|
}]
|
|
5699
6987
|
};
|
|
5700
|
-
|
|
5701
6988
|
//#endregion
|
|
5702
6989
|
//#region src/pages/observability.tsx
|
|
5703
6990
|
const TABS = [
|
|
5704
6991
|
{
|
|
5705
6992
|
key: "services",
|
|
5706
|
-
label: "
|
|
5707
|
-
shortcutKey: "
|
|
6993
|
+
label: "Traces",
|
|
6994
|
+
shortcutKey: "T"
|
|
5708
6995
|
},
|
|
5709
6996
|
{
|
|
5710
6997
|
key: "logs",
|
|
@@ -5724,20 +7011,53 @@ function readURLState() {
|
|
|
5724
7011
|
const span = params.get("span");
|
|
5725
7012
|
const dashboardId = params.get("dashboardId");
|
|
5726
7013
|
const rawTab = params.get("tab");
|
|
7014
|
+
const tab = service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services";
|
|
7015
|
+
const rawLimit = params.get("limit");
|
|
7016
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) : null;
|
|
5727
7017
|
return {
|
|
5728
|
-
tab
|
|
7018
|
+
tab,
|
|
5729
7019
|
service,
|
|
7020
|
+
operation: params.get("operation"),
|
|
7021
|
+
tags: params.get("tags"),
|
|
7022
|
+
lookback: params.get("lookback"),
|
|
7023
|
+
tsMin: params.get("tsMin"),
|
|
7024
|
+
tsMax: params.get("tsMax"),
|
|
7025
|
+
minDuration: params.get("minDuration"),
|
|
7026
|
+
maxDuration: params.get("maxDuration"),
|
|
7027
|
+
limit: limit !== null && !isNaN(limit) ? limit : null,
|
|
7028
|
+
sort: params.get("sort"),
|
|
5730
7029
|
trace,
|
|
5731
7030
|
span,
|
|
7031
|
+
view: params.get("view"),
|
|
7032
|
+
uiFind: params.get("uiFind"),
|
|
7033
|
+
compare: params.get("compare"),
|
|
7034
|
+
viewStart: params.get("viewStart"),
|
|
7035
|
+
viewEnd: params.get("viewEnd"),
|
|
5732
7036
|
dashboardId
|
|
5733
7037
|
};
|
|
5734
7038
|
}
|
|
5735
7039
|
function pushURLState(state, { replace = false } = {}) {
|
|
5736
7040
|
const params = new URLSearchParams();
|
|
5737
7041
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
5738
|
-
if (state.
|
|
5739
|
-
|
|
5740
|
-
|
|
7042
|
+
if (state.tab === "services") {
|
|
7043
|
+
if (state.service) params.set("service", state.service);
|
|
7044
|
+
if (state.operation) params.set("operation", state.operation);
|
|
7045
|
+
if (state.tags) params.set("tags", state.tags);
|
|
7046
|
+
if (state.lookback) params.set("lookback", state.lookback);
|
|
7047
|
+
if (state.tsMin) params.set("tsMin", state.tsMin);
|
|
7048
|
+
if (state.tsMax) params.set("tsMax", state.tsMax);
|
|
7049
|
+
if (state.minDuration) params.set("minDuration", state.minDuration);
|
|
7050
|
+
if (state.maxDuration) params.set("maxDuration", state.maxDuration);
|
|
7051
|
+
if (state.limit != null && state.limit !== 20) params.set("limit", String(state.limit));
|
|
7052
|
+
if (state.sort) params.set("sort", state.sort);
|
|
7053
|
+
if (state.trace) params.set("trace", state.trace);
|
|
7054
|
+
if (state.span) params.set("span", state.span);
|
|
7055
|
+
if (state.view) params.set("view", state.view);
|
|
7056
|
+
if (state.uiFind) params.set("uiFind", state.uiFind);
|
|
7057
|
+
if (state.compare) params.set("compare", state.compare);
|
|
7058
|
+
if (state.viewStart) params.set("viewStart", state.viewStart);
|
|
7059
|
+
if (state.viewEnd) params.set("viewEnd", state.viewEnd);
|
|
7060
|
+
}
|
|
5741
7061
|
const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
|
|
5742
7062
|
if (dashboardId) params.set("dashboardId", dashboardId);
|
|
5743
7063
|
const qs = params.toString();
|
|
@@ -5754,8 +7074,22 @@ let _cachedSearch = "";
|
|
|
5754
7074
|
let _cachedState = {
|
|
5755
7075
|
tab: "services",
|
|
5756
7076
|
service: null,
|
|
7077
|
+
operation: null,
|
|
7078
|
+
tags: null,
|
|
7079
|
+
lookback: null,
|
|
7080
|
+
tsMin: null,
|
|
7081
|
+
tsMax: null,
|
|
7082
|
+
minDuration: null,
|
|
7083
|
+
maxDuration: null,
|
|
7084
|
+
limit: null,
|
|
7085
|
+
sort: null,
|
|
5757
7086
|
trace: null,
|
|
5758
7087
|
span: null,
|
|
7088
|
+
view: null,
|
|
7089
|
+
uiFind: null,
|
|
7090
|
+
compare: null,
|
|
7091
|
+
viewStart: null,
|
|
7092
|
+
viewEnd: null,
|
|
5759
7093
|
dashboardId: null
|
|
5760
7094
|
};
|
|
5761
7095
|
function getURLSnapshot() {
|
|
@@ -5865,6 +7199,26 @@ function parseDuration(input) {
|
|
|
5865
7199
|
s: 1e9
|
|
5866
7200
|
}[unit]));
|
|
5867
7201
|
}
|
|
7202
|
+
function parseLogfmt(str) {
|
|
7203
|
+
const result = {};
|
|
7204
|
+
const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
|
|
7205
|
+
let m;
|
|
7206
|
+
while ((m = re.exec(str)) !== null) {
|
|
7207
|
+
const key = m[1];
|
|
7208
|
+
if (key) result[key] = m[2] ?? m[3] ?? "";
|
|
7209
|
+
}
|
|
7210
|
+
return result;
|
|
7211
|
+
}
|
|
7212
|
+
const LOOKBACK_MS = {
|
|
7213
|
+
"5m": 5 * 6e4,
|
|
7214
|
+
"15m": 15 * 6e4,
|
|
7215
|
+
"30m": 30 * 6e4,
|
|
7216
|
+
"1h": 60 * 6e4,
|
|
7217
|
+
"2h": 120 * 6e4,
|
|
7218
|
+
"6h": 360 * 6e4,
|
|
7219
|
+
"12h": 720 * 6e4,
|
|
7220
|
+
"24h": 1440 * 6e4
|
|
7221
|
+
};
|
|
5868
7222
|
function LogsTab() {
|
|
5869
7223
|
const [initState] = useState(() => readLogFilters());
|
|
5870
7224
|
const [filters, setFilters] = useState(initState.filters);
|
|
@@ -5924,185 +7278,145 @@ function LogsTab() {
|
|
|
5924
7278
|
})]
|
|
5925
7279
|
});
|
|
5926
7280
|
}
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
sortOrder: "DESC"
|
|
5932
|
-
}
|
|
5933
|
-
};
|
|
5934
|
-
function ServiceListView({ onSelect }) {
|
|
5935
|
-
const { data, loading, error } = useKopaiData(SERVICES_DS);
|
|
5936
|
-
return /* @__PURE__ */ jsx(ServiceList, {
|
|
5937
|
-
services: useMemo(() => {
|
|
5938
|
-
if (!data?.data) return [];
|
|
5939
|
-
const names = /* @__PURE__ */ new Set();
|
|
5940
|
-
for (const row of data.data) names.add(row.ServiceName ?? "unknown");
|
|
5941
|
-
return Array.from(names).sort().map((name) => ({ name }));
|
|
5942
|
-
}, [data]),
|
|
5943
|
-
isLoading: loading,
|
|
5944
|
-
error: error ?? void 0,
|
|
5945
|
-
onSelect
|
|
5946
|
-
});
|
|
5947
|
-
}
|
|
5948
|
-
function TraceSearchView({ service, onBack, onSelectTrace }) {
|
|
5949
|
-
const [ds, setDs] = useState(() => ({
|
|
5950
|
-
method: "searchTracesPage",
|
|
5951
|
-
params: {
|
|
5952
|
-
serviceName: service,
|
|
5953
|
-
limit: 20,
|
|
5954
|
-
sortOrder: "DESC"
|
|
5955
|
-
}
|
|
5956
|
-
}));
|
|
5957
|
-
const handleSearch = useCallback((filters) => {
|
|
7281
|
+
function TraceSearchView({ onSelectTrace, onCompare }) {
|
|
7282
|
+
const urlState = useURLState();
|
|
7283
|
+
const service = urlState.service;
|
|
7284
|
+
const ds = useMemo(() => {
|
|
5958
7285
|
const params = {
|
|
5959
|
-
|
|
5960
|
-
limit: filters.limit,
|
|
7286
|
+
limit: urlState.limit ?? 20,
|
|
5961
7287
|
sortOrder: "DESC"
|
|
5962
7288
|
};
|
|
5963
|
-
if (
|
|
5964
|
-
if (
|
|
5965
|
-
if (
|
|
5966
|
-
const
|
|
7289
|
+
if (service) params.serviceName = service;
|
|
7290
|
+
if (urlState.operation) params.spanName = urlState.operation;
|
|
7291
|
+
if (urlState.lookback) {
|
|
7292
|
+
const ms = LOOKBACK_MS[urlState.lookback];
|
|
7293
|
+
if (ms) params.timestampMin = String((Date.now() - ms) * 1e6);
|
|
7294
|
+
}
|
|
7295
|
+
if (urlState.tsMin) params.timestampMin = urlState.tsMin;
|
|
7296
|
+
if (urlState.tsMax) params.timestampMax = urlState.tsMax;
|
|
7297
|
+
if (urlState.minDuration) {
|
|
7298
|
+
const parsed = parseDuration(urlState.minDuration);
|
|
5967
7299
|
if (parsed) params.durationMin = parsed;
|
|
5968
7300
|
}
|
|
5969
|
-
if (
|
|
5970
|
-
const parsed = parseDuration(
|
|
7301
|
+
if (urlState.maxDuration) {
|
|
7302
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
5971
7303
|
if (parsed) params.durationMax = parsed;
|
|
5972
7304
|
}
|
|
5973
|
-
|
|
5974
|
-
|
|
7305
|
+
if (urlState.tags) {
|
|
7306
|
+
const tagMap = parseLogfmt(urlState.tags);
|
|
7307
|
+
if (Object.keys(tagMap).length > 0) params.tags = tagMap;
|
|
7308
|
+
}
|
|
7309
|
+
return {
|
|
7310
|
+
method: "searchTraceSummariesPage",
|
|
5975
7311
|
params
|
|
7312
|
+
};
|
|
7313
|
+
}, [
|
|
7314
|
+
service,
|
|
7315
|
+
urlState.operation,
|
|
7316
|
+
urlState.lookback,
|
|
7317
|
+
urlState.tsMin,
|
|
7318
|
+
urlState.tsMax,
|
|
7319
|
+
urlState.minDuration,
|
|
7320
|
+
urlState.maxDuration,
|
|
7321
|
+
urlState.limit,
|
|
7322
|
+
urlState.tags
|
|
7323
|
+
]);
|
|
7324
|
+
const handleSearch = useCallback((filters) => {
|
|
7325
|
+
pushURLState({
|
|
7326
|
+
tab: "services",
|
|
7327
|
+
service: filters.service ?? service,
|
|
7328
|
+
operation: filters.operation ?? null,
|
|
7329
|
+
tags: filters.tags ?? null,
|
|
7330
|
+
lookback: filters.lookback ?? null,
|
|
7331
|
+
minDuration: filters.minDuration ?? null,
|
|
7332
|
+
maxDuration: filters.maxDuration ?? null,
|
|
7333
|
+
limit: filters.limit
|
|
5976
7334
|
});
|
|
5977
7335
|
}, [service]);
|
|
5978
7336
|
const { data, loading, error } = useKopaiData(ds);
|
|
5979
|
-
const
|
|
5980
|
-
const
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
5984
|
-
|
|
5985
|
-
|
|
5986
|
-
|
|
5987
|
-
const ac = new AbortController();
|
|
5988
|
-
Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
|
|
5989
|
-
if (!ac.signal.aborted) {
|
|
5990
|
-
const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
5991
|
-
setFullTraces(new Map(entries));
|
|
5992
|
-
}
|
|
5993
|
-
}).catch((err) => {
|
|
5994
|
-
if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
|
|
5995
|
-
});
|
|
5996
|
-
return () => ac.abort();
|
|
5997
|
-
}, [data, client]);
|
|
5998
|
-
const operations = useMemo(() => {
|
|
7337
|
+
const { data: servicesData } = useKopaiData(useMemo(() => ({ method: "getServices" }), []));
|
|
7338
|
+
const _services = servicesData?.services ?? [];
|
|
7339
|
+
const { data: opsData } = useKopaiData(useMemo(() => service ? {
|
|
7340
|
+
method: "getOperations",
|
|
7341
|
+
params: { serviceName: service }
|
|
7342
|
+
} : void 0, [service]));
|
|
7343
|
+
const operations = opsData?.operations ?? [];
|
|
7344
|
+
const traces = useMemo(() => {
|
|
5999
7345
|
if (!data?.data) return [];
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
7346
|
+
return data.data.map((row) => ({
|
|
7347
|
+
traceId: row.traceId,
|
|
7348
|
+
rootSpanName: row.rootSpanName,
|
|
7349
|
+
serviceName: row.rootServiceName,
|
|
7350
|
+
durationMs: parseInt(row.durationNs, 10) / 1e6,
|
|
7351
|
+
statusCode: row.errorCount > 0 ? "ERROR" : "OK",
|
|
7352
|
+
timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
|
|
7353
|
+
spanCount: row.spanCount,
|
|
7354
|
+
services: row.services,
|
|
7355
|
+
errorCount: row.errorCount
|
|
7356
|
+
}));
|
|
6003
7357
|
}, [data]);
|
|
6004
7358
|
return /* @__PURE__ */ jsx(TraceSearch, {
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6009
|
-
for (const row of data.data) {
|
|
6010
|
-
const tid = row.TraceId;
|
|
6011
|
-
if (!grouped.has(tid)) grouped.set(tid, []);
|
|
6012
|
-
grouped.get(tid).push(row);
|
|
6013
|
-
}
|
|
6014
|
-
return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
|
|
6015
|
-
const spans = fullTraces.get(traceId) ?? searchSpans;
|
|
6016
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
|
|
6017
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
6018
|
-
const svcMap = /* @__PURE__ */ new Map();
|
|
6019
|
-
let errorCount = 0;
|
|
6020
|
-
for (const s of spans) {
|
|
6021
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
6022
|
-
const entry = svcMap.get(svcName) ?? {
|
|
6023
|
-
count: 0,
|
|
6024
|
-
hasError: false
|
|
6025
|
-
};
|
|
6026
|
-
entry.count++;
|
|
6027
|
-
if (s.StatusCode === "ERROR") {
|
|
6028
|
-
entry.hasError = true;
|
|
6029
|
-
errorCount++;
|
|
6030
|
-
}
|
|
6031
|
-
svcMap.set(svcName, entry);
|
|
6032
|
-
}
|
|
6033
|
-
const services = Array.from(svcMap.entries()).map(([name, v]) => ({
|
|
6034
|
-
name,
|
|
6035
|
-
count: v.count,
|
|
6036
|
-
hasError: v.hasError
|
|
6037
|
-
})).sort((a, b) => b.count - a.count);
|
|
6038
|
-
return {
|
|
6039
|
-
traceId,
|
|
6040
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
6041
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
6042
|
-
durationMs: durationNs / 1e6,
|
|
6043
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
6044
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
6045
|
-
spanCount: spans.length,
|
|
6046
|
-
services,
|
|
6047
|
-
errorCount
|
|
6048
|
-
};
|
|
6049
|
-
});
|
|
6050
|
-
}, [data, fullTraces]),
|
|
7359
|
+
services: _services,
|
|
7360
|
+
service: service ?? "",
|
|
7361
|
+
traces,
|
|
6051
7362
|
operations,
|
|
6052
7363
|
isLoading: loading,
|
|
6053
7364
|
error: error ?? void 0,
|
|
6054
7365
|
onSelectTrace,
|
|
6055
|
-
|
|
7366
|
+
onCompare,
|
|
6056
7367
|
onSearch: handleSearch
|
|
6057
7368
|
});
|
|
6058
7369
|
}
|
|
6059
|
-
function TraceDetailView({
|
|
7370
|
+
function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
|
|
6060
7371
|
const { data, loading, error } = useKopaiData(useMemo(() => ({
|
|
6061
7372
|
method: "getTrace",
|
|
6062
7373
|
params: { traceId }
|
|
6063
7374
|
}), [traceId]));
|
|
6064
7375
|
return /* @__PURE__ */ jsx(TraceDetail, {
|
|
6065
|
-
service,
|
|
6066
7376
|
traceId,
|
|
6067
7377
|
rows: data ?? [],
|
|
6068
7378
|
isLoading: loading,
|
|
6069
7379
|
error: error ?? void 0,
|
|
6070
7380
|
selectedSpanId: selectedSpanId ?? void 0,
|
|
6071
7381
|
onSpanClick: (span) => onSelectSpan(span.spanId),
|
|
7382
|
+
onSpanDeselect: onDeselectSpan,
|
|
6072
7383
|
onBack
|
|
6073
7384
|
});
|
|
6074
7385
|
}
|
|
6075
|
-
function ServicesTab({
|
|
7386
|
+
function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
|
|
6076
7387
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
6077
|
-
const
|
|
6078
|
-
|
|
6079
|
-
const backToTraceListRef = useRef(onBackToTraceList);
|
|
6080
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
7388
|
+
const backRef = useRef(onBack);
|
|
7389
|
+
backRef.current = onBack;
|
|
6081
7390
|
useEffect(() => {
|
|
6082
7391
|
const handleKeyDown = (e) => {
|
|
6083
7392
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
|
6084
7393
|
if (e.key === "Backspace") {
|
|
6085
7394
|
e.preventDefault();
|
|
6086
|
-
|
|
6087
|
-
else if (selectedService) backToServicesRef.current();
|
|
7395
|
+
backRef.current();
|
|
6088
7396
|
}
|
|
6089
7397
|
};
|
|
6090
7398
|
window.addEventListener("keydown", handleKeyDown);
|
|
6091
7399
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
6092
|
-
}, [
|
|
6093
|
-
if (
|
|
6094
|
-
|
|
7400
|
+
}, []);
|
|
7401
|
+
if (compareParam) {
|
|
7402
|
+
const [traceIdA, traceIdB] = compareParam.split(",");
|
|
7403
|
+
if (traceIdA && traceIdB) return /* @__PURE__ */ jsx(TraceComparison, {
|
|
7404
|
+
traceIdA,
|
|
7405
|
+
traceIdB,
|
|
7406
|
+
onBack
|
|
7407
|
+
});
|
|
7408
|
+
}
|
|
7409
|
+
if (selectedTraceId) return /* @__PURE__ */ jsx(TraceDetailView, {
|
|
6095
7410
|
traceId: selectedTraceId,
|
|
6096
7411
|
selectedSpanId,
|
|
6097
7412
|
onSelectSpan,
|
|
6098
|
-
|
|
7413
|
+
onDeselectSpan,
|
|
7414
|
+
onBack
|
|
6099
7415
|
});
|
|
6100
|
-
|
|
6101
|
-
|
|
6102
|
-
|
|
6103
|
-
onSelectTrace
|
|
7416
|
+
return /* @__PURE__ */ jsx(TraceSearchView, {
|
|
7417
|
+
onSelectTrace,
|
|
7418
|
+
onCompare
|
|
6104
7419
|
});
|
|
6105
|
-
return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
|
|
6106
7420
|
}
|
|
6107
7421
|
const METRICS_TREE = {
|
|
6108
7422
|
root: "root",
|
|
@@ -6112,6 +7426,9 @@ const METRICS_TREE = {
|
|
|
6112
7426
|
type: "Stack",
|
|
6113
7427
|
children: [
|
|
6114
7428
|
"heading",
|
|
7429
|
+
"ingestion-heading",
|
|
7430
|
+
"ingestion-grid",
|
|
7431
|
+
"discovery-heading",
|
|
6115
7432
|
"description",
|
|
6116
7433
|
"discovery-card"
|
|
6117
7434
|
],
|
|
@@ -6132,6 +7449,96 @@ const METRICS_TREE = {
|
|
|
6132
7449
|
level: "h2"
|
|
6133
7450
|
}
|
|
6134
7451
|
},
|
|
7452
|
+
"ingestion-heading": {
|
|
7453
|
+
key: "ingestion-heading",
|
|
7454
|
+
type: "Heading",
|
|
7455
|
+
children: [],
|
|
7456
|
+
parentKey: "root",
|
|
7457
|
+
props: {
|
|
7458
|
+
text: "OTEL Ingestion",
|
|
7459
|
+
level: "h3"
|
|
7460
|
+
}
|
|
7461
|
+
},
|
|
7462
|
+
"ingestion-grid": {
|
|
7463
|
+
key: "ingestion-grid",
|
|
7464
|
+
type: "Grid",
|
|
7465
|
+
children: ["card-bytes", "card-requests"],
|
|
7466
|
+
parentKey: "root",
|
|
7467
|
+
props: {
|
|
7468
|
+
columns: 2,
|
|
7469
|
+
gap: "md"
|
|
7470
|
+
}
|
|
7471
|
+
},
|
|
7472
|
+
"card-bytes": {
|
|
7473
|
+
key: "card-bytes",
|
|
7474
|
+
type: "Card",
|
|
7475
|
+
children: ["stat-bytes"],
|
|
7476
|
+
parentKey: "ingestion-grid",
|
|
7477
|
+
props: {
|
|
7478
|
+
title: "Total Bytes Ingested",
|
|
7479
|
+
description: null,
|
|
7480
|
+
padding: null
|
|
7481
|
+
}
|
|
7482
|
+
},
|
|
7483
|
+
"stat-bytes": {
|
|
7484
|
+
key: "stat-bytes",
|
|
7485
|
+
type: "MetricStat",
|
|
7486
|
+
children: [],
|
|
7487
|
+
parentKey: "card-bytes",
|
|
7488
|
+
dataSource: {
|
|
7489
|
+
method: "searchMetricsPage",
|
|
7490
|
+
params: {
|
|
7491
|
+
metricType: "Sum",
|
|
7492
|
+
metricName: "kopai.ingestion.bytes",
|
|
7493
|
+
aggregate: "sum"
|
|
7494
|
+
},
|
|
7495
|
+
refetchIntervalMs: 1e4
|
|
7496
|
+
},
|
|
7497
|
+
props: {
|
|
7498
|
+
label: "Bytes",
|
|
7499
|
+
showSparkline: false
|
|
7500
|
+
}
|
|
7501
|
+
},
|
|
7502
|
+
"card-requests": {
|
|
7503
|
+
key: "card-requests",
|
|
7504
|
+
type: "Card",
|
|
7505
|
+
children: ["stat-requests"],
|
|
7506
|
+
parentKey: "ingestion-grid",
|
|
7507
|
+
props: {
|
|
7508
|
+
title: "Total Requests",
|
|
7509
|
+
description: null,
|
|
7510
|
+
padding: null
|
|
7511
|
+
}
|
|
7512
|
+
},
|
|
7513
|
+
"stat-requests": {
|
|
7514
|
+
key: "stat-requests",
|
|
7515
|
+
type: "MetricStat",
|
|
7516
|
+
children: [],
|
|
7517
|
+
parentKey: "card-requests",
|
|
7518
|
+
dataSource: {
|
|
7519
|
+
method: "searchMetricsPage",
|
|
7520
|
+
params: {
|
|
7521
|
+
metricType: "Sum",
|
|
7522
|
+
metricName: "kopai.ingestion.requests",
|
|
7523
|
+
aggregate: "sum"
|
|
7524
|
+
},
|
|
7525
|
+
refetchIntervalMs: 1e4
|
|
7526
|
+
},
|
|
7527
|
+
props: {
|
|
7528
|
+
label: "Requests",
|
|
7529
|
+
showSparkline: false
|
|
7530
|
+
}
|
|
7531
|
+
},
|
|
7532
|
+
"discovery-heading": {
|
|
7533
|
+
key: "discovery-heading",
|
|
7534
|
+
type: "Heading",
|
|
7535
|
+
children: [],
|
|
7536
|
+
parentKey: "root",
|
|
7537
|
+
props: {
|
|
7538
|
+
text: "Discovered Metrics",
|
|
7539
|
+
level: "h3"
|
|
7540
|
+
}
|
|
7541
|
+
},
|
|
6135
7542
|
description: {
|
|
6136
7543
|
key: "description",
|
|
6137
7544
|
type: "Text",
|
|
@@ -6209,40 +7616,56 @@ function getDefaultClient() {
|
|
|
6209
7616
|
}
|
|
6210
7617
|
function ObservabilityPage({ client }) {
|
|
6211
7618
|
const activeClient = client ?? getDefaultClient();
|
|
6212
|
-
const { tab: activeTab,
|
|
7619
|
+
const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
|
|
6213
7620
|
const handleTabChange = useCallback((tab) => {
|
|
6214
7621
|
pushURLState({ tab });
|
|
6215
7622
|
}, []);
|
|
6216
|
-
const handleSelectService = useCallback((service) => {
|
|
6217
|
-
pushURLState({
|
|
6218
|
-
tab: "services",
|
|
6219
|
-
service
|
|
6220
|
-
});
|
|
6221
|
-
}, []);
|
|
6222
7623
|
const handleSelectTrace = useCallback((traceId) => {
|
|
6223
7624
|
pushURLState({
|
|
7625
|
+
...readURLState(),
|
|
6224
7626
|
tab: "services",
|
|
6225
|
-
service: selectedService,
|
|
6226
7627
|
trace: traceId
|
|
6227
7628
|
});
|
|
6228
|
-
}, [
|
|
7629
|
+
}, []);
|
|
6229
7630
|
const handleSelectSpan = useCallback((spanId) => {
|
|
6230
7631
|
pushURLState({
|
|
7632
|
+
...readURLState(),
|
|
6231
7633
|
tab: "services",
|
|
6232
|
-
service: selectedService,
|
|
6233
|
-
trace: selectedTraceId,
|
|
6234
7634
|
span: spanId
|
|
6235
7635
|
}, { replace: true });
|
|
6236
|
-
}, [selectedService, selectedTraceId]);
|
|
6237
|
-
const handleBackToServices = useCallback(() => {
|
|
6238
|
-
pushURLState({ tab: "services" });
|
|
6239
7636
|
}, []);
|
|
6240
|
-
const
|
|
7637
|
+
const handleDeselectSpan = useCallback(() => {
|
|
7638
|
+
pushURLState({
|
|
7639
|
+
...readURLState(),
|
|
7640
|
+
span: null
|
|
7641
|
+
}, { replace: true });
|
|
7642
|
+
}, []);
|
|
7643
|
+
const handleCompare = useCallback((traceIds) => {
|
|
6241
7644
|
pushURLState({
|
|
7645
|
+
...readURLState(),
|
|
6242
7646
|
tab: "services",
|
|
6243
|
-
|
|
7647
|
+
trace: null,
|
|
7648
|
+
span: null,
|
|
7649
|
+
view: null,
|
|
7650
|
+
uiFind: null,
|
|
7651
|
+
viewStart: null,
|
|
7652
|
+
viewEnd: null,
|
|
7653
|
+
compare: traceIds.join(",")
|
|
6244
7654
|
});
|
|
6245
|
-
}, [
|
|
7655
|
+
}, []);
|
|
7656
|
+
const handleBack = useCallback(() => {
|
|
7657
|
+
pushURLState({
|
|
7658
|
+
...readURLState(),
|
|
7659
|
+
tab: "services",
|
|
7660
|
+
trace: null,
|
|
7661
|
+
span: null,
|
|
7662
|
+
view: null,
|
|
7663
|
+
uiFind: null,
|
|
7664
|
+
viewStart: null,
|
|
7665
|
+
viewEnd: null,
|
|
7666
|
+
compare: null
|
|
7667
|
+
});
|
|
7668
|
+
}, []);
|
|
6246
7669
|
return /* @__PURE__ */ jsx(KopaiSDKProvider, {
|
|
6247
7670
|
client: activeClient,
|
|
6248
7671
|
children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
|
|
@@ -6257,21 +7680,20 @@ function ObservabilityPage({ client }) {
|
|
|
6257
7680
|
}),
|
|
6258
7681
|
activeTab === "logs" && /* @__PURE__ */ jsx(LogsTab, {}),
|
|
6259
7682
|
activeTab === "services" && /* @__PURE__ */ jsx(ServicesTab, {
|
|
6260
|
-
selectedService,
|
|
6261
7683
|
selectedTraceId,
|
|
6262
7684
|
selectedSpanId,
|
|
6263
|
-
|
|
7685
|
+
compareParam,
|
|
6264
7686
|
onSelectTrace: handleSelectTrace,
|
|
6265
7687
|
onSelectSpan: handleSelectSpan,
|
|
6266
|
-
|
|
6267
|
-
|
|
7688
|
+
onDeselectSpan: handleDeselectSpan,
|
|
7689
|
+
onBack: handleBack,
|
|
7690
|
+
onCompare: handleCompare
|
|
6268
7691
|
}),
|
|
6269
7692
|
activeTab === "metrics" && /* @__PURE__ */ jsx(MetricsTab, {})
|
|
6270
7693
|
] })
|
|
6271
7694
|
})
|
|
6272
7695
|
});
|
|
6273
7696
|
}
|
|
6274
|
-
|
|
6275
7697
|
//#endregion
|
|
6276
7698
|
//#region src/lib/generate-prompt-instructions.ts
|
|
6277
7699
|
function formatPropType(prop) {
|
|
@@ -6401,7 +7823,7 @@ ${JSON.stringify(unifiedSchema)}
|
|
|
6401
7823
|
|
|
6402
7824
|
${JSON.stringify(exampleElements)}`;
|
|
6403
7825
|
}
|
|
6404
|
-
|
|
6405
7826
|
//#endregion
|
|
6406
7827
|
export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
|
|
7828
|
+
|
|
6407
7829
|
//# sourceMappingURL=index.mjs.map
|