@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.cjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value:
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
//#region \0rolldown/runtime.js
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
@@ -7,16 +7,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
|
7
7
|
var __getProtoOf = Object.getPrototypeOf;
|
|
8
8
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
9
|
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
}
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
20
16
|
}
|
|
21
17
|
return to;
|
|
22
18
|
};
|
|
@@ -24,7 +20,6 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
20
|
value: mod,
|
|
25
21
|
enumerable: true
|
|
26
22
|
}) : target, mod));
|
|
27
|
-
|
|
28
23
|
//#endregion
|
|
29
24
|
let react = require("react");
|
|
30
25
|
react = __toESM(react);
|
|
@@ -34,10 +29,9 @@ let _kopai_sdk = require("@kopai/sdk");
|
|
|
34
29
|
let zod = require("zod");
|
|
35
30
|
zod = __toESM(zod);
|
|
36
31
|
let _kopai_core = require("@kopai/core");
|
|
37
|
-
let _tanstack_react_virtual = require("@tanstack/react-virtual");
|
|
38
|
-
let react_dom = require("react-dom");
|
|
39
32
|
let recharts = require("recharts");
|
|
40
|
-
|
|
33
|
+
let react_dom = require("react-dom");
|
|
34
|
+
let _tanstack_react_virtual = require("@tanstack/react-virtual");
|
|
41
35
|
//#region src/providers/kopai-provider.tsx
|
|
42
36
|
const KopaiSDKContext = (0, react.createContext)(null);
|
|
43
37
|
const queryClient = new _tanstack_react_query.QueryClient({ defaultOptions: { queries: {
|
|
@@ -59,16 +53,25 @@ function useKopaiSDK() {
|
|
|
59
53
|
if (!ctx) throw new Error("useKopaiSDK must be used within KopaiSDKProvider");
|
|
60
54
|
return ctx.client;
|
|
61
55
|
}
|
|
62
|
-
|
|
63
56
|
//#endregion
|
|
64
57
|
//#region src/hooks/use-kopai-data.ts
|
|
65
58
|
function fetchForDataSource(client, dataSource, signal) {
|
|
66
59
|
switch (dataSource.method) {
|
|
67
60
|
case "searchTracesPage": return client.searchTracesPage(dataSource.params, { signal });
|
|
68
61
|
case "searchLogsPage": return client.searchLogsPage(dataSource.params, { signal });
|
|
69
|
-
case "searchMetricsPage":
|
|
62
|
+
case "searchMetricsPage": {
|
|
63
|
+
const params = dataSource.params;
|
|
64
|
+
if (params.aggregate) return client.searchAggregatedMetrics({
|
|
65
|
+
...params,
|
|
66
|
+
aggregate: params.aggregate
|
|
67
|
+
}, { signal });
|
|
68
|
+
return client.searchMetricsPage(params, { signal });
|
|
69
|
+
}
|
|
70
70
|
case "getTrace": return client.getTrace(dataSource.params.traceId, { signal });
|
|
71
71
|
case "discoverMetrics": return client.discoverMetrics({ signal });
|
|
72
|
+
case "getServices": return client.getServices({ signal });
|
|
73
|
+
case "getOperations": return client.getOperations(dataSource.params.serviceName, { signal });
|
|
74
|
+
case "searchTraceSummariesPage": return client.searchTraceSummariesPage(dataSource.params, { signal });
|
|
72
75
|
default: {
|
|
73
76
|
const exhaustiveCheck = dataSource;
|
|
74
77
|
throw new Error(`Unknown method: ${exhaustiveCheck.method}`);
|
|
@@ -94,7 +97,6 @@ function useKopaiData(dataSource) {
|
|
|
94
97
|
refetch
|
|
95
98
|
};
|
|
96
99
|
}
|
|
97
|
-
|
|
98
100
|
//#endregion
|
|
99
101
|
//#region src/lib/log-buffer.ts
|
|
100
102
|
function logKey(row) {
|
|
@@ -146,7 +148,6 @@ var LogBuffer = class {
|
|
|
146
148
|
this.keys.clear();
|
|
147
149
|
}
|
|
148
150
|
};
|
|
149
|
-
|
|
150
151
|
//#endregion
|
|
151
152
|
//#region src/hooks/use-live-logs.ts
|
|
152
153
|
function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = true }) {
|
|
@@ -201,7 +202,6 @@ function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = tr
|
|
|
201
202
|
setLive
|
|
202
203
|
};
|
|
203
204
|
}
|
|
204
|
-
|
|
205
205
|
//#endregion
|
|
206
206
|
//#region src/lib/component-catalog.ts
|
|
207
207
|
const dataSourceSchema = zod.z.discriminatedUnion("method", [
|
|
@@ -229,6 +229,21 @@ const dataSourceSchema = zod.z.discriminatedUnion("method", [
|
|
|
229
229
|
method: zod.z.literal("discoverMetrics"),
|
|
230
230
|
params: zod.z.object({}).optional(),
|
|
231
231
|
refetchIntervalMs: zod.z.number().optional()
|
|
232
|
+
}),
|
|
233
|
+
zod.z.object({
|
|
234
|
+
method: zod.z.literal("getServices"),
|
|
235
|
+
params: zod.z.object({}).optional(),
|
|
236
|
+
refetchIntervalMs: zod.z.number().optional()
|
|
237
|
+
}),
|
|
238
|
+
zod.z.object({
|
|
239
|
+
method: zod.z.literal("getOperations"),
|
|
240
|
+
params: zod.z.object({ serviceName: zod.z.string() }),
|
|
241
|
+
refetchIntervalMs: zod.z.number().optional()
|
|
242
|
+
}),
|
|
243
|
+
zod.z.object({
|
|
244
|
+
method: zod.z.literal("searchTraceSummariesPage"),
|
|
245
|
+
params: _kopai_core.dataFilterSchemas.traceSummariesFilterSchema,
|
|
246
|
+
refetchIntervalMs: zod.z.number().optional()
|
|
232
247
|
})
|
|
233
248
|
]);
|
|
234
249
|
const componentDefinitionSchema = zod.z.object({
|
|
@@ -236,7 +251,7 @@ const componentDefinitionSchema = zod.z.object({
|
|
|
236
251
|
description: zod.z.string().describe("Component description to be displayed by the prompt generator"),
|
|
237
252
|
props: zod.z.unknown()
|
|
238
253
|
}).describe("All options and properties necessary to render the React component with renderer");
|
|
239
|
-
|
|
254
|
+
zod.z.object({
|
|
240
255
|
name: zod.z.string().describe("catalog name"),
|
|
241
256
|
components: zod.z.record(zod.z.string().describe("React component name"), componentDefinitionSchema)
|
|
242
257
|
});
|
|
@@ -283,7 +298,6 @@ function createCatalog(catalogConfig) {
|
|
|
283
298
|
uiTreeSchema
|
|
284
299
|
};
|
|
285
300
|
}
|
|
286
|
-
|
|
287
301
|
//#endregion
|
|
288
302
|
//#region src/lib/observability-catalog.ts
|
|
289
303
|
const observabilityCatalog = createCatalog({
|
|
@@ -442,7 +456,6 @@ const observabilityCatalog = createCatalog({
|
|
|
442
456
|
}
|
|
443
457
|
}
|
|
444
458
|
});
|
|
445
|
-
|
|
446
459
|
//#endregion
|
|
447
460
|
//#region src/components/observability/TabBar/index.tsx
|
|
448
461
|
function renderLabel(label, shortcutKey) {
|
|
@@ -471,7 +484,6 @@ function TabBar({ tabs, active, onChange }) {
|
|
|
471
484
|
}, t.key))
|
|
472
485
|
});
|
|
473
486
|
}
|
|
474
|
-
|
|
475
487
|
//#endregion
|
|
476
488
|
//#region src/components/observability/utils/colors.ts
|
|
477
489
|
/**
|
|
@@ -491,38 +503,6 @@ function getSpanBarColor(serviceName, isError) {
|
|
|
491
503
|
if (isError) return ERROR_COLOR;
|
|
492
504
|
return getServiceColor(serviceName);
|
|
493
505
|
}
|
|
494
|
-
|
|
495
|
-
//#endregion
|
|
496
|
-
//#region src/components/observability/ServiceList/index.tsx
|
|
497
|
-
function ServiceList({ services, isLoading, error, onSelect }) {
|
|
498
|
-
if (isLoading) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
499
|
-
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
500
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading services..."]
|
|
501
|
-
});
|
|
502
|
-
if (error) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
503
|
-
className: "text-red-400 py-4",
|
|
504
|
-
children: ["Error loading services: ", error.message]
|
|
505
|
-
});
|
|
506
|
-
if (services.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
507
|
-
className: "text-muted-foreground py-8",
|
|
508
|
-
children: "No services found"
|
|
509
|
-
});
|
|
510
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
511
|
-
className: "space-y-1",
|
|
512
|
-
children: services.map((svc) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
513
|
-
onClick: () => onSelect(svc.name),
|
|
514
|
-
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",
|
|
515
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
516
|
-
className: "flex items-center gap-2 font-medium text-foreground",
|
|
517
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
518
|
-
className: "inline-block w-2.5 h-2.5 rounded-full shrink-0",
|
|
519
|
-
style: { backgroundColor: getServiceColor(svc.name) }
|
|
520
|
-
}), svc.name]
|
|
521
|
-
})
|
|
522
|
-
}, svc.name))
|
|
523
|
-
});
|
|
524
|
-
}
|
|
525
|
-
|
|
526
506
|
//#endregion
|
|
527
507
|
//#region src/components/observability/utils/time.ts
|
|
528
508
|
/**
|
|
@@ -544,6 +524,10 @@ function formatTimestamp$1(timestampMs) {
|
|
|
544
524
|
timeZoneName: "short"
|
|
545
525
|
});
|
|
546
526
|
}
|
|
527
|
+
function formatRelativeTime$1(eventTimeMs, spanStartMs) {
|
|
528
|
+
const relativeMs = eventTimeMs - spanStartMs;
|
|
529
|
+
return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
|
|
530
|
+
}
|
|
547
531
|
function calculateRelativeTime(timeMs, minTimeMs, maxTimeMs) {
|
|
548
532
|
const totalDuration = maxTimeMs - minTimeMs;
|
|
549
533
|
if (totalDuration === 0) return 0;
|
|
@@ -553,313 +537,658 @@ function calculateRelativeDuration(durationMs, totalDurationMs) {
|
|
|
553
537
|
if (totalDurationMs === 0) return 0;
|
|
554
538
|
return durationMs / totalDurationMs;
|
|
555
539
|
}
|
|
556
|
-
|
|
557
540
|
//#endregion
|
|
558
|
-
//#region src/components/observability/TraceSearch/
|
|
541
|
+
//#region src/components/observability/TraceSearch/SearchForm.tsx
|
|
542
|
+
/**
|
|
543
|
+
* SearchForm - Jaeger-style sidebar search form for trace filtering.
|
|
544
|
+
* Owns its own form state; parent only receives values on submit.
|
|
545
|
+
*/
|
|
559
546
|
const LOOKBACK_OPTIONS$1 = [
|
|
560
547
|
{
|
|
561
548
|
label: "Last 5 Minutes",
|
|
562
|
-
|
|
549
|
+
value: "5m"
|
|
563
550
|
},
|
|
564
551
|
{
|
|
565
552
|
label: "Last 15 Minutes",
|
|
566
|
-
|
|
553
|
+
value: "15m"
|
|
567
554
|
},
|
|
568
555
|
{
|
|
569
556
|
label: "Last 30 Minutes",
|
|
570
|
-
|
|
557
|
+
value: "30m"
|
|
571
558
|
},
|
|
572
559
|
{
|
|
573
560
|
label: "Last 1 Hour",
|
|
574
|
-
|
|
561
|
+
value: "1h"
|
|
575
562
|
},
|
|
576
563
|
{
|
|
577
564
|
label: "Last 2 Hours",
|
|
578
|
-
|
|
565
|
+
value: "2h"
|
|
579
566
|
},
|
|
580
567
|
{
|
|
581
568
|
label: "Last 6 Hours",
|
|
582
|
-
|
|
569
|
+
value: "6h"
|
|
583
570
|
},
|
|
584
571
|
{
|
|
585
572
|
label: "Last 12 Hours",
|
|
586
|
-
|
|
573
|
+
value: "12h"
|
|
587
574
|
},
|
|
588
575
|
{
|
|
589
576
|
label: "Last 24 Hours",
|
|
590
|
-
|
|
577
|
+
value: "24h"
|
|
591
578
|
}
|
|
592
579
|
];
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const [
|
|
596
|
-
const [
|
|
597
|
-
const [
|
|
598
|
-
const [
|
|
599
|
-
const [
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
580
|
+
const inputClass = "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground";
|
|
581
|
+
function SearchForm({ services, operations, initialValues, onSubmit, isLoading }) {
|
|
582
|
+
const [service, setService] = (0, react.useState)(initialValues?.service ?? "");
|
|
583
|
+
const [operation, setOperation] = (0, react.useState)(initialValues?.operation ?? "");
|
|
584
|
+
const [tags, setTags] = (0, react.useState)(initialValues?.tags ?? "");
|
|
585
|
+
const [lookback, setLookback] = (0, react.useState)(initialValues?.lookback ?? "");
|
|
586
|
+
const [minDuration, setMinDuration] = (0, react.useState)(initialValues?.minDuration ?? "");
|
|
587
|
+
const [maxDuration, setMaxDuration] = (0, react.useState)(initialValues?.maxDuration ?? "");
|
|
588
|
+
const [limit, setLimit] = (0, react.useState)(initialValues?.limit ?? 20);
|
|
589
|
+
(0, react.useEffect)(() => {
|
|
590
|
+
if (initialValues?.service != null) setService(initialValues.service);
|
|
591
|
+
}, [initialValues?.service]);
|
|
592
|
+
const handleSubmit = () => {
|
|
593
|
+
onSubmit({
|
|
594
|
+
service,
|
|
595
|
+
operation,
|
|
596
|
+
tags,
|
|
597
|
+
lookback,
|
|
598
|
+
minDuration,
|
|
599
|
+
maxDuration,
|
|
606
600
|
limit
|
|
607
601
|
});
|
|
608
602
|
};
|
|
609
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
children: filtersOpen ? "▲" : "▼"
|
|
603
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
604
|
+
className: "space-y-4",
|
|
605
|
+
children: [
|
|
606
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
607
|
+
className: "text-sm font-semibold text-foreground uppercase tracking-wider",
|
|
608
|
+
children: "Search"
|
|
609
|
+
}),
|
|
610
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
611
|
+
className: "block space-y-1",
|
|
612
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
613
|
+
className: "text-xs text-muted-foreground",
|
|
614
|
+
children: "Service"
|
|
615
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
616
|
+
value: service,
|
|
617
|
+
onChange: (e) => setService(e.target.value),
|
|
618
|
+
className: inputClass,
|
|
619
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
620
|
+
value: "",
|
|
621
|
+
children: "All Services"
|
|
622
|
+
}), services.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
623
|
+
value: s,
|
|
624
|
+
children: s
|
|
625
|
+
}, s))]
|
|
633
626
|
})]
|
|
634
|
-
}),
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
}), operations.map((op) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
652
|
-
value: op,
|
|
653
|
-
children: op
|
|
654
|
-
}, op))]
|
|
655
|
-
})]
|
|
656
|
-
}),
|
|
657
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
658
|
-
className: "space-y-1",
|
|
659
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
660
|
-
className: "text-xs text-muted-foreground",
|
|
661
|
-
children: "Lookback"
|
|
662
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
663
|
-
value: lookbackIdx,
|
|
664
|
-
onChange: (e) => setLookbackIdx(Number(e.target.value)),
|
|
665
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
666
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
667
|
-
value: -1,
|
|
668
|
-
children: "All time"
|
|
669
|
-
}), LOOKBACK_OPTIONS$1.map((opt, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
670
|
-
value: i,
|
|
671
|
-
children: opt.label
|
|
672
|
-
}, i))]
|
|
673
|
-
})]
|
|
674
|
-
}),
|
|
675
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
676
|
-
className: "space-y-1",
|
|
677
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
678
|
-
className: "text-xs text-muted-foreground",
|
|
679
|
-
children: "Limit"
|
|
680
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
681
|
-
type: "number",
|
|
682
|
-
min: 1,
|
|
683
|
-
max: 1e3,
|
|
684
|
-
value: limit,
|
|
685
|
-
onChange: (e) => {
|
|
686
|
-
const n = Number(e.target.value);
|
|
687
|
-
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
688
|
-
},
|
|
689
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
|
|
690
|
-
})]
|
|
691
|
-
}),
|
|
692
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
693
|
-
className: "space-y-1",
|
|
694
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
695
|
-
className: "text-xs text-muted-foreground",
|
|
696
|
-
children: "Min Duration"
|
|
697
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
698
|
-
type: "text",
|
|
699
|
-
placeholder: "e.g. 100ms",
|
|
700
|
-
value: minDuration,
|
|
701
|
-
onChange: (e) => setMinDuration(e.target.value),
|
|
702
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
703
|
-
})]
|
|
704
|
-
}),
|
|
705
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
706
|
-
className: "space-y-1",
|
|
707
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
708
|
-
className: "text-xs text-muted-foreground",
|
|
709
|
-
children: "Max Duration"
|
|
710
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
711
|
-
type: "text",
|
|
712
|
-
placeholder: "e.g. 5s",
|
|
713
|
-
value: maxDuration,
|
|
714
|
-
onChange: (e) => setMaxDuration(e.target.value),
|
|
715
|
-
className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
|
|
716
|
-
})]
|
|
717
|
-
})
|
|
718
|
-
]
|
|
719
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
720
|
-
onClick: handleFindTraces,
|
|
721
|
-
className: "px-4 py-1.5 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
722
|
-
children: "Find Traces"
|
|
627
|
+
}),
|
|
628
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
629
|
+
className: "block space-y-1",
|
|
630
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
631
|
+
className: "text-xs text-muted-foreground",
|
|
632
|
+
children: "Operation"
|
|
633
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
634
|
+
value: operation,
|
|
635
|
+
onChange: (e) => setOperation(e.target.value),
|
|
636
|
+
className: inputClass,
|
|
637
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
638
|
+
value: "",
|
|
639
|
+
children: "All Operations"
|
|
640
|
+
}), operations.map((op) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
641
|
+
value: op,
|
|
642
|
+
children: op
|
|
643
|
+
}, op))]
|
|
723
644
|
})]
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
645
|
+
}),
|
|
646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
647
|
+
className: "block space-y-1",
|
|
648
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
649
|
+
className: "text-xs text-muted-foreground",
|
|
650
|
+
children: "Tags"
|
|
651
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
652
|
+
value: tags,
|
|
653
|
+
onChange: (e) => setTags(e.target.value),
|
|
654
|
+
placeholder: "key=value key2=\"quoted value\"",
|
|
655
|
+
rows: 3,
|
|
656
|
+
className: `${inputClass} placeholder:text-muted-foreground/50 resize-y`
|
|
657
|
+
})]
|
|
658
|
+
}),
|
|
659
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
660
|
+
className: "block space-y-1",
|
|
661
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
662
|
+
className: "text-xs text-muted-foreground",
|
|
663
|
+
children: "Lookback"
|
|
664
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
665
|
+
value: lookback,
|
|
666
|
+
onChange: (e) => setLookback(e.target.value),
|
|
667
|
+
className: inputClass,
|
|
668
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
669
|
+
value: "",
|
|
670
|
+
children: "All time"
|
|
671
|
+
}), LOOKBACK_OPTIONS$1.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
672
|
+
value: opt.value,
|
|
673
|
+
children: opt.label
|
|
674
|
+
}, opt.value))]
|
|
675
|
+
})]
|
|
676
|
+
}),
|
|
677
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
678
|
+
className: "grid grid-cols-2 gap-2",
|
|
679
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
680
|
+
className: "block space-y-1",
|
|
681
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
682
|
+
className: "text-xs text-muted-foreground",
|
|
683
|
+
children: "Min Duration"
|
|
684
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
685
|
+
type: "text",
|
|
686
|
+
placeholder: "e.g. 100ms",
|
|
687
|
+
value: minDuration,
|
|
688
|
+
onChange: (e) => setMinDuration(e.target.value),
|
|
689
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
690
|
+
})]
|
|
691
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
692
|
+
className: "block space-y-1",
|
|
693
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
694
|
+
className: "text-xs text-muted-foreground",
|
|
695
|
+
children: "Max Duration"
|
|
696
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
697
|
+
type: "text",
|
|
698
|
+
placeholder: "e.g. 5s",
|
|
699
|
+
value: maxDuration,
|
|
700
|
+
onChange: (e) => setMaxDuration(e.target.value),
|
|
701
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
702
|
+
})]
|
|
703
|
+
})]
|
|
704
|
+
}),
|
|
705
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
706
|
+
className: "block space-y-1",
|
|
707
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
708
|
+
className: "text-xs text-muted-foreground",
|
|
709
|
+
children: "Limit"
|
|
710
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
711
|
+
type: "number",
|
|
712
|
+
min: 1,
|
|
713
|
+
max: 1e3,
|
|
714
|
+
value: limit,
|
|
715
|
+
onChange: (e) => {
|
|
716
|
+
const n = Number(e.target.value);
|
|
717
|
+
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
718
|
+
},
|
|
719
|
+
className: inputClass
|
|
720
|
+
})]
|
|
721
|
+
}),
|
|
722
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
723
|
+
onClick: handleSubmit,
|
|
724
|
+
disabled: isLoading,
|
|
725
|
+
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",
|
|
726
|
+
children: isLoading ? "Searching..." : "Find Traces"
|
|
727
|
+
})
|
|
728
|
+
]
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
//#endregion
|
|
732
|
+
//#region src/components/observability/TraceSearch/ScatterPlot.tsx
|
|
733
|
+
/**
|
|
734
|
+
* ScatterPlot - Scatter chart showing trace duration vs timestamp.
|
|
735
|
+
*/
|
|
736
|
+
function CustomTooltip$1({ active, payload }) {
|
|
737
|
+
if (!active || !payload?.[0]) return null;
|
|
738
|
+
const d = payload[0].payload;
|
|
739
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
740
|
+
className: "bg-background border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
741
|
+
children: [
|
|
742
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
743
|
+
className: "font-medium text-foreground",
|
|
743
744
|
children: [
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
745
|
+
d.serviceName,
|
|
746
|
+
": ",
|
|
747
|
+
d.rootSpanName
|
|
748
|
+
]
|
|
749
|
+
}),
|
|
750
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
751
|
+
className: "text-muted-foreground mt-1",
|
|
752
|
+
children: [
|
|
753
|
+
d.spanCount,
|
|
754
|
+
" span",
|
|
755
|
+
d.spanCount !== 1 ? "s" : "",
|
|
756
|
+
" ·",
|
|
757
|
+
" ",
|
|
758
|
+
formatDuration(d.y)
|
|
759
|
+
]
|
|
760
|
+
}),
|
|
761
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
762
|
+
className: "text-muted-foreground",
|
|
763
|
+
children: formatTimestamp$1(d.x)
|
|
764
|
+
})
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
function ScatterPlot({ traces, onSelectTrace }) {
|
|
769
|
+
const data = (0, react.useMemo)(() => traces.map((t) => ({
|
|
770
|
+
x: t.timestampMs,
|
|
771
|
+
y: t.durationMs,
|
|
772
|
+
traceId: t.traceId,
|
|
773
|
+
serviceName: t.serviceName,
|
|
774
|
+
rootSpanName: t.rootSpanName,
|
|
775
|
+
spanCount: t.spanCount,
|
|
776
|
+
hasError: t.errorCount > 0
|
|
777
|
+
})), [traces]);
|
|
778
|
+
const handleClick = (0, react.useCallback)((entry) => {
|
|
779
|
+
const payload = entry?.payload;
|
|
780
|
+
if (payload?.traceId) onSelectTrace(payload.traceId);
|
|
781
|
+
}, [onSelectTrace]);
|
|
782
|
+
if (traces.length === 0) return null;
|
|
783
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
784
|
+
className: "border border-border rounded-lg p-4 bg-background",
|
|
785
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.ResponsiveContainer, {
|
|
786
|
+
width: "100%",
|
|
787
|
+
height: 200,
|
|
788
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(recharts.ScatterChart, {
|
|
789
|
+
margin: {
|
|
790
|
+
top: 8,
|
|
791
|
+
right: 8,
|
|
792
|
+
bottom: 4,
|
|
793
|
+
left: 0
|
|
794
|
+
},
|
|
795
|
+
children: [
|
|
796
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.CartesianGrid, {
|
|
797
|
+
strokeDasharray: "3 3",
|
|
798
|
+
stroke: "hsl(var(--border))",
|
|
799
|
+
opacity: .4
|
|
763
800
|
}),
|
|
764
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
t.errorCount,
|
|
779
|
-
" Error",
|
|
780
|
-
t.errorCount !== 1 ? "s" : ""
|
|
781
|
-
]
|
|
782
|
-
}),
|
|
783
|
-
t.services.map((svc) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
784
|
-
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
785
|
-
style: {
|
|
786
|
-
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
787
|
-
color: getServiceColor(svc.name)
|
|
788
|
-
},
|
|
789
|
-
children: [
|
|
790
|
-
svc.hasError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
791
|
-
svc.name,
|
|
792
|
-
" (",
|
|
793
|
-
svc.count,
|
|
794
|
-
")"
|
|
795
|
-
]
|
|
796
|
-
}, svc.name))
|
|
797
|
-
]
|
|
801
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.XAxis, {
|
|
802
|
+
dataKey: "x",
|
|
803
|
+
type: "number",
|
|
804
|
+
domain: ["dataMin", "dataMax"],
|
|
805
|
+
tickFormatter: (v) => {
|
|
806
|
+
const d = new Date(v);
|
|
807
|
+
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
808
|
+
},
|
|
809
|
+
tick: {
|
|
810
|
+
fontSize: 11,
|
|
811
|
+
fill: "hsl(var(--muted-foreground))"
|
|
812
|
+
},
|
|
813
|
+
stroke: "hsl(var(--border))",
|
|
814
|
+
name: "Time"
|
|
798
815
|
}),
|
|
799
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
800
|
-
|
|
801
|
-
|
|
816
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.YAxis, {
|
|
817
|
+
dataKey: "y",
|
|
818
|
+
type: "number",
|
|
819
|
+
tickFormatter: (v) => formatDuration(v),
|
|
820
|
+
tick: {
|
|
821
|
+
fontSize: 11,
|
|
822
|
+
fill: "hsl(var(--muted-foreground))"
|
|
823
|
+
},
|
|
824
|
+
stroke: "hsl(var(--border))",
|
|
825
|
+
name: "Duration",
|
|
826
|
+
width: 70
|
|
827
|
+
}),
|
|
828
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.Tooltip, { content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomTooltip$1, {}) }),
|
|
829
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.Scatter, {
|
|
830
|
+
data,
|
|
831
|
+
onClick: handleClick,
|
|
832
|
+
cursor: "pointer",
|
|
833
|
+
children: data.map((point, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.Cell, {
|
|
834
|
+
fill: point.hasError ? "#ef4444" : getServiceColor(point.serviceName),
|
|
835
|
+
stroke: point.hasError ? "#ef4444" : "none",
|
|
836
|
+
strokeWidth: point.hasError ? 2 : 0
|
|
837
|
+
}, i))
|
|
802
838
|
})
|
|
803
839
|
]
|
|
804
|
-
}
|
|
840
|
+
})
|
|
805
841
|
})
|
|
806
|
-
|
|
842
|
+
});
|
|
807
843
|
}
|
|
808
|
-
|
|
809
844
|
//#endregion
|
|
810
|
-
//#region src/components/observability/
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
845
|
+
//#region src/components/observability/TraceSearch/SortDropdown.tsx
|
|
846
|
+
const SORT_OPTIONS = [
|
|
847
|
+
{
|
|
848
|
+
value: "recent",
|
|
849
|
+
label: "Most Recent"
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
value: "longest",
|
|
853
|
+
label: "Longest First"
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
value: "shortest",
|
|
857
|
+
label: "Shortest First"
|
|
858
|
+
},
|
|
859
|
+
{
|
|
860
|
+
value: "mostSpans",
|
|
861
|
+
label: "Most Spans"
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
value: "leastSpans",
|
|
865
|
+
label: "Least Spans"
|
|
819
866
|
}
|
|
820
|
-
|
|
821
|
-
|
|
867
|
+
];
|
|
868
|
+
function SortDropdown({ value, onChange }) {
|
|
869
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
870
|
+
value,
|
|
871
|
+
onChange: (e) => onChange(e.target.value),
|
|
872
|
+
className: "bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
873
|
+
children: SORT_OPTIONS.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
874
|
+
value: opt.value,
|
|
875
|
+
children: opt.label
|
|
876
|
+
}, opt.value))
|
|
877
|
+
});
|
|
822
878
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return
|
|
879
|
+
//#endregion
|
|
880
|
+
//#region src/components/observability/TraceSearch/DurationBar.tsx
|
|
881
|
+
/**
|
|
882
|
+
* DurationBar - Horizontal bar showing relative trace duration.
|
|
883
|
+
*/
|
|
884
|
+
function DurationBar({ durationMs, maxDurationMs, color }) {
|
|
885
|
+
const rawPct = maxDurationMs > 0 ? durationMs / maxDurationMs * 100 : 0;
|
|
886
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
887
|
+
className: "flex items-center gap-2",
|
|
888
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
889
|
+
className: "flex-1 h-2 bg-muted/30 rounded overflow-hidden",
|
|
890
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
891
|
+
className: "h-full rounded",
|
|
892
|
+
style: {
|
|
893
|
+
width: `${durationMs <= 0 ? 0 : Math.min(Math.max(rawPct, 1), 100)}%`,
|
|
894
|
+
backgroundColor: color,
|
|
895
|
+
opacity: .7
|
|
896
|
+
}
|
|
897
|
+
})
|
|
898
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
899
|
+
className: "text-xs text-foreground/80 shrink-0 w-16 text-right font-mono",
|
|
900
|
+
children: formatDuration(durationMs)
|
|
901
|
+
})]
|
|
902
|
+
});
|
|
831
903
|
}
|
|
832
|
-
|
|
833
904
|
//#endregion
|
|
834
|
-
//#region src/components/observability/
|
|
835
|
-
function
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
905
|
+
//#region src/components/observability/TraceSearch/index.tsx
|
|
906
|
+
function sortTraces(traces, sort) {
|
|
907
|
+
const sorted = [...traces];
|
|
908
|
+
switch (sort) {
|
|
909
|
+
case "longest": return sorted.sort((a, b) => b.durationMs - a.durationMs);
|
|
910
|
+
case "shortest": return sorted.sort((a, b) => a.durationMs - b.durationMs);
|
|
911
|
+
case "mostSpans": return sorted.sort((a, b) => b.spanCount - a.spanCount);
|
|
912
|
+
case "leastSpans": return sorted.sort((a, b) => a.spanCount - b.spanCount);
|
|
913
|
+
default: return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function TraceSearch({ services = [], service, operations = [], traces, isLoading, error, onSelectTrace, onSearch, onCompare, sort: controlledSort, onSortChange }) {
|
|
917
|
+
const [internalSort, setInternalSort] = (0, react.useState)("recent");
|
|
918
|
+
const currentSort = controlledSort ?? internalSort;
|
|
919
|
+
const handleSortChange = (s) => {
|
|
920
|
+
if (onSortChange) onSortChange(s);
|
|
921
|
+
else setInternalSort(s);
|
|
849
922
|
};
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
923
|
+
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
924
|
+
const toggleSelected = (traceId) => {
|
|
925
|
+
setSelected((prev) => {
|
|
926
|
+
const next = new Set(prev);
|
|
927
|
+
if (next.has(traceId)) next.delete(traceId);
|
|
928
|
+
else {
|
|
929
|
+
if (next.size >= 2) return prev;
|
|
930
|
+
next.add(traceId);
|
|
931
|
+
}
|
|
932
|
+
return next;
|
|
933
|
+
});
|
|
934
|
+
};
|
|
935
|
+
const handleFormSubmit = (values) => {
|
|
936
|
+
onSearch?.({
|
|
937
|
+
service: values.service || void 0,
|
|
938
|
+
operation: values.operation || void 0,
|
|
939
|
+
tags: values.tags || void 0,
|
|
940
|
+
lookback: values.lookback || void 0,
|
|
941
|
+
minDuration: values.minDuration || void 0,
|
|
942
|
+
maxDuration: values.maxDuration || void 0,
|
|
943
|
+
limit: values.limit
|
|
944
|
+
});
|
|
945
|
+
};
|
|
946
|
+
const sortedTraces = (0, react.useMemo)(() => sortTraces(traces, currentSort), [traces, currentSort]);
|
|
947
|
+
const maxDurationMs = (0, react.useMemo)(() => Math.max(...traces.map((t) => t.durationMs), 0), [traces]);
|
|
948
|
+
const selectedArr = Array.from(selected);
|
|
949
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
950
|
+
className: "flex gap-6 min-h-0",
|
|
951
|
+
children: [onSearch && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
952
|
+
className: "w-72 shrink-0 border border-border rounded-lg p-4 self-start",
|
|
953
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchForm, {
|
|
954
|
+
services,
|
|
955
|
+
operations,
|
|
956
|
+
initialValues: { service },
|
|
957
|
+
onSubmit: handleFormSubmit,
|
|
958
|
+
isLoading
|
|
959
|
+
})
|
|
960
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
961
|
+
className: "flex-1 min-w-0 space-y-4",
|
|
962
|
+
children: [
|
|
963
|
+
traces.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScatterPlot, {
|
|
964
|
+
traces,
|
|
965
|
+
onSelectTrace
|
|
966
|
+
}),
|
|
967
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
968
|
+
className: "flex items-center justify-between gap-2",
|
|
969
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
970
|
+
className: "text-sm text-muted-foreground",
|
|
971
|
+
children: [
|
|
972
|
+
traces.length,
|
|
973
|
+
" Trace",
|
|
974
|
+
traces.length !== 1 ? "s" : ""
|
|
975
|
+
]
|
|
976
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
977
|
+
className: "flex items-center gap-2",
|
|
978
|
+
children: [onCompare && selected.size === 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
979
|
+
onClick: () => onCompare(selectedArr),
|
|
980
|
+
className: "px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
981
|
+
children: "Compare"
|
|
982
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SortDropdown, {
|
|
983
|
+
value: currentSort,
|
|
984
|
+
onChange: handleSortChange
|
|
985
|
+
})]
|
|
986
|
+
})]
|
|
987
|
+
}),
|
|
988
|
+
isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
989
|
+
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
990
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading traces..."]
|
|
991
|
+
}),
|
|
992
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
993
|
+
className: "text-red-400 py-4",
|
|
994
|
+
children: ["Error loading traces: ", error.message]
|
|
995
|
+
}),
|
|
996
|
+
!isLoading && !error && traces.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
997
|
+
className: "text-muted-foreground py-8",
|
|
998
|
+
children: "No traces found"
|
|
999
|
+
}),
|
|
1000
|
+
sortedTraces.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1001
|
+
className: "space-y-2",
|
|
1002
|
+
children: sortedTraces.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1003
|
+
className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
|
|
1004
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1005
|
+
className: "flex items-center gap-2",
|
|
1006
|
+
children: [onCompare && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1007
|
+
type: "checkbox",
|
|
1008
|
+
checked: selected.has(t.traceId),
|
|
1009
|
+
onChange: () => toggleSelected(t.traceId),
|
|
1010
|
+
onClick: (e) => e.stopPropagation(),
|
|
1011
|
+
className: "shrink-0",
|
|
1012
|
+
disabled: !selected.has(t.traceId) && selected.size >= 2
|
|
1013
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1014
|
+
className: "flex-1 min-w-0",
|
|
1015
|
+
onClick: () => onSelectTrace(t.traceId),
|
|
1016
|
+
children: [
|
|
1017
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1018
|
+
className: "flex items-baseline justify-between gap-2",
|
|
1019
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1020
|
+
className: "flex items-baseline gap-1.5 min-w-0",
|
|
1021
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1022
|
+
className: "font-medium text-foreground truncate",
|
|
1023
|
+
children: [
|
|
1024
|
+
t.serviceName,
|
|
1025
|
+
": ",
|
|
1026
|
+
t.rootSpanName
|
|
1027
|
+
]
|
|
1028
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1029
|
+
className: "text-xs font-mono text-muted-foreground shrink-0",
|
|
1030
|
+
children: t.traceId.slice(0, 7)
|
|
1031
|
+
})]
|
|
1032
|
+
})
|
|
1033
|
+
}),
|
|
1034
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1035
|
+
className: "mt-1.5",
|
|
1036
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DurationBar, {
|
|
1037
|
+
durationMs: t.durationMs,
|
|
1038
|
+
maxDurationMs,
|
|
1039
|
+
color: getServiceColor(t.serviceName)
|
|
1040
|
+
})
|
|
1041
|
+
}),
|
|
1042
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1043
|
+
className: "flex items-center flex-wrap gap-1.5 mt-1.5",
|
|
1044
|
+
children: [
|
|
1045
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1046
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
|
|
1047
|
+
children: [
|
|
1048
|
+
t.spanCount,
|
|
1049
|
+
" Span",
|
|
1050
|
+
t.spanCount !== 1 ? "s" : ""
|
|
1051
|
+
]
|
|
1052
|
+
}),
|
|
1053
|
+
t.errorCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1054
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
|
|
1055
|
+
children: [
|
|
1056
|
+
t.errorCount,
|
|
1057
|
+
" Error",
|
|
1058
|
+
t.errorCount !== 1 ? "s" : ""
|
|
1059
|
+
]
|
|
1060
|
+
}),
|
|
1061
|
+
t.services.map((svc) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1062
|
+
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
1063
|
+
style: {
|
|
1064
|
+
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
1065
|
+
color: getServiceColor(svc.name)
|
|
1066
|
+
},
|
|
1067
|
+
children: [
|
|
1068
|
+
svc.hasError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
1069
|
+
svc.name,
|
|
1070
|
+
" (",
|
|
1071
|
+
svc.count,
|
|
1072
|
+
")"
|
|
1073
|
+
]
|
|
1074
|
+
}, svc.name))
|
|
1075
|
+
]
|
|
1076
|
+
}),
|
|
1077
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1078
|
+
className: "text-xs text-muted-foreground mt-1 text-right",
|
|
1079
|
+
children: formatTimestamp$1(t.timestampMs)
|
|
1080
|
+
})
|
|
1081
|
+
]
|
|
1082
|
+
})]
|
|
1083
|
+
})
|
|
1084
|
+
}, t.traceId))
|
|
1085
|
+
})
|
|
1086
|
+
]
|
|
1087
|
+
})]
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
//#endregion
|
|
1091
|
+
//#region src/components/observability/utils/flatten-tree.ts
|
|
1092
|
+
function flattenTree(rootSpans, collapsedIds) {
|
|
1093
|
+
const result = [];
|
|
1094
|
+
function traverse(span, level) {
|
|
1095
|
+
result.push({
|
|
1096
|
+
span,
|
|
1097
|
+
level
|
|
1098
|
+
});
|
|
1099
|
+
if (!collapsedIds.has(span.spanId)) span.children.forEach((child) => traverse(child, level + 1));
|
|
1100
|
+
}
|
|
1101
|
+
rootSpans.forEach((root) => traverse(root, 0));
|
|
1102
|
+
return result;
|
|
1103
|
+
}
|
|
1104
|
+
/** Flatten all spans (ignoring collapse state) with depth. */
|
|
1105
|
+
function flattenAllSpans(rootSpans) {
|
|
1106
|
+
return flattenTree(rootSpans, /* @__PURE__ */ new Set());
|
|
1107
|
+
}
|
|
1108
|
+
function spanMatchesSearch(span, query) {
|
|
1109
|
+
const q = query.toLowerCase();
|
|
1110
|
+
if (span.name.toLowerCase().includes(q)) return true;
|
|
1111
|
+
if (span.serviceName.toLowerCase().includes(q)) return true;
|
|
1112
|
+
for (const val of Object.values(span.attributes)) if (String(val).toLowerCase().includes(q)) return true;
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
function getAllSpanIds(rootSpans) {
|
|
1116
|
+
const ids = [];
|
|
1117
|
+
function traverse(span) {
|
|
1118
|
+
ids.push(span.spanId);
|
|
1119
|
+
span.children.forEach((child) => traverse(child));
|
|
1120
|
+
}
|
|
1121
|
+
rootSpans.forEach((root) => traverse(root));
|
|
1122
|
+
return ids;
|
|
1123
|
+
}
|
|
1124
|
+
//#endregion
|
|
1125
|
+
//#region src/components/observability/TraceTimeline/TraceHeader.tsx
|
|
1126
|
+
function computeMaxDepth(spans) {
|
|
1127
|
+
let max = 0;
|
|
1128
|
+
function walk(nodes, depth) {
|
|
1129
|
+
for (const node of nodes) {
|
|
1130
|
+
if (depth > max) max = depth;
|
|
1131
|
+
walk(node.children, depth + 1);
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
walk(spans, 1);
|
|
1135
|
+
return max;
|
|
1136
|
+
}
|
|
1137
|
+
function TraceHeader({ trace, services = [], onHeaderToggle, isCollapsed = false }) {
|
|
1138
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1139
|
+
const rootSpan = trace.rootSpans[0];
|
|
1140
|
+
const rootServiceName = rootSpan?.serviceName ?? "unknown";
|
|
1141
|
+
const rootSpanName = rootSpan?.name ?? "unknown";
|
|
1142
|
+
const totalDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
1143
|
+
const maxDepth = computeMaxDepth(trace.rootSpans);
|
|
1144
|
+
const handleCopyTraceId = async () => {
|
|
1145
|
+
try {
|
|
1146
|
+
await navigator.clipboard.writeText(trace.traceId);
|
|
1147
|
+
setCopied(true);
|
|
1148
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
console.error("Failed to copy trace ID:", err);
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1154
|
+
className: "bg-background border-b border-border px-4 py-3",
|
|
1155
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1156
|
+
className: "flex items-center gap-2 mb-1",
|
|
1157
|
+
children: [onHeaderToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1158
|
+
onClick: onHeaderToggle,
|
|
1159
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground",
|
|
1160
|
+
"aria-label": isCollapsed ? "Expand header" : "Collapse header",
|
|
1161
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1162
|
+
className: `w-4 h-4 transition-transform ${isCollapsed ? "-rotate-90" : ""}`,
|
|
1163
|
+
fill: "none",
|
|
1164
|
+
stroke: "currentColor",
|
|
1165
|
+
viewBox: "0 0 24 24",
|
|
1166
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1167
|
+
strokeLinecap: "round",
|
|
1168
|
+
strokeLinejoin: "round",
|
|
1169
|
+
strokeWidth: 2,
|
|
1170
|
+
d: "M19 9l-7 7-7-7"
|
|
1171
|
+
})
|
|
1172
|
+
})
|
|
1173
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1174
|
+
className: "text-sm font-semibold text-foreground",
|
|
1175
|
+
children: [
|
|
1176
|
+
rootServiceName,
|
|
1177
|
+
": ",
|
|
1178
|
+
rootSpanName
|
|
1179
|
+
]
|
|
1180
|
+
})]
|
|
1181
|
+
}), !isCollapsed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1182
|
+
className: "flex items-center gap-6 flex-wrap",
|
|
1183
|
+
children: [
|
|
1184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1185
|
+
className: "flex items-center gap-2",
|
|
1186
|
+
children: [
|
|
1187
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1188
|
+
className: "text-xs font-semibold text-muted-foreground",
|
|
1189
|
+
children: "Trace ID:"
|
|
1190
|
+
}),
|
|
1191
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
863
1192
|
onClick: handleCopyTraceId,
|
|
864
1193
|
className: "text-sm font-mono bg-muted px-2 py-1 rounded hover:bg-muted/80 transition-colors text-foreground",
|
|
865
1194
|
title: "Click to copy",
|
|
@@ -875,43 +1204,30 @@ function TraceHeader({ trace }) {
|
|
|
875
1204
|
className: "flex items-center gap-2",
|
|
876
1205
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
877
1206
|
className: "text-xs font-semibold text-muted-foreground",
|
|
878
|
-
children: "
|
|
879
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
880
|
-
className: "text-sm",
|
|
881
|
-
children:
|
|
882
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
883
|
-
className: "text-muted-foreground",
|
|
884
|
-
children: rootServiceName
|
|
885
|
-
}),
|
|
886
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
887
|
-
className: "mx-1 text-muted-foreground/70",
|
|
888
|
-
children: "/"
|
|
889
|
-
}),
|
|
890
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
891
|
-
className: "font-medium text-foreground",
|
|
892
|
-
children: rootSpanName
|
|
893
|
-
})
|
|
894
|
-
]
|
|
1207
|
+
children: "Duration:"
|
|
1208
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1209
|
+
className: "text-sm font-medium text-foreground",
|
|
1210
|
+
children: formatDuration(totalDuration)
|
|
895
1211
|
})]
|
|
896
1212
|
}),
|
|
897
1213
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
898
1214
|
className: "flex items-center gap-2",
|
|
899
1215
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
900
1216
|
className: "text-xs font-semibold text-muted-foreground",
|
|
901
|
-
children: "
|
|
1217
|
+
children: "Spans:"
|
|
902
1218
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
903
1219
|
className: "text-sm font-medium text-foreground",
|
|
904
|
-
children:
|
|
1220
|
+
children: trace.totalSpanCount
|
|
905
1221
|
})]
|
|
906
1222
|
}),
|
|
907
1223
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
908
1224
|
className: "flex items-center gap-2",
|
|
909
1225
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
910
1226
|
className: "text-xs font-semibold text-muted-foreground",
|
|
911
|
-
children: "
|
|
1227
|
+
children: "Depth:"
|
|
912
1228
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
913
1229
|
className: "text-sm font-medium text-foreground",
|
|
914
|
-
children:
|
|
1230
|
+
children: maxDepth
|
|
915
1231
|
})]
|
|
916
1232
|
}),
|
|
917
1233
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -925,10 +1241,21 @@ function TraceHeader({ trace }) {
|
|
|
925
1241
|
})]
|
|
926
1242
|
})
|
|
927
1243
|
]
|
|
928
|
-
})
|
|
1244
|
+
}), services.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1245
|
+
className: "flex items-center gap-3 mt-2 flex-wrap",
|
|
1246
|
+
children: services.map((svc) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1247
|
+
className: "flex items-center gap-1.5",
|
|
1248
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1249
|
+
className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
|
|
1250
|
+
style: { backgroundColor: getServiceColor(svc) }
|
|
1251
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1252
|
+
className: "text-xs text-muted-foreground",
|
|
1253
|
+
children: svc
|
|
1254
|
+
})]
|
|
1255
|
+
}, svc))
|
|
1256
|
+
})] })]
|
|
929
1257
|
});
|
|
930
1258
|
}
|
|
931
|
-
|
|
932
1259
|
//#endregion
|
|
933
1260
|
//#region src/components/observability/TraceTimeline/Tooltip.tsx
|
|
934
1261
|
function Tooltip$2({ content, children }) {
|
|
@@ -958,7 +1285,6 @@ function Tooltip$2({ content, children }) {
|
|
|
958
1285
|
children: content
|
|
959
1286
|
}), document.body)] });
|
|
960
1287
|
}
|
|
961
|
-
|
|
962
1288
|
//#endregion
|
|
963
1289
|
//#region src/components/observability/TraceTimeline/TimelineBar.tsx
|
|
964
1290
|
function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
@@ -966,45 +1292,48 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
|
966
1292
|
const barColor = getSpanBarColor(span.serviceName, isError);
|
|
967
1293
|
const leftPercent = relativeStart * 100;
|
|
968
1294
|
const widthPercent = Math.max(.2, relativeDuration * 100);
|
|
1295
|
+
const isWide = widthPercent > 8;
|
|
1296
|
+
const tooltipText = `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`;
|
|
1297
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
969
1298
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
970
1299
|
className: "relative h-full",
|
|
971
1300
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip$2, {
|
|
972
|
-
content:
|
|
973
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1301
|
+
content: tooltipText,
|
|
1302
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
974
1303
|
className: "absolute inset-0",
|
|
975
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
976
|
-
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity",
|
|
1304
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1305
|
+
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity flex items-center",
|
|
977
1306
|
style: {
|
|
978
1307
|
left: `${leftPercent}%`,
|
|
979
1308
|
width: `max(2px, ${widthPercent}%)`,
|
|
980
1309
|
backgroundColor: barColor
|
|
981
|
-
}
|
|
982
|
-
|
|
1310
|
+
},
|
|
1311
|
+
children: isWide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1312
|
+
className: "text-[10px] font-mono text-white px-1 truncate",
|
|
1313
|
+
children: durationLabel
|
|
1314
|
+
})
|
|
1315
|
+
}), !isWide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1316
|
+
className: "absolute top-1/2 -translate-y-1/2 text-[10px] font-mono text-muted-foreground whitespace-nowrap",
|
|
1317
|
+
style: { left: `calc(${leftPercent + widthPercent}% + 4px)` },
|
|
1318
|
+
children: durationLabel
|
|
1319
|
+
})]
|
|
983
1320
|
})
|
|
984
1321
|
})
|
|
985
1322
|
});
|
|
986
1323
|
}
|
|
987
|
-
|
|
988
1324
|
//#endregion
|
|
989
1325
|
//#region src/components/observability/TraceTimeline/SpanRow.tsx
|
|
990
|
-
function
|
|
991
|
-
const attrs = span.attributes;
|
|
992
|
-
const method = attrs["http.method"];
|
|
993
|
-
const url = attrs["http.url"] || attrs["http.target"];
|
|
994
|
-
const statusCode = attrs["http.status_code"];
|
|
995
|
-
if (!method && !url) return null;
|
|
996
|
-
const parts = [];
|
|
997
|
-
if (method) parts.push(String(method));
|
|
998
|
-
if (url) parts.push(String(url));
|
|
999
|
-
if (statusCode) parts.push(`[${statusCode}]`);
|
|
1000
|
-
return parts.join(" ");
|
|
1001
|
-
}
|
|
1002
|
-
const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave }) {
|
|
1326
|
+
const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave, uiFind }) {
|
|
1003
1327
|
const hasChildren = span.children.length > 0;
|
|
1004
1328
|
const isError = span.status === "ERROR";
|
|
1005
|
-
const
|
|
1329
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1330
|
+
const isDimmed = uiFind ? !spanMatchesSearch(span, uiFind) : false;
|
|
1006
1331
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1007
1332
|
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" : ""}`,
|
|
1333
|
+
style: {
|
|
1334
|
+
borderLeft: `3px solid ${serviceColor}`,
|
|
1335
|
+
opacity: isDimmed ? .4 : 1
|
|
1336
|
+
},
|
|
1008
1337
|
onClick,
|
|
1009
1338
|
onMouseEnter,
|
|
1010
1339
|
onMouseLeave,
|
|
@@ -1059,7 +1388,8 @@ const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isS
|
|
|
1059
1388
|
})
|
|
1060
1389
|
}),
|
|
1061
1390
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1062
|
-
className: "text-xs
|
|
1391
|
+
className: "text-xs flex-shrink-0 mr-2 font-medium",
|
|
1392
|
+
style: { color: serviceColor },
|
|
1063
1393
|
children: span.serviceName
|
|
1064
1394
|
}),
|
|
1065
1395
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1074,10 +1404,6 @@ const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isS
|
|
|
1074
1404
|
")"
|
|
1075
1405
|
]
|
|
1076
1406
|
}),
|
|
1077
|
-
httpContext && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1078
|
-
className: "text-xs text-muted-foreground truncate ml-2 flex-shrink-0 max-w-xs",
|
|
1079
|
-
children: httpContext
|
|
1080
|
-
}),
|
|
1081
1407
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1082
1408
|
className: "text-xs text-muted-foreground flex-shrink-0 ml-2",
|
|
1083
1409
|
children: formatDuration(span.durationMs)
|
|
@@ -1093,7 +1419,6 @@ const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isS
|
|
|
1093
1419
|
})]
|
|
1094
1420
|
});
|
|
1095
1421
|
});
|
|
1096
|
-
|
|
1097
1422
|
//#endregion
|
|
1098
1423
|
//#region src/components/observability/utils/attributes.ts
|
|
1099
1424
|
function formatAttributeValue(value) {
|
|
@@ -1112,500 +1437,203 @@ function formatSeriesLabel(labels) {
|
|
|
1112
1437
|
function isComplexValue(value) {
|
|
1113
1438
|
return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
|
|
1114
1439
|
}
|
|
1115
|
-
|
|
1116
1440
|
//#endregion
|
|
1117
|
-
//#region src/components/observability/TraceTimeline/
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
"
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
"http.request_content_length",
|
|
1128
|
-
"http.response_content_length"
|
|
1129
|
-
]);
|
|
1130
|
-
function AttributesTab$1({ span }) {
|
|
1131
|
-
const { httpAttributes, otherAttributes, resourceAttributes } = (0, react.useMemo)(() => {
|
|
1132
|
-
const http = [];
|
|
1133
|
-
const other = [];
|
|
1134
|
-
const resource = [];
|
|
1135
|
-
if (span.attributes) Object.entries(span.attributes).forEach(([key, value]) => {
|
|
1136
|
-
if (HTTP_SEMANTIC_CONVENTIONS.has(key)) http.push([key, value]);
|
|
1137
|
-
else other.push([key, value]);
|
|
1138
|
-
});
|
|
1139
|
-
if (span.resourceAttributes) Object.entries(span.resourceAttributes).forEach(([key, value]) => {
|
|
1140
|
-
resource.push([key, value]);
|
|
1141
|
-
});
|
|
1142
|
-
http.sort(([a], [b]) => a.localeCompare(b));
|
|
1143
|
-
other.sort(([a], [b]) => a.localeCompare(b));
|
|
1144
|
-
resource.sort(([a], [b]) => a.localeCompare(b));
|
|
1145
|
-
return {
|
|
1146
|
-
httpAttributes: http,
|
|
1147
|
-
otherAttributes: other,
|
|
1148
|
-
resourceAttributes: resource
|
|
1149
|
-
};
|
|
1150
|
-
}, [span]);
|
|
1151
|
-
if (!(httpAttributes.length > 0 || otherAttributes.length > 0 || resourceAttributes.length > 0)) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1152
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1153
|
-
children: "No attributes available"
|
|
1154
|
-
});
|
|
1155
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1156
|
-
className: "space-y-6",
|
|
1441
|
+
//#region src/components/observability/TraceTimeline/SpanDetailInline.tsx
|
|
1442
|
+
function CollapsibleSection({ title, count, children }) {
|
|
1443
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1444
|
+
if (count === 0) return null;
|
|
1445
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1446
|
+
className: "flex items-center gap-1 text-xs font-medium text-foreground hover:text-blue-600 dark:hover:text-blue-400 py-1",
|
|
1447
|
+
onClick: (e) => {
|
|
1448
|
+
e.stopPropagation();
|
|
1449
|
+
setOpen((p) => !p);
|
|
1450
|
+
},
|
|
1157
1451
|
children: [
|
|
1158
|
-
|
|
1159
|
-
className: "
|
|
1160
|
-
children:
|
|
1161
|
-
}),
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
children: "Span Attributes"
|
|
1172
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1173
|
-
className: "space-y-2",
|
|
1174
|
-
children: otherAttributes.map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttributeRow, {
|
|
1175
|
-
attrKey: key,
|
|
1176
|
-
value
|
|
1177
|
-
}, key))
|
|
1178
|
-
})] }),
|
|
1179
|
-
resourceAttributes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
1180
|
-
className: "text-sm font-semibold text-foreground mb-3",
|
|
1181
|
-
children: "Resource Attributes"
|
|
1182
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1183
|
-
className: "space-y-2",
|
|
1184
|
-
children: resourceAttributes.map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttributeRow, {
|
|
1185
|
-
attrKey: key,
|
|
1186
|
-
value
|
|
1187
|
-
}, key))
|
|
1188
|
-
})] })
|
|
1452
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1453
|
+
className: "w-3 text-center",
|
|
1454
|
+
children: open ? "▾" : "▸"
|
|
1455
|
+
}),
|
|
1456
|
+
title,
|
|
1457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1458
|
+
className: "text-muted-foreground",
|
|
1459
|
+
children: [
|
|
1460
|
+
"(",
|
|
1461
|
+
count,
|
|
1462
|
+
")"
|
|
1463
|
+
]
|
|
1464
|
+
})
|
|
1189
1465
|
]
|
|
1190
|
-
})
|
|
1466
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1467
|
+
className: "ml-4 mt-1 space-y-1",
|
|
1468
|
+
children
|
|
1469
|
+
})] });
|
|
1191
1470
|
}
|
|
1192
|
-
function
|
|
1193
|
-
const
|
|
1194
|
-
const formattedValue = formatAttributeValue(value);
|
|
1471
|
+
function KeyValueRow({ k, v }) {
|
|
1472
|
+
const formatted = formatAttributeValue(v);
|
|
1195
1473
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1196
|
-
className:
|
|
1197
|
-
children: [
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
children: isComplex ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1204
|
-
className: "text-xs text-foreground bg-background p-2 rounded border border-border overflow-x-auto",
|
|
1205
|
-
children: formattedValue
|
|
1206
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1474
|
+
className: "flex gap-2 text-xs font-mono py-0.5",
|
|
1475
|
+
children: [
|
|
1476
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1477
|
+
className: "text-muted-foreground flex-shrink-0",
|
|
1478
|
+
children: k
|
|
1479
|
+
}),
|
|
1480
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1207
1481
|
className: "text-foreground",
|
|
1208
|
-
children:
|
|
1482
|
+
children: "="
|
|
1483
|
+
}),
|
|
1484
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1485
|
+
className: "text-foreground break-all",
|
|
1486
|
+
children: formatted
|
|
1209
1487
|
})
|
|
1210
|
-
|
|
1488
|
+
]
|
|
1211
1489
|
});
|
|
1212
1490
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1491
|
+
function SpanDetailInline({ span, traceStartMs }) {
|
|
1492
|
+
const [copiedId, setCopiedId] = (0, react.useState)(false);
|
|
1493
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1494
|
+
const relativeStartMs = span.startTimeUnixMs - traceStartMs;
|
|
1495
|
+
const handleCopy = (0, react.useCallback)(async () => {
|
|
1496
|
+
try {
|
|
1497
|
+
await navigator.clipboard.writeText(span.spanId);
|
|
1498
|
+
setCopiedId(true);
|
|
1499
|
+
setTimeout(() => setCopiedId(false), 2e3);
|
|
1500
|
+
} catch {}
|
|
1501
|
+
}, [span.spanId]);
|
|
1502
|
+
const spanAttrs = Object.entries(span.attributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1503
|
+
const resourceAttrs = Object.entries(span.resourceAttributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1504
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1505
|
+
className: "border-b border-border bg-muted/50 px-4 py-3",
|
|
1506
|
+
style: { borderLeft: `3px solid ${serviceColor}` },
|
|
1507
|
+
onClick: (e) => e.stopPropagation(),
|
|
1508
|
+
children: [
|
|
1509
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1510
|
+
className: "mb-2",
|
|
1511
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1512
|
+
className: "text-sm font-medium text-foreground",
|
|
1513
|
+
children: span.name
|
|
1514
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1515
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground mt-1",
|
|
1516
|
+
children: [
|
|
1517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Service: ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1518
|
+
className: "text-foreground",
|
|
1519
|
+
children: span.serviceName
|
|
1520
|
+
})] }),
|
|
1521
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1522
|
+
"Duration:",
|
|
1523
|
+
" ",
|
|
1524
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1525
|
+
className: "text-foreground",
|
|
1526
|
+
children: formatDuration(span.durationMs)
|
|
1527
|
+
})
|
|
1528
|
+
] }),
|
|
1529
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1530
|
+
"Start:",
|
|
1531
|
+
" ",
|
|
1532
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1533
|
+
className: "text-foreground",
|
|
1534
|
+
children: formatDuration(relativeStartMs)
|
|
1535
|
+
})
|
|
1536
|
+
] }),
|
|
1537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Kind: ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1538
|
+
className: "text-foreground",
|
|
1539
|
+
children: span.kind
|
|
1540
|
+
})] }),
|
|
1541
|
+
span.status !== "UNSET" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1542
|
+
"Status:",
|
|
1543
|
+
" ",
|
|
1544
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1545
|
+
className: span.status === "ERROR" ? "text-red-500" : "text-foreground",
|
|
1546
|
+
children: span.status
|
|
1547
|
+
})
|
|
1548
|
+
] })
|
|
1549
|
+
]
|
|
1550
|
+
})]
|
|
1551
|
+
}),
|
|
1552
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1553
|
+
className: "space-y-1",
|
|
1242
1554
|
children: [
|
|
1243
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1555
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1556
|
+
title: "Tags",
|
|
1557
|
+
count: spanAttrs.length,
|
|
1558
|
+
children: spanAttrs.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueRow, {
|
|
1559
|
+
k,
|
|
1560
|
+
v
|
|
1561
|
+
}, k))
|
|
1562
|
+
}),
|
|
1563
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1564
|
+
title: "Process",
|
|
1565
|
+
count: resourceAttrs.length,
|
|
1566
|
+
children: resourceAttrs.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueRow, {
|
|
1567
|
+
k,
|
|
1568
|
+
v
|
|
1569
|
+
}, k))
|
|
1570
|
+
}),
|
|
1571
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1572
|
+
title: "Events",
|
|
1573
|
+
count: span.events.length,
|
|
1574
|
+
children: span.events.map((event, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1575
|
+
className: "text-xs border-l-2 border-border pl-2 py-1.5 space-y-0.5",
|
|
1247
1576
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1248
|
-
className: "flex-
|
|
1249
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
1250
|
-
className: "font-
|
|
1577
|
+
className: "flex items-center gap-2",
|
|
1578
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1579
|
+
className: "font-mono text-muted-foreground flex-shrink-0",
|
|
1580
|
+
children: formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs)
|
|
1581
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1582
|
+
className: "font-medium text-foreground",
|
|
1251
1583
|
children: event.name
|
|
1252
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1253
|
-
className: "text-xs text-muted-foreground mt-1 font-mono",
|
|
1254
|
-
children: [relativeTime, " from span start"]
|
|
1255
1584
|
})]
|
|
1256
|
-
}),
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1262
|
-
className: `w-4 h-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-180" : ""}`,
|
|
1263
|
-
fill: "none",
|
|
1264
|
-
stroke: "currentColor",
|
|
1265
|
-
viewBox: "0 0 24 24",
|
|
1266
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1267
|
-
strokeLinecap: "round",
|
|
1268
|
-
strokeLinejoin: "round",
|
|
1269
|
-
strokeWidth: 2,
|
|
1270
|
-
d: "M19 9l-7 7-7-7"
|
|
1271
|
-
})
|
|
1272
|
-
})
|
|
1273
|
-
})]
|
|
1274
|
-
})
|
|
1585
|
+
}), Object.entries(event.attributes).map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueRow, {
|
|
1586
|
+
k,
|
|
1587
|
+
v
|
|
1588
|
+
}, k))]
|
|
1589
|
+
}, i))
|
|
1275
1590
|
}),
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
})
|
|
1591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1592
|
+
title: "Links",
|
|
1593
|
+
count: span.links.length,
|
|
1594
|
+
children: span.links.map((link, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1595
|
+
className: "text-xs font-mono py-0.5",
|
|
1596
|
+
children: [
|
|
1597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1598
|
+
className: "text-muted-foreground",
|
|
1599
|
+
children: "trace:"
|
|
1600
|
+
}),
|
|
1601
|
+
" ",
|
|
1602
|
+
link.traceId,
|
|
1603
|
+
" ",
|
|
1604
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1605
|
+
className: "text-muted-foreground",
|
|
1606
|
+
children: "span:"
|
|
1607
|
+
}),
|
|
1608
|
+
" ",
|
|
1609
|
+
link.spanId
|
|
1610
|
+
]
|
|
1611
|
+
}, i))
|
|
1612
|
+
})
|
|
1613
|
+
]
|
|
1614
|
+
}),
|
|
1615
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1616
|
+
className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
|
|
1617
|
+
children: [
|
|
1618
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1619
|
+
className: "text-xs text-muted-foreground",
|
|
1620
|
+
children: "SpanID:"
|
|
1297
1621
|
}),
|
|
1298
|
-
|
|
1299
|
-
className: "
|
|
1300
|
-
children:
|
|
1622
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1623
|
+
className: "text-xs font-mono text-foreground",
|
|
1624
|
+
children: span.spanId
|
|
1625
|
+
}),
|
|
1626
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1627
|
+
onClick: handleCopy,
|
|
1628
|
+
className: "text-xs text-muted-foreground hover:text-foreground",
|
|
1629
|
+
"aria-label": "Copy span ID",
|
|
1630
|
+
children: copiedId ? "✓" : "Copy"
|
|
1301
1631
|
})
|
|
1302
1632
|
]
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1633
|
+
})
|
|
1634
|
+
]
|
|
1305
1635
|
});
|
|
1306
1636
|
}
|
|
1307
|
-
|
|
1308
|
-
//#endregion
|
|
1309
|
-
//#region src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx
|
|
1310
|
-
function truncateId(id) {
|
|
1311
|
-
return id.length > 8 ? `${id.substring(0, 8)}...` : id;
|
|
1312
|
-
}
|
|
1313
|
-
function LinksTab({ span, onLinkClick }) {
|
|
1314
|
-
const [expandedLinks, setExpandedLinks] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
1315
|
-
const [copiedId, setCopiedId] = (0, react.useState)(null);
|
|
1316
|
-
const toggleLinkExpanded = (index) => {
|
|
1317
|
-
setExpandedLinks((prev) => {
|
|
1318
|
-
const next = new Set(prev);
|
|
1319
|
-
if (next.has(index)) next.delete(index);
|
|
1320
|
-
else next.add(index);
|
|
1321
|
-
return next;
|
|
1322
|
-
});
|
|
1323
|
-
};
|
|
1324
|
-
const copyToClipboard = async (text, type, index) => {
|
|
1325
|
-
try {
|
|
1326
|
-
await navigator.clipboard.writeText(text);
|
|
1327
|
-
setCopiedId(`${type}-${index}-${text}`);
|
|
1328
|
-
setTimeout(() => setCopiedId(null), 2e3);
|
|
1329
|
-
} catch (err) {
|
|
1330
|
-
console.error("Failed to copy:", err);
|
|
1331
|
-
}
|
|
1332
|
-
};
|
|
1333
|
-
if (!span.links || span.links.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1334
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1335
|
-
children: "No links available"
|
|
1336
|
-
});
|
|
1337
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1338
|
-
className: "space-y-3",
|
|
1339
|
-
children: span.links.map((link, index) => {
|
|
1340
|
-
const isExpanded = expandedLinks.has(index);
|
|
1341
|
-
const hasAttributes = link.attributes && Object.keys(link.attributes).length > 0;
|
|
1342
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1343
|
-
className: "border border-border rounded-lg overflow-hidden",
|
|
1344
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1345
|
-
className: "bg-muted p-3",
|
|
1346
|
-
children: [
|
|
1347
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1348
|
-
className: "mb-2",
|
|
1349
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1350
|
-
className: "text-xs font-semibold text-muted-foreground mb-1",
|
|
1351
|
-
children: "Trace ID"
|
|
1352
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1353
|
-
className: "flex items-center gap-2",
|
|
1354
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1355
|
-
className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
|
|
1356
|
-
title: link.traceId,
|
|
1357
|
-
children: truncateId(link.traceId)
|
|
1358
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1359
|
-
onClick: () => copyToClipboard(link.traceId, "trace", index),
|
|
1360
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1361
|
-
"aria-label": "Copy trace ID",
|
|
1362
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1363
|
-
className: `w-4 h-4 ${copiedId === `trace-${index}-${link.traceId}` ? "text-green-600" : "text-muted-foreground"}`,
|
|
1364
|
-
fill: "none",
|
|
1365
|
-
stroke: "currentColor",
|
|
1366
|
-
viewBox: "0 0 24 24",
|
|
1367
|
-
children: copiedId === `trace-${index}-${link.traceId}` ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1368
|
-
strokeLinecap: "round",
|
|
1369
|
-
strokeLinejoin: "round",
|
|
1370
|
-
strokeWidth: 2,
|
|
1371
|
-
d: "M5 13l4 4L19 7"
|
|
1372
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1373
|
-
strokeLinecap: "round",
|
|
1374
|
-
strokeLinejoin: "round",
|
|
1375
|
-
strokeWidth: 2,
|
|
1376
|
-
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"
|
|
1377
|
-
})
|
|
1378
|
-
})
|
|
1379
|
-
})]
|
|
1380
|
-
})]
|
|
1381
|
-
}),
|
|
1382
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1383
|
-
className: "mb-2",
|
|
1384
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1385
|
-
className: "text-xs font-semibold text-muted-foreground mb-1",
|
|
1386
|
-
children: "Span ID"
|
|
1387
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1388
|
-
className: "flex items-center gap-2",
|
|
1389
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1390
|
-
className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
|
|
1391
|
-
title: link.spanId,
|
|
1392
|
-
children: truncateId(link.spanId)
|
|
1393
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1394
|
-
onClick: () => copyToClipboard(link.spanId, "span", index),
|
|
1395
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1396
|
-
"aria-label": "Copy span ID",
|
|
1397
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1398
|
-
className: `w-4 h-4 ${copiedId === `span-${index}-${link.spanId}` ? "text-green-600" : "text-muted-foreground"}`,
|
|
1399
|
-
fill: "none",
|
|
1400
|
-
stroke: "currentColor",
|
|
1401
|
-
viewBox: "0 0 24 24",
|
|
1402
|
-
children: copiedId === `span-${index}-${link.spanId}` ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1403
|
-
strokeLinecap: "round",
|
|
1404
|
-
strokeLinejoin: "round",
|
|
1405
|
-
strokeWidth: 2,
|
|
1406
|
-
d: "M5 13l4 4L19 7"
|
|
1407
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1408
|
-
strokeLinecap: "round",
|
|
1409
|
-
strokeLinejoin: "round",
|
|
1410
|
-
strokeWidth: 2,
|
|
1411
|
-
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"
|
|
1412
|
-
})
|
|
1413
|
-
})
|
|
1414
|
-
})]
|
|
1415
|
-
})]
|
|
1416
|
-
}),
|
|
1417
|
-
onLinkClick && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1418
|
-
onClick: () => onLinkClick(link.traceId, link.spanId),
|
|
1419
|
-
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",
|
|
1420
|
-
children: "Navigate to Span"
|
|
1421
|
-
}),
|
|
1422
|
-
hasAttributes && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1423
|
-
onClick: () => toggleLinkExpanded(index),
|
|
1424
|
-
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",
|
|
1425
|
-
"aria-expanded": isExpanded,
|
|
1426
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1427
|
-
isExpanded ? "Hide" : "Show",
|
|
1428
|
-
" Attributes (",
|
|
1429
|
-
Object.keys(link.attributes).length,
|
|
1430
|
-
")"
|
|
1431
|
-
] }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1432
|
-
className: `w-3 h-3 transition-transform ${isExpanded ? "rotate-180" : ""}`,
|
|
1433
|
-
fill: "none",
|
|
1434
|
-
stroke: "currentColor",
|
|
1435
|
-
viewBox: "0 0 24 24",
|
|
1436
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1437
|
-
strokeLinecap: "round",
|
|
1438
|
-
strokeLinejoin: "round",
|
|
1439
|
-
strokeWidth: 2,
|
|
1440
|
-
d: "M19 9l-7 7-7-7"
|
|
1441
|
-
})
|
|
1442
|
-
})]
|
|
1443
|
-
})
|
|
1444
|
-
]
|
|
1445
|
-
}), hasAttributes && isExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1446
|
-
className: "p-3 bg-background border-t border-border",
|
|
1447
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1448
|
-
className: "space-y-2",
|
|
1449
|
-
children: Object.entries(link.attributes).map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1450
|
-
className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
|
|
1451
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1452
|
-
className: "font-mono font-medium text-foreground break-words",
|
|
1453
|
-
children: key
|
|
1454
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1455
|
-
className: "text-foreground break-words",
|
|
1456
|
-
children: typeof value === "object" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1457
|
-
className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
|
|
1458
|
-
children: formatAttributeValue(value)
|
|
1459
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatAttributeValue(value) })
|
|
1460
|
-
})]
|
|
1461
|
-
}, key))
|
|
1462
|
-
})
|
|
1463
|
-
})]
|
|
1464
|
-
}, index);
|
|
1465
|
-
})
|
|
1466
|
-
});
|
|
1467
|
-
}
|
|
1468
|
-
|
|
1469
|
-
//#endregion
|
|
1470
|
-
//#region src/components/observability/TraceTimeline/DetailPane/index.tsx
|
|
1471
|
-
function DetailPane({ span, onClose, onLinkClick, initialTab = "attributes" }) {
|
|
1472
|
-
const [activeTab, setActiveTab] = (0, react.useState)(initialTab);
|
|
1473
|
-
const [copiedId, setCopiedId] = (0, react.useState)(false);
|
|
1474
|
-
const handleTabChange = (0, react.useCallback)((tab) => {
|
|
1475
|
-
setActiveTab(tab);
|
|
1476
|
-
}, []);
|
|
1477
|
-
const handleCopySpanId = (0, react.useCallback)(async () => {
|
|
1478
|
-
try {
|
|
1479
|
-
await navigator.clipboard.writeText(span.spanId);
|
|
1480
|
-
setCopiedId(true);
|
|
1481
|
-
setTimeout(() => setCopiedId(false), 2e3);
|
|
1482
|
-
} catch (err) {
|
|
1483
|
-
console.error("Failed to copy span ID:", err);
|
|
1484
|
-
}
|
|
1485
|
-
}, [span.spanId]);
|
|
1486
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1487
|
-
className: "flex flex-col h-full bg-background border-l border-border",
|
|
1488
|
-
onKeyDown: (0, react.useCallback)((e) => {
|
|
1489
|
-
if (e.key === "Escape") onClose();
|
|
1490
|
-
}, [onClose]),
|
|
1491
|
-
tabIndex: -1,
|
|
1492
|
-
role: "complementary",
|
|
1493
|
-
"aria-label": "Span details",
|
|
1494
|
-
children: [
|
|
1495
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1496
|
-
className: "p-4 border-b border-border",
|
|
1497
|
-
children: [
|
|
1498
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1499
|
-
className: "flex items-center justify-between mb-3",
|
|
1500
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
1501
|
-
className: "text-lg font-semibold text-foreground truncate",
|
|
1502
|
-
children: "Span Details"
|
|
1503
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1504
|
-
onClick: onClose,
|
|
1505
|
-
className: "p-1 hover:bg-muted rounded transition-colors",
|
|
1506
|
-
"aria-label": "Close detail pane",
|
|
1507
|
-
title: "Close (Esc)",
|
|
1508
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1509
|
-
className: "w-5 h-5 text-muted-foreground",
|
|
1510
|
-
fill: "none",
|
|
1511
|
-
stroke: "currentColor",
|
|
1512
|
-
viewBox: "0 0 24 24",
|
|
1513
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1514
|
-
strokeLinecap: "round",
|
|
1515
|
-
strokeLinejoin: "round",
|
|
1516
|
-
strokeWidth: 2,
|
|
1517
|
-
d: "M6 18L18 6M6 6l12 12"
|
|
1518
|
-
})
|
|
1519
|
-
})
|
|
1520
|
-
})]
|
|
1521
|
-
}),
|
|
1522
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1523
|
-
className: "mb-2",
|
|
1524
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1525
|
-
className: "text-sm font-medium text-foreground truncate",
|
|
1526
|
-
title: span.name,
|
|
1527
|
-
children: span.name
|
|
1528
|
-
})
|
|
1529
|
-
}),
|
|
1530
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1531
|
-
className: "flex items-center gap-2",
|
|
1532
|
-
children: [
|
|
1533
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1534
|
-
className: "text-xs text-muted-foreground",
|
|
1535
|
-
children: "Span ID:"
|
|
1536
|
-
}),
|
|
1537
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1538
|
-
className: "text-xs font-mono text-foreground bg-muted px-2 py-1 rounded flex-1 truncate",
|
|
1539
|
-
title: span.spanId,
|
|
1540
|
-
children: span.spanId
|
|
1541
|
-
}),
|
|
1542
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1543
|
-
onClick: handleCopySpanId,
|
|
1544
|
-
className: "p-1 hover:bg-muted rounded transition-colors",
|
|
1545
|
-
"aria-label": "Copy span ID",
|
|
1546
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1547
|
-
className: `w-4 h-4 ${copiedId ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`,
|
|
1548
|
-
fill: "none",
|
|
1549
|
-
stroke: "currentColor",
|
|
1550
|
-
viewBox: "0 0 24 24",
|
|
1551
|
-
children: copiedId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1552
|
-
strokeLinecap: "round",
|
|
1553
|
-
strokeLinejoin: "round",
|
|
1554
|
-
strokeWidth: 2,
|
|
1555
|
-
d: "M5 13l4 4L19 7"
|
|
1556
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1557
|
-
strokeLinecap: "round",
|
|
1558
|
-
strokeLinejoin: "round",
|
|
1559
|
-
strokeWidth: 2,
|
|
1560
|
-
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"
|
|
1561
|
-
})
|
|
1562
|
-
})
|
|
1563
|
-
})
|
|
1564
|
-
]
|
|
1565
|
-
})
|
|
1566
|
-
]
|
|
1567
|
-
}),
|
|
1568
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1569
|
-
className: "flex border-b border-border",
|
|
1570
|
-
role: "tablist",
|
|
1571
|
-
"aria-label": "Span detail tabs",
|
|
1572
|
-
children: [
|
|
1573
|
-
"attributes",
|
|
1574
|
-
"events",
|
|
1575
|
-
"links"
|
|
1576
|
-
].map((tab) => {
|
|
1577
|
-
const count = tab === "attributes" ? Object.keys(span.attributes).length : tab === "events" ? span.events.length : span.links.length;
|
|
1578
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1579
|
-
role: "tab",
|
|
1580
|
-
"aria-selected": activeTab === tab,
|
|
1581
|
-
onClick: () => handleTabChange(tab),
|
|
1582
|
-
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"}`,
|
|
1583
|
-
children: [tab.charAt(0).toUpperCase() + tab.slice(1), count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1584
|
-
className: "ml-1 text-xs text-muted-foreground",
|
|
1585
|
-
children: [
|
|
1586
|
-
"(",
|
|
1587
|
-
count,
|
|
1588
|
-
")"
|
|
1589
|
-
]
|
|
1590
|
-
})]
|
|
1591
|
-
}, tab);
|
|
1592
|
-
})
|
|
1593
|
-
}),
|
|
1594
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1595
|
-
className: "flex-1 overflow-auto p-4",
|
|
1596
|
-
children: [
|
|
1597
|
-
activeTab === "attributes" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttributesTab$1, { span }),
|
|
1598
|
-
activeTab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventsTab, { span }),
|
|
1599
|
-
activeTab === "links" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinksTab, {
|
|
1600
|
-
span,
|
|
1601
|
-
onLinkClick
|
|
1602
|
-
})
|
|
1603
|
-
]
|
|
1604
|
-
})
|
|
1605
|
-
]
|
|
1606
|
-
});
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
1637
|
//#endregion
|
|
1610
1638
|
//#region src/components/observability/shared/LoadingSkeleton.tsx
|
|
1611
1639
|
function LoadingSkeleton() {
|
|
@@ -1647,7 +1675,6 @@ function LoadingSkeleton() {
|
|
|
1647
1675
|
})]
|
|
1648
1676
|
});
|
|
1649
1677
|
}
|
|
1650
|
-
|
|
1651
1678
|
//#endregion
|
|
1652
1679
|
//#region src/components/KeyboardShortcuts/context.ts
|
|
1653
1680
|
const noop = () => {};
|
|
@@ -1667,7 +1694,6 @@ function useRegisterShortcuts(id, group) {
|
|
|
1667
1694
|
unregister
|
|
1668
1695
|
]);
|
|
1669
1696
|
}
|
|
1670
|
-
|
|
1671
1697
|
//#endregion
|
|
1672
1698
|
//#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
|
|
1673
1699
|
function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
@@ -1724,154 +1750,1059 @@ function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
|
1724
1750
|
})
|
|
1725
1751
|
});
|
|
1726
1752
|
}
|
|
1727
|
-
|
|
1728
1753
|
//#endregion
|
|
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", "
|
|
1739
|
-
description: "
|
|
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] = (0, react.useState)(() => /* @__PURE__ */ new Map());
|
|
1753
|
-
const [isOpen, setIsOpen] = (0, react.useState)(false);
|
|
1754
|
-
const register = (0, react.useCallback)((id, group) => {
|
|
1755
|
-
setRegistry((prev) => {
|
|
1756
|
-
const next = new Map(prev);
|
|
1757
|
-
next.set(id, group);
|
|
1758
|
-
return next;
|
|
1759
|
-
});
|
|
1754
|
+
//#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
|
|
1755
|
+
const GENERAL_GROUP = {
|
|
1756
|
+
name: "General",
|
|
1757
|
+
shortcuts: [
|
|
1758
|
+
{
|
|
1759
|
+
keys: ["Shift", "?"],
|
|
1760
|
+
description: "Toggle shortcuts help"
|
|
1761
|
+
},
|
|
1762
|
+
{
|
|
1763
|
+
keys: ["Shift", "T"],
|
|
1764
|
+
description: "Traces tab"
|
|
1765
|
+
},
|
|
1766
|
+
{
|
|
1767
|
+
keys: ["Shift", "L"],
|
|
1768
|
+
description: "Logs tab"
|
|
1769
|
+
},
|
|
1770
|
+
{
|
|
1771
|
+
keys: ["Shift", "M"],
|
|
1772
|
+
description: "Metrics tab"
|
|
1773
|
+
}
|
|
1774
|
+
]
|
|
1775
|
+
};
|
|
1776
|
+
function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLogs, onNavigateMetrics }) {
|
|
1777
|
+
const [registry, setRegistry] = (0, react.useState)(() => /* @__PURE__ */ new Map());
|
|
1778
|
+
const [isOpen, setIsOpen] = (0, react.useState)(false);
|
|
1779
|
+
const register = (0, react.useCallback)((id, group) => {
|
|
1780
|
+
setRegistry((prev) => {
|
|
1781
|
+
const next = new Map(prev);
|
|
1782
|
+
next.set(id, group);
|
|
1783
|
+
return next;
|
|
1784
|
+
});
|
|
1785
|
+
}, []);
|
|
1786
|
+
const unregister = (0, react.useCallback)((id) => {
|
|
1787
|
+
setRegistry((prev) => {
|
|
1788
|
+
const next = new Map(prev);
|
|
1789
|
+
next.delete(id);
|
|
1790
|
+
return next;
|
|
1791
|
+
});
|
|
1792
|
+
}, []);
|
|
1793
|
+
(0, react.useEffect)(() => {
|
|
1794
|
+
function handleKeyDown(e) {
|
|
1795
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
1796
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || e.target.isContentEditable) return;
|
|
1797
|
+
if (e.shiftKey && e.key === "?") {
|
|
1798
|
+
e.preventDefault();
|
|
1799
|
+
setIsOpen((v) => !v);
|
|
1800
|
+
return;
|
|
1801
|
+
}
|
|
1802
|
+
if (e.key === "Escape" && isOpen) {
|
|
1803
|
+
e.preventDefault();
|
|
1804
|
+
setIsOpen(false);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
if (e.shiftKey && e.key === "T") {
|
|
1808
|
+
e.preventDefault();
|
|
1809
|
+
onNavigateServices();
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (e.shiftKey && e.key === "L") {
|
|
1813
|
+
e.preventDefault();
|
|
1814
|
+
onNavigateLogs();
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
if (e.shiftKey && e.key === "M") {
|
|
1818
|
+
e.preventDefault();
|
|
1819
|
+
onNavigateMetrics();
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
document.addEventListener("keydown", handleKeyDown);
|
|
1824
|
+
return () => document.removeEventListener("keydown", handleKeyDown);
|
|
1825
|
+
}, [
|
|
1826
|
+
isOpen,
|
|
1827
|
+
onNavigateServices,
|
|
1828
|
+
onNavigateLogs,
|
|
1829
|
+
onNavigateMetrics
|
|
1830
|
+
]);
|
|
1831
|
+
const groups = (0, react.useMemo)(() => {
|
|
1832
|
+
return [GENERAL_GROUP, ...registry.values()];
|
|
1833
|
+
}, [registry]);
|
|
1834
|
+
const contextValue = (0, react.useMemo)(() => ({
|
|
1835
|
+
register,
|
|
1836
|
+
unregister
|
|
1837
|
+
}), [register, unregister]);
|
|
1838
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(KeyboardShortcutsContext.Provider, {
|
|
1839
|
+
value: contextValue,
|
|
1840
|
+
children: [children, /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShortcutsHelpDialog, {
|
|
1841
|
+
open: isOpen,
|
|
1842
|
+
onClose: () => setIsOpen(false),
|
|
1843
|
+
groups
|
|
1844
|
+
})]
|
|
1845
|
+
});
|
|
1846
|
+
}
|
|
1847
|
+
//#endregion
|
|
1848
|
+
//#region src/components/observability/TraceTimeline/shortcuts.ts
|
|
1849
|
+
const TRACE_VIEWER_SHORTCUTS = {
|
|
1850
|
+
name: "Trace Viewer",
|
|
1851
|
+
shortcuts: [
|
|
1852
|
+
{
|
|
1853
|
+
keys: ["↑/K"],
|
|
1854
|
+
description: "Previous span"
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
keys: ["↓/J"],
|
|
1858
|
+
description: "Next span"
|
|
1859
|
+
},
|
|
1860
|
+
{
|
|
1861
|
+
keys: ["←"],
|
|
1862
|
+
description: "Collapse span"
|
|
1863
|
+
},
|
|
1864
|
+
{
|
|
1865
|
+
keys: ["→"],
|
|
1866
|
+
description: "Expand span"
|
|
1867
|
+
},
|
|
1868
|
+
{
|
|
1869
|
+
keys: ["Enter"],
|
|
1870
|
+
description: "Focus detail pane"
|
|
1871
|
+
},
|
|
1872
|
+
{
|
|
1873
|
+
keys: ["C"],
|
|
1874
|
+
description: "Copy span name"
|
|
1875
|
+
},
|
|
1876
|
+
{
|
|
1877
|
+
keys: ["Esc"],
|
|
1878
|
+
description: "Deselect span"
|
|
1879
|
+
},
|
|
1880
|
+
{
|
|
1881
|
+
keys: [
|
|
1882
|
+
"Ctrl",
|
|
1883
|
+
"Shift",
|
|
1884
|
+
"E"
|
|
1885
|
+
],
|
|
1886
|
+
description: "Expand all"
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
keys: [
|
|
1890
|
+
"Ctrl",
|
|
1891
|
+
"Shift",
|
|
1892
|
+
"C"
|
|
1893
|
+
],
|
|
1894
|
+
description: "Collapse all"
|
|
1895
|
+
}
|
|
1896
|
+
]
|
|
1897
|
+
};
|
|
1898
|
+
//#endregion
|
|
1899
|
+
//#region src/components/observability/TraceTimeline/TimeRuler.tsx
|
|
1900
|
+
const TICK_COUNT = 5;
|
|
1901
|
+
function TimeRuler({ totalDurationMs, leftColumnWidth, offsetMs = 0 }) {
|
|
1902
|
+
const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => {
|
|
1903
|
+
const fraction = i / TICK_COUNT;
|
|
1904
|
+
return {
|
|
1905
|
+
label: formatDuration(offsetMs + totalDurationMs * fraction),
|
|
1906
|
+
percent: fraction * 100
|
|
1907
|
+
};
|
|
1908
|
+
});
|
|
1909
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1910
|
+
className: "flex border-b border-border bg-background",
|
|
1911
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1912
|
+
className: "flex-shrink-0",
|
|
1913
|
+
style: { width: leftColumnWidth }
|
|
1914
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1915
|
+
className: "flex-1 relative h-6 px-2",
|
|
1916
|
+
children: ticks.map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1917
|
+
className: "absolute top-0 h-full flex flex-col justify-end",
|
|
1918
|
+
style: { left: `${tick.percent}%` },
|
|
1919
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 border-l border-muted-foreground/40" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1920
|
+
className: "text-[10px] text-muted-foreground font-mono -translate-x-1/2 absolute bottom-0 whitespace-nowrap",
|
|
1921
|
+
style: {
|
|
1922
|
+
left: 0,
|
|
1923
|
+
transform: tick.percent === 100 ? "translateX(-100%)" : tick.percent === 0 ? "none" : "translateX(-50%)"
|
|
1924
|
+
},
|
|
1925
|
+
children: tick.label
|
|
1926
|
+
})]
|
|
1927
|
+
}, tick.percent))
|
|
1928
|
+
})]
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
//#endregion
|
|
1932
|
+
//#region src/components/observability/TraceTimeline/SpanSearch.tsx
|
|
1933
|
+
function SpanSearch({ value, onChange, matchCount, currentMatch, onPrev, onNext }) {
|
|
1934
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1935
|
+
className: "flex items-center gap-1 px-2 py-1 border-b border-border bg-background",
|
|
1936
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1937
|
+
type: "text",
|
|
1938
|
+
placeholder: "Find...",
|
|
1939
|
+
value,
|
|
1940
|
+
onChange: (e) => onChange(e.target.value),
|
|
1941
|
+
className: "bg-muted text-foreground text-sm px-2 py-0.5 rounded border border-border outline-none focus:border-blue-500 w-48"
|
|
1942
|
+
}), value && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1944
|
+
className: "text-xs text-muted-foreground whitespace-nowrap",
|
|
1945
|
+
children: matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : "0 matches"
|
|
1946
|
+
}),
|
|
1947
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1948
|
+
onClick: onPrev,
|
|
1949
|
+
disabled: matchCount === 0,
|
|
1950
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1951
|
+
"aria-label": "Previous match",
|
|
1952
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1953
|
+
className: "w-3.5 h-3.5",
|
|
1954
|
+
fill: "none",
|
|
1955
|
+
stroke: "currentColor",
|
|
1956
|
+
viewBox: "0 0 24 24",
|
|
1957
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1958
|
+
strokeLinecap: "round",
|
|
1959
|
+
strokeLinejoin: "round",
|
|
1960
|
+
strokeWidth: 2,
|
|
1961
|
+
d: "M5 15l7-7 7 7"
|
|
1962
|
+
})
|
|
1963
|
+
})
|
|
1964
|
+
}),
|
|
1965
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1966
|
+
onClick: onNext,
|
|
1967
|
+
disabled: matchCount === 0,
|
|
1968
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1969
|
+
"aria-label": "Next match",
|
|
1970
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1971
|
+
className: "w-3.5 h-3.5",
|
|
1972
|
+
fill: "none",
|
|
1973
|
+
stroke: "currentColor",
|
|
1974
|
+
viewBox: "0 0 24 24",
|
|
1975
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1976
|
+
strokeLinecap: "round",
|
|
1977
|
+
strokeLinejoin: "round",
|
|
1978
|
+
strokeWidth: 2,
|
|
1979
|
+
d: "M19 9l-7 7-7-7"
|
|
1980
|
+
})
|
|
1981
|
+
})
|
|
1982
|
+
})
|
|
1983
|
+
] })]
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
//#endregion
|
|
1987
|
+
//#region src/components/observability/TraceTimeline/ViewTabs.tsx
|
|
1988
|
+
const VIEWS = [
|
|
1989
|
+
"timeline",
|
|
1990
|
+
"graph",
|
|
1991
|
+
"statistics",
|
|
1992
|
+
"flamegraph"
|
|
1993
|
+
];
|
|
1994
|
+
const VIEW_LABELS = {
|
|
1995
|
+
timeline: "Timeline",
|
|
1996
|
+
graph: "Graph",
|
|
1997
|
+
statistics: "Statistics",
|
|
1998
|
+
flamegraph: "Flamegraph"
|
|
1999
|
+
};
|
|
2000
|
+
function ViewTabs({ activeView, onChange }) {
|
|
2001
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2002
|
+
className: "flex border-b border-border bg-background",
|
|
2003
|
+
children: VIEWS.map((view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2004
|
+
onClick: () => onChange(view),
|
|
2005
|
+
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"}`,
|
|
2006
|
+
children: VIEW_LABELS[view]
|
|
2007
|
+
}, view))
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
//#endregion
|
|
2011
|
+
//#region src/components/observability/TraceTimeline/GraphView.tsx
|
|
2012
|
+
/**
|
|
2013
|
+
* GraphView - SVG-based DAG showing service dependencies within a trace.
|
|
2014
|
+
*/
|
|
2015
|
+
function buildDAG(trace) {
|
|
2016
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2017
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
2018
|
+
const childServices = /* @__PURE__ */ new Map();
|
|
2019
|
+
function walk(span, parentService) {
|
|
2020
|
+
const svc = span.serviceName;
|
|
2021
|
+
const existing = nodeMap.get(svc);
|
|
2022
|
+
if (existing) {
|
|
2023
|
+
existing.spanCount++;
|
|
2024
|
+
if (span.status === "ERROR") existing.errorCount++;
|
|
2025
|
+
} else nodeMap.set(svc, {
|
|
2026
|
+
spanCount: 1,
|
|
2027
|
+
errorCount: span.status === "ERROR" ? 1 : 0
|
|
2028
|
+
});
|
|
2029
|
+
if (parentService && parentService !== svc) {
|
|
2030
|
+
const key = `${parentService}→${svc}`;
|
|
2031
|
+
const edge = edgeMap.get(key);
|
|
2032
|
+
if (edge) {
|
|
2033
|
+
edge.callCount++;
|
|
2034
|
+
edge.totalDurationMs += span.durationMs;
|
|
2035
|
+
} else edgeMap.set(key, {
|
|
2036
|
+
callCount: 1,
|
|
2037
|
+
totalDurationMs: span.durationMs
|
|
2038
|
+
});
|
|
2039
|
+
if (!childServices.has(parentService)) childServices.set(parentService, /* @__PURE__ */ new Set());
|
|
2040
|
+
const parentChildren = childServices.get(parentService);
|
|
2041
|
+
if (parentChildren) parentChildren.add(svc);
|
|
2042
|
+
}
|
|
2043
|
+
for (const child of span.children) walk(child, svc);
|
|
2044
|
+
}
|
|
2045
|
+
for (const root of trace.rootSpans) walk(root);
|
|
2046
|
+
const edges = [];
|
|
2047
|
+
for (const [key, meta] of edgeMap) {
|
|
2048
|
+
const [from, to] = key.split("→");
|
|
2049
|
+
if (from && to) edges.push({
|
|
2050
|
+
from,
|
|
2051
|
+
to,
|
|
2052
|
+
...meta
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
return {
|
|
2056
|
+
nodeMap,
|
|
2057
|
+
edges,
|
|
2058
|
+
childServices
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
const NODE_W = 160;
|
|
2062
|
+
const NODE_H = 60;
|
|
2063
|
+
const LAYER_GAP_Y = 100;
|
|
2064
|
+
const NODE_GAP_X = 40;
|
|
2065
|
+
function layoutNodes(nodeMap, edges) {
|
|
2066
|
+
const children = /* @__PURE__ */ new Map();
|
|
2067
|
+
const hasParent = /* @__PURE__ */ new Set();
|
|
2068
|
+
for (const e of edges) {
|
|
2069
|
+
if (!children.has(e.from)) children.set(e.from, /* @__PURE__ */ new Set());
|
|
2070
|
+
const fromChildren = children.get(e.from);
|
|
2071
|
+
if (fromChildren) fromChildren.add(e.to);
|
|
2072
|
+
hasParent.add(e.to);
|
|
2073
|
+
}
|
|
2074
|
+
const roots = [...nodeMap.keys()].filter((s) => !hasParent.has(s));
|
|
2075
|
+
if (roots.length === 0 && nodeMap.size > 0) {
|
|
2076
|
+
const firstKey = nodeMap.keys().next().value;
|
|
2077
|
+
if (firstKey !== void 0) roots.push(firstKey);
|
|
2078
|
+
}
|
|
2079
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
2080
|
+
const enqueueCount = /* @__PURE__ */ new Map();
|
|
2081
|
+
const maxEnqueue = nodeMap.size * 2;
|
|
2082
|
+
const queue = [];
|
|
2083
|
+
for (const r of roots) {
|
|
2084
|
+
layerOf.set(r, 0);
|
|
2085
|
+
queue.push(r);
|
|
2086
|
+
}
|
|
2087
|
+
while (queue.length > 0) {
|
|
2088
|
+
const cur = queue.shift();
|
|
2089
|
+
if (!cur) continue;
|
|
2090
|
+
const curLayer = layerOf.get(cur);
|
|
2091
|
+
if (curLayer === void 0) continue;
|
|
2092
|
+
const kids = children.get(cur);
|
|
2093
|
+
if (!kids) continue;
|
|
2094
|
+
for (const kid of kids) {
|
|
2095
|
+
const prev = layerOf.get(kid);
|
|
2096
|
+
const count = enqueueCount.get(kid) ?? 0;
|
|
2097
|
+
if (prev === void 0 && count < maxEnqueue) {
|
|
2098
|
+
layerOf.set(kid, curLayer + 1);
|
|
2099
|
+
enqueueCount.set(kid, count + 1);
|
|
2100
|
+
queue.push(kid);
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
for (const name of nodeMap.keys()) if (!layerOf.has(name)) layerOf.set(name, 0);
|
|
2105
|
+
const layers = /* @__PURE__ */ new Map();
|
|
2106
|
+
for (const [name, layer] of layerOf) {
|
|
2107
|
+
if (!layers.has(layer)) layers.set(layer, []);
|
|
2108
|
+
const layerNames = layers.get(layer);
|
|
2109
|
+
if (layerNames) layerNames.push(name);
|
|
2110
|
+
}
|
|
2111
|
+
const nodes = [];
|
|
2112
|
+
const totalWidth = Math.max(...Array.from(layers.values()).map((l) => l.length), 1) * (NODE_W + NODE_GAP_X) - NODE_GAP_X;
|
|
2113
|
+
for (const [layer, names] of layers) {
|
|
2114
|
+
const offsetX = (totalWidth - (names.length * (NODE_W + NODE_GAP_X) - NODE_GAP_X)) / 2;
|
|
2115
|
+
names.forEach((name, i) => {
|
|
2116
|
+
const meta = nodeMap.get(name);
|
|
2117
|
+
if (!meta) return;
|
|
2118
|
+
nodes.push({
|
|
2119
|
+
name,
|
|
2120
|
+
spanCount: meta.spanCount,
|
|
2121
|
+
errorCount: meta.errorCount,
|
|
2122
|
+
layer,
|
|
2123
|
+
x: offsetX + i * (NODE_W + NODE_GAP_X),
|
|
2124
|
+
y: layer * (NODE_H + LAYER_GAP_Y)
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
}
|
|
2128
|
+
return nodes;
|
|
2129
|
+
}
|
|
2130
|
+
function GraphView({ trace }) {
|
|
2131
|
+
const { nodes, edges, svgWidth, svgHeight } = (0, react.useMemo)(() => {
|
|
2132
|
+
const { nodeMap, edges } = buildDAG(trace);
|
|
2133
|
+
const nodes = layoutNodes(nodeMap, edges);
|
|
2134
|
+
const maxX = Math.max(...nodes.map((n) => n.x + NODE_W), NODE_W);
|
|
2135
|
+
const maxY = Math.max(...nodes.map((n) => n.y + NODE_H), NODE_H);
|
|
2136
|
+
const padding = 40;
|
|
2137
|
+
return {
|
|
2138
|
+
nodes,
|
|
2139
|
+
edges,
|
|
2140
|
+
svgWidth: maxX + padding * 2,
|
|
2141
|
+
svgHeight: maxY + padding * 2
|
|
2142
|
+
};
|
|
2143
|
+
}, [trace]);
|
|
2144
|
+
const nodeByName = (0, react.useMemo)(() => {
|
|
2145
|
+
const m = /* @__PURE__ */ new Map();
|
|
2146
|
+
for (const n of nodes) m.set(n.name, n);
|
|
2147
|
+
return m;
|
|
2148
|
+
}, [nodes]);
|
|
2149
|
+
const padding = 40;
|
|
2150
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2151
|
+
className: "flex-1 overflow-auto bg-background p-4 flex justify-center",
|
|
2152
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2153
|
+
viewBox: `0 0 ${svgWidth} ${svgHeight}`,
|
|
2154
|
+
width: svgWidth,
|
|
2155
|
+
height: svgHeight,
|
|
2156
|
+
role: "img",
|
|
2157
|
+
"aria-label": "Service dependency graph",
|
|
2158
|
+
children: [
|
|
2159
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("marker", {
|
|
2160
|
+
id: "arrowhead",
|
|
2161
|
+
markerWidth: "10",
|
|
2162
|
+
markerHeight: "7",
|
|
2163
|
+
refX: "9",
|
|
2164
|
+
refY: "3.5",
|
|
2165
|
+
orient: "auto",
|
|
2166
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polygon", {
|
|
2167
|
+
points: "0 0, 10 3.5, 0 7",
|
|
2168
|
+
fill: "#94a3b8"
|
|
2169
|
+
})
|
|
2170
|
+
}) }),
|
|
2171
|
+
edges.map((edge) => {
|
|
2172
|
+
const from = nodeByName.get(edge.from);
|
|
2173
|
+
const to = nodeByName.get(edge.to);
|
|
2174
|
+
if (!from || !to) return null;
|
|
2175
|
+
const x1 = padding + from.x + NODE_W / 2;
|
|
2176
|
+
const y1 = padding + from.y + NODE_H;
|
|
2177
|
+
const x2 = padding + to.x + NODE_W / 2;
|
|
2178
|
+
const y2 = padding + to.y;
|
|
2179
|
+
const midY = (y1 + y2) / 2;
|
|
2180
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
2181
|
+
d: `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`,
|
|
2182
|
+
fill: "none",
|
|
2183
|
+
stroke: "#475569",
|
|
2184
|
+
strokeWidth: 1.5,
|
|
2185
|
+
markerEnd: "url(#arrowhead)"
|
|
2186
|
+
}), edge.callCount > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("text", {
|
|
2187
|
+
x: (x1 + x2) / 2,
|
|
2188
|
+
y: midY - 6,
|
|
2189
|
+
textAnchor: "middle",
|
|
2190
|
+
fontSize: 11,
|
|
2191
|
+
fill: "#94a3b8",
|
|
2192
|
+
children: [edge.callCount, "x"]
|
|
2193
|
+
})] }, `${edge.from}→${edge.to}`);
|
|
2194
|
+
}),
|
|
2195
|
+
nodes.map((node) => {
|
|
2196
|
+
const color = getServiceColor(node.name);
|
|
2197
|
+
const hasError = node.errorCount > 0;
|
|
2198
|
+
const textColor = "#f8fafc";
|
|
2199
|
+
const nx = padding + node.x;
|
|
2200
|
+
const ny = padding + node.y;
|
|
2201
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
2202
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2203
|
+
x: nx,
|
|
2204
|
+
y: ny,
|
|
2205
|
+
width: NODE_W,
|
|
2206
|
+
height: NODE_H,
|
|
2207
|
+
rx: 8,
|
|
2208
|
+
ry: 8,
|
|
2209
|
+
fill: color,
|
|
2210
|
+
stroke: hasError ? "#ef4444" : "none",
|
|
2211
|
+
strokeWidth: hasError ? 2 : 0
|
|
2212
|
+
}),
|
|
2213
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
2214
|
+
x: nx + NODE_W / 2,
|
|
2215
|
+
y: ny + 24,
|
|
2216
|
+
textAnchor: "middle",
|
|
2217
|
+
fontSize: 13,
|
|
2218
|
+
fontWeight: 600,
|
|
2219
|
+
fill: textColor,
|
|
2220
|
+
children: node.name.length > 18 ? node.name.slice(0, 16) + "..." : node.name
|
|
2221
|
+
}),
|
|
2222
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("text", {
|
|
2223
|
+
x: nx + NODE_W / 2,
|
|
2224
|
+
y: ny + 44,
|
|
2225
|
+
textAnchor: "middle",
|
|
2226
|
+
fontSize: 11,
|
|
2227
|
+
fill: textColor,
|
|
2228
|
+
opacity: .85,
|
|
2229
|
+
children: [
|
|
2230
|
+
node.spanCount,
|
|
2231
|
+
" span",
|
|
2232
|
+
node.spanCount !== 1 ? "s" : "",
|
|
2233
|
+
node.errorCount > 0 && ` · ${node.errorCount} err`
|
|
2234
|
+
]
|
|
2235
|
+
})
|
|
2236
|
+
] }, node.name);
|
|
2237
|
+
})
|
|
2238
|
+
]
|
|
2239
|
+
})
|
|
2240
|
+
});
|
|
2241
|
+
}
|
|
2242
|
+
//#endregion
|
|
2243
|
+
//#region src/components/observability/TraceTimeline/StatisticsView.tsx
|
|
2244
|
+
function computeSelfTime(span) {
|
|
2245
|
+
const childrenTotal = span.children.reduce((sum, child) => sum + child.durationMs, 0);
|
|
2246
|
+
return Math.max(0, span.durationMs - childrenTotal);
|
|
2247
|
+
}
|
|
2248
|
+
function computeStats(trace) {
|
|
2249
|
+
const allFlattened = flattenAllSpans(trace.rootSpans);
|
|
2250
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2251
|
+
for (const { span } of allFlattened) {
|
|
2252
|
+
const key = `${span.serviceName}:${span.name}`;
|
|
2253
|
+
let group = groups.get(key);
|
|
2254
|
+
if (!group) {
|
|
2255
|
+
group = {
|
|
2256
|
+
spans: [],
|
|
2257
|
+
selfTimes: []
|
|
2258
|
+
};
|
|
2259
|
+
groups.set(key, group);
|
|
2260
|
+
}
|
|
2261
|
+
group.spans.push(span);
|
|
2262
|
+
group.selfTimes.push(computeSelfTime(span));
|
|
2263
|
+
}
|
|
2264
|
+
const stats = [];
|
|
2265
|
+
for (const [key, { spans, selfTimes }] of groups) {
|
|
2266
|
+
const durations = spans.map((s) => s.durationMs);
|
|
2267
|
+
const count = spans.length;
|
|
2268
|
+
const totalDuration = durations.reduce((a, b) => a + b, 0);
|
|
2269
|
+
const selfTimeTotal = selfTimes.reduce((a, b) => a + b, 0);
|
|
2270
|
+
const firstSpan = spans[0];
|
|
2271
|
+
if (!firstSpan) continue;
|
|
2272
|
+
stats.push({
|
|
2273
|
+
key,
|
|
2274
|
+
serviceName: firstSpan.serviceName,
|
|
2275
|
+
spanName: firstSpan.name,
|
|
2276
|
+
count,
|
|
2277
|
+
totalDuration,
|
|
2278
|
+
avgDuration: totalDuration / count,
|
|
2279
|
+
minDuration: Math.min(...durations),
|
|
2280
|
+
maxDuration: Math.max(...durations),
|
|
2281
|
+
selfTimeTotal,
|
|
2282
|
+
selfTimeAvg: selfTimeTotal / count,
|
|
2283
|
+
selfTimeMin: Math.min(...selfTimes),
|
|
2284
|
+
selfTimeMax: Math.max(...selfTimes)
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
return stats;
|
|
2288
|
+
}
|
|
2289
|
+
function getSortValue(stat, field) {
|
|
2290
|
+
switch (field) {
|
|
2291
|
+
case "name": return stat.key.toLowerCase();
|
|
2292
|
+
case "count": return stat.count;
|
|
2293
|
+
case "total": return stat.totalDuration;
|
|
2294
|
+
case "avg": return stat.avgDuration;
|
|
2295
|
+
case "min": return stat.minDuration;
|
|
2296
|
+
case "max": return stat.maxDuration;
|
|
2297
|
+
case "selfTotal": return stat.selfTimeTotal;
|
|
2298
|
+
case "selfAvg": return stat.selfTimeAvg;
|
|
2299
|
+
case "selfMin": return stat.selfTimeMin;
|
|
2300
|
+
case "selfMax": return stat.selfTimeMax;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
const COLUMNS = [
|
|
2304
|
+
{
|
|
2305
|
+
label: "Name",
|
|
2306
|
+
field: "name"
|
|
2307
|
+
},
|
|
2308
|
+
{
|
|
2309
|
+
label: "Count",
|
|
2310
|
+
field: "count"
|
|
2311
|
+
},
|
|
2312
|
+
{
|
|
2313
|
+
label: "Total",
|
|
2314
|
+
field: "total"
|
|
2315
|
+
},
|
|
2316
|
+
{
|
|
2317
|
+
label: "Avg",
|
|
2318
|
+
field: "avg"
|
|
2319
|
+
},
|
|
2320
|
+
{
|
|
2321
|
+
label: "Min",
|
|
2322
|
+
field: "min"
|
|
2323
|
+
},
|
|
2324
|
+
{
|
|
2325
|
+
label: "Max",
|
|
2326
|
+
field: "max"
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
label: "ST Total",
|
|
2330
|
+
field: "selfTotal"
|
|
2331
|
+
},
|
|
2332
|
+
{
|
|
2333
|
+
label: "ST Avg",
|
|
2334
|
+
field: "selfAvg"
|
|
2335
|
+
},
|
|
2336
|
+
{
|
|
2337
|
+
label: "ST Min",
|
|
2338
|
+
field: "selfMin"
|
|
2339
|
+
},
|
|
2340
|
+
{
|
|
2341
|
+
label: "ST Max",
|
|
2342
|
+
field: "selfMax"
|
|
2343
|
+
}
|
|
2344
|
+
];
|
|
2345
|
+
function StatisticsView({ trace }) {
|
|
2346
|
+
const [sortField, setSortField] = (0, react.useState)("total");
|
|
2347
|
+
const [sortAsc, setSortAsc] = (0, react.useState)(false);
|
|
2348
|
+
const stats = (0, react.useMemo)(() => computeStats(trace), [trace]);
|
|
2349
|
+
const sorted = (0, react.useMemo)(() => {
|
|
2350
|
+
const copy = [...stats];
|
|
2351
|
+
copy.sort((a, b) => {
|
|
2352
|
+
const aVal = getSortValue(a, sortField);
|
|
2353
|
+
const bVal = getSortValue(b, sortField);
|
|
2354
|
+
let cmp;
|
|
2355
|
+
if (typeof aVal === "string" && typeof bVal === "string") cmp = aVal.localeCompare(bVal);
|
|
2356
|
+
else if (typeof aVal === "number" && typeof bVal === "number") cmp = aVal - bVal;
|
|
2357
|
+
else cmp = 0;
|
|
2358
|
+
return sortAsc ? cmp : -cmp;
|
|
2359
|
+
});
|
|
2360
|
+
return copy;
|
|
2361
|
+
}, [
|
|
2362
|
+
stats,
|
|
2363
|
+
sortField,
|
|
2364
|
+
sortAsc
|
|
2365
|
+
]);
|
|
2366
|
+
const handleSort = (field) => {
|
|
2367
|
+
if (sortField === field) setSortAsc((p) => !p);
|
|
2368
|
+
else {
|
|
2369
|
+
setSortField(field);
|
|
2370
|
+
setSortAsc(false);
|
|
2371
|
+
}
|
|
2372
|
+
};
|
|
2373
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2374
|
+
className: "flex-1 overflow-auto p-2",
|
|
2375
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
2376
|
+
className: "w-full text-sm border-collapse",
|
|
2377
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", {
|
|
2378
|
+
className: "border-b border-border",
|
|
2379
|
+
children: COLUMNS.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("th", {
|
|
2380
|
+
className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground cursor-pointer select-none hover:text-foreground whitespace-nowrap",
|
|
2381
|
+
onClick: () => handleSort(col.field),
|
|
2382
|
+
children: [
|
|
2383
|
+
col.label,
|
|
2384
|
+
" ",
|
|
2385
|
+
sortField === col.field ? sortAsc ? "▲" : "▼" : ""
|
|
2386
|
+
]
|
|
2387
|
+
}, col.field))
|
|
2388
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: sorted.map((stat, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
2389
|
+
className: `border-b border-border/50 ${i % 2 === 0 ? "bg-background" : "bg-muted/30"}`,
|
|
2390
|
+
children: [
|
|
2391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
|
|
2392
|
+
className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
|
|
2393
|
+
children: [
|
|
2394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2395
|
+
className: "text-muted-foreground",
|
|
2396
|
+
children: stat.serviceName
|
|
2397
|
+
}),
|
|
2398
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2399
|
+
className: "text-muted-foreground/50",
|
|
2400
|
+
children: ":"
|
|
2401
|
+
}),
|
|
2402
|
+
" ",
|
|
2403
|
+
stat.spanName
|
|
2404
|
+
]
|
|
2405
|
+
}),
|
|
2406
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2407
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2408
|
+
children: stat.count
|
|
2409
|
+
}),
|
|
2410
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2411
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2412
|
+
children: formatDuration(stat.totalDuration)
|
|
2413
|
+
}),
|
|
2414
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2415
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2416
|
+
children: formatDuration(stat.avgDuration)
|
|
2417
|
+
}),
|
|
2418
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2419
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2420
|
+
children: formatDuration(stat.minDuration)
|
|
2421
|
+
}),
|
|
2422
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2423
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2424
|
+
children: formatDuration(stat.maxDuration)
|
|
2425
|
+
}),
|
|
2426
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2427
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2428
|
+
children: formatDuration(stat.selfTimeTotal)
|
|
2429
|
+
}),
|
|
2430
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2431
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2432
|
+
children: formatDuration(stat.selfTimeAvg)
|
|
2433
|
+
}),
|
|
2434
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2435
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2436
|
+
children: formatDuration(stat.selfTimeMin)
|
|
2437
|
+
}),
|
|
2438
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2439
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2440
|
+
children: formatDuration(stat.selfTimeMax)
|
|
2441
|
+
})
|
|
2442
|
+
]
|
|
2443
|
+
}, stat.key)) })]
|
|
2444
|
+
})
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
//#endregion
|
|
2448
|
+
//#region src/components/observability/TraceTimeline/FlamegraphView.tsx
|
|
2449
|
+
const ROW_HEIGHT = 24;
|
|
2450
|
+
const MIN_WIDTH = 1;
|
|
2451
|
+
const LABEL_MIN_WIDTH = 40;
|
|
2452
|
+
function findSpanById(rootSpans, spanId) {
|
|
2453
|
+
for (const root of rootSpans) {
|
|
2454
|
+
if (root.spanId === spanId) return root;
|
|
2455
|
+
const found = findSpanById(root.children, spanId);
|
|
2456
|
+
if (found) return found;
|
|
2457
|
+
}
|
|
2458
|
+
return null;
|
|
2459
|
+
}
|
|
2460
|
+
function getAncestorPath(rootSpans, targetId) {
|
|
2461
|
+
const path = [];
|
|
2462
|
+
function walk(span, ancestors) {
|
|
2463
|
+
if (span.spanId === targetId) {
|
|
2464
|
+
path.push(...ancestors, span);
|
|
2465
|
+
return true;
|
|
2466
|
+
}
|
|
2467
|
+
for (const child of span.children) if (walk(child, [...ancestors, span])) return true;
|
|
2468
|
+
return false;
|
|
2469
|
+
}
|
|
2470
|
+
for (const root of rootSpans) if (walk(root, [])) break;
|
|
2471
|
+
return path;
|
|
2472
|
+
}
|
|
2473
|
+
function FlamegraphView({ trace, onSpanClick, selectedSpanId }) {
|
|
2474
|
+
const [zoomSpanId, setZoomSpanId] = (0, react.useState)(null);
|
|
2475
|
+
const [tooltip, setTooltip] = (0, react.useState)(null);
|
|
2476
|
+
const zoomRoot = (0, react.useMemo)(() => {
|
|
2477
|
+
if (!zoomSpanId) return null;
|
|
2478
|
+
return findSpanById(trace.rootSpans, zoomSpanId);
|
|
2479
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2480
|
+
const breadcrumbs = (0, react.useMemo)(() => {
|
|
2481
|
+
if (!zoomSpanId) return [];
|
|
2482
|
+
return getAncestorPath(trace.rootSpans, zoomSpanId);
|
|
2483
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2484
|
+
const viewRoots = zoomRoot ? [zoomRoot] : trace.rootSpans;
|
|
2485
|
+
const viewMinTime = zoomRoot ? zoomRoot.startTimeUnixMs : trace.minTimeMs;
|
|
2486
|
+
const viewDuration = (zoomRoot ? zoomRoot.endTimeUnixMs : trace.maxTimeMs) - viewMinTime;
|
|
2487
|
+
const flatSpans = (0, react.useMemo)(() => flattenAllSpans(viewRoots).map((fs) => ({
|
|
2488
|
+
span: fs.span,
|
|
2489
|
+
depth: fs.level
|
|
2490
|
+
})), [viewRoots]);
|
|
2491
|
+
const maxDepth = (0, react.useMemo)(() => flatSpans.reduce((max, fs) => Math.max(max, fs.depth), 0) + 1, [flatSpans]);
|
|
2492
|
+
const svgWidth = 1200;
|
|
2493
|
+
const svgHeight = maxDepth * ROW_HEIGHT;
|
|
2494
|
+
const handleClick = (0, react.useCallback)((span) => {
|
|
2495
|
+
onSpanClick?.(span);
|
|
2496
|
+
setZoomSpanId(span.spanId);
|
|
2497
|
+
}, [onSpanClick]);
|
|
2498
|
+
const handleZoomOut = (0, react.useCallback)((spanId) => {
|
|
2499
|
+
setZoomSpanId(spanId);
|
|
2500
|
+
}, []);
|
|
2501
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2502
|
+
className: "flex-1 overflow-auto p-2",
|
|
2503
|
+
children: [
|
|
2504
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2505
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground mb-2 flex-wrap",
|
|
2506
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2507
|
+
className: "hover:text-foreground underline",
|
|
2508
|
+
onClick: () => handleZoomOut(null),
|
|
2509
|
+
children: "root"
|
|
2510
|
+
}), breadcrumbs.map((bc, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2511
|
+
className: "flex items-center gap-1",
|
|
2512
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2513
|
+
className: "text-muted-foreground/50",
|
|
2514
|
+
children: ">"
|
|
2515
|
+
}), i < breadcrumbs.length - 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2516
|
+
className: "hover:text-foreground underline",
|
|
2517
|
+
onClick: () => handleZoomOut(bc.spanId),
|
|
2518
|
+
children: [
|
|
2519
|
+
bc.serviceName,
|
|
2520
|
+
": ",
|
|
2521
|
+
bc.name
|
|
2522
|
+
]
|
|
2523
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2524
|
+
className: "text-foreground",
|
|
2525
|
+
children: [
|
|
2526
|
+
bc.serviceName,
|
|
2527
|
+
": ",
|
|
2528
|
+
bc.name
|
|
2529
|
+
]
|
|
2530
|
+
})]
|
|
2531
|
+
}, bc.spanId))]
|
|
2532
|
+
}),
|
|
2533
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2534
|
+
className: "overflow-x-auto",
|
|
2535
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2536
|
+
width: svgWidth,
|
|
2537
|
+
height: svgHeight,
|
|
2538
|
+
className: "block",
|
|
2539
|
+
onMouseLeave: () => setTooltip(null),
|
|
2540
|
+
children: flatSpans.map(({ span, depth }) => {
|
|
2541
|
+
const x = viewDuration > 0 ? (span.startTimeUnixMs - viewMinTime) / viewDuration * svgWidth : 0;
|
|
2542
|
+
const w = viewDuration > 0 ? Math.max(MIN_WIDTH, span.durationMs / viewDuration * svgWidth) : svgWidth;
|
|
2543
|
+
const y = depth * ROW_HEIGHT;
|
|
2544
|
+
const color = getServiceColor(span.serviceName);
|
|
2545
|
+
const isSelected = span.spanId === selectedSpanId;
|
|
2546
|
+
const showLabel = w >= LABEL_MIN_WIDTH;
|
|
2547
|
+
const label = `${span.serviceName}: ${span.name}`;
|
|
2548
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
|
|
2549
|
+
className: "cursor-pointer",
|
|
2550
|
+
onClick: () => handleClick(span),
|
|
2551
|
+
onMouseEnter: (e) => setTooltip({
|
|
2552
|
+
span,
|
|
2553
|
+
x: e.clientX,
|
|
2554
|
+
y: e.clientY
|
|
2555
|
+
}),
|
|
2556
|
+
onMouseMove: (e) => setTooltip((prev) => prev ? {
|
|
2557
|
+
...prev,
|
|
2558
|
+
x: e.clientX,
|
|
2559
|
+
y: e.clientY
|
|
2560
|
+
} : null),
|
|
2561
|
+
onMouseLeave: () => setTooltip(null),
|
|
2562
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2563
|
+
x,
|
|
2564
|
+
y,
|
|
2565
|
+
width: w,
|
|
2566
|
+
height: ROW_HEIGHT - 1,
|
|
2567
|
+
fill: color,
|
|
2568
|
+
opacity: .85,
|
|
2569
|
+
rx: 2,
|
|
2570
|
+
stroke: isSelected ? "#ffffff" : "transparent",
|
|
2571
|
+
strokeWidth: isSelected ? 2 : 0,
|
|
2572
|
+
className: "hover:opacity-100"
|
|
2573
|
+
}), showLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
2574
|
+
x: x + 4,
|
|
2575
|
+
y: y + ROW_HEIGHT / 2 + 1,
|
|
2576
|
+
dominantBaseline: "middle",
|
|
2577
|
+
fill: "#ffffff",
|
|
2578
|
+
fontSize: 11,
|
|
2579
|
+
fontFamily: "monospace",
|
|
2580
|
+
clipPath: `inset(0 0 0 0)`,
|
|
2581
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tspan", { children: label.length > w / 7 ? label.slice(0, Math.floor(w / 7) - 1) + "…" : label })
|
|
2582
|
+
})]
|
|
2583
|
+
}, span.spanId);
|
|
2584
|
+
})
|
|
2585
|
+
})
|
|
2586
|
+
}),
|
|
2587
|
+
tooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2588
|
+
className: "fixed z-50 pointer-events-none bg-popover border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
2589
|
+
style: {
|
|
2590
|
+
left: tooltip.x + 12,
|
|
2591
|
+
top: tooltip.y + 12
|
|
2592
|
+
},
|
|
2593
|
+
children: [
|
|
2594
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2595
|
+
className: "font-medium text-foreground",
|
|
2596
|
+
children: tooltip.span.name
|
|
2597
|
+
}),
|
|
2598
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2599
|
+
className: "text-muted-foreground",
|
|
2600
|
+
children: tooltip.span.serviceName
|
|
2601
|
+
}),
|
|
2602
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2603
|
+
className: "text-foreground mt-1",
|
|
2604
|
+
children: formatDuration(tooltip.span.durationMs)
|
|
2605
|
+
})
|
|
2606
|
+
]
|
|
2607
|
+
})
|
|
2608
|
+
]
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
//#endregion
|
|
2612
|
+
//#region src/components/observability/TraceTimeline/Minimap.tsx
|
|
2613
|
+
/**
|
|
2614
|
+
* Minimap - Compressed overview of all spans with a draggable viewport.
|
|
2615
|
+
*/
|
|
2616
|
+
const MINIMAP_HEIGHT = 40;
|
|
2617
|
+
const SPAN_HEIGHT = 2;
|
|
2618
|
+
const SPAN_GAP = 1;
|
|
2619
|
+
const MIN_VIEWPORT_WIDTH = .02;
|
|
2620
|
+
const HANDLE_WIDTH = 6;
|
|
2621
|
+
function Minimap({ trace, viewStart, viewEnd, onViewChange }) {
|
|
2622
|
+
const containerRef = (0, react.useRef)(null);
|
|
2623
|
+
const dragRef = (0, react.useRef)(null);
|
|
2624
|
+
const cleanupRef = (0, react.useRef)(null);
|
|
2625
|
+
(0, react.useEffect)(() => {
|
|
2626
|
+
return () => {
|
|
2627
|
+
cleanupRef.current?.();
|
|
2628
|
+
};
|
|
1760
2629
|
}, []);
|
|
1761
|
-
const
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
2630
|
+
const allSpans = (0, react.useMemo)(() => flattenAllSpans(trace.rootSpans), [trace.rootSpans]);
|
|
2631
|
+
const traceDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
2632
|
+
const getFraction = (0, react.useCallback)((clientX) => {
|
|
2633
|
+
const el = containerRef.current;
|
|
2634
|
+
if (!el) return 0;
|
|
2635
|
+
const rect = el.getBoundingClientRect();
|
|
2636
|
+
if (!rect.width) return 0;
|
|
2637
|
+
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
1767
2638
|
}, []);
|
|
1768
|
-
(0, react.
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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 === "S") {
|
|
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
|
-
}
|
|
2639
|
+
const clampView = (0, react.useCallback)((start, end) => {
|
|
2640
|
+
let s = Math.max(0, Math.min(1 - MIN_VIEWPORT_WIDTH, start));
|
|
2641
|
+
let e = Math.max(s + MIN_VIEWPORT_WIDTH, Math.min(1, end));
|
|
2642
|
+
if (e > 1) {
|
|
2643
|
+
e = 1;
|
|
2644
|
+
s = Math.max(0, e - Math.max(MIN_VIEWPORT_WIDTH, end - start));
|
|
1797
2645
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
2646
|
+
return [s, e];
|
|
2647
|
+
}, []);
|
|
2648
|
+
const handleMouseDown = (0, react.useCallback)((e, mode) => {
|
|
2649
|
+
e.preventDefault();
|
|
2650
|
+
e.stopPropagation();
|
|
2651
|
+
cleanupRef.current?.();
|
|
2652
|
+
dragRef.current = {
|
|
2653
|
+
mode,
|
|
2654
|
+
startX: e.clientX,
|
|
2655
|
+
origViewStart: viewStart,
|
|
2656
|
+
origViewEnd: viewEnd
|
|
2657
|
+
};
|
|
2658
|
+
const handleMouseMove = (ev) => {
|
|
2659
|
+
const drag = dragRef.current;
|
|
2660
|
+
if (!drag || !containerRef.current) return;
|
|
2661
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
2662
|
+
if (!rect.width) return;
|
|
2663
|
+
const deltaFrac = (ev.clientX - drag.startX) / rect.width;
|
|
2664
|
+
let newStart;
|
|
2665
|
+
let newEnd;
|
|
2666
|
+
if (drag.mode === "pan") {
|
|
2667
|
+
const width = drag.origViewEnd - drag.origViewStart;
|
|
2668
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2669
|
+
newEnd = newStart + width;
|
|
2670
|
+
if (newStart < 0) {
|
|
2671
|
+
newStart = 0;
|
|
2672
|
+
newEnd = width;
|
|
2673
|
+
}
|
|
2674
|
+
if (newEnd > 1) {
|
|
2675
|
+
newEnd = 1;
|
|
2676
|
+
newStart = 1 - width;
|
|
2677
|
+
}
|
|
2678
|
+
} else if (drag.mode === "resize-left") {
|
|
2679
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2680
|
+
newEnd = drag.origViewEnd;
|
|
2681
|
+
} else {
|
|
2682
|
+
newStart = drag.origViewStart;
|
|
2683
|
+
newEnd = drag.origViewEnd + deltaFrac;
|
|
2684
|
+
}
|
|
2685
|
+
const [s, e] = clampView(newStart, newEnd);
|
|
2686
|
+
onViewChange(s, e);
|
|
2687
|
+
};
|
|
2688
|
+
const handleMouseUp = () => {
|
|
2689
|
+
dragRef.current = null;
|
|
2690
|
+
cleanupRef.current = null;
|
|
2691
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
2692
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
2693
|
+
};
|
|
2694
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
2695
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
2696
|
+
cleanupRef.current = handleMouseUp;
|
|
1800
2697
|
}, [
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
2698
|
+
viewStart,
|
|
2699
|
+
viewEnd,
|
|
2700
|
+
onViewChange,
|
|
2701
|
+
clampView
|
|
1805
2702
|
]);
|
|
1806
|
-
const
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
2703
|
+
const handleBackgroundClick = (0, react.useCallback)((e) => {
|
|
2704
|
+
if (dragRef.current) return;
|
|
2705
|
+
if (e.target !== e.currentTarget) return;
|
|
2706
|
+
const frac = getFraction(e.clientX);
|
|
2707
|
+
const half = (viewEnd - viewStart) / 2;
|
|
2708
|
+
const [s, eVal] = clampView(frac - half, frac + half);
|
|
2709
|
+
onViewChange(s, eVal);
|
|
2710
|
+
}, [
|
|
2711
|
+
viewStart,
|
|
2712
|
+
viewEnd,
|
|
2713
|
+
onViewChange,
|
|
2714
|
+
getFraction,
|
|
2715
|
+
clampView
|
|
2716
|
+
]);
|
|
2717
|
+
const handleKeyDown = (0, react.useCallback)((e) => {
|
|
2718
|
+
const step = .05;
|
|
2719
|
+
const width = viewEnd - viewStart;
|
|
2720
|
+
let newStart;
|
|
2721
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowUp") newStart = viewStart - step;
|
|
2722
|
+
else if (e.key === "ArrowRight" || e.key === "ArrowDown") newStart = viewStart + step;
|
|
2723
|
+
else return;
|
|
2724
|
+
e.preventDefault();
|
|
2725
|
+
const [s, eVal] = clampView(newStart, newStart + width);
|
|
2726
|
+
onViewChange(s, eVal);
|
|
2727
|
+
}, [
|
|
2728
|
+
viewStart,
|
|
2729
|
+
viewEnd,
|
|
2730
|
+
onViewChange,
|
|
2731
|
+
clampView
|
|
2732
|
+
]);
|
|
2733
|
+
const viewStartPct = viewStart * 100;
|
|
2734
|
+
const viewEndPct = viewEnd * 100;
|
|
2735
|
+
const viewWidthPct = viewEndPct - viewStartPct;
|
|
2736
|
+
const totalRows = allSpans.length;
|
|
2737
|
+
const availableHeight = MINIMAP_HEIGHT - 4;
|
|
2738
|
+
const rowHeight = totalRows > 0 ? Math.min(SPAN_HEIGHT + SPAN_GAP, availableHeight / totalRows) : SPAN_HEIGHT;
|
|
2739
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2740
|
+
ref: containerRef,
|
|
2741
|
+
className: "relative w-full border-b border-border bg-muted/30 select-none",
|
|
2742
|
+
style: { height: MINIMAP_HEIGHT },
|
|
2743
|
+
onClick: handleBackgroundClick,
|
|
2744
|
+
onKeyDown: handleKeyDown,
|
|
2745
|
+
role: "slider",
|
|
2746
|
+
tabIndex: 0,
|
|
2747
|
+
"aria-label": "Trace minimap viewport",
|
|
2748
|
+
"aria-valuemin": 0,
|
|
2749
|
+
"aria-valuemax": 100,
|
|
2750
|
+
"aria-valuenow": Math.round(viewStartPct),
|
|
2751
|
+
children: [
|
|
2752
|
+
traceDuration > 0 && allSpans.map(({ span }, i) => {
|
|
2753
|
+
const left = (span.startTimeUnixMs - trace.minTimeMs) / traceDuration * 100;
|
|
2754
|
+
const width = Math.max(.2, span.durationMs / traceDuration * 100);
|
|
2755
|
+
const color = getSpanBarColor(span.serviceName, span.status === "ERROR");
|
|
2756
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2757
|
+
className: "absolute pointer-events-none",
|
|
2758
|
+
style: {
|
|
2759
|
+
left: `${left}%`,
|
|
2760
|
+
width: `${width}%`,
|
|
2761
|
+
top: 2 + i * rowHeight,
|
|
2762
|
+
height: Math.max(1, rowHeight - SPAN_GAP),
|
|
2763
|
+
backgroundColor: color,
|
|
2764
|
+
opacity: .8,
|
|
2765
|
+
borderRadius: 1
|
|
2766
|
+
}
|
|
2767
|
+
}, span.spanId);
|
|
2768
|
+
}),
|
|
2769
|
+
viewStartPct > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2770
|
+
className: "absolute top-0 left-0 h-full bg-black/30 pointer-events-none",
|
|
2771
|
+
style: { width: `${viewStartPct}%` }
|
|
2772
|
+
}),
|
|
2773
|
+
viewEndPct < 100 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2774
|
+
className: "absolute top-0 h-full bg-black/30 pointer-events-none",
|
|
2775
|
+
style: {
|
|
2776
|
+
left: `${viewEndPct}%`,
|
|
2777
|
+
right: 0
|
|
2778
|
+
}
|
|
2779
|
+
}),
|
|
2780
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2781
|
+
className: "absolute top-0 h-full border border-blue-500/50 bg-blue-500/10 cursor-grab active:cursor-grabbing",
|
|
2782
|
+
style: {
|
|
2783
|
+
left: `${viewStartPct}%`,
|
|
2784
|
+
width: `${viewWidthPct}%`
|
|
2785
|
+
},
|
|
2786
|
+
onMouseDown: (e) => handleMouseDown(e, "pan"),
|
|
2787
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2788
|
+
className: "absolute top-0 left-0 h-full cursor-ew-resize z-10",
|
|
2789
|
+
style: {
|
|
2790
|
+
width: HANDLE_WIDTH,
|
|
2791
|
+
marginLeft: -HANDLE_WIDTH / 2
|
|
2792
|
+
},
|
|
2793
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-left")
|
|
2794
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2795
|
+
className: "absolute top-0 right-0 h-full cursor-ew-resize z-10",
|
|
2796
|
+
style: {
|
|
2797
|
+
width: HANDLE_WIDTH,
|
|
2798
|
+
marginRight: -HANDLE_WIDTH / 2
|
|
2799
|
+
},
|
|
2800
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-right")
|
|
2801
|
+
})]
|
|
2802
|
+
})
|
|
2803
|
+
]
|
|
1820
2804
|
});
|
|
1821
2805
|
}
|
|
1822
|
-
|
|
1823
|
-
//#endregion
|
|
1824
|
-
//#region src/components/observability/TraceTimeline/shortcuts.ts
|
|
1825
|
-
const TRACE_VIEWER_SHORTCUTS = {
|
|
1826
|
-
name: "Trace Viewer",
|
|
1827
|
-
shortcuts: [
|
|
1828
|
-
{
|
|
1829
|
-
keys: ["↑/K"],
|
|
1830
|
-
description: "Previous span"
|
|
1831
|
-
},
|
|
1832
|
-
{
|
|
1833
|
-
keys: ["↓/J"],
|
|
1834
|
-
description: "Next span"
|
|
1835
|
-
},
|
|
1836
|
-
{
|
|
1837
|
-
keys: ["←"],
|
|
1838
|
-
description: "Collapse span"
|
|
1839
|
-
},
|
|
1840
|
-
{
|
|
1841
|
-
keys: ["→"],
|
|
1842
|
-
description: "Expand span"
|
|
1843
|
-
},
|
|
1844
|
-
{
|
|
1845
|
-
keys: ["Enter"],
|
|
1846
|
-
description: "Focus detail pane"
|
|
1847
|
-
},
|
|
1848
|
-
{
|
|
1849
|
-
keys: ["C"],
|
|
1850
|
-
description: "Copy span name"
|
|
1851
|
-
},
|
|
1852
|
-
{
|
|
1853
|
-
keys: ["Esc"],
|
|
1854
|
-
description: "Deselect span"
|
|
1855
|
-
},
|
|
1856
|
-
{
|
|
1857
|
-
keys: [
|
|
1858
|
-
"Ctrl",
|
|
1859
|
-
"Shift",
|
|
1860
|
-
"E"
|
|
1861
|
-
],
|
|
1862
|
-
description: "Expand all"
|
|
1863
|
-
},
|
|
1864
|
-
{
|
|
1865
|
-
keys: [
|
|
1866
|
-
"Ctrl",
|
|
1867
|
-
"Shift",
|
|
1868
|
-
"C"
|
|
1869
|
-
],
|
|
1870
|
-
description: "Collapse all"
|
|
1871
|
-
}
|
|
1872
|
-
]
|
|
1873
|
-
};
|
|
1874
|
-
|
|
1875
2806
|
//#endregion
|
|
1876
2807
|
//#region src/components/observability/TraceTimeline/index.tsx
|
|
1877
2808
|
/**
|
|
@@ -1960,25 +2891,43 @@ function isSpanAncestorOf(potentialAncestor, descendantId, flattenedSpans) {
|
|
|
1960
2891
|
}
|
|
1961
2892
|
return false;
|
|
1962
2893
|
}
|
|
1963
|
-
function
|
|
2894
|
+
function collectServices(rootSpans) {
|
|
2895
|
+
const set = /* @__PURE__ */ new Set();
|
|
2896
|
+
function walk(span) {
|
|
2897
|
+
set.add(span.serviceName);
|
|
2898
|
+
span.children.forEach(walk);
|
|
2899
|
+
}
|
|
2900
|
+
rootSpans.forEach(walk);
|
|
2901
|
+
return Array.from(set).sort();
|
|
2902
|
+
}
|
|
2903
|
+
function TraceTimeline({ rows, onSpanClick, onSpanDeselect, selectedSpanId: externalSelectedSpanId, isLoading, error, view: externalView, onViewChange, uiFind: externalUiFind, onUiFindChange, viewStart: externalViewStart, viewEnd: externalViewEnd, onViewRangeChange }) {
|
|
1964
2904
|
useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
|
|
1965
2905
|
const [collapsedIds, setCollapsedIds] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
1966
2906
|
const [internalSelectedSpanId, setInternalSelectedSpanId] = (0, react.useState)(null);
|
|
1967
2907
|
const [hoveredSpanId, setHoveredSpanId] = (0, react.useState)(null);
|
|
2908
|
+
const [internalView, setInternalView] = (0, react.useState)("timeline");
|
|
2909
|
+
const [internalUiFind, setInternalUiFind] = (0, react.useState)("");
|
|
2910
|
+
const [currentMatchIndex, setCurrentMatchIndex] = (0, react.useState)(0);
|
|
2911
|
+
const [headerCollapsed, setHeaderCollapsed] = (0, react.useState)(false);
|
|
2912
|
+
const [internalViewStart, setInternalViewStart] = (0, react.useState)(0);
|
|
2913
|
+
const [internalViewEnd, setInternalViewEnd] = (0, react.useState)(1);
|
|
1968
2914
|
const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
|
|
2915
|
+
const viewStart = externalViewStart ?? internalViewStart;
|
|
2916
|
+
const viewEnd = externalViewEnd ?? internalViewEnd;
|
|
2917
|
+
const activeView = externalView ?? internalView;
|
|
2918
|
+
const uiFind = externalUiFind ?? internalUiFind;
|
|
1969
2919
|
const scrollRef = (0, react.useRef)(null);
|
|
1970
2920
|
const announcementRef = (0, react.useRef)(null);
|
|
1971
2921
|
const parsedTrace = (0, react.useMemo)(() => buildTrace(rows), [rows]);
|
|
2922
|
+
const services = (0, react.useMemo)(() => parsedTrace ? collectServices(parsedTrace.rootSpans) : [], [parsedTrace]);
|
|
1972
2923
|
const flattenedSpans = (0, react.useMemo)(() => {
|
|
1973
2924
|
if (!parsedTrace) return [];
|
|
1974
2925
|
return flattenTree(parsedTrace.rootSpans, collapsedIds);
|
|
1975
2926
|
}, [parsedTrace, collapsedIds]);
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
overscan: 5
|
|
1981
|
-
});
|
|
2927
|
+
const matchingIndices = (0, react.useMemo)(() => {
|
|
2928
|
+
if (!uiFind) return [];
|
|
2929
|
+
return flattenedSpans.map((item, idx) => spanMatchesSearch(item.span, uiFind) ? idx : -1).filter((idx) => idx !== -1);
|
|
2930
|
+
}, [flattenedSpans, uiFind]);
|
|
1982
2931
|
const handleToggleCollapse = (spanId) => {
|
|
1983
2932
|
setCollapsedIds((prev) => {
|
|
1984
2933
|
const next = new Set(prev);
|
|
@@ -1987,11 +2936,22 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
1987
2936
|
return next;
|
|
1988
2937
|
});
|
|
1989
2938
|
};
|
|
2939
|
+
const handleDeselect = (0, react.useCallback)(() => {
|
|
2940
|
+
setInternalSelectedSpanId(null);
|
|
2941
|
+
onSpanDeselect?.();
|
|
2942
|
+
}, [onSpanDeselect]);
|
|
1990
2943
|
const handleSpanClick = (0, react.useCallback)((span) => {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2944
|
+
if (selectedSpanId === span.spanId) handleDeselect();
|
|
2945
|
+
else {
|
|
2946
|
+
setInternalSelectedSpanId(span.spanId);
|
|
2947
|
+
onSpanClick?.(span);
|
|
2948
|
+
if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
|
|
2949
|
+
}
|
|
2950
|
+
}, [
|
|
2951
|
+
onSpanClick,
|
|
2952
|
+
selectedSpanId,
|
|
2953
|
+
handleDeselect
|
|
2954
|
+
]);
|
|
1995
2955
|
const handleExpandAll = (0, react.useCallback)(() => {
|
|
1996
2956
|
setCollapsedIds(/* @__PURE__ */ new Set());
|
|
1997
2957
|
}, []);
|
|
@@ -2040,23 +3000,77 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2040
3000
|
return next;
|
|
2041
3001
|
});
|
|
2042
3002
|
}, [selectedSpanId, flattenedSpans]);
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
3003
|
+
const handleViewChange = (0, react.useCallback)((view) => {
|
|
3004
|
+
if (onViewChange) onViewChange(view);
|
|
3005
|
+
else setInternalView(view);
|
|
3006
|
+
}, [onViewChange]);
|
|
3007
|
+
const handleUiFindChange = (0, react.useCallback)((value) => {
|
|
3008
|
+
if (onUiFindChange) onUiFindChange(value);
|
|
3009
|
+
else setInternalUiFind(value);
|
|
3010
|
+
setCurrentMatchIndex(0);
|
|
3011
|
+
}, [onUiFindChange]);
|
|
3012
|
+
const handleViewRangeChange = (0, react.useCallback)((start, end) => {
|
|
3013
|
+
if (onViewRangeChange) onViewRangeChange(start, end);
|
|
3014
|
+
else {
|
|
3015
|
+
setInternalViewStart(start);
|
|
3016
|
+
setInternalViewEnd(end);
|
|
3017
|
+
}
|
|
3018
|
+
}, [onViewRangeChange]);
|
|
3019
|
+
const scrollToSpan = (0, react.useCallback)((spanId) => {
|
|
3020
|
+
(scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`))?.scrollIntoView({
|
|
3021
|
+
block: "center",
|
|
2051
3022
|
behavior: "smooth"
|
|
2052
3023
|
});
|
|
3024
|
+
}, []);
|
|
3025
|
+
const handleSearchNext = (0, react.useCallback)(() => {
|
|
3026
|
+
if (matchingIndices.length === 0) return;
|
|
3027
|
+
const next = (currentMatchIndex + 1) % matchingIndices.length;
|
|
3028
|
+
setCurrentMatchIndex(next);
|
|
3029
|
+
const idx = matchingIndices[next];
|
|
3030
|
+
if (idx !== void 0) {
|
|
3031
|
+
const item = flattenedSpans[idx];
|
|
3032
|
+
if (item) {
|
|
3033
|
+
handleSpanClick(item.span);
|
|
3034
|
+
scrollToSpan(item.span.spanId);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
2053
3037
|
}, [
|
|
2054
|
-
|
|
3038
|
+
matchingIndices,
|
|
3039
|
+
currentMatchIndex,
|
|
2055
3040
|
flattenedSpans,
|
|
2056
|
-
|
|
3041
|
+
handleSpanClick,
|
|
3042
|
+
scrollToSpan
|
|
3043
|
+
]);
|
|
3044
|
+
const handleSearchPrev = (0, react.useCallback)(() => {
|
|
3045
|
+
if (matchingIndices.length === 0) return;
|
|
3046
|
+
const prev = (currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
|
|
3047
|
+
setCurrentMatchIndex(prev);
|
|
3048
|
+
const idx = matchingIndices[prev];
|
|
3049
|
+
if (idx !== void 0) {
|
|
3050
|
+
const item = flattenedSpans[idx];
|
|
3051
|
+
if (item) {
|
|
3052
|
+
handleSpanClick(item.span);
|
|
3053
|
+
scrollToSpan(item.span.spanId);
|
|
3054
|
+
}
|
|
3055
|
+
}
|
|
3056
|
+
}, [
|
|
3057
|
+
matchingIndices,
|
|
3058
|
+
currentMatchIndex,
|
|
3059
|
+
flattenedSpans,
|
|
3060
|
+
handleSpanClick,
|
|
3061
|
+
scrollToSpan
|
|
2057
3062
|
]);
|
|
3063
|
+
(0, react.useEffect)(() => {
|
|
3064
|
+
if (!selectedSpanId) return;
|
|
3065
|
+
scrollToSpan(selectedSpanId);
|
|
3066
|
+
}, [selectedSpanId, scrollToSpan]);
|
|
2058
3067
|
(0, react.useEffect)(() => {
|
|
2059
3068
|
const handleKeyDown = (e) => {
|
|
3069
|
+
if (e.key === "Escape" && selectedSpanId) {
|
|
3070
|
+
e.preventDefault();
|
|
3071
|
+
handleDeselect();
|
|
3072
|
+
return;
|
|
3073
|
+
}
|
|
2060
3074
|
if (!(scrollRef.current?.parentElement)?.contains(document.activeElement)) return;
|
|
2061
3075
|
switch (e.key) {
|
|
2062
3076
|
case "ArrowUp":
|
|
@@ -2079,10 +3093,7 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2079
3093
|
e.preventDefault();
|
|
2080
3094
|
handleCollapseExpand(false);
|
|
2081
3095
|
break;
|
|
2082
|
-
case "Escape":
|
|
2083
|
-
e.preventDefault();
|
|
2084
|
-
handleDeselect();
|
|
2085
|
-
break;
|
|
3096
|
+
case "Escape": break;
|
|
2086
3097
|
case "Enter":
|
|
2087
3098
|
if (selectedSpanId) {
|
|
2088
3099
|
e.preventDefault();
|
|
@@ -2150,10 +3161,9 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2150
3161
|
})
|
|
2151
3162
|
});
|
|
2152
3163
|
const totalDurationMs = parsedTrace.maxTimeMs - parsedTrace.minTimeMs;
|
|
2153
|
-
|
|
2154
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3164
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2155
3165
|
className: "flex h-full bg-background",
|
|
2156
|
-
children:
|
|
3166
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2157
3167
|
className: "flex flex-col flex-1 min-w-0",
|
|
2158
3168
|
children: [
|
|
2159
3169
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -2163,39 +3173,58 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2163
3173
|
"aria-live": "polite",
|
|
2164
3174
|
"aria-atomic": "true"
|
|
2165
3175
|
}),
|
|
2166
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceHeader, {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
3176
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceHeader, {
|
|
3177
|
+
trace: parsedTrace,
|
|
3178
|
+
services,
|
|
3179
|
+
onHeaderToggle: () => setHeaderCollapsed((p) => !p),
|
|
3180
|
+
isCollapsed: headerCollapsed
|
|
3181
|
+
}),
|
|
3182
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewTabs, {
|
|
3183
|
+
activeView,
|
|
3184
|
+
onChange: handleViewChange
|
|
3185
|
+
}),
|
|
3186
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanSearch, {
|
|
3187
|
+
value: uiFind,
|
|
3188
|
+
onChange: handleUiFindChange,
|
|
3189
|
+
matchCount: matchingIndices.length,
|
|
3190
|
+
currentMatch: currentMatchIndex,
|
|
3191
|
+
onPrev: handleSearchPrev,
|
|
3192
|
+
onNext: handleSearchNext
|
|
3193
|
+
}),
|
|
3194
|
+
activeView === "graph" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(GraphView, { trace: parsedTrace }) : activeView === "statistics" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StatisticsView, { trace: parsedTrace }) : activeView === "flamegraph" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FlamegraphView, {
|
|
3195
|
+
trace: parsedTrace,
|
|
3196
|
+
onSpanClick: handleSpanClick,
|
|
3197
|
+
selectedSpanId: selectedSpanId ?? void 0
|
|
3198
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3199
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minimap, {
|
|
3200
|
+
trace: parsedTrace,
|
|
3201
|
+
viewStart,
|
|
3202
|
+
viewEnd,
|
|
3203
|
+
onViewChange: handleViewRangeChange
|
|
3204
|
+
}),
|
|
3205
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeRuler, {
|
|
3206
|
+
totalDurationMs: totalDurationMs * (viewEnd - viewStart),
|
|
3207
|
+
leftColumnWidth: "24rem",
|
|
3208
|
+
offsetMs: totalDurationMs * viewStart
|
|
3209
|
+
}),
|
|
3210
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3211
|
+
ref: scrollRef,
|
|
3212
|
+
className: "flex-1 overflow-auto outline-none",
|
|
3213
|
+
role: "tree",
|
|
3214
|
+
"aria-label": "Trace timeline",
|
|
3215
|
+
tabIndex: 0,
|
|
3216
|
+
children: flattenedSpans.map((item) => {
|
|
2182
3217
|
const { span, level } = item;
|
|
2183
3218
|
const isCollapsed = collapsedIds.has(span.spanId);
|
|
2184
3219
|
const isSelected = span.spanId === selectedSpanId;
|
|
2185
3220
|
const isHovered = span.spanId === hoveredSpanId;
|
|
2186
3221
|
const isParentOfHovered = hoveredSpanId ? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans) : false;
|
|
2187
|
-
const
|
|
2188
|
-
const
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
left: 0,
|
|
2194
|
-
width: "100%",
|
|
2195
|
-
height: `${virtualItem.size}px`,
|
|
2196
|
-
transform: `translateY(${virtualItem.start}px)`
|
|
2197
|
-
},
|
|
2198
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanRow, {
|
|
3222
|
+
const viewRange = viewEnd - viewStart;
|
|
3223
|
+
const relativeStart = (calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs) - viewStart) / viewRange;
|
|
3224
|
+
const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs) / viewRange;
|
|
3225
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3226
|
+
"data-span-id": span.spanId,
|
|
3227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanRow, {
|
|
2199
3228
|
span,
|
|
2200
3229
|
level,
|
|
2201
3230
|
isCollapsed,
|
|
@@ -2207,34 +3236,30 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2207
3236
|
onClick: () => handleSpanClick(span),
|
|
2208
3237
|
onToggleCollapse: () => handleToggleCollapse(span.spanId),
|
|
2209
3238
|
onMouseEnter: () => setHoveredSpanId(span.spanId),
|
|
2210
|
-
onMouseLeave: () => setHoveredSpanId(null)
|
|
2211
|
-
|
|
3239
|
+
onMouseLeave: () => setHoveredSpanId(null),
|
|
3240
|
+
uiFind: uiFind || void 0
|
|
3241
|
+
}), isSelected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanDetailInline, {
|
|
3242
|
+
span,
|
|
3243
|
+
traceStartMs: parsedTrace.minTimeMs
|
|
3244
|
+
})]
|
|
2212
3245
|
}, span.spanId);
|
|
2213
3246
|
})
|
|
2214
3247
|
})
|
|
2215
|
-
})
|
|
3248
|
+
] })
|
|
2216
3249
|
]
|
|
2217
|
-
})
|
|
2218
|
-
className: "w-96 h-full flex-shrink-0",
|
|
2219
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailPane, {
|
|
2220
|
-
span: selectedSpan,
|
|
2221
|
-
onClose: handleDeselect,
|
|
2222
|
-
onLinkClick: void 0
|
|
2223
|
-
})
|
|
2224
|
-
})]
|
|
3250
|
+
})
|
|
2225
3251
|
});
|
|
2226
3252
|
}
|
|
2227
|
-
|
|
2228
3253
|
//#endregion
|
|
2229
3254
|
//#region src/components/observability/TraceDetail/index.tsx
|
|
2230
|
-
function TraceDetail({
|
|
3255
|
+
function TraceDetail({ traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onSpanDeselect, onBack }) {
|
|
2231
3256
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2232
3257
|
className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
|
|
2233
3258
|
children: [
|
|
2234
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3259
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2235
3260
|
onClick: onBack,
|
|
2236
3261
|
className: "hover:text-foreground transition-colors",
|
|
2237
|
-
children:
|
|
3262
|
+
children: "Traces"
|
|
2238
3263
|
}),
|
|
2239
3264
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "/" }),
|
|
2240
3265
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -2247,10 +3272,10 @@ function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId,
|
|
|
2247
3272
|
isLoading,
|
|
2248
3273
|
error,
|
|
2249
3274
|
selectedSpanId,
|
|
2250
|
-
onSpanClick
|
|
3275
|
+
onSpanClick,
|
|
3276
|
+
onSpanDeselect
|
|
2251
3277
|
})] });
|
|
2252
3278
|
}
|
|
2253
|
-
|
|
2254
3279
|
//#endregion
|
|
2255
3280
|
//#region src/components/observability/LogTimeline/LogRow.tsx
|
|
2256
3281
|
function formatTimestamp(timeMs) {
|
|
@@ -2376,7 +3401,6 @@ const LogRow = (0, react.memo)(function LogRow({ log, isSelected, onClick, searc
|
|
|
2376
3401
|
]
|
|
2377
3402
|
});
|
|
2378
3403
|
});
|
|
2379
|
-
|
|
2380
3404
|
//#endregion
|
|
2381
3405
|
//#region src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx
|
|
2382
3406
|
function AttributesTab({ log }) {
|
|
@@ -2409,7 +3433,6 @@ function AttributesTab({ log }) {
|
|
|
2409
3433
|
})
|
|
2410
3434
|
});
|
|
2411
3435
|
}
|
|
2412
|
-
|
|
2413
3436
|
//#endregion
|
|
2414
3437
|
//#region src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx
|
|
2415
3438
|
function JsonTreeView({ data, level = 0 }) {
|
|
@@ -2497,7 +3520,6 @@ function formatPrimitiveValue(value) {
|
|
|
2497
3520
|
if (typeof value === "number") return String(value);
|
|
2498
3521
|
return String(value);
|
|
2499
3522
|
}
|
|
2500
|
-
|
|
2501
3523
|
//#endregion
|
|
2502
3524
|
//#region src/components/observability/LogTimeline/LogDetailPane/index.tsx
|
|
2503
3525
|
function LogDetailPane({ log, onClose, onTraceLinkClick, initialTab = "message", wordWrap = true }) {
|
|
@@ -2709,7 +3731,6 @@ function getSeverityColor(severity) {
|
|
|
2709
3731
|
bg: "bg-gray-50 dark:bg-gray-800/20"
|
|
2710
3732
|
};
|
|
2711
3733
|
}
|
|
2712
|
-
|
|
2713
3734
|
//#endregion
|
|
2714
3735
|
//#region src/components/observability/LogTimeline/shortcuts.ts
|
|
2715
3736
|
const LOG_VIEWER_SHORTCUTS = {
|
|
@@ -2761,7 +3782,6 @@ const LOG_VIEWER_SHORTCUTS = {
|
|
|
2761
3782
|
}
|
|
2762
3783
|
]
|
|
2763
3784
|
};
|
|
2764
|
-
|
|
2765
3785
|
//#endregion
|
|
2766
3786
|
//#region src/components/observability/LogTimeline/index.tsx
|
|
2767
3787
|
/**
|
|
@@ -2967,7 +3987,7 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
2967
3987
|
(0, react.useEffect)(() => {
|
|
2968
3988
|
const handleKeyDown = (e) => {
|
|
2969
3989
|
const isFormField = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement;
|
|
2970
|
-
if (isFormField && e.key === "Escape") {
|
|
3990
|
+
if (isFormField && e.key === "Escape" && e.target instanceof HTMLElement) {
|
|
2971
3991
|
e.target.blur();
|
|
2972
3992
|
return;
|
|
2973
3993
|
}
|
|
@@ -3208,7 +4228,6 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
3208
4228
|
})]
|
|
3209
4229
|
});
|
|
3210
4230
|
}
|
|
3211
|
-
|
|
3212
4231
|
//#endregion
|
|
3213
4232
|
//#region src/components/observability/LogTimeline/LogFilter.tsx
|
|
3214
4233
|
/**
|
|
@@ -3309,7 +4328,7 @@ function MultiSelect({ options, selected, onChange, testId }) {
|
|
|
3309
4328
|
(0, react.useEffect)(() => {
|
|
3310
4329
|
if (!dropOpen) return;
|
|
3311
4330
|
const handler = (e) => {
|
|
3312
|
-
if (ref.current && !ref.current.contains(e.target)) setDropOpen(false);
|
|
4331
|
+
if (ref.current && e.target instanceof Node && !ref.current.contains(e.target)) setDropOpen(false);
|
|
3313
4332
|
};
|
|
3314
4333
|
document.addEventListener("mousedown", handler);
|
|
3315
4334
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -3760,7 +4779,6 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
|
|
|
3760
4779
|
})]
|
|
3761
4780
|
});
|
|
3762
4781
|
}
|
|
3763
|
-
|
|
3764
4782
|
//#endregion
|
|
3765
4783
|
//#region src/components/observability/utils/lttb.ts
|
|
3766
4784
|
function triangleArea(p1, p2, p3) {
|
|
@@ -3826,7 +4844,27 @@ function downsampleLTTB(data, targetPoints) {
|
|
|
3826
4844
|
if (lastPoint) sampled.push(lastPoint);
|
|
3827
4845
|
return sampled;
|
|
3828
4846
|
}
|
|
3829
|
-
|
|
4847
|
+
//#endregion
|
|
4848
|
+
//#region src/components/observability/shared/TooltipEntryList.tsx
|
|
4849
|
+
function TooltipEntryList({ payload, displayLabelMap, formatValue }) {
|
|
4850
|
+
return payload.map((entry, i) => {
|
|
4851
|
+
const dataKey = entry.dataKey;
|
|
4852
|
+
const value = entry.value;
|
|
4853
|
+
if (typeof dataKey !== "string" || typeof value !== "number") return null;
|
|
4854
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4855
|
+
className: "text-sm",
|
|
4856
|
+
style: { color: entry.color },
|
|
4857
|
+
children: [
|
|
4858
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4859
|
+
className: "font-medium",
|
|
4860
|
+
children: [displayLabelMap.get(dataKey) ?? dataKey, ":"]
|
|
4861
|
+
}),
|
|
4862
|
+
" ",
|
|
4863
|
+
formatValue(value)
|
|
4864
|
+
]
|
|
4865
|
+
}, i);
|
|
4866
|
+
});
|
|
4867
|
+
}
|
|
3830
4868
|
//#endregion
|
|
3831
4869
|
//#region src/components/observability/utils/units.ts
|
|
3832
4870
|
const BYTE_SCALES = [
|
|
@@ -4001,7 +5039,6 @@ function formatDisplayValue(value, scale) {
|
|
|
4001
5039
|
function formatOtelValue(value, unit) {
|
|
4002
5040
|
return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
|
|
4003
5041
|
}
|
|
4004
|
-
|
|
4005
5042
|
//#endregion
|
|
4006
5043
|
//#region src/components/observability/MetricTimeSeries/index.tsx
|
|
4007
5044
|
/**
|
|
@@ -4310,18 +5347,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
|
|
|
4310
5347
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4311
5348
|
className: "text-gray-400 text-xs mb-2",
|
|
4312
5349
|
children: formatTime(typeof label === "number" ? label : Number(label))
|
|
4313
|
-
}),
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
className: "font-medium",
|
|
4319
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4320
|
-
}),
|
|
4321
|
-
" ",
|
|
4322
|
-
formatValue(entry.value)
|
|
4323
|
-
]
|
|
4324
|
-
}, i))]
|
|
5350
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipEntryList, {
|
|
5351
|
+
payload,
|
|
5352
|
+
displayLabelMap,
|
|
5353
|
+
formatValue
|
|
5354
|
+
})]
|
|
4325
5355
|
});
|
|
4326
5356
|
}
|
|
4327
5357
|
function MetricLoadingSkeleton({ height = 400 }) {
|
|
@@ -4368,7 +5398,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
|
|
|
4368
5398
|
})
|
|
4369
5399
|
});
|
|
4370
5400
|
}
|
|
4371
|
-
|
|
4372
5401
|
//#endregion
|
|
4373
5402
|
//#region src/components/observability/MetricHistogram/index.tsx
|
|
4374
5403
|
/**
|
|
@@ -4382,6 +5411,9 @@ const COLORS = [
|
|
|
4382
5411
|
"#00C49F",
|
|
4383
5412
|
"#0088FE"
|
|
4384
5413
|
];
|
|
5414
|
+
function isBucketData(v) {
|
|
5415
|
+
return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
|
|
5416
|
+
}
|
|
4385
5417
|
const defaultFormatBucketLabel = (bound, index, bounds) => {
|
|
4386
5418
|
if (index === 0) return `≤${bound}`;
|
|
4387
5419
|
if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
|
|
@@ -4424,7 +5456,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
|
|
|
4424
5456
|
};
|
|
4425
5457
|
buckets.push(bucket);
|
|
4426
5458
|
}
|
|
4427
|
-
|
|
5459
|
+
const prev = bucket[seriesName];
|
|
5460
|
+
bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
|
|
4428
5461
|
}
|
|
4429
5462
|
}
|
|
4430
5463
|
buckets.sort((a, b) => a.lowerBound - b.lowerBound);
|
|
@@ -4561,25 +5594,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
|
|
|
4561
5594
|
}
|
|
4562
5595
|
function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
|
|
4563
5596
|
if (!active || !payload?.length) return null;
|
|
4564
|
-
const
|
|
4565
|
-
if (!
|
|
5597
|
+
const raw = payload[0]?.payload;
|
|
5598
|
+
if (!isBucketData(raw)) return null;
|
|
4566
5599
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4567
5600
|
className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
|
|
4568
5601
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4569
5602
|
className: "text-gray-300 text-sm font-medium mb-2",
|
|
4570
|
-
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(
|
|
4571
|
-
}),
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
className: "font-medium",
|
|
4577
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4578
|
-
}),
|
|
4579
|
-
" ",
|
|
4580
|
-
formatValue(entry.value)
|
|
4581
|
-
]
|
|
4582
|
-
}, i))]
|
|
5603
|
+
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(raw.lowerBound, boundsScale)} – ${raw.upperBound === Infinity ? "∞" : formatDisplayValue(raw.upperBound, boundsScale)}` : raw.bucket]
|
|
5604
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipEntryList, {
|
|
5605
|
+
payload,
|
|
5606
|
+
displayLabelMap,
|
|
5607
|
+
formatValue
|
|
5608
|
+
})]
|
|
4583
5609
|
});
|
|
4584
5610
|
}
|
|
4585
5611
|
function HistogramLoadingSkeleton({ height = 400 }) {
|
|
@@ -4619,7 +5645,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
|
|
|
4619
5645
|
})
|
|
4620
5646
|
});
|
|
4621
5647
|
}
|
|
4622
|
-
|
|
4623
5648
|
//#endregion
|
|
4624
5649
|
//#region src/components/observability/MetricStat/index.tsx
|
|
4625
5650
|
/**
|
|
@@ -4707,8 +5732,11 @@ function buildStatData(rows) {
|
|
|
4707
5732
|
metricName
|
|
4708
5733
|
};
|
|
4709
5734
|
}
|
|
4710
|
-
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 }) {
|
|
4711
|
-
const
|
|
5735
|
+
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 }) {
|
|
5736
|
+
const statData = (0, react.useMemo)(() => buildStatData(rows), [rows]);
|
|
5737
|
+
const latestValue = directValue ?? statData.latestValue;
|
|
5738
|
+
const unit = directUnit ?? statData.unit;
|
|
5739
|
+
const { timestamp, dataPoints, metricName } = statData;
|
|
4712
5740
|
const sparklineData = (0, react.useMemo)(() => {
|
|
4713
5741
|
if (!showSparkline || dataPoints.length === 0) return [];
|
|
4714
5742
|
return dataPoints.slice(-sparklinePoints).map((dp) => ({ value: dp.value }));
|
|
@@ -4812,7 +5840,6 @@ function TrendIndicator({ direction, value }) {
|
|
|
4812
5840
|
children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
|
|
4813
5841
|
});
|
|
4814
5842
|
}
|
|
4815
|
-
|
|
4816
5843
|
//#endregion
|
|
4817
5844
|
//#region src/components/observability/MetricTable/index.tsx
|
|
4818
5845
|
/**
|
|
@@ -4952,7 +5979,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
|
|
|
4952
5979
|
})]
|
|
4953
5980
|
});
|
|
4954
5981
|
}
|
|
4955
|
-
|
|
4956
5982
|
//#endregion
|
|
4957
5983
|
//#region src/lib/renderer.tsx
|
|
4958
5984
|
/**
|
|
@@ -5057,7 +6083,6 @@ function Renderer({ tree, registry, fallback }) {
|
|
|
5057
6083
|
fallback
|
|
5058
6084
|
});
|
|
5059
6085
|
}
|
|
5060
|
-
|
|
5061
6086
|
//#endregion
|
|
5062
6087
|
//#region src/lib/catalog.ts
|
|
5063
6088
|
const dashboardCatalog = createCatalog({
|
|
@@ -5257,8 +6282,7 @@ const dashboardCatalog = createCatalog({
|
|
|
5257
6282
|
}
|
|
5258
6283
|
}
|
|
5259
6284
|
});
|
|
5260
|
-
|
|
5261
|
-
|
|
6285
|
+
Object.keys(dashboardCatalog.components);
|
|
5262
6286
|
//#endregion
|
|
5263
6287
|
//#region src/components/dashboard/Badge/index.tsx
|
|
5264
6288
|
function Badge({ element }) {
|
|
@@ -5282,7 +6306,6 @@ function Badge({ element }) {
|
|
|
5282
6306
|
children: text
|
|
5283
6307
|
});
|
|
5284
6308
|
}
|
|
5285
|
-
|
|
5286
6309
|
//#endregion
|
|
5287
6310
|
//#region src/components/dashboard/Card/index.tsx
|
|
5288
6311
|
function Card({ element, children }) {
|
|
@@ -5323,7 +6346,6 @@ function Card({ element, children }) {
|
|
|
5323
6346
|
})]
|
|
5324
6347
|
});
|
|
5325
6348
|
}
|
|
5326
|
-
|
|
5327
6349
|
//#endregion
|
|
5328
6350
|
//#region src/components/dashboard/Divider/index.tsx
|
|
5329
6351
|
function Divider({ element }) {
|
|
@@ -5361,7 +6383,6 @@ function Divider({ element }) {
|
|
|
5361
6383
|
margin: "16px 0"
|
|
5362
6384
|
} });
|
|
5363
6385
|
}
|
|
5364
|
-
|
|
5365
6386
|
//#endregion
|
|
5366
6387
|
//#region src/components/dashboard/Empty/index.tsx
|
|
5367
6388
|
function Empty({ element, onAction }) {
|
|
@@ -5404,7 +6425,6 @@ function Empty({ element, onAction }) {
|
|
|
5404
6425
|
]
|
|
5405
6426
|
});
|
|
5406
6427
|
}
|
|
5407
|
-
|
|
5408
6428
|
//#endregion
|
|
5409
6429
|
//#region src/components/dashboard/Grid/index.tsx
|
|
5410
6430
|
function Grid({ element, children }) {
|
|
@@ -5422,7 +6442,6 @@ function Grid({ element, children }) {
|
|
|
5422
6442
|
children
|
|
5423
6443
|
});
|
|
5424
6444
|
}
|
|
5425
|
-
|
|
5426
6445
|
//#endregion
|
|
5427
6446
|
//#region src/components/dashboard/Heading/index.tsx
|
|
5428
6447
|
function Heading({ element }) {
|
|
@@ -5441,7 +6460,6 @@ function Heading({ element }) {
|
|
|
5441
6460
|
children: text
|
|
5442
6461
|
});
|
|
5443
6462
|
}
|
|
5444
|
-
|
|
5445
6463
|
//#endregion
|
|
5446
6464
|
//#region src/components/dashboard/Stack/index.tsx
|
|
5447
6465
|
function Stack({ element, children }) {
|
|
@@ -5465,7 +6483,6 @@ function Stack({ element, children }) {
|
|
|
5465
6483
|
children
|
|
5466
6484
|
});
|
|
5467
6485
|
}
|
|
5468
|
-
|
|
5469
6486
|
//#endregion
|
|
5470
6487
|
//#region src/components/dashboard/Text/index.tsx
|
|
5471
6488
|
function Text({ element }) {
|
|
@@ -5484,7 +6501,6 @@ function Text({ element }) {
|
|
|
5484
6501
|
children: content
|
|
5485
6502
|
});
|
|
5486
6503
|
}
|
|
5487
|
-
|
|
5488
6504
|
//#endregion
|
|
5489
6505
|
//#region src/components/observability/renderers/OtelLogTimeline.tsx
|
|
5490
6506
|
function OtelLogTimeline(props) {
|
|
@@ -5506,7 +6522,6 @@ function OtelLogTimeline(props) {
|
|
|
5506
6522
|
})
|
|
5507
6523
|
});
|
|
5508
6524
|
}
|
|
5509
|
-
|
|
5510
6525
|
//#endregion
|
|
5511
6526
|
//#region src/components/observability/renderers/OtelMetricDiscovery.tsx
|
|
5512
6527
|
const TYPE_ORDER = {
|
|
@@ -5584,7 +6599,6 @@ function OtelMetricDiscovery(props) {
|
|
|
5584
6599
|
})
|
|
5585
6600
|
});
|
|
5586
6601
|
}
|
|
5587
|
-
|
|
5588
6602
|
//#endregion
|
|
5589
6603
|
//#region src/components/observability/renderers/OtelMetricHistogram.tsx
|
|
5590
6604
|
function OtelMetricHistogram(props) {
|
|
@@ -5605,9 +6619,15 @@ function OtelMetricHistogram(props) {
|
|
|
5605
6619
|
unit: props.element.props.unit ?? void 0
|
|
5606
6620
|
});
|
|
5607
6621
|
}
|
|
5608
|
-
|
|
5609
6622
|
//#endregion
|
|
5610
6623
|
//#region src/components/observability/renderers/OtelMetricStat.tsx
|
|
6624
|
+
const EMPTY_ROWS = [];
|
|
6625
|
+
const GROUPED_AGGREGATE_ERROR = /* @__PURE__ */ new Error("MetricStat cannot display grouped aggregates. Remove groupBy or use MetricTable.");
|
|
6626
|
+
function isAggregatedRequest(props) {
|
|
6627
|
+
const ds = props.element.dataSource;
|
|
6628
|
+
if (!ds || ds.method !== "searchMetricsPage" || !ds.params) return false;
|
|
6629
|
+
return !!ds.params.aggregate;
|
|
6630
|
+
}
|
|
5611
6631
|
function OtelMetricStat(props) {
|
|
5612
6632
|
if (!props.hasData) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5613
6633
|
style: {
|
|
@@ -5616,6 +6636,24 @@ function OtelMetricStat(props) {
|
|
|
5616
6636
|
},
|
|
5617
6637
|
children: "No data source"
|
|
5618
6638
|
});
|
|
6639
|
+
if (isAggregatedRequest(props)) {
|
|
6640
|
+
const rows = props.data?.data ?? [];
|
|
6641
|
+
if (rows.length > 1) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricStat, {
|
|
6642
|
+
rows: EMPTY_ROWS,
|
|
6643
|
+
error: GROUPED_AGGREGATE_ERROR,
|
|
6644
|
+
label: props.element.props.label ?? void 0,
|
|
6645
|
+
formatValue: formatOtelValue
|
|
6646
|
+
});
|
|
6647
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricStat, {
|
|
6648
|
+
rows: EMPTY_ROWS,
|
|
6649
|
+
value: rows[0]?.value,
|
|
6650
|
+
isLoading: props.loading,
|
|
6651
|
+
error: props.error ?? void 0,
|
|
6652
|
+
label: props.element.props.label ?? void 0,
|
|
6653
|
+
showSparkline: false,
|
|
6654
|
+
formatValue: formatOtelValue
|
|
6655
|
+
});
|
|
6656
|
+
}
|
|
5619
6657
|
const response = props.data;
|
|
5620
6658
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricStat, {
|
|
5621
6659
|
rows: response?.data ?? [],
|
|
@@ -5626,7 +6664,6 @@ function OtelMetricStat(props) {
|
|
|
5626
6664
|
formatValue: formatOtelValue
|
|
5627
6665
|
});
|
|
5628
6666
|
}
|
|
5629
|
-
|
|
5630
6667
|
//#endregion
|
|
5631
6668
|
//#region src/components/observability/renderers/OtelMetricTable.tsx
|
|
5632
6669
|
function OtelMetricTable(props) {
|
|
@@ -5645,7 +6682,6 @@ function OtelMetricTable(props) {
|
|
|
5645
6682
|
maxRows: props.element.props.maxRows ?? 100
|
|
5646
6683
|
});
|
|
5647
6684
|
}
|
|
5648
|
-
|
|
5649
6685
|
//#endregion
|
|
5650
6686
|
//#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
|
|
5651
6687
|
function OtelMetricTimeSeries(props) {
|
|
@@ -5667,7 +6703,6 @@ function OtelMetricTimeSeries(props) {
|
|
|
5667
6703
|
unit: props.element.props.unit ?? void 0
|
|
5668
6704
|
});
|
|
5669
6705
|
}
|
|
5670
|
-
|
|
5671
6706
|
//#endregion
|
|
5672
6707
|
//#region src/components/observability/renderers/OtelTraceDetail.tsx
|
|
5673
6708
|
function OtelTraceDetail(props) {
|
|
@@ -5679,19 +6714,15 @@ function OtelTraceDetail(props) {
|
|
|
5679
6714
|
children: "No data source"
|
|
5680
6715
|
});
|
|
5681
6716
|
const rows = props.data?.data ?? [];
|
|
5682
|
-
const
|
|
5683
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
5684
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
6717
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
5685
6718
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetail, {
|
|
5686
6719
|
rows,
|
|
5687
6720
|
isLoading: props.loading,
|
|
5688
6721
|
error: props.error ?? void 0,
|
|
5689
|
-
service,
|
|
5690
6722
|
traceId,
|
|
5691
6723
|
onBack: () => {}
|
|
5692
6724
|
});
|
|
5693
6725
|
}
|
|
5694
|
-
|
|
5695
6726
|
//#endregion
|
|
5696
6727
|
//#region src/components/observability/DynamicDashboard/index.tsx
|
|
5697
6728
|
const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
|
|
@@ -5717,24 +6748,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
|
|
|
5717
6748
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricsRenderer, { tree: uiTree })
|
|
5718
6749
|
});
|
|
5719
6750
|
}
|
|
5720
|
-
|
|
6751
|
+
//#endregion
|
|
6752
|
+
//#region src/components/observability/TraceComparison/index.tsx
|
|
6753
|
+
function computeTraceStats(rows) {
|
|
6754
|
+
if (rows.length === 0) return {
|
|
6755
|
+
durationMs: 0,
|
|
6756
|
+
spanCount: 0
|
|
6757
|
+
};
|
|
6758
|
+
let minTs = Infinity;
|
|
6759
|
+
let maxEnd = -Infinity;
|
|
6760
|
+
for (const row of rows) {
|
|
6761
|
+
const startMs = parseInt(row.Timestamp, 10) / 1e6;
|
|
6762
|
+
const endMs = startMs + (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6763
|
+
minTs = Math.min(minTs, startMs);
|
|
6764
|
+
maxEnd = Math.max(maxEnd, endMs);
|
|
6765
|
+
}
|
|
6766
|
+
return {
|
|
6767
|
+
durationMs: maxEnd - minTs,
|
|
6768
|
+
spanCount: rows.length
|
|
6769
|
+
};
|
|
6770
|
+
}
|
|
6771
|
+
function collectSignatures(rows) {
|
|
6772
|
+
const map = /* @__PURE__ */ new Map();
|
|
6773
|
+
for (const row of rows) {
|
|
6774
|
+
const key = `${row.ServiceName ?? "unknown"}::${row.SpanName ?? ""}`;
|
|
6775
|
+
const durMs = (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6776
|
+
const existing = map.get(key);
|
|
6777
|
+
if (existing) {
|
|
6778
|
+
existing.count++;
|
|
6779
|
+
existing.totalDurationMs += durMs;
|
|
6780
|
+
} else map.set(key, {
|
|
6781
|
+
count: 1,
|
|
6782
|
+
totalDurationMs: durMs
|
|
6783
|
+
});
|
|
6784
|
+
}
|
|
6785
|
+
return map;
|
|
6786
|
+
}
|
|
6787
|
+
function computeDiff(rowsA, rowsB) {
|
|
6788
|
+
const sigA = collectSignatures(rowsA);
|
|
6789
|
+
const sigB = collectSignatures(rowsB);
|
|
6790
|
+
const allKeys = new Set([...sigA.keys(), ...sigB.keys()]);
|
|
6791
|
+
const result = [];
|
|
6792
|
+
for (const key of allKeys) {
|
|
6793
|
+
const [serviceName = "unknown", spanName = ""] = key.split("::");
|
|
6794
|
+
const a = sigA.get(key);
|
|
6795
|
+
const b = sigB.get(key);
|
|
6796
|
+
const countA = a?.count ?? 0;
|
|
6797
|
+
const countB = b?.count ?? 0;
|
|
6798
|
+
const avgA = a ? a.totalDurationMs / a.count : 0;
|
|
6799
|
+
const avgB = b ? b.totalDurationMs / b.count : 0;
|
|
6800
|
+
result.push({
|
|
6801
|
+
serviceName,
|
|
6802
|
+
spanName,
|
|
6803
|
+
countA,
|
|
6804
|
+
countB,
|
|
6805
|
+
avgDurationA: avgA,
|
|
6806
|
+
avgDurationB: avgB,
|
|
6807
|
+
deltaMs: avgB - avgA
|
|
6808
|
+
});
|
|
6809
|
+
}
|
|
6810
|
+
return result.sort((a, b) => {
|
|
6811
|
+
const aShared = a.countA > 0 && a.countB > 0;
|
|
6812
|
+
if (aShared !== (b.countA > 0 && b.countB > 0)) return aShared ? 1 : -1;
|
|
6813
|
+
return Math.abs(b.deltaMs) - Math.abs(a.deltaMs);
|
|
6814
|
+
});
|
|
6815
|
+
}
|
|
6816
|
+
function formatDelta(ms) {
|
|
6817
|
+
return `${ms > 0 ? "+" : ""}${formatDuration(ms)}`;
|
|
6818
|
+
}
|
|
6819
|
+
function TraceComparison({ traceIdA, traceIdB, onBack }) {
|
|
6820
|
+
const dsA = (0, react.useMemo)(() => ({
|
|
6821
|
+
method: "getTrace",
|
|
6822
|
+
params: { traceId: traceIdA }
|
|
6823
|
+
}), [traceIdA]);
|
|
6824
|
+
const dsB = (0, react.useMemo)(() => ({
|
|
6825
|
+
method: "getTrace",
|
|
6826
|
+
params: { traceId: traceIdB }
|
|
6827
|
+
}), [traceIdB]);
|
|
6828
|
+
const { data: rowsA, loading: loadingA, error: errorA } = useKopaiData(dsA);
|
|
6829
|
+
const { data: rowsB, loading: loadingB, error: errorB } = useKopaiData(dsB);
|
|
6830
|
+
const statsA = (0, react.useMemo)(() => computeTraceStats(rowsA ?? []), [rowsA]);
|
|
6831
|
+
const statsB = (0, react.useMemo)(() => computeTraceStats(rowsB ?? []), [rowsB]);
|
|
6832
|
+
const diff = (0, react.useMemo)(() => computeDiff(rowsA ?? [], rowsB ?? []), [rowsA, rowsB]);
|
|
6833
|
+
const durationDelta = statsB.durationMs - statsA.durationMs;
|
|
6834
|
+
const spanDelta = statsB.spanCount - statsA.spanCount;
|
|
6835
|
+
const isLoading = loadingA || loadingB;
|
|
6836
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6837
|
+
className: "flex flex-col gap-4",
|
|
6838
|
+
children: [
|
|
6839
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6840
|
+
className: "flex items-center justify-between bg-background border border-border rounded-lg p-4",
|
|
6841
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6842
|
+
className: "flex items-center gap-4",
|
|
6843
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6844
|
+
onClick: onBack,
|
|
6845
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
6846
|
+
children: "← Back"
|
|
6847
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6848
|
+
className: "flex items-center gap-6 text-sm",
|
|
6849
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6850
|
+
className: "text-muted-foreground mr-1",
|
|
6851
|
+
children: "A:"
|
|
6852
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6853
|
+
className: "font-mono text-xs text-foreground",
|
|
6854
|
+
children: [traceIdA.slice(0, 16), "..."]
|
|
6855
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6856
|
+
className: "text-muted-foreground mr-1",
|
|
6857
|
+
children: "B:"
|
|
6858
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6859
|
+
className: "font-mono text-xs text-foreground",
|
|
6860
|
+
children: [traceIdB.slice(0, 16), "..."]
|
|
6861
|
+
})] })]
|
|
6862
|
+
})]
|
|
6863
|
+
}), !isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6864
|
+
className: "flex items-center gap-6 text-sm",
|
|
6865
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6866
|
+
className: "text-muted-foreground mr-1",
|
|
6867
|
+
children: "Duration delta:"
|
|
6868
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6869
|
+
className: durationDelta > 0 ? "text-red-400" : durationDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6870
|
+
children: formatDelta(durationDelta)
|
|
6871
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6872
|
+
className: "text-muted-foreground mr-1",
|
|
6873
|
+
children: "Span count delta:"
|
|
6874
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6875
|
+
className: spanDelta > 0 ? "text-red-400" : spanDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6876
|
+
children: spanDelta > 0 ? `+${spanDelta}` : String(spanDelta)
|
|
6877
|
+
})] })]
|
|
6878
|
+
})]
|
|
6879
|
+
}),
|
|
6880
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6881
|
+
className: "grid grid-cols-2 gap-4",
|
|
6882
|
+
style: { height: "50vh" },
|
|
6883
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6884
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6885
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceTimeline, {
|
|
6886
|
+
rows: rowsA ?? [],
|
|
6887
|
+
isLoading: loadingA,
|
|
6888
|
+
error: errorA ?? void 0
|
|
6889
|
+
})
|
|
6890
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6891
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6892
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceTimeline, {
|
|
6893
|
+
rows: rowsB ?? [],
|
|
6894
|
+
isLoading: loadingB,
|
|
6895
|
+
error: errorB ?? void 0
|
|
6896
|
+
})
|
|
6897
|
+
})]
|
|
6898
|
+
}),
|
|
6899
|
+
!isLoading && diff.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6900
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6901
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6902
|
+
className: "px-4 py-3 border-b border-border bg-background",
|
|
6903
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
6904
|
+
className: "text-sm font-medium text-foreground",
|
|
6905
|
+
children: "Structural Diff"
|
|
6906
|
+
})
|
|
6907
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6908
|
+
className: "overflow-x-auto",
|
|
6909
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
6910
|
+
className: "w-full text-sm",
|
|
6911
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
6912
|
+
className: "border-b border-border bg-muted/30",
|
|
6913
|
+
children: [
|
|
6914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6915
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6916
|
+
children: "Service"
|
|
6917
|
+
}),
|
|
6918
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6919
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6920
|
+
children: "Span"
|
|
6921
|
+
}),
|
|
6922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6923
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6924
|
+
children: "Count A"
|
|
6925
|
+
}),
|
|
6926
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6927
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6928
|
+
children: "Count B"
|
|
6929
|
+
}),
|
|
6930
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6931
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6932
|
+
children: "Avg Dur A"
|
|
6933
|
+
}),
|
|
6934
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6935
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6936
|
+
children: "Avg Dur B"
|
|
6937
|
+
}),
|
|
6938
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6939
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6940
|
+
children: "Delta"
|
|
6941
|
+
})
|
|
6942
|
+
]
|
|
6943
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: diff.map((row) => {
|
|
6944
|
+
const onlyA = row.countA > 0 && row.countB === 0;
|
|
6945
|
+
const onlyB = row.countA === 0 && row.countB > 0;
|
|
6946
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
6947
|
+
className: `border-b border-border/50 ${onlyA ? "bg-red-500/5" : onlyB ? "bg-green-500/5" : ""}`,
|
|
6948
|
+
children: [
|
|
6949
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6950
|
+
className: "px-4 py-1.5 text-foreground",
|
|
6951
|
+
children: row.serviceName
|
|
6952
|
+
}),
|
|
6953
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6954
|
+
className: "px-4 py-1.5 font-mono text-xs text-foreground",
|
|
6955
|
+
children: row.spanName
|
|
6956
|
+
}),
|
|
6957
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6958
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6959
|
+
children: row.countA || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6960
|
+
className: "text-muted-foreground",
|
|
6961
|
+
children: "-"
|
|
6962
|
+
})
|
|
6963
|
+
}),
|
|
6964
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6965
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6966
|
+
children: row.countB || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6967
|
+
className: "text-muted-foreground",
|
|
6968
|
+
children: "-"
|
|
6969
|
+
})
|
|
6970
|
+
}),
|
|
6971
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6972
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6973
|
+
children: row.countA > 0 ? formatDuration(row.avgDurationA) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6974
|
+
className: "text-muted-foreground",
|
|
6975
|
+
children: "-"
|
|
6976
|
+
})
|
|
6977
|
+
}),
|
|
6978
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6979
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6980
|
+
children: row.countB > 0 ? formatDuration(row.avgDurationB) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6981
|
+
className: "text-muted-foreground",
|
|
6982
|
+
children: "-"
|
|
6983
|
+
})
|
|
6984
|
+
}),
|
|
6985
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6986
|
+
className: "px-4 py-1.5 text-right",
|
|
6987
|
+
children: row.countA > 0 && row.countB > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6988
|
+
className: row.deltaMs > 0 ? "text-red-400" : row.deltaMs < 0 ? "text-green-400" : "text-foreground",
|
|
6989
|
+
children: formatDelta(row.deltaMs)
|
|
6990
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6991
|
+
className: onlyA ? "text-red-400" : "text-green-400",
|
|
6992
|
+
children: onlyA ? "removed" : "added"
|
|
6993
|
+
})
|
|
6994
|
+
})
|
|
6995
|
+
]
|
|
6996
|
+
}, `${row.serviceName}::${row.spanName}`);
|
|
6997
|
+
}) })]
|
|
6998
|
+
})
|
|
6999
|
+
})]
|
|
7000
|
+
})
|
|
7001
|
+
]
|
|
7002
|
+
});
|
|
7003
|
+
}
|
|
5721
7004
|
//#endregion
|
|
5722
7005
|
//#region src/components/observability/ServiceList/shortcuts.ts
|
|
5723
7006
|
const SERVICES_SHORTCUTS = {
|
|
5724
|
-
name: "
|
|
7007
|
+
name: "Traces",
|
|
5725
7008
|
shortcuts: [{
|
|
5726
7009
|
keys: ["Backspace"],
|
|
5727
7010
|
description: "Go back"
|
|
5728
7011
|
}]
|
|
5729
7012
|
};
|
|
5730
|
-
|
|
5731
7013
|
//#endregion
|
|
5732
7014
|
//#region src/pages/observability.tsx
|
|
5733
7015
|
const TABS = [
|
|
5734
7016
|
{
|
|
5735
7017
|
key: "services",
|
|
5736
|
-
label: "
|
|
5737
|
-
shortcutKey: "
|
|
7018
|
+
label: "Traces",
|
|
7019
|
+
shortcutKey: "T"
|
|
5738
7020
|
},
|
|
5739
7021
|
{
|
|
5740
7022
|
key: "logs",
|
|
@@ -5754,20 +7036,53 @@ function readURLState() {
|
|
|
5754
7036
|
const span = params.get("span");
|
|
5755
7037
|
const dashboardId = params.get("dashboardId");
|
|
5756
7038
|
const rawTab = params.get("tab");
|
|
7039
|
+
const tab = service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services";
|
|
7040
|
+
const rawLimit = params.get("limit");
|
|
7041
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) : null;
|
|
5757
7042
|
return {
|
|
5758
|
-
tab
|
|
7043
|
+
tab,
|
|
5759
7044
|
service,
|
|
7045
|
+
operation: params.get("operation"),
|
|
7046
|
+
tags: params.get("tags"),
|
|
7047
|
+
lookback: params.get("lookback"),
|
|
7048
|
+
tsMin: params.get("tsMin"),
|
|
7049
|
+
tsMax: params.get("tsMax"),
|
|
7050
|
+
minDuration: params.get("minDuration"),
|
|
7051
|
+
maxDuration: params.get("maxDuration"),
|
|
7052
|
+
limit: limit !== null && !isNaN(limit) ? limit : null,
|
|
7053
|
+
sort: params.get("sort"),
|
|
5760
7054
|
trace,
|
|
5761
7055
|
span,
|
|
7056
|
+
view: params.get("view"),
|
|
7057
|
+
uiFind: params.get("uiFind"),
|
|
7058
|
+
compare: params.get("compare"),
|
|
7059
|
+
viewStart: params.get("viewStart"),
|
|
7060
|
+
viewEnd: params.get("viewEnd"),
|
|
5762
7061
|
dashboardId
|
|
5763
7062
|
};
|
|
5764
7063
|
}
|
|
5765
7064
|
function pushURLState(state, { replace = false } = {}) {
|
|
5766
7065
|
const params = new URLSearchParams();
|
|
5767
7066
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
5768
|
-
if (state.
|
|
5769
|
-
|
|
5770
|
-
|
|
7067
|
+
if (state.tab === "services") {
|
|
7068
|
+
if (state.service) params.set("service", state.service);
|
|
7069
|
+
if (state.operation) params.set("operation", state.operation);
|
|
7070
|
+
if (state.tags) params.set("tags", state.tags);
|
|
7071
|
+
if (state.lookback) params.set("lookback", state.lookback);
|
|
7072
|
+
if (state.tsMin) params.set("tsMin", state.tsMin);
|
|
7073
|
+
if (state.tsMax) params.set("tsMax", state.tsMax);
|
|
7074
|
+
if (state.minDuration) params.set("minDuration", state.minDuration);
|
|
7075
|
+
if (state.maxDuration) params.set("maxDuration", state.maxDuration);
|
|
7076
|
+
if (state.limit != null && state.limit !== 20) params.set("limit", String(state.limit));
|
|
7077
|
+
if (state.sort) params.set("sort", state.sort);
|
|
7078
|
+
if (state.trace) params.set("trace", state.trace);
|
|
7079
|
+
if (state.span) params.set("span", state.span);
|
|
7080
|
+
if (state.view) params.set("view", state.view);
|
|
7081
|
+
if (state.uiFind) params.set("uiFind", state.uiFind);
|
|
7082
|
+
if (state.compare) params.set("compare", state.compare);
|
|
7083
|
+
if (state.viewStart) params.set("viewStart", state.viewStart);
|
|
7084
|
+
if (state.viewEnd) params.set("viewEnd", state.viewEnd);
|
|
7085
|
+
}
|
|
5771
7086
|
const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
|
|
5772
7087
|
if (dashboardId) params.set("dashboardId", dashboardId);
|
|
5773
7088
|
const qs = params.toString();
|
|
@@ -5784,8 +7099,22 @@ let _cachedSearch = "";
|
|
|
5784
7099
|
let _cachedState = {
|
|
5785
7100
|
tab: "services",
|
|
5786
7101
|
service: null,
|
|
7102
|
+
operation: null,
|
|
7103
|
+
tags: null,
|
|
7104
|
+
lookback: null,
|
|
7105
|
+
tsMin: null,
|
|
7106
|
+
tsMax: null,
|
|
7107
|
+
minDuration: null,
|
|
7108
|
+
maxDuration: null,
|
|
7109
|
+
limit: null,
|
|
7110
|
+
sort: null,
|
|
5787
7111
|
trace: null,
|
|
5788
7112
|
span: null,
|
|
7113
|
+
view: null,
|
|
7114
|
+
uiFind: null,
|
|
7115
|
+
compare: null,
|
|
7116
|
+
viewStart: null,
|
|
7117
|
+
viewEnd: null,
|
|
5789
7118
|
dashboardId: null
|
|
5790
7119
|
};
|
|
5791
7120
|
function getURLSnapshot() {
|
|
@@ -5895,6 +7224,26 @@ function parseDuration(input) {
|
|
|
5895
7224
|
s: 1e9
|
|
5896
7225
|
}[unit]));
|
|
5897
7226
|
}
|
|
7227
|
+
function parseLogfmt(str) {
|
|
7228
|
+
const result = {};
|
|
7229
|
+
const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
|
|
7230
|
+
let m;
|
|
7231
|
+
while ((m = re.exec(str)) !== null) {
|
|
7232
|
+
const key = m[1];
|
|
7233
|
+
if (key) result[key] = m[2] ?? m[3] ?? "";
|
|
7234
|
+
}
|
|
7235
|
+
return result;
|
|
7236
|
+
}
|
|
7237
|
+
const LOOKBACK_MS = {
|
|
7238
|
+
"5m": 5 * 6e4,
|
|
7239
|
+
"15m": 15 * 6e4,
|
|
7240
|
+
"30m": 30 * 6e4,
|
|
7241
|
+
"1h": 60 * 6e4,
|
|
7242
|
+
"2h": 120 * 6e4,
|
|
7243
|
+
"6h": 360 * 6e4,
|
|
7244
|
+
"12h": 720 * 6e4,
|
|
7245
|
+
"24h": 1440 * 6e4
|
|
7246
|
+
};
|
|
5898
7247
|
function LogsTab() {
|
|
5899
7248
|
const [initState] = (0, react.useState)(() => readLogFilters());
|
|
5900
7249
|
const [filters, setFilters] = (0, react.useState)(initState.filters);
|
|
@@ -5954,185 +7303,145 @@ function LogsTab() {
|
|
|
5954
7303
|
})]
|
|
5955
7304
|
});
|
|
5956
7305
|
}
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
sortOrder: "DESC"
|
|
5962
|
-
}
|
|
5963
|
-
};
|
|
5964
|
-
function ServiceListView({ onSelect }) {
|
|
5965
|
-
const { data, loading, error } = useKopaiData(SERVICES_DS);
|
|
5966
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServiceList, {
|
|
5967
|
-
services: (0, react.useMemo)(() => {
|
|
5968
|
-
if (!data?.data) return [];
|
|
5969
|
-
const names = /* @__PURE__ */ new Set();
|
|
5970
|
-
for (const row of data.data) names.add(row.ServiceName ?? "unknown");
|
|
5971
|
-
return Array.from(names).sort().map((name) => ({ name }));
|
|
5972
|
-
}, [data]),
|
|
5973
|
-
isLoading: loading,
|
|
5974
|
-
error: error ?? void 0,
|
|
5975
|
-
onSelect
|
|
5976
|
-
});
|
|
5977
|
-
}
|
|
5978
|
-
function TraceSearchView({ service, onBack, onSelectTrace }) {
|
|
5979
|
-
const [ds, setDs] = (0, react.useState)(() => ({
|
|
5980
|
-
method: "searchTracesPage",
|
|
5981
|
-
params: {
|
|
5982
|
-
serviceName: service,
|
|
5983
|
-
limit: 20,
|
|
5984
|
-
sortOrder: "DESC"
|
|
5985
|
-
}
|
|
5986
|
-
}));
|
|
5987
|
-
const handleSearch = (0, react.useCallback)((filters) => {
|
|
7306
|
+
function TraceSearchView({ onSelectTrace, onCompare }) {
|
|
7307
|
+
const urlState = useURLState();
|
|
7308
|
+
const service = urlState.service;
|
|
7309
|
+
const ds = (0, react.useMemo)(() => {
|
|
5988
7310
|
const params = {
|
|
5989
|
-
|
|
5990
|
-
limit: filters.limit,
|
|
7311
|
+
limit: urlState.limit ?? 20,
|
|
5991
7312
|
sortOrder: "DESC"
|
|
5992
7313
|
};
|
|
5993
|
-
if (
|
|
5994
|
-
if (
|
|
5995
|
-
if (
|
|
5996
|
-
const
|
|
7314
|
+
if (service) params.serviceName = service;
|
|
7315
|
+
if (urlState.operation) params.spanName = urlState.operation;
|
|
7316
|
+
if (urlState.lookback) {
|
|
7317
|
+
const ms = LOOKBACK_MS[urlState.lookback];
|
|
7318
|
+
if (ms) params.timestampMin = String((Date.now() - ms) * 1e6);
|
|
7319
|
+
}
|
|
7320
|
+
if (urlState.tsMin) params.timestampMin = urlState.tsMin;
|
|
7321
|
+
if (urlState.tsMax) params.timestampMax = urlState.tsMax;
|
|
7322
|
+
if (urlState.minDuration) {
|
|
7323
|
+
const parsed = parseDuration(urlState.minDuration);
|
|
5997
7324
|
if (parsed) params.durationMin = parsed;
|
|
5998
7325
|
}
|
|
5999
|
-
if (
|
|
6000
|
-
const parsed = parseDuration(
|
|
7326
|
+
if (urlState.maxDuration) {
|
|
7327
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
6001
7328
|
if (parsed) params.durationMax = parsed;
|
|
6002
7329
|
}
|
|
6003
|
-
|
|
6004
|
-
|
|
7330
|
+
if (urlState.tags) {
|
|
7331
|
+
const tagMap = parseLogfmt(urlState.tags);
|
|
7332
|
+
if (Object.keys(tagMap).length > 0) params.tags = tagMap;
|
|
7333
|
+
}
|
|
7334
|
+
return {
|
|
7335
|
+
method: "searchTraceSummariesPage",
|
|
6005
7336
|
params
|
|
7337
|
+
};
|
|
7338
|
+
}, [
|
|
7339
|
+
service,
|
|
7340
|
+
urlState.operation,
|
|
7341
|
+
urlState.lookback,
|
|
7342
|
+
urlState.tsMin,
|
|
7343
|
+
urlState.tsMax,
|
|
7344
|
+
urlState.minDuration,
|
|
7345
|
+
urlState.maxDuration,
|
|
7346
|
+
urlState.limit,
|
|
7347
|
+
urlState.tags
|
|
7348
|
+
]);
|
|
7349
|
+
const handleSearch = (0, react.useCallback)((filters) => {
|
|
7350
|
+
pushURLState({
|
|
7351
|
+
tab: "services",
|
|
7352
|
+
service: filters.service ?? service,
|
|
7353
|
+
operation: filters.operation ?? null,
|
|
7354
|
+
tags: filters.tags ?? null,
|
|
7355
|
+
lookback: filters.lookback ?? null,
|
|
7356
|
+
minDuration: filters.minDuration ?? null,
|
|
7357
|
+
maxDuration: filters.maxDuration ?? null,
|
|
7358
|
+
limit: filters.limit
|
|
6006
7359
|
});
|
|
6007
7360
|
}, [service]);
|
|
6008
7361
|
const { data, loading, error } = useKopaiData(ds);
|
|
6009
|
-
const
|
|
6010
|
-
const
|
|
6011
|
-
(0, react.
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
const ac = new AbortController();
|
|
6018
|
-
Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
|
|
6019
|
-
if (!ac.signal.aborted) {
|
|
6020
|
-
const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
6021
|
-
setFullTraces(new Map(entries));
|
|
6022
|
-
}
|
|
6023
|
-
}).catch((err) => {
|
|
6024
|
-
if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
|
|
6025
|
-
});
|
|
6026
|
-
return () => ac.abort();
|
|
6027
|
-
}, [data, client]);
|
|
6028
|
-
const operations = (0, react.useMemo)(() => {
|
|
7362
|
+
const { data: servicesData } = useKopaiData((0, react.useMemo)(() => ({ method: "getServices" }), []));
|
|
7363
|
+
const _services = servicesData?.services ?? [];
|
|
7364
|
+
const { data: opsData } = useKopaiData((0, react.useMemo)(() => service ? {
|
|
7365
|
+
method: "getOperations",
|
|
7366
|
+
params: { serviceName: service }
|
|
7367
|
+
} : void 0, [service]));
|
|
7368
|
+
const operations = opsData?.operations ?? [];
|
|
7369
|
+
const traces = (0, react.useMemo)(() => {
|
|
6029
7370
|
if (!data?.data) return [];
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
7371
|
+
return data.data.map((row) => ({
|
|
7372
|
+
traceId: row.traceId,
|
|
7373
|
+
rootSpanName: row.rootSpanName,
|
|
7374
|
+
serviceName: row.rootServiceName,
|
|
7375
|
+
durationMs: parseInt(row.durationNs, 10) / 1e6,
|
|
7376
|
+
statusCode: row.errorCount > 0 ? "ERROR" : "OK",
|
|
7377
|
+
timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
|
|
7378
|
+
spanCount: row.spanCount,
|
|
7379
|
+
services: row.services,
|
|
7380
|
+
errorCount: row.errorCount
|
|
7381
|
+
}));
|
|
6033
7382
|
}, [data]);
|
|
6034
7383
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearch, {
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6039
|
-
for (const row of data.data) {
|
|
6040
|
-
const tid = row.TraceId;
|
|
6041
|
-
if (!grouped.has(tid)) grouped.set(tid, []);
|
|
6042
|
-
grouped.get(tid).push(row);
|
|
6043
|
-
}
|
|
6044
|
-
return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
|
|
6045
|
-
const spans = fullTraces.get(traceId) ?? searchSpans;
|
|
6046
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
|
|
6047
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
6048
|
-
const svcMap = /* @__PURE__ */ new Map();
|
|
6049
|
-
let errorCount = 0;
|
|
6050
|
-
for (const s of spans) {
|
|
6051
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
6052
|
-
const entry = svcMap.get(svcName) ?? {
|
|
6053
|
-
count: 0,
|
|
6054
|
-
hasError: false
|
|
6055
|
-
};
|
|
6056
|
-
entry.count++;
|
|
6057
|
-
if (s.StatusCode === "ERROR") {
|
|
6058
|
-
entry.hasError = true;
|
|
6059
|
-
errorCount++;
|
|
6060
|
-
}
|
|
6061
|
-
svcMap.set(svcName, entry);
|
|
6062
|
-
}
|
|
6063
|
-
const services = Array.from(svcMap.entries()).map(([name, v]) => ({
|
|
6064
|
-
name,
|
|
6065
|
-
count: v.count,
|
|
6066
|
-
hasError: v.hasError
|
|
6067
|
-
})).sort((a, b) => b.count - a.count);
|
|
6068
|
-
return {
|
|
6069
|
-
traceId,
|
|
6070
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
6071
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
6072
|
-
durationMs: durationNs / 1e6,
|
|
6073
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
6074
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
6075
|
-
spanCount: spans.length,
|
|
6076
|
-
services,
|
|
6077
|
-
errorCount
|
|
6078
|
-
};
|
|
6079
|
-
});
|
|
6080
|
-
}, [data, fullTraces]),
|
|
7384
|
+
services: _services,
|
|
7385
|
+
service: service ?? "",
|
|
7386
|
+
traces,
|
|
6081
7387
|
operations,
|
|
6082
7388
|
isLoading: loading,
|
|
6083
7389
|
error: error ?? void 0,
|
|
6084
7390
|
onSelectTrace,
|
|
6085
|
-
|
|
7391
|
+
onCompare,
|
|
6086
7392
|
onSearch: handleSearch
|
|
6087
7393
|
});
|
|
6088
7394
|
}
|
|
6089
|
-
function TraceDetailView({
|
|
7395
|
+
function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
|
|
6090
7396
|
const { data, loading, error } = useKopaiData((0, react.useMemo)(() => ({
|
|
6091
7397
|
method: "getTrace",
|
|
6092
7398
|
params: { traceId }
|
|
6093
7399
|
}), [traceId]));
|
|
6094
7400
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetail, {
|
|
6095
|
-
service,
|
|
6096
7401
|
traceId,
|
|
6097
7402
|
rows: data ?? [],
|
|
6098
7403
|
isLoading: loading,
|
|
6099
7404
|
error: error ?? void 0,
|
|
6100
7405
|
selectedSpanId: selectedSpanId ?? void 0,
|
|
6101
7406
|
onSpanClick: (span) => onSelectSpan(span.spanId),
|
|
7407
|
+
onSpanDeselect: onDeselectSpan,
|
|
6102
7408
|
onBack
|
|
6103
7409
|
});
|
|
6104
7410
|
}
|
|
6105
|
-
function ServicesTab({
|
|
7411
|
+
function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
|
|
6106
7412
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
6107
|
-
const
|
|
6108
|
-
|
|
6109
|
-
const backToTraceListRef = (0, react.useRef)(onBackToTraceList);
|
|
6110
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
7413
|
+
const backRef = (0, react.useRef)(onBack);
|
|
7414
|
+
backRef.current = onBack;
|
|
6111
7415
|
(0, react.useEffect)(() => {
|
|
6112
7416
|
const handleKeyDown = (e) => {
|
|
6113
7417
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
|
6114
7418
|
if (e.key === "Backspace") {
|
|
6115
7419
|
e.preventDefault();
|
|
6116
|
-
|
|
6117
|
-
else if (selectedService) backToServicesRef.current();
|
|
7420
|
+
backRef.current();
|
|
6118
7421
|
}
|
|
6119
7422
|
};
|
|
6120
7423
|
window.addEventListener("keydown", handleKeyDown);
|
|
6121
7424
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
6122
|
-
}, [
|
|
6123
|
-
if (
|
|
6124
|
-
|
|
7425
|
+
}, []);
|
|
7426
|
+
if (compareParam) {
|
|
7427
|
+
const [traceIdA, traceIdB] = compareParam.split(",");
|
|
7428
|
+
if (traceIdA && traceIdB) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceComparison, {
|
|
7429
|
+
traceIdA,
|
|
7430
|
+
traceIdB,
|
|
7431
|
+
onBack
|
|
7432
|
+
});
|
|
7433
|
+
}
|
|
7434
|
+
if (selectedTraceId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetailView, {
|
|
6125
7435
|
traceId: selectedTraceId,
|
|
6126
7436
|
selectedSpanId,
|
|
6127
7437
|
onSelectSpan,
|
|
6128
|
-
|
|
7438
|
+
onDeselectSpan,
|
|
7439
|
+
onBack
|
|
6129
7440
|
});
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
onSelectTrace
|
|
7441
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearchView, {
|
|
7442
|
+
onSelectTrace,
|
|
7443
|
+
onCompare
|
|
6134
7444
|
});
|
|
6135
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServiceListView, { onSelect: onSelectService });
|
|
6136
7445
|
}
|
|
6137
7446
|
const METRICS_TREE = {
|
|
6138
7447
|
root: "root",
|
|
@@ -6142,6 +7451,9 @@ const METRICS_TREE = {
|
|
|
6142
7451
|
type: "Stack",
|
|
6143
7452
|
children: [
|
|
6144
7453
|
"heading",
|
|
7454
|
+
"ingestion-heading",
|
|
7455
|
+
"ingestion-grid",
|
|
7456
|
+
"discovery-heading",
|
|
6145
7457
|
"description",
|
|
6146
7458
|
"discovery-card"
|
|
6147
7459
|
],
|
|
@@ -6162,6 +7474,96 @@ const METRICS_TREE = {
|
|
|
6162
7474
|
level: "h2"
|
|
6163
7475
|
}
|
|
6164
7476
|
},
|
|
7477
|
+
"ingestion-heading": {
|
|
7478
|
+
key: "ingestion-heading",
|
|
7479
|
+
type: "Heading",
|
|
7480
|
+
children: [],
|
|
7481
|
+
parentKey: "root",
|
|
7482
|
+
props: {
|
|
7483
|
+
text: "OTEL Ingestion",
|
|
7484
|
+
level: "h3"
|
|
7485
|
+
}
|
|
7486
|
+
},
|
|
7487
|
+
"ingestion-grid": {
|
|
7488
|
+
key: "ingestion-grid",
|
|
7489
|
+
type: "Grid",
|
|
7490
|
+
children: ["card-bytes", "card-requests"],
|
|
7491
|
+
parentKey: "root",
|
|
7492
|
+
props: {
|
|
7493
|
+
columns: 2,
|
|
7494
|
+
gap: "md"
|
|
7495
|
+
}
|
|
7496
|
+
},
|
|
7497
|
+
"card-bytes": {
|
|
7498
|
+
key: "card-bytes",
|
|
7499
|
+
type: "Card",
|
|
7500
|
+
children: ["stat-bytes"],
|
|
7501
|
+
parentKey: "ingestion-grid",
|
|
7502
|
+
props: {
|
|
7503
|
+
title: "Total Bytes Ingested",
|
|
7504
|
+
description: null,
|
|
7505
|
+
padding: null
|
|
7506
|
+
}
|
|
7507
|
+
},
|
|
7508
|
+
"stat-bytes": {
|
|
7509
|
+
key: "stat-bytes",
|
|
7510
|
+
type: "MetricStat",
|
|
7511
|
+
children: [],
|
|
7512
|
+
parentKey: "card-bytes",
|
|
7513
|
+
dataSource: {
|
|
7514
|
+
method: "searchMetricsPage",
|
|
7515
|
+
params: {
|
|
7516
|
+
metricType: "Sum",
|
|
7517
|
+
metricName: "kopai.ingestion.bytes",
|
|
7518
|
+
aggregate: "sum"
|
|
7519
|
+
},
|
|
7520
|
+
refetchIntervalMs: 1e4
|
|
7521
|
+
},
|
|
7522
|
+
props: {
|
|
7523
|
+
label: "Bytes",
|
|
7524
|
+
showSparkline: false
|
|
7525
|
+
}
|
|
7526
|
+
},
|
|
7527
|
+
"card-requests": {
|
|
7528
|
+
key: "card-requests",
|
|
7529
|
+
type: "Card",
|
|
7530
|
+
children: ["stat-requests"],
|
|
7531
|
+
parentKey: "ingestion-grid",
|
|
7532
|
+
props: {
|
|
7533
|
+
title: "Total Requests",
|
|
7534
|
+
description: null,
|
|
7535
|
+
padding: null
|
|
7536
|
+
}
|
|
7537
|
+
},
|
|
7538
|
+
"stat-requests": {
|
|
7539
|
+
key: "stat-requests",
|
|
7540
|
+
type: "MetricStat",
|
|
7541
|
+
children: [],
|
|
7542
|
+
parentKey: "card-requests",
|
|
7543
|
+
dataSource: {
|
|
7544
|
+
method: "searchMetricsPage",
|
|
7545
|
+
params: {
|
|
7546
|
+
metricType: "Sum",
|
|
7547
|
+
metricName: "kopai.ingestion.requests",
|
|
7548
|
+
aggregate: "sum"
|
|
7549
|
+
},
|
|
7550
|
+
refetchIntervalMs: 1e4
|
|
7551
|
+
},
|
|
7552
|
+
props: {
|
|
7553
|
+
label: "Requests",
|
|
7554
|
+
showSparkline: false
|
|
7555
|
+
}
|
|
7556
|
+
},
|
|
7557
|
+
"discovery-heading": {
|
|
7558
|
+
key: "discovery-heading",
|
|
7559
|
+
type: "Heading",
|
|
7560
|
+
children: [],
|
|
7561
|
+
parentKey: "root",
|
|
7562
|
+
props: {
|
|
7563
|
+
text: "Discovered Metrics",
|
|
7564
|
+
level: "h3"
|
|
7565
|
+
}
|
|
7566
|
+
},
|
|
6165
7567
|
description: {
|
|
6166
7568
|
key: "description",
|
|
6167
7569
|
type: "Text",
|
|
@@ -6239,40 +7641,56 @@ function getDefaultClient() {
|
|
|
6239
7641
|
}
|
|
6240
7642
|
function ObservabilityPage({ client }) {
|
|
6241
7643
|
const activeClient = client ?? getDefaultClient();
|
|
6242
|
-
const { tab: activeTab,
|
|
7644
|
+
const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
|
|
6243
7645
|
const handleTabChange = (0, react.useCallback)((tab) => {
|
|
6244
7646
|
pushURLState({ tab });
|
|
6245
7647
|
}, []);
|
|
6246
|
-
const handleSelectService = (0, react.useCallback)((service) => {
|
|
6247
|
-
pushURLState({
|
|
6248
|
-
tab: "services",
|
|
6249
|
-
service
|
|
6250
|
-
});
|
|
6251
|
-
}, []);
|
|
6252
7648
|
const handleSelectTrace = (0, react.useCallback)((traceId) => {
|
|
6253
7649
|
pushURLState({
|
|
7650
|
+
...readURLState(),
|
|
6254
7651
|
tab: "services",
|
|
6255
|
-
service: selectedService,
|
|
6256
7652
|
trace: traceId
|
|
6257
7653
|
});
|
|
6258
|
-
}, [
|
|
7654
|
+
}, []);
|
|
6259
7655
|
const handleSelectSpan = (0, react.useCallback)((spanId) => {
|
|
6260
7656
|
pushURLState({
|
|
7657
|
+
...readURLState(),
|
|
6261
7658
|
tab: "services",
|
|
6262
|
-
service: selectedService,
|
|
6263
|
-
trace: selectedTraceId,
|
|
6264
7659
|
span: spanId
|
|
6265
7660
|
}, { replace: true });
|
|
6266
|
-
}, [selectedService, selectedTraceId]);
|
|
6267
|
-
const handleBackToServices = (0, react.useCallback)(() => {
|
|
6268
|
-
pushURLState({ tab: "services" });
|
|
6269
7661
|
}, []);
|
|
6270
|
-
const
|
|
7662
|
+
const handleDeselectSpan = (0, react.useCallback)(() => {
|
|
7663
|
+
pushURLState({
|
|
7664
|
+
...readURLState(),
|
|
7665
|
+
span: null
|
|
7666
|
+
}, { replace: true });
|
|
7667
|
+
}, []);
|
|
7668
|
+
const handleCompare = (0, react.useCallback)((traceIds) => {
|
|
7669
|
+
pushURLState({
|
|
7670
|
+
...readURLState(),
|
|
7671
|
+
tab: "services",
|
|
7672
|
+
trace: null,
|
|
7673
|
+
span: null,
|
|
7674
|
+
view: null,
|
|
7675
|
+
uiFind: null,
|
|
7676
|
+
viewStart: null,
|
|
7677
|
+
viewEnd: null,
|
|
7678
|
+
compare: traceIds.join(",")
|
|
7679
|
+
});
|
|
7680
|
+
}, []);
|
|
7681
|
+
const handleBack = (0, react.useCallback)(() => {
|
|
6271
7682
|
pushURLState({
|
|
7683
|
+
...readURLState(),
|
|
6272
7684
|
tab: "services",
|
|
6273
|
-
|
|
7685
|
+
trace: null,
|
|
7686
|
+
span: null,
|
|
7687
|
+
view: null,
|
|
7688
|
+
uiFind: null,
|
|
7689
|
+
viewStart: null,
|
|
7690
|
+
viewEnd: null,
|
|
7691
|
+
compare: null
|
|
6274
7692
|
});
|
|
6275
|
-
}, [
|
|
7693
|
+
}, []);
|
|
6276
7694
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KopaiSDKProvider, {
|
|
6277
7695
|
client: activeClient,
|
|
6278
7696
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyboardShortcutsProvider, {
|
|
@@ -6287,21 +7705,20 @@ function ObservabilityPage({ client }) {
|
|
|
6287
7705
|
}),
|
|
6288
7706
|
activeTab === "logs" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogsTab, {}),
|
|
6289
7707
|
activeTab === "services" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServicesTab, {
|
|
6290
|
-
selectedService,
|
|
6291
7708
|
selectedTraceId,
|
|
6292
7709
|
selectedSpanId,
|
|
6293
|
-
|
|
7710
|
+
compareParam,
|
|
6294
7711
|
onSelectTrace: handleSelectTrace,
|
|
6295
7712
|
onSelectSpan: handleSelectSpan,
|
|
6296
|
-
|
|
6297
|
-
|
|
7713
|
+
onDeselectSpan: handleDeselectSpan,
|
|
7714
|
+
onBack: handleBack,
|
|
7715
|
+
onCompare: handleCompare
|
|
6298
7716
|
}),
|
|
6299
7717
|
activeTab === "metrics" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricsTab, {})
|
|
6300
7718
|
] })
|
|
6301
7719
|
})
|
|
6302
7720
|
});
|
|
6303
7721
|
}
|
|
6304
|
-
|
|
6305
7722
|
//#endregion
|
|
6306
7723
|
//#region src/lib/generate-prompt-instructions.ts
|
|
6307
7724
|
function formatPropType(prop) {
|
|
@@ -6431,7 +7848,6 @@ ${JSON.stringify(unifiedSchema)}
|
|
|
6431
7848
|
|
|
6432
7849
|
${JSON.stringify(exampleElements)}`;
|
|
6433
7850
|
}
|
|
6434
|
-
|
|
6435
7851
|
//#endregion
|
|
6436
7852
|
exports.KopaiSDKProvider = KopaiSDKProvider;
|
|
6437
7853
|
exports.ObservabilityPage = ObservabilityPage;
|
|
@@ -6440,4 +7856,4 @@ exports.createCatalog = createCatalog;
|
|
|
6440
7856
|
exports.createRendererFromCatalog = createRendererFromCatalog;
|
|
6441
7857
|
exports.generatePromptInstructions = generatePromptInstructions;
|
|
6442
7858
|
exports.observabilityCatalog = observabilityCatalog;
|
|
6443
|
-
exports.useKopaiSDK = useKopaiSDK;
|
|
7859
|
+
exports.useKopaiSDK = useKopaiSDK;
|