@kopai/ui 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2451 -1157
- package/dist/index.d.cts +36 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +36 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2399 -1099
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
- package/src/components/observability/LogTimeline/index.tsx +6 -2
- package/src/components/observability/MetricHistogram/index.tsx +20 -19
- package/src/components/observability/MetricTimeSeries/index.tsx +25 -14
- 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/OtelLogTimeline.tsx +9 -5
- 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 +4 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- 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/pages/observability.test.tsx +16 -12
- package/src/pages/observability.tsx +323 -245
- 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,7 +53,6 @@ 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) {
|
|
@@ -69,6 +62,9 @@ function fetchForDataSource(client, dataSource, signal) {
|
|
|
69
62
|
case "searchMetricsPage": return client.searchMetricsPage(dataSource.params, { signal });
|
|
70
63
|
case "getTrace": return client.getTrace(dataSource.params.traceId, { signal });
|
|
71
64
|
case "discoverMetrics": return client.discoverMetrics({ signal });
|
|
65
|
+
case "getServices": return client.getServices({ signal });
|
|
66
|
+
case "getOperations": return client.getOperations(dataSource.params.serviceName, { signal });
|
|
67
|
+
case "searchTraceSummariesPage": return client.searchTraceSummariesPage(dataSource.params, { signal });
|
|
72
68
|
default: {
|
|
73
69
|
const exhaustiveCheck = dataSource;
|
|
74
70
|
throw new Error(`Unknown method: ${exhaustiveCheck.method}`);
|
|
@@ -94,7 +90,6 @@ function useKopaiData(dataSource) {
|
|
|
94
90
|
refetch
|
|
95
91
|
};
|
|
96
92
|
}
|
|
97
|
-
|
|
98
93
|
//#endregion
|
|
99
94
|
//#region src/lib/log-buffer.ts
|
|
100
95
|
function logKey(row) {
|
|
@@ -146,7 +141,6 @@ var LogBuffer = class {
|
|
|
146
141
|
this.keys.clear();
|
|
147
142
|
}
|
|
148
143
|
};
|
|
149
|
-
|
|
150
144
|
//#endregion
|
|
151
145
|
//#region src/hooks/use-live-logs.ts
|
|
152
146
|
function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = true }) {
|
|
@@ -201,7 +195,6 @@ function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = tr
|
|
|
201
195
|
setLive
|
|
202
196
|
};
|
|
203
197
|
}
|
|
204
|
-
|
|
205
198
|
//#endregion
|
|
206
199
|
//#region src/lib/component-catalog.ts
|
|
207
200
|
const dataSourceSchema = zod.z.discriminatedUnion("method", [
|
|
@@ -229,6 +222,21 @@ const dataSourceSchema = zod.z.discriminatedUnion("method", [
|
|
|
229
222
|
method: zod.z.literal("discoverMetrics"),
|
|
230
223
|
params: zod.z.object({}).optional(),
|
|
231
224
|
refetchIntervalMs: zod.z.number().optional()
|
|
225
|
+
}),
|
|
226
|
+
zod.z.object({
|
|
227
|
+
method: zod.z.literal("getServices"),
|
|
228
|
+
params: zod.z.object({}).optional(),
|
|
229
|
+
refetchIntervalMs: zod.z.number().optional()
|
|
230
|
+
}),
|
|
231
|
+
zod.z.object({
|
|
232
|
+
method: zod.z.literal("getOperations"),
|
|
233
|
+
params: zod.z.object({ serviceName: zod.z.string() }),
|
|
234
|
+
refetchIntervalMs: zod.z.number().optional()
|
|
235
|
+
}),
|
|
236
|
+
zod.z.object({
|
|
237
|
+
method: zod.z.literal("searchTraceSummariesPage"),
|
|
238
|
+
params: _kopai_core.dataFilterSchemas.traceSummariesFilterSchema,
|
|
239
|
+
refetchIntervalMs: zod.z.number().optional()
|
|
232
240
|
})
|
|
233
241
|
]);
|
|
234
242
|
const componentDefinitionSchema = zod.z.object({
|
|
@@ -236,7 +244,7 @@ const componentDefinitionSchema = zod.z.object({
|
|
|
236
244
|
description: zod.z.string().describe("Component description to be displayed by the prompt generator"),
|
|
237
245
|
props: zod.z.unknown()
|
|
238
246
|
}).describe("All options and properties necessary to render the React component with renderer");
|
|
239
|
-
|
|
247
|
+
zod.z.object({
|
|
240
248
|
name: zod.z.string().describe("catalog name"),
|
|
241
249
|
components: zod.z.record(zod.z.string().describe("React component name"), componentDefinitionSchema)
|
|
242
250
|
});
|
|
@@ -283,7 +291,6 @@ function createCatalog(catalogConfig) {
|
|
|
283
291
|
uiTreeSchema
|
|
284
292
|
};
|
|
285
293
|
}
|
|
286
|
-
|
|
287
294
|
//#endregion
|
|
288
295
|
//#region src/lib/observability-catalog.ts
|
|
289
296
|
const observabilityCatalog = createCatalog({
|
|
@@ -442,7 +449,6 @@ const observabilityCatalog = createCatalog({
|
|
|
442
449
|
}
|
|
443
450
|
}
|
|
444
451
|
});
|
|
445
|
-
|
|
446
452
|
//#endregion
|
|
447
453
|
//#region src/components/observability/TabBar/index.tsx
|
|
448
454
|
function renderLabel(label, shortcutKey) {
|
|
@@ -471,7 +477,6 @@ function TabBar({ tabs, active, onChange }) {
|
|
|
471
477
|
}, t.key))
|
|
472
478
|
});
|
|
473
479
|
}
|
|
474
|
-
|
|
475
480
|
//#endregion
|
|
476
481
|
//#region src/components/observability/utils/colors.ts
|
|
477
482
|
/**
|
|
@@ -491,38 +496,6 @@ function getSpanBarColor(serviceName, isError) {
|
|
|
491
496
|
if (isError) return ERROR_COLOR;
|
|
492
497
|
return getServiceColor(serviceName);
|
|
493
498
|
}
|
|
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
499
|
//#endregion
|
|
527
500
|
//#region src/components/observability/utils/time.ts
|
|
528
501
|
/**
|
|
@@ -544,6 +517,10 @@ function formatTimestamp$1(timestampMs) {
|
|
|
544
517
|
timeZoneName: "short"
|
|
545
518
|
});
|
|
546
519
|
}
|
|
520
|
+
function formatRelativeTime$1(eventTimeMs, spanStartMs) {
|
|
521
|
+
const relativeMs = eventTimeMs - spanStartMs;
|
|
522
|
+
return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
|
|
523
|
+
}
|
|
547
524
|
function calculateRelativeTime(timeMs, minTimeMs, maxTimeMs) {
|
|
548
525
|
const totalDuration = maxTimeMs - minTimeMs;
|
|
549
526
|
if (totalDuration === 0) return 0;
|
|
@@ -553,314 +530,659 @@ function calculateRelativeDuration(durationMs, totalDurationMs) {
|
|
|
553
530
|
if (totalDurationMs === 0) return 0;
|
|
554
531
|
return durationMs / totalDurationMs;
|
|
555
532
|
}
|
|
556
|
-
|
|
557
533
|
//#endregion
|
|
558
|
-
//#region src/components/observability/TraceSearch/
|
|
534
|
+
//#region src/components/observability/TraceSearch/SearchForm.tsx
|
|
535
|
+
/**
|
|
536
|
+
* SearchForm - Jaeger-style sidebar search form for trace filtering.
|
|
537
|
+
* Owns its own form state; parent only receives values on submit.
|
|
538
|
+
*/
|
|
559
539
|
const LOOKBACK_OPTIONS$1 = [
|
|
560
540
|
{
|
|
561
541
|
label: "Last 5 Minutes",
|
|
562
|
-
|
|
542
|
+
value: "5m"
|
|
563
543
|
},
|
|
564
544
|
{
|
|
565
545
|
label: "Last 15 Minutes",
|
|
566
|
-
|
|
546
|
+
value: "15m"
|
|
567
547
|
},
|
|
568
548
|
{
|
|
569
549
|
label: "Last 30 Minutes",
|
|
570
|
-
|
|
550
|
+
value: "30m"
|
|
571
551
|
},
|
|
572
552
|
{
|
|
573
553
|
label: "Last 1 Hour",
|
|
574
|
-
|
|
554
|
+
value: "1h"
|
|
575
555
|
},
|
|
576
556
|
{
|
|
577
557
|
label: "Last 2 Hours",
|
|
578
|
-
|
|
558
|
+
value: "2h"
|
|
579
559
|
},
|
|
580
560
|
{
|
|
581
561
|
label: "Last 6 Hours",
|
|
582
|
-
|
|
562
|
+
value: "6h"
|
|
583
563
|
},
|
|
584
564
|
{
|
|
585
565
|
label: "Last 12 Hours",
|
|
586
|
-
|
|
566
|
+
value: "12h"
|
|
587
567
|
},
|
|
588
568
|
{
|
|
589
569
|
label: "Last 24 Hours",
|
|
590
|
-
|
|
570
|
+
value: "24h"
|
|
591
571
|
}
|
|
592
572
|
];
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const [
|
|
596
|
-
const [
|
|
597
|
-
const [
|
|
598
|
-
const [
|
|
599
|
-
const [
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
573
|
+
const inputClass = "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground";
|
|
574
|
+
function SearchForm({ services, operations, initialValues, onSubmit, isLoading }) {
|
|
575
|
+
const [service, setService] = (0, react.useState)(initialValues?.service ?? "");
|
|
576
|
+
const [operation, setOperation] = (0, react.useState)(initialValues?.operation ?? "");
|
|
577
|
+
const [tags, setTags] = (0, react.useState)(initialValues?.tags ?? "");
|
|
578
|
+
const [lookback, setLookback] = (0, react.useState)(initialValues?.lookback ?? "");
|
|
579
|
+
const [minDuration, setMinDuration] = (0, react.useState)(initialValues?.minDuration ?? "");
|
|
580
|
+
const [maxDuration, setMaxDuration] = (0, react.useState)(initialValues?.maxDuration ?? "");
|
|
581
|
+
const [limit, setLimit] = (0, react.useState)(initialValues?.limit ?? 20);
|
|
582
|
+
(0, react.useEffect)(() => {
|
|
583
|
+
if (initialValues?.service != null) setService(initialValues.service);
|
|
584
|
+
}, [initialValues?.service]);
|
|
585
|
+
const handleSubmit = () => {
|
|
586
|
+
onSubmit({
|
|
587
|
+
service,
|
|
588
|
+
operation,
|
|
589
|
+
tags,
|
|
590
|
+
lookback,
|
|
591
|
+
minDuration,
|
|
592
|
+
maxDuration,
|
|
606
593
|
limit
|
|
607
594
|
});
|
|
608
595
|
};
|
|
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 ? "▲" : "▼"
|
|
596
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
597
|
+
className: "space-y-4",
|
|
598
|
+
children: [
|
|
599
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
600
|
+
className: "text-sm font-semibold text-foreground uppercase tracking-wider",
|
|
601
|
+
children: "Search"
|
|
602
|
+
}),
|
|
603
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
604
|
+
className: "block space-y-1",
|
|
605
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
606
|
+
className: "text-xs text-muted-foreground",
|
|
607
|
+
children: "Service"
|
|
608
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
609
|
+
value: service,
|
|
610
|
+
onChange: (e) => setService(e.target.value),
|
|
611
|
+
className: inputClass,
|
|
612
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
613
|
+
value: "",
|
|
614
|
+
children: "All Services"
|
|
615
|
+
}), services.map((s) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
616
|
+
value: s,
|
|
617
|
+
children: s
|
|
618
|
+
}, s))]
|
|
633
619
|
})]
|
|
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"
|
|
620
|
+
}),
|
|
621
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
622
|
+
className: "block space-y-1",
|
|
623
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
624
|
+
className: "text-xs text-muted-foreground",
|
|
625
|
+
children: "Operation"
|
|
626
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
627
|
+
value: operation,
|
|
628
|
+
onChange: (e) => setOperation(e.target.value),
|
|
629
|
+
className: inputClass,
|
|
630
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
631
|
+
value: "",
|
|
632
|
+
children: "All Operations"
|
|
633
|
+
}), operations.map((op) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
634
|
+
value: op,
|
|
635
|
+
children: op
|
|
636
|
+
}, op))]
|
|
723
637
|
})]
|
|
724
|
-
})
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
638
|
+
}),
|
|
639
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
640
|
+
className: "block space-y-1",
|
|
641
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
642
|
+
className: "text-xs text-muted-foreground",
|
|
643
|
+
children: "Tags"
|
|
644
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
|
|
645
|
+
value: tags,
|
|
646
|
+
onChange: (e) => setTags(e.target.value),
|
|
647
|
+
placeholder: "key=value key2=\"quoted value\"",
|
|
648
|
+
rows: 3,
|
|
649
|
+
className: `${inputClass} placeholder:text-muted-foreground/50 resize-y`
|
|
650
|
+
})]
|
|
651
|
+
}),
|
|
652
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
653
|
+
className: "block space-y-1",
|
|
654
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
655
|
+
className: "text-xs text-muted-foreground",
|
|
656
|
+
children: "Lookback"
|
|
657
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
|
|
658
|
+
value: lookback,
|
|
659
|
+
onChange: (e) => setLookback(e.target.value),
|
|
660
|
+
className: inputClass,
|
|
661
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
662
|
+
value: "",
|
|
663
|
+
children: "All time"
|
|
664
|
+
}), LOOKBACK_OPTIONS$1.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
665
|
+
value: opt.value,
|
|
666
|
+
children: opt.label
|
|
667
|
+
}, opt.value))]
|
|
668
|
+
})]
|
|
669
|
+
}),
|
|
670
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
671
|
+
className: "grid grid-cols-2 gap-2",
|
|
672
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
673
|
+
className: "block space-y-1",
|
|
674
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
675
|
+
className: "text-xs text-muted-foreground",
|
|
676
|
+
children: "Min Duration"
|
|
677
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
678
|
+
type: "text",
|
|
679
|
+
placeholder: "e.g. 100ms",
|
|
680
|
+
value: minDuration,
|
|
681
|
+
onChange: (e) => setMinDuration(e.target.value),
|
|
682
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
683
|
+
})]
|
|
684
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
685
|
+
className: "block space-y-1",
|
|
686
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
687
|
+
className: "text-xs text-muted-foreground",
|
|
688
|
+
children: "Max Duration"
|
|
689
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
690
|
+
type: "text",
|
|
691
|
+
placeholder: "e.g. 5s",
|
|
692
|
+
value: maxDuration,
|
|
693
|
+
onChange: (e) => setMaxDuration(e.target.value),
|
|
694
|
+
className: `${inputClass} placeholder:text-muted-foreground/50`
|
|
695
|
+
})]
|
|
696
|
+
})]
|
|
697
|
+
}),
|
|
698
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
699
|
+
className: "block space-y-1",
|
|
700
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
701
|
+
className: "text-xs text-muted-foreground",
|
|
702
|
+
children: "Limit"
|
|
703
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
704
|
+
type: "number",
|
|
705
|
+
min: 1,
|
|
706
|
+
max: 1e3,
|
|
707
|
+
value: limit,
|
|
708
|
+
onChange: (e) => {
|
|
709
|
+
const n = Number(e.target.value);
|
|
710
|
+
setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
|
|
711
|
+
},
|
|
712
|
+
className: inputClass
|
|
713
|
+
})]
|
|
714
|
+
}),
|
|
715
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
716
|
+
onClick: handleSubmit,
|
|
717
|
+
disabled: isLoading,
|
|
718
|
+
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",
|
|
719
|
+
children: isLoading ? "Searching..." : "Find Traces"
|
|
720
|
+
})
|
|
721
|
+
]
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
//#endregion
|
|
725
|
+
//#region src/components/observability/TraceSearch/ScatterPlot.tsx
|
|
726
|
+
/**
|
|
727
|
+
* ScatterPlot - Scatter chart showing trace duration vs timestamp.
|
|
728
|
+
*/
|
|
729
|
+
function CustomTooltip$1({ active, payload }) {
|
|
730
|
+
if (!active || !payload?.[0]) return null;
|
|
731
|
+
const d = payload[0].payload;
|
|
732
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
733
|
+
className: "bg-background border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
734
|
+
children: [
|
|
735
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
736
|
+
className: "font-medium text-foreground",
|
|
743
737
|
children: [
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
738
|
+
d.serviceName,
|
|
739
|
+
": ",
|
|
740
|
+
d.rootSpanName
|
|
741
|
+
]
|
|
742
|
+
}),
|
|
743
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
744
|
+
className: "text-muted-foreground mt-1",
|
|
745
|
+
children: [
|
|
746
|
+
d.spanCount,
|
|
747
|
+
" span",
|
|
748
|
+
d.spanCount !== 1 ? "s" : "",
|
|
749
|
+
" ·",
|
|
750
|
+
" ",
|
|
751
|
+
formatDuration(d.y)
|
|
752
|
+
]
|
|
753
|
+
}),
|
|
754
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
755
|
+
className: "text-muted-foreground",
|
|
756
|
+
children: formatTimestamp$1(d.x)
|
|
757
|
+
})
|
|
758
|
+
]
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
function ScatterPlot({ traces, onSelectTrace }) {
|
|
762
|
+
const data = (0, react.useMemo)(() => traces.map((t) => ({
|
|
763
|
+
x: t.timestampMs,
|
|
764
|
+
y: t.durationMs,
|
|
765
|
+
traceId: t.traceId,
|
|
766
|
+
serviceName: t.serviceName,
|
|
767
|
+
rootSpanName: t.rootSpanName,
|
|
768
|
+
spanCount: t.spanCount,
|
|
769
|
+
hasError: t.errorCount > 0
|
|
770
|
+
})), [traces]);
|
|
771
|
+
const handleClick = (0, react.useCallback)((entry) => {
|
|
772
|
+
const payload = entry?.payload;
|
|
773
|
+
if (payload?.traceId) onSelectTrace(payload.traceId);
|
|
774
|
+
}, [onSelectTrace]);
|
|
775
|
+
if (traces.length === 0) return null;
|
|
776
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
777
|
+
className: "border border-border rounded-lg p-4 bg-background",
|
|
778
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.ResponsiveContainer, {
|
|
779
|
+
width: "100%",
|
|
780
|
+
height: 200,
|
|
781
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(recharts.ScatterChart, {
|
|
782
|
+
margin: {
|
|
783
|
+
top: 8,
|
|
784
|
+
right: 8,
|
|
785
|
+
bottom: 4,
|
|
786
|
+
left: 0
|
|
787
|
+
},
|
|
788
|
+
children: [
|
|
789
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.CartesianGrid, {
|
|
790
|
+
strokeDasharray: "3 3",
|
|
791
|
+
stroke: "hsl(var(--border))",
|
|
792
|
+
opacity: .4
|
|
763
793
|
}),
|
|
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
|
-
]
|
|
794
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.XAxis, {
|
|
795
|
+
dataKey: "x",
|
|
796
|
+
type: "number",
|
|
797
|
+
domain: ["dataMin", "dataMax"],
|
|
798
|
+
tickFormatter: (v) => {
|
|
799
|
+
const d = new Date(v);
|
|
800
|
+
return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
|
|
801
|
+
},
|
|
802
|
+
tick: {
|
|
803
|
+
fontSize: 11,
|
|
804
|
+
fill: "hsl(var(--muted-foreground))"
|
|
805
|
+
},
|
|
806
|
+
stroke: "hsl(var(--border))",
|
|
807
|
+
name: "Time"
|
|
798
808
|
}),
|
|
799
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
800
|
-
|
|
801
|
-
|
|
809
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.YAxis, {
|
|
810
|
+
dataKey: "y",
|
|
811
|
+
type: "number",
|
|
812
|
+
tickFormatter: (v) => formatDuration(v),
|
|
813
|
+
tick: {
|
|
814
|
+
fontSize: 11,
|
|
815
|
+
fill: "hsl(var(--muted-foreground))"
|
|
816
|
+
},
|
|
817
|
+
stroke: "hsl(var(--border))",
|
|
818
|
+
name: "Duration",
|
|
819
|
+
width: 70
|
|
820
|
+
}),
|
|
821
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.Tooltip, { content: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CustomTooltip$1, {}) }),
|
|
822
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.Scatter, {
|
|
823
|
+
data,
|
|
824
|
+
onClick: handleClick,
|
|
825
|
+
cursor: "pointer",
|
|
826
|
+
children: data.map((point, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(recharts.Cell, {
|
|
827
|
+
fill: point.hasError ? "#ef4444" : getServiceColor(point.serviceName),
|
|
828
|
+
stroke: point.hasError ? "#ef4444" : "none",
|
|
829
|
+
strokeWidth: point.hasError ? 2 : 0
|
|
830
|
+
}, i))
|
|
802
831
|
})
|
|
803
832
|
]
|
|
804
|
-
}
|
|
833
|
+
})
|
|
805
834
|
})
|
|
806
|
-
|
|
835
|
+
});
|
|
807
836
|
}
|
|
808
|
-
|
|
809
837
|
//#endregion
|
|
810
|
-
//#region src/components/observability/
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
838
|
+
//#region src/components/observability/TraceSearch/SortDropdown.tsx
|
|
839
|
+
const SORT_OPTIONS = [
|
|
840
|
+
{
|
|
841
|
+
value: "recent",
|
|
842
|
+
label: "Most Recent"
|
|
843
|
+
},
|
|
844
|
+
{
|
|
845
|
+
value: "longest",
|
|
846
|
+
label: "Longest First"
|
|
847
|
+
},
|
|
848
|
+
{
|
|
849
|
+
value: "shortest",
|
|
850
|
+
label: "Shortest First"
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
value: "mostSpans",
|
|
854
|
+
label: "Most Spans"
|
|
855
|
+
},
|
|
856
|
+
{
|
|
857
|
+
value: "leastSpans",
|
|
858
|
+
label: "Least Spans"
|
|
819
859
|
}
|
|
820
|
-
|
|
821
|
-
|
|
860
|
+
];
|
|
861
|
+
function SortDropdown({ value, onChange }) {
|
|
862
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
|
|
863
|
+
value,
|
|
864
|
+
onChange: (e) => onChange(e.target.value),
|
|
865
|
+
className: "bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
|
|
866
|
+
children: SORT_OPTIONS.map((opt) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
|
|
867
|
+
value: opt.value,
|
|
868
|
+
children: opt.label
|
|
869
|
+
}, opt.value))
|
|
870
|
+
});
|
|
822
871
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
return
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/components/observability/TraceSearch/DurationBar.tsx
|
|
874
|
+
/**
|
|
875
|
+
* DurationBar - Horizontal bar showing relative trace duration.
|
|
876
|
+
*/
|
|
877
|
+
function DurationBar({ durationMs, maxDurationMs, color }) {
|
|
878
|
+
const rawPct = maxDurationMs > 0 ? durationMs / maxDurationMs * 100 : 0;
|
|
879
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
880
|
+
className: "flex items-center gap-2",
|
|
881
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
882
|
+
className: "flex-1 h-2 bg-muted/30 rounded overflow-hidden",
|
|
883
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
884
|
+
className: "h-full rounded",
|
|
885
|
+
style: {
|
|
886
|
+
width: `${durationMs <= 0 ? 0 : Math.min(Math.max(rawPct, 1), 100)}%`,
|
|
887
|
+
backgroundColor: color,
|
|
888
|
+
opacity: .7
|
|
889
|
+
}
|
|
890
|
+
})
|
|
891
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
892
|
+
className: "text-xs text-foreground/80 shrink-0 w-16 text-right font-mono",
|
|
893
|
+
children: formatDuration(durationMs)
|
|
894
|
+
})]
|
|
895
|
+
});
|
|
831
896
|
}
|
|
832
|
-
|
|
833
897
|
//#endregion
|
|
834
|
-
//#region src/components/observability/
|
|
835
|
-
function
|
|
836
|
-
const
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
898
|
+
//#region src/components/observability/TraceSearch/index.tsx
|
|
899
|
+
function sortTraces(traces, sort) {
|
|
900
|
+
const sorted = [...traces];
|
|
901
|
+
switch (sort) {
|
|
902
|
+
case "longest": return sorted.sort((a, b) => b.durationMs - a.durationMs);
|
|
903
|
+
case "shortest": return sorted.sort((a, b) => a.durationMs - b.durationMs);
|
|
904
|
+
case "mostSpans": return sorted.sort((a, b) => b.spanCount - a.spanCount);
|
|
905
|
+
case "leastSpans": return sorted.sort((a, b) => a.spanCount - b.spanCount);
|
|
906
|
+
default: return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function TraceSearch({ services = [], service, operations = [], traces, isLoading, error, onSelectTrace, onSearch, onCompare, sort: controlledSort, onSortChange }) {
|
|
910
|
+
const [internalSort, setInternalSort] = (0, react.useState)("recent");
|
|
911
|
+
const currentSort = controlledSort ?? internalSort;
|
|
912
|
+
const handleSortChange = (s) => {
|
|
913
|
+
if (onSortChange) onSortChange(s);
|
|
914
|
+
else setInternalSort(s);
|
|
849
915
|
};
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
916
|
+
const [selected, setSelected] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
917
|
+
const toggleSelected = (traceId) => {
|
|
918
|
+
setSelected((prev) => {
|
|
919
|
+
const next = new Set(prev);
|
|
920
|
+
if (next.has(traceId)) next.delete(traceId);
|
|
921
|
+
else {
|
|
922
|
+
if (next.size >= 2) return prev;
|
|
923
|
+
next.add(traceId);
|
|
924
|
+
}
|
|
925
|
+
return next;
|
|
926
|
+
});
|
|
927
|
+
};
|
|
928
|
+
const handleFormSubmit = (values) => {
|
|
929
|
+
onSearch?.({
|
|
930
|
+
service: values.service || void 0,
|
|
931
|
+
operation: values.operation || void 0,
|
|
932
|
+
tags: values.tags || void 0,
|
|
933
|
+
lookback: values.lookback || void 0,
|
|
934
|
+
minDuration: values.minDuration || void 0,
|
|
935
|
+
maxDuration: values.maxDuration || void 0,
|
|
936
|
+
limit: values.limit
|
|
937
|
+
});
|
|
938
|
+
};
|
|
939
|
+
const sortedTraces = (0, react.useMemo)(() => sortTraces(traces, currentSort), [traces, currentSort]);
|
|
940
|
+
const maxDurationMs = (0, react.useMemo)(() => Math.max(...traces.map((t) => t.durationMs), 0), [traces]);
|
|
941
|
+
const selectedArr = Array.from(selected);
|
|
942
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
943
|
+
className: "flex gap-6 min-h-0",
|
|
944
|
+
children: [onSearch && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
945
|
+
className: "w-72 shrink-0 border border-border rounded-lg p-4 self-start",
|
|
946
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SearchForm, {
|
|
947
|
+
services,
|
|
948
|
+
operations,
|
|
949
|
+
initialValues: { service },
|
|
950
|
+
onSubmit: handleFormSubmit,
|
|
951
|
+
isLoading
|
|
952
|
+
})
|
|
953
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
954
|
+
className: "flex-1 min-w-0 space-y-4",
|
|
955
|
+
children: [
|
|
956
|
+
traces.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScatterPlot, {
|
|
957
|
+
traces,
|
|
958
|
+
onSelectTrace
|
|
959
|
+
}),
|
|
960
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
961
|
+
className: "flex items-center justify-between gap-2",
|
|
962
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
963
|
+
className: "text-sm text-muted-foreground",
|
|
964
|
+
children: [
|
|
965
|
+
traces.length,
|
|
966
|
+
" Trace",
|
|
967
|
+
traces.length !== 1 ? "s" : ""
|
|
968
|
+
]
|
|
969
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
970
|
+
className: "flex items-center gap-2",
|
|
971
|
+
children: [onCompare && selected.size === 2 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
972
|
+
onClick: () => onCompare(selectedArr),
|
|
973
|
+
className: "px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
|
|
974
|
+
children: "Compare"
|
|
975
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SortDropdown, {
|
|
976
|
+
value: currentSort,
|
|
977
|
+
onChange: handleSortChange
|
|
978
|
+
})]
|
|
979
|
+
})]
|
|
980
|
+
}),
|
|
981
|
+
isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
982
|
+
className: "flex items-center gap-2 text-muted-foreground py-8",
|
|
983
|
+
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..."]
|
|
984
|
+
}),
|
|
985
|
+
error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
986
|
+
className: "text-red-400 py-4",
|
|
987
|
+
children: ["Error loading traces: ", error.message]
|
|
988
|
+
}),
|
|
989
|
+
!isLoading && !error && traces.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
990
|
+
className: "text-muted-foreground py-8",
|
|
991
|
+
children: "No traces found"
|
|
992
|
+
}),
|
|
993
|
+
sortedTraces.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
994
|
+
className: "space-y-2",
|
|
995
|
+
children: sortedTraces.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
996
|
+
className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
|
|
997
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
998
|
+
className: "flex items-center gap-2",
|
|
999
|
+
children: [onCompare && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1000
|
+
type: "checkbox",
|
|
1001
|
+
checked: selected.has(t.traceId),
|
|
1002
|
+
onChange: () => toggleSelected(t.traceId),
|
|
1003
|
+
onClick: (e) => e.stopPropagation(),
|
|
1004
|
+
className: "shrink-0",
|
|
1005
|
+
disabled: !selected.has(t.traceId) && selected.size >= 2
|
|
1006
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1007
|
+
className: "flex-1 min-w-0",
|
|
1008
|
+
onClick: () => onSelectTrace(t.traceId),
|
|
1009
|
+
children: [
|
|
1010
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1011
|
+
className: "flex items-baseline justify-between gap-2",
|
|
1012
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1013
|
+
className: "flex items-baseline gap-1.5 min-w-0",
|
|
1014
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1015
|
+
className: "font-medium text-foreground truncate",
|
|
1016
|
+
children: [
|
|
1017
|
+
t.serviceName,
|
|
1018
|
+
": ",
|
|
1019
|
+
t.rootSpanName
|
|
1020
|
+
]
|
|
1021
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1022
|
+
className: "text-xs font-mono text-muted-foreground shrink-0",
|
|
1023
|
+
children: t.traceId.slice(0, 7)
|
|
1024
|
+
})]
|
|
1025
|
+
})
|
|
1026
|
+
}),
|
|
1027
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1028
|
+
className: "mt-1.5",
|
|
1029
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DurationBar, {
|
|
1030
|
+
durationMs: t.durationMs,
|
|
1031
|
+
maxDurationMs,
|
|
1032
|
+
color: getServiceColor(t.serviceName)
|
|
1033
|
+
})
|
|
1034
|
+
}),
|
|
1035
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1036
|
+
className: "flex items-center flex-wrap gap-1.5 mt-1.5",
|
|
1037
|
+
children: [
|
|
1038
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1039
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
|
|
1040
|
+
children: [
|
|
1041
|
+
t.spanCount,
|
|
1042
|
+
" Span",
|
|
1043
|
+
t.spanCount !== 1 ? "s" : ""
|
|
1044
|
+
]
|
|
1045
|
+
}),
|
|
1046
|
+
t.errorCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1047
|
+
className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
|
|
1048
|
+
children: [
|
|
1049
|
+
t.errorCount,
|
|
1050
|
+
" Error",
|
|
1051
|
+
t.errorCount !== 1 ? "s" : ""
|
|
1052
|
+
]
|
|
1053
|
+
}),
|
|
1054
|
+
t.services.map((svc) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1055
|
+
className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
|
|
1056
|
+
style: {
|
|
1057
|
+
backgroundColor: `${getServiceColor(svc.name)}20`,
|
|
1058
|
+
color: getServiceColor(svc.name)
|
|
1059
|
+
},
|
|
1060
|
+
children: [
|
|
1061
|
+
svc.hasError && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
|
|
1062
|
+
svc.name,
|
|
1063
|
+
" (",
|
|
1064
|
+
svc.count,
|
|
1065
|
+
")"
|
|
1066
|
+
]
|
|
1067
|
+
}, svc.name))
|
|
1068
|
+
]
|
|
1069
|
+
}),
|
|
1070
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1071
|
+
className: "text-xs text-muted-foreground mt-1 text-right",
|
|
1072
|
+
children: formatTimestamp$1(t.timestampMs)
|
|
1073
|
+
})
|
|
1074
|
+
]
|
|
1075
|
+
})]
|
|
1076
|
+
})
|
|
1077
|
+
}, t.traceId))
|
|
1078
|
+
})
|
|
1079
|
+
]
|
|
1080
|
+
})]
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
//#endregion
|
|
1084
|
+
//#region src/components/observability/utils/flatten-tree.ts
|
|
1085
|
+
function flattenTree(rootSpans, collapsedIds) {
|
|
1086
|
+
const result = [];
|
|
1087
|
+
function traverse(span, level) {
|
|
1088
|
+
result.push({
|
|
1089
|
+
span,
|
|
1090
|
+
level
|
|
1091
|
+
});
|
|
1092
|
+
if (!collapsedIds.has(span.spanId)) span.children.forEach((child) => traverse(child, level + 1));
|
|
1093
|
+
}
|
|
1094
|
+
rootSpans.forEach((root) => traverse(root, 0));
|
|
1095
|
+
return result;
|
|
1096
|
+
}
|
|
1097
|
+
/** Flatten all spans (ignoring collapse state) with depth. */
|
|
1098
|
+
function flattenAllSpans(rootSpans) {
|
|
1099
|
+
return flattenTree(rootSpans, /* @__PURE__ */ new Set());
|
|
1100
|
+
}
|
|
1101
|
+
function spanMatchesSearch(span, query) {
|
|
1102
|
+
const q = query.toLowerCase();
|
|
1103
|
+
if (span.name.toLowerCase().includes(q)) return true;
|
|
1104
|
+
if (span.serviceName.toLowerCase().includes(q)) return true;
|
|
1105
|
+
for (const val of Object.values(span.attributes)) if (String(val).toLowerCase().includes(q)) return true;
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
function getAllSpanIds(rootSpans) {
|
|
1109
|
+
const ids = [];
|
|
1110
|
+
function traverse(span) {
|
|
1111
|
+
ids.push(span.spanId);
|
|
1112
|
+
span.children.forEach((child) => traverse(child));
|
|
1113
|
+
}
|
|
1114
|
+
rootSpans.forEach((root) => traverse(root));
|
|
1115
|
+
return ids;
|
|
1116
|
+
}
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region src/components/observability/TraceTimeline/TraceHeader.tsx
|
|
1119
|
+
function computeMaxDepth(spans) {
|
|
1120
|
+
let max = 0;
|
|
1121
|
+
function walk(nodes, depth) {
|
|
1122
|
+
for (const node of nodes) {
|
|
1123
|
+
if (depth > max) max = depth;
|
|
1124
|
+
walk(node.children, depth + 1);
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
walk(spans, 1);
|
|
1128
|
+
return max;
|
|
1129
|
+
}
|
|
1130
|
+
function TraceHeader({ trace, services = [], onHeaderToggle, isCollapsed = false }) {
|
|
1131
|
+
const [copied, setCopied] = (0, react.useState)(false);
|
|
1132
|
+
const rootSpan = trace.rootSpans[0];
|
|
1133
|
+
const rootServiceName = rootSpan?.serviceName ?? "unknown";
|
|
1134
|
+
const rootSpanName = rootSpan?.name ?? "unknown";
|
|
1135
|
+
const totalDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
1136
|
+
const maxDepth = computeMaxDepth(trace.rootSpans);
|
|
1137
|
+
const handleCopyTraceId = async () => {
|
|
1138
|
+
try {
|
|
1139
|
+
await navigator.clipboard.writeText(trace.traceId);
|
|
1140
|
+
setCopied(true);
|
|
1141
|
+
setTimeout(() => setCopied(false), 2e3);
|
|
1142
|
+
} catch (err) {
|
|
1143
|
+
console.error("Failed to copy trace ID:", err);
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1147
|
+
className: "bg-background border-b border-border px-4 py-3",
|
|
1148
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1149
|
+
className: "flex items-center gap-2 mb-1",
|
|
1150
|
+
children: [onHeaderToggle && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1151
|
+
onClick: onHeaderToggle,
|
|
1152
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground",
|
|
1153
|
+
"aria-label": isCollapsed ? "Expand header" : "Collapse header",
|
|
1154
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1155
|
+
className: `w-4 h-4 transition-transform ${isCollapsed ? "-rotate-90" : ""}`,
|
|
1156
|
+
fill: "none",
|
|
1157
|
+
stroke: "currentColor",
|
|
1158
|
+
viewBox: "0 0 24 24",
|
|
1159
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1160
|
+
strokeLinecap: "round",
|
|
1161
|
+
strokeLinejoin: "round",
|
|
1162
|
+
strokeWidth: 2,
|
|
1163
|
+
d: "M19 9l-7 7-7-7"
|
|
1164
|
+
})
|
|
1165
|
+
})
|
|
1166
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1167
|
+
className: "text-sm font-semibold text-foreground",
|
|
1168
|
+
children: [
|
|
1169
|
+
rootServiceName,
|
|
1170
|
+
": ",
|
|
1171
|
+
rootSpanName
|
|
1172
|
+
]
|
|
1173
|
+
})]
|
|
1174
|
+
}), !isCollapsed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1175
|
+
className: "flex items-center gap-6 flex-wrap",
|
|
1176
|
+
children: [
|
|
1177
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1178
|
+
className: "flex items-center gap-2",
|
|
1179
|
+
children: [
|
|
1180
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1181
|
+
className: "text-xs font-semibold text-muted-foreground",
|
|
1182
|
+
children: "Trace ID:"
|
|
1183
|
+
}),
|
|
1184
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1185
|
+
onClick: handleCopyTraceId,
|
|
864
1186
|
className: "text-sm font-mono bg-muted px-2 py-1 rounded hover:bg-muted/80 transition-colors text-foreground",
|
|
865
1187
|
title: "Click to copy",
|
|
866
1188
|
children: [trace.traceId.slice(0, 16), "..."]
|
|
@@ -875,43 +1197,30 @@ function TraceHeader({ trace }) {
|
|
|
875
1197
|
className: "flex items-center gap-2",
|
|
876
1198
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
877
1199
|
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
|
-
]
|
|
1200
|
+
children: "Duration:"
|
|
1201
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1202
|
+
className: "text-sm font-medium text-foreground",
|
|
1203
|
+
children: formatDuration(totalDuration)
|
|
895
1204
|
})]
|
|
896
1205
|
}),
|
|
897
1206
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
898
1207
|
className: "flex items-center gap-2",
|
|
899
1208
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
900
1209
|
className: "text-xs font-semibold text-muted-foreground",
|
|
901
|
-
children: "
|
|
1210
|
+
children: "Spans:"
|
|
902
1211
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
903
1212
|
className: "text-sm font-medium text-foreground",
|
|
904
|
-
children:
|
|
1213
|
+
children: trace.totalSpanCount
|
|
905
1214
|
})]
|
|
906
1215
|
}),
|
|
907
1216
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
908
1217
|
className: "flex items-center gap-2",
|
|
909
1218
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
910
1219
|
className: "text-xs font-semibold text-muted-foreground",
|
|
911
|
-
children: "
|
|
1220
|
+
children: "Depth:"
|
|
912
1221
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
913
1222
|
className: "text-sm font-medium text-foreground",
|
|
914
|
-
children:
|
|
1223
|
+
children: maxDepth
|
|
915
1224
|
})]
|
|
916
1225
|
}),
|
|
917
1226
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -925,10 +1234,21 @@ function TraceHeader({ trace }) {
|
|
|
925
1234
|
})]
|
|
926
1235
|
})
|
|
927
1236
|
]
|
|
928
|
-
})
|
|
1237
|
+
}), services.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1238
|
+
className: "flex items-center gap-3 mt-2 flex-wrap",
|
|
1239
|
+
children: services.map((svc) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1240
|
+
className: "flex items-center gap-1.5",
|
|
1241
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1242
|
+
className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
|
|
1243
|
+
style: { backgroundColor: getServiceColor(svc) }
|
|
1244
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1245
|
+
className: "text-xs text-muted-foreground",
|
|
1246
|
+
children: svc
|
|
1247
|
+
})]
|
|
1248
|
+
}, svc))
|
|
1249
|
+
})] })]
|
|
929
1250
|
});
|
|
930
1251
|
}
|
|
931
|
-
|
|
932
1252
|
//#endregion
|
|
933
1253
|
//#region src/components/observability/TraceTimeline/Tooltip.tsx
|
|
934
1254
|
function Tooltip$2({ content, children }) {
|
|
@@ -958,7 +1278,6 @@ function Tooltip$2({ content, children }) {
|
|
|
958
1278
|
children: content
|
|
959
1279
|
}), document.body)] });
|
|
960
1280
|
}
|
|
961
|
-
|
|
962
1281
|
//#endregion
|
|
963
1282
|
//#region src/components/observability/TraceTimeline/TimelineBar.tsx
|
|
964
1283
|
function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
@@ -966,45 +1285,48 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
|
|
|
966
1285
|
const barColor = getSpanBarColor(span.serviceName, isError);
|
|
967
1286
|
const leftPercent = relativeStart * 100;
|
|
968
1287
|
const widthPercent = Math.max(.2, relativeDuration * 100);
|
|
1288
|
+
const isWide = widthPercent > 8;
|
|
1289
|
+
const tooltipText = `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`;
|
|
1290
|
+
const durationLabel = formatDuration(span.durationMs);
|
|
969
1291
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
970
1292
|
className: "relative h-full",
|
|
971
1293
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tooltip$2, {
|
|
972
|
-
content:
|
|
973
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1294
|
+
content: tooltipText,
|
|
1295
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
974
1296
|
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",
|
|
1297
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1298
|
+
className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity flex items-center",
|
|
977
1299
|
style: {
|
|
978
1300
|
left: `${leftPercent}%`,
|
|
979
1301
|
width: `max(2px, ${widthPercent}%)`,
|
|
980
1302
|
backgroundColor: barColor
|
|
981
|
-
}
|
|
982
|
-
|
|
1303
|
+
},
|
|
1304
|
+
children: isWide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1305
|
+
className: "text-[10px] font-mono text-white px-1 truncate",
|
|
1306
|
+
children: durationLabel
|
|
1307
|
+
})
|
|
1308
|
+
}), !isWide && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1309
|
+
className: "absolute top-1/2 -translate-y-1/2 text-[10px] font-mono text-muted-foreground whitespace-nowrap",
|
|
1310
|
+
style: { left: `calc(${leftPercent + widthPercent}% + 4px)` },
|
|
1311
|
+
children: durationLabel
|
|
1312
|
+
})]
|
|
983
1313
|
})
|
|
984
1314
|
})
|
|
985
1315
|
});
|
|
986
1316
|
}
|
|
987
|
-
|
|
988
1317
|
//#endregion
|
|
989
1318
|
//#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 }) {
|
|
1319
|
+
const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave, uiFind }) {
|
|
1003
1320
|
const hasChildren = span.children.length > 0;
|
|
1004
1321
|
const isError = span.status === "ERROR";
|
|
1005
|
-
const
|
|
1322
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1323
|
+
const isDimmed = uiFind ? !spanMatchesSearch(span, uiFind) : false;
|
|
1006
1324
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1007
1325
|
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" : ""}`,
|
|
1326
|
+
style: {
|
|
1327
|
+
borderLeft: `3px solid ${serviceColor}`,
|
|
1328
|
+
opacity: isDimmed ? .4 : 1
|
|
1329
|
+
},
|
|
1008
1330
|
onClick,
|
|
1009
1331
|
onMouseEnter,
|
|
1010
1332
|
onMouseLeave,
|
|
@@ -1059,7 +1381,8 @@ const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isS
|
|
|
1059
1381
|
})
|
|
1060
1382
|
}),
|
|
1061
1383
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1062
|
-
className: "text-xs
|
|
1384
|
+
className: "text-xs flex-shrink-0 mr-2 font-medium",
|
|
1385
|
+
style: { color: serviceColor },
|
|
1063
1386
|
children: span.serviceName
|
|
1064
1387
|
}),
|
|
1065
1388
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
@@ -1074,10 +1397,6 @@ const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isS
|
|
|
1074
1397
|
")"
|
|
1075
1398
|
]
|
|
1076
1399
|
}),
|
|
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
1400
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1082
1401
|
className: "text-xs text-muted-foreground flex-shrink-0 ml-2",
|
|
1083
1402
|
children: formatDuration(span.durationMs)
|
|
@@ -1093,7 +1412,6 @@ const SpanRow = (0, react.memo)(function SpanRow({ span, level, isCollapsed, isS
|
|
|
1093
1412
|
})]
|
|
1094
1413
|
});
|
|
1095
1414
|
});
|
|
1096
|
-
|
|
1097
1415
|
//#endregion
|
|
1098
1416
|
//#region src/components/observability/utils/attributes.ts
|
|
1099
1417
|
function formatAttributeValue(value) {
|
|
@@ -1112,500 +1430,203 @@ function formatSeriesLabel(labels) {
|
|
|
1112
1430
|
function isComplexValue(value) {
|
|
1113
1431
|
return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
|
|
1114
1432
|
}
|
|
1115
|
-
|
|
1116
1433
|
//#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",
|
|
1434
|
+
//#region src/components/observability/TraceTimeline/SpanDetailInline.tsx
|
|
1435
|
+
function CollapsibleSection({ title, count, children }) {
|
|
1436
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
1437
|
+
if (count === 0) return null;
|
|
1438
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
1439
|
+
className: "flex items-center gap-1 text-xs font-medium text-foreground hover:text-blue-600 dark:hover:text-blue-400 py-1",
|
|
1440
|
+
onClick: (e) => {
|
|
1441
|
+
e.stopPropagation();
|
|
1442
|
+
setOpen((p) => !p);
|
|
1443
|
+
},
|
|
1157
1444
|
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
|
-
})] })
|
|
1445
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1446
|
+
className: "w-3 text-center",
|
|
1447
|
+
children: open ? "▾" : "▸"
|
|
1448
|
+
}),
|
|
1449
|
+
title,
|
|
1450
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
1451
|
+
className: "text-muted-foreground",
|
|
1452
|
+
children: [
|
|
1453
|
+
"(",
|
|
1454
|
+
count,
|
|
1455
|
+
")"
|
|
1456
|
+
]
|
|
1457
|
+
})
|
|
1189
1458
|
]
|
|
1190
|
-
})
|
|
1459
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1460
|
+
className: "ml-4 mt-1 space-y-1",
|
|
1461
|
+
children
|
|
1462
|
+
})] });
|
|
1191
1463
|
}
|
|
1192
|
-
function
|
|
1193
|
-
const
|
|
1194
|
-
const formattedValue = formatAttributeValue(value);
|
|
1464
|
+
function KeyValueRow({ k, v }) {
|
|
1465
|
+
const formatted = formatAttributeValue(v);
|
|
1195
1466
|
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", {
|
|
1467
|
+
className: "flex gap-2 text-xs font-mono py-0.5",
|
|
1468
|
+
children: [
|
|
1469
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1470
|
+
className: "text-muted-foreground flex-shrink-0",
|
|
1471
|
+
children: k
|
|
1472
|
+
}),
|
|
1473
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1207
1474
|
className: "text-foreground",
|
|
1208
|
-
children:
|
|
1475
|
+
children: "="
|
|
1476
|
+
}),
|
|
1477
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1478
|
+
className: "text-foreground break-all",
|
|
1479
|
+
children: formatted
|
|
1209
1480
|
})
|
|
1210
|
-
|
|
1211
|
-
});
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
//#endregion
|
|
1215
|
-
//#region src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx
|
|
1216
|
-
function formatRelativeTime$1(eventTimeMs, spanStartMs) {
|
|
1217
|
-
const relativeMs = eventTimeMs - spanStartMs;
|
|
1218
|
-
return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
|
|
1219
|
-
}
|
|
1220
|
-
function EventsTab({ span }) {
|
|
1221
|
-
const [expandedEvents, setExpandedEvents] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
1222
|
-
const toggleEventExpanded = (index) => {
|
|
1223
|
-
setExpandedEvents((prev) => {
|
|
1224
|
-
const next = new Set(prev);
|
|
1225
|
-
if (next.has(index)) next.delete(index);
|
|
1226
|
-
else next.add(index);
|
|
1227
|
-
return next;
|
|
1228
|
-
});
|
|
1229
|
-
};
|
|
1230
|
-
if (!span.events || span.events.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1231
|
-
className: "text-sm text-muted-foreground text-center py-8",
|
|
1232
|
-
children: "No events available"
|
|
1233
|
-
});
|
|
1234
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1235
|
-
className: "space-y-3",
|
|
1236
|
-
children: span.events.map((event, index) => {
|
|
1237
|
-
const isExpanded = expandedEvents.has(index);
|
|
1238
|
-
const hasAttributes = event.attributes && Object.keys(event.attributes).length > 0;
|
|
1239
|
-
const relativeTime = formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs);
|
|
1240
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1241
|
-
className: "border border-border rounded-lg overflow-hidden",
|
|
1242
|
-
children: [
|
|
1243
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1244
|
-
className: "bg-muted p-3",
|
|
1245
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1246
|
-
className: "flex items-start justify-between gap-2",
|
|
1247
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1248
|
-
className: "flex-1 min-w-0",
|
|
1249
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1250
|
-
className: "font-medium text-sm text-foreground truncate",
|
|
1251
|
-
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
|
-
})]
|
|
1256
|
-
}), hasAttributes && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1257
|
-
onClick: () => toggleEventExpanded(index),
|
|
1258
|
-
className: "p-1 hover:bg-muted/80 rounded transition-colors",
|
|
1259
|
-
"aria-label": isExpanded ? "Collapse attributes" : "Expand attributes",
|
|
1260
|
-
"aria-expanded": isExpanded,
|
|
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
|
-
})
|
|
1275
|
-
}),
|
|
1276
|
-
hasAttributes && isExpanded && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1277
|
-
className: "p-3 bg-background border-t border-border",
|
|
1278
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1279
|
-
className: "text-xs font-semibold text-foreground mb-2",
|
|
1280
|
-
children: "Attributes"
|
|
1281
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1282
|
-
className: "space-y-2",
|
|
1283
|
-
children: Object.entries(event.attributes).map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1284
|
-
className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
|
|
1285
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1286
|
-
className: "font-mono font-medium text-foreground break-words",
|
|
1287
|
-
children: key
|
|
1288
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1289
|
-
className: "text-foreground break-words",
|
|
1290
|
-
children: typeof value === "object" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("pre", {
|
|
1291
|
-
className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
|
|
1292
|
-
children: formatAttributeValue(value)
|
|
1293
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: formatAttributeValue(value) })
|
|
1294
|
-
})]
|
|
1295
|
-
}, key))
|
|
1296
|
-
})]
|
|
1297
|
-
}),
|
|
1298
|
-
!hasAttributes && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1299
|
-
className: "px-3 pb-3 text-xs text-muted-foreground italic",
|
|
1300
|
-
children: "No attributes"
|
|
1301
|
-
})
|
|
1302
|
-
]
|
|
1303
|
-
}, index);
|
|
1304
|
-
})
|
|
1305
|
-
});
|
|
1306
|
-
}
|
|
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
|
-
})
|
|
1481
|
+
]
|
|
1466
1482
|
});
|
|
1467
1483
|
}
|
|
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);
|
|
1484
|
+
function SpanDetailInline({ span, traceStartMs }) {
|
|
1473
1485
|
const [copiedId, setCopiedId] = (0, react.useState)(false);
|
|
1474
|
-
const
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
const handleCopySpanId = (0, react.useCallback)(async () => {
|
|
1486
|
+
const serviceColor = getServiceColor(span.serviceName);
|
|
1487
|
+
const relativeStartMs = span.startTimeUnixMs - traceStartMs;
|
|
1488
|
+
const handleCopy = (0, react.useCallback)(async () => {
|
|
1478
1489
|
try {
|
|
1479
1490
|
await navigator.clipboard.writeText(span.spanId);
|
|
1480
1491
|
setCopiedId(true);
|
|
1481
1492
|
setTimeout(() => setCopiedId(false), 2e3);
|
|
1482
|
-
} catch
|
|
1483
|
-
console.error("Failed to copy span ID:", err);
|
|
1484
|
-
}
|
|
1493
|
+
} catch {}
|
|
1485
1494
|
}, [span.spanId]);
|
|
1495
|
+
const spanAttrs = Object.entries(span.attributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1496
|
+
const resourceAttrs = Object.entries(span.resourceAttributes).sort(([a], [b]) => a.localeCompare(b));
|
|
1486
1497
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1487
|
-
className: "
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
}, [onClose]),
|
|
1491
|
-
tabIndex: -1,
|
|
1492
|
-
role: "complementary",
|
|
1493
|
-
"aria-label": "Span details",
|
|
1498
|
+
className: "border-b border-border bg-muted/50 px-4 py-3",
|
|
1499
|
+
style: { borderLeft: `3px solid ${serviceColor}` },
|
|
1500
|
+
onClick: (e) => e.stopPropagation(),
|
|
1494
1501
|
children: [
|
|
1495
1502
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1496
|
-
className: "
|
|
1497
|
-
children: [
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
})
|
|
1503
|
+
className: "mb-2",
|
|
1504
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1505
|
+
className: "text-sm font-medium text-foreground",
|
|
1506
|
+
children: span.name
|
|
1507
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1508
|
+
className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground mt-1",
|
|
1509
|
+
children: [
|
|
1510
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Service: ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1511
|
+
className: "text-foreground",
|
|
1512
|
+
children: span.serviceName
|
|
1513
|
+
})] }),
|
|
1514
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1515
|
+
"Duration:",
|
|
1516
|
+
" ",
|
|
1517
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1518
|
+
className: "text-foreground",
|
|
1519
|
+
children: formatDuration(span.durationMs)
|
|
1519
1520
|
})
|
|
1520
|
-
})
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
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: [
|
|
1521
|
+
] }),
|
|
1522
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1523
|
+
"Start:",
|
|
1524
|
+
" ",
|
|
1533
1525
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1534
|
-
className: "text-
|
|
1535
|
-
children:
|
|
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
|
-
})
|
|
1526
|
+
className: "text-foreground",
|
|
1527
|
+
children: formatDuration(relativeStartMs)
|
|
1563
1528
|
})
|
|
1564
|
-
]
|
|
1565
|
-
|
|
1566
|
-
|
|
1529
|
+
] }),
|
|
1530
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: ["Kind: ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1531
|
+
className: "text-foreground",
|
|
1532
|
+
children: span.kind
|
|
1533
|
+
})] }),
|
|
1534
|
+
span.status !== "UNSET" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", { children: [
|
|
1535
|
+
"Status:",
|
|
1536
|
+
" ",
|
|
1537
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1538
|
+
className: span.status === "ERROR" ? "text-red-500" : "text-foreground",
|
|
1539
|
+
children: span.status
|
|
1540
|
+
})
|
|
1541
|
+
] })
|
|
1542
|
+
]
|
|
1543
|
+
})]
|
|
1567
1544
|
}),
|
|
1568
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
1569
|
-
className: "
|
|
1570
|
-
role: "tablist",
|
|
1571
|
-
"aria-label": "Span detail tabs",
|
|
1545
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1546
|
+
className: "space-y-1",
|
|
1572
1547
|
children: [
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1548
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1549
|
+
title: "Tags",
|
|
1550
|
+
count: spanAttrs.length,
|
|
1551
|
+
children: spanAttrs.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueRow, {
|
|
1552
|
+
k,
|
|
1553
|
+
v
|
|
1554
|
+
}, k))
|
|
1555
|
+
}),
|
|
1556
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1557
|
+
title: "Process",
|
|
1558
|
+
count: resourceAttrs.length,
|
|
1559
|
+
children: resourceAttrs.map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueRow, {
|
|
1560
|
+
k,
|
|
1561
|
+
v
|
|
1562
|
+
}, k))
|
|
1563
|
+
}),
|
|
1564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1565
|
+
title: "Events",
|
|
1566
|
+
count: span.events.length,
|
|
1567
|
+
children: span.events.map((event, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1568
|
+
className: "text-xs border-l-2 border-border pl-2 py-1.5 space-y-0.5",
|
|
1569
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1570
|
+
className: "flex items-center gap-2",
|
|
1571
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1572
|
+
className: "font-mono text-muted-foreground flex-shrink-0",
|
|
1573
|
+
children: formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs)
|
|
1574
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1575
|
+
className: "font-medium text-foreground",
|
|
1576
|
+
children: event.name
|
|
1577
|
+
})]
|
|
1578
|
+
}), Object.entries(event.attributes).map(([k, v]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyValueRow, {
|
|
1579
|
+
k,
|
|
1580
|
+
v
|
|
1581
|
+
}, k))]
|
|
1582
|
+
}, i))
|
|
1583
|
+
}),
|
|
1584
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CollapsibleSection, {
|
|
1585
|
+
title: "Links",
|
|
1586
|
+
count: span.links.length,
|
|
1587
|
+
children: span.links.map((link, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1588
|
+
className: "text-xs font-mono py-0.5",
|
|
1585
1589
|
children: [
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1590
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1591
|
+
className: "text-muted-foreground",
|
|
1592
|
+
children: "trace:"
|
|
1593
|
+
}),
|
|
1594
|
+
" ",
|
|
1595
|
+
link.traceId,
|
|
1596
|
+
" ",
|
|
1597
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1598
|
+
className: "text-muted-foreground",
|
|
1599
|
+
children: "span:"
|
|
1600
|
+
}),
|
|
1601
|
+
" ",
|
|
1602
|
+
link.spanId
|
|
1589
1603
|
]
|
|
1590
|
-
})
|
|
1591
|
-
}
|
|
1592
|
-
|
|
1604
|
+
}, i))
|
|
1605
|
+
})
|
|
1606
|
+
]
|
|
1593
1607
|
}),
|
|
1594
1608
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1595
|
-
className: "flex-
|
|
1609
|
+
className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
|
|
1596
1610
|
children: [
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1611
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1612
|
+
className: "text-xs text-muted-foreground",
|
|
1613
|
+
children: "SpanID:"
|
|
1614
|
+
}),
|
|
1615
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
1616
|
+
className: "text-xs font-mono text-foreground",
|
|
1617
|
+
children: span.spanId
|
|
1618
|
+
}),
|
|
1619
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1620
|
+
onClick: handleCopy,
|
|
1621
|
+
className: "text-xs text-muted-foreground hover:text-foreground",
|
|
1622
|
+
"aria-label": "Copy span ID",
|
|
1623
|
+
children: copiedId ? "✓" : "Copy"
|
|
1602
1624
|
})
|
|
1603
1625
|
]
|
|
1604
1626
|
})
|
|
1605
1627
|
]
|
|
1606
1628
|
});
|
|
1607
1629
|
}
|
|
1608
|
-
|
|
1609
1630
|
//#endregion
|
|
1610
1631
|
//#region src/components/observability/shared/LoadingSkeleton.tsx
|
|
1611
1632
|
function LoadingSkeleton() {
|
|
@@ -1647,7 +1668,6 @@ function LoadingSkeleton() {
|
|
|
1647
1668
|
})]
|
|
1648
1669
|
});
|
|
1649
1670
|
}
|
|
1650
|
-
|
|
1651
1671
|
//#endregion
|
|
1652
1672
|
//#region src/components/KeyboardShortcuts/context.ts
|
|
1653
1673
|
const noop = () => {};
|
|
@@ -1667,7 +1687,6 @@ function useRegisterShortcuts(id, group) {
|
|
|
1667
1687
|
unregister
|
|
1668
1688
|
]);
|
|
1669
1689
|
}
|
|
1670
|
-
|
|
1671
1690
|
//#endregion
|
|
1672
1691
|
//#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
|
|
1673
1692
|
function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
@@ -1724,7 +1743,6 @@ function ShortcutsHelpDialog({ open, onClose, groups }) {
|
|
|
1724
1743
|
})
|
|
1725
1744
|
});
|
|
1726
1745
|
}
|
|
1727
|
-
|
|
1728
1746
|
//#endregion
|
|
1729
1747
|
//#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
|
|
1730
1748
|
const GENERAL_GROUP = {
|
|
@@ -1735,8 +1753,8 @@ const GENERAL_GROUP = {
|
|
|
1735
1753
|
description: "Toggle shortcuts help"
|
|
1736
1754
|
},
|
|
1737
1755
|
{
|
|
1738
|
-
keys: ["Shift", "
|
|
1739
|
-
description: "
|
|
1756
|
+
keys: ["Shift", "T"],
|
|
1757
|
+
description: "Traces tab"
|
|
1740
1758
|
},
|
|
1741
1759
|
{
|
|
1742
1760
|
keys: ["Shift", "L"],
|
|
@@ -1767,8 +1785,8 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
|
|
|
1767
1785
|
}, []);
|
|
1768
1786
|
(0, react.useEffect)(() => {
|
|
1769
1787
|
function handleKeyDown(e) {
|
|
1770
|
-
|
|
1771
|
-
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable) return;
|
|
1788
|
+
if (!(e.target instanceof HTMLElement)) return;
|
|
1789
|
+
if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || e.target.isContentEditable) return;
|
|
1772
1790
|
if (e.shiftKey && e.key === "?") {
|
|
1773
1791
|
e.preventDefault();
|
|
1774
1792
|
setIsOpen((v) => !v);
|
|
@@ -1779,7 +1797,7 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
|
|
|
1779
1797
|
setIsOpen(false);
|
|
1780
1798
|
return;
|
|
1781
1799
|
}
|
|
1782
|
-
if (e.shiftKey && e.key === "
|
|
1800
|
+
if (e.shiftKey && e.key === "T") {
|
|
1783
1801
|
e.preventDefault();
|
|
1784
1802
|
onNavigateServices();
|
|
1785
1803
|
return;
|
|
@@ -1819,7 +1837,6 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
|
|
|
1819
1837
|
})]
|
|
1820
1838
|
});
|
|
1821
1839
|
}
|
|
1822
|
-
|
|
1823
1840
|
//#endregion
|
|
1824
1841
|
//#region src/components/observability/TraceTimeline/shortcuts.ts
|
|
1825
1842
|
const TRACE_VIEWER_SHORTCUTS = {
|
|
@@ -1871,7 +1888,914 @@ const TRACE_VIEWER_SHORTCUTS = {
|
|
|
1871
1888
|
}
|
|
1872
1889
|
]
|
|
1873
1890
|
};
|
|
1874
|
-
|
|
1891
|
+
//#endregion
|
|
1892
|
+
//#region src/components/observability/TraceTimeline/TimeRuler.tsx
|
|
1893
|
+
const TICK_COUNT = 5;
|
|
1894
|
+
function TimeRuler({ totalDurationMs, leftColumnWidth, offsetMs = 0 }) {
|
|
1895
|
+
const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => {
|
|
1896
|
+
const fraction = i / TICK_COUNT;
|
|
1897
|
+
return {
|
|
1898
|
+
label: formatDuration(offsetMs + totalDurationMs * fraction),
|
|
1899
|
+
percent: fraction * 100
|
|
1900
|
+
};
|
|
1901
|
+
});
|
|
1902
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1903
|
+
className: "flex border-b border-border bg-background",
|
|
1904
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1905
|
+
className: "flex-shrink-0",
|
|
1906
|
+
style: { width: leftColumnWidth }
|
|
1907
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1908
|
+
className: "flex-1 relative h-6 px-2",
|
|
1909
|
+
children: ticks.map((tick) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1910
|
+
className: "absolute top-0 h-full flex flex-col justify-end",
|
|
1911
|
+
style: { left: `${tick.percent}%` },
|
|
1912
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "h-2 border-l border-muted-foreground/40" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1913
|
+
className: "text-[10px] text-muted-foreground font-mono -translate-x-1/2 absolute bottom-0 whitespace-nowrap",
|
|
1914
|
+
style: {
|
|
1915
|
+
left: 0,
|
|
1916
|
+
transform: tick.percent === 100 ? "translateX(-100%)" : tick.percent === 0 ? "none" : "translateX(-50%)"
|
|
1917
|
+
},
|
|
1918
|
+
children: tick.label
|
|
1919
|
+
})]
|
|
1920
|
+
}, tick.percent))
|
|
1921
|
+
})]
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
//#endregion
|
|
1925
|
+
//#region src/components/observability/TraceTimeline/SpanSearch.tsx
|
|
1926
|
+
function SpanSearch({ value, onChange, matchCount, currentMatch, onPrev, onNext }) {
|
|
1927
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1928
|
+
className: "flex items-center gap-1 px-2 py-1 border-b border-border bg-background",
|
|
1929
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1930
|
+
type: "text",
|
|
1931
|
+
placeholder: "Find...",
|
|
1932
|
+
value,
|
|
1933
|
+
onChange: (e) => onChange(e.target.value),
|
|
1934
|
+
className: "bg-muted text-foreground text-sm px-2 py-0.5 rounded border border-border outline-none focus:border-blue-500 w-48"
|
|
1935
|
+
}), value && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1936
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
1937
|
+
className: "text-xs text-muted-foreground whitespace-nowrap",
|
|
1938
|
+
children: matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : "0 matches"
|
|
1939
|
+
}),
|
|
1940
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1941
|
+
onClick: onPrev,
|
|
1942
|
+
disabled: matchCount === 0,
|
|
1943
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1944
|
+
"aria-label": "Previous match",
|
|
1945
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1946
|
+
className: "w-3.5 h-3.5",
|
|
1947
|
+
fill: "none",
|
|
1948
|
+
stroke: "currentColor",
|
|
1949
|
+
viewBox: "0 0 24 24",
|
|
1950
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1951
|
+
strokeLinecap: "round",
|
|
1952
|
+
strokeLinejoin: "round",
|
|
1953
|
+
strokeWidth: 2,
|
|
1954
|
+
d: "M5 15l7-7 7 7"
|
|
1955
|
+
})
|
|
1956
|
+
})
|
|
1957
|
+
}),
|
|
1958
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1959
|
+
onClick: onNext,
|
|
1960
|
+
disabled: matchCount === 0,
|
|
1961
|
+
className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
|
|
1962
|
+
"aria-label": "Next match",
|
|
1963
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
1964
|
+
className: "w-3.5 h-3.5",
|
|
1965
|
+
fill: "none",
|
|
1966
|
+
stroke: "currentColor",
|
|
1967
|
+
viewBox: "0 0 24 24",
|
|
1968
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
1969
|
+
strokeLinecap: "round",
|
|
1970
|
+
strokeLinejoin: "round",
|
|
1971
|
+
strokeWidth: 2,
|
|
1972
|
+
d: "M19 9l-7 7-7-7"
|
|
1973
|
+
})
|
|
1974
|
+
})
|
|
1975
|
+
})
|
|
1976
|
+
] })]
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
//#endregion
|
|
1980
|
+
//#region src/components/observability/TraceTimeline/ViewTabs.tsx
|
|
1981
|
+
const VIEWS = [
|
|
1982
|
+
"timeline",
|
|
1983
|
+
"graph",
|
|
1984
|
+
"statistics",
|
|
1985
|
+
"flamegraph"
|
|
1986
|
+
];
|
|
1987
|
+
const VIEW_LABELS = {
|
|
1988
|
+
timeline: "Timeline",
|
|
1989
|
+
graph: "Graph",
|
|
1990
|
+
statistics: "Statistics",
|
|
1991
|
+
flamegraph: "Flamegraph"
|
|
1992
|
+
};
|
|
1993
|
+
function ViewTabs({ activeView, onChange }) {
|
|
1994
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1995
|
+
className: "flex border-b border-border bg-background",
|
|
1996
|
+
children: VIEWS.map((view) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
1997
|
+
onClick: () => onChange(view),
|
|
1998
|
+
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"}`,
|
|
1999
|
+
children: VIEW_LABELS[view]
|
|
2000
|
+
}, view))
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
2003
|
+
//#endregion
|
|
2004
|
+
//#region src/components/observability/TraceTimeline/GraphView.tsx
|
|
2005
|
+
/**
|
|
2006
|
+
* GraphView - SVG-based DAG showing service dependencies within a trace.
|
|
2007
|
+
*/
|
|
2008
|
+
function buildDAG(trace) {
|
|
2009
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
2010
|
+
const edgeMap = /* @__PURE__ */ new Map();
|
|
2011
|
+
const childServices = /* @__PURE__ */ new Map();
|
|
2012
|
+
function walk(span, parentService) {
|
|
2013
|
+
const svc = span.serviceName;
|
|
2014
|
+
const existing = nodeMap.get(svc);
|
|
2015
|
+
if (existing) {
|
|
2016
|
+
existing.spanCount++;
|
|
2017
|
+
if (span.status === "ERROR") existing.errorCount++;
|
|
2018
|
+
} else nodeMap.set(svc, {
|
|
2019
|
+
spanCount: 1,
|
|
2020
|
+
errorCount: span.status === "ERROR" ? 1 : 0
|
|
2021
|
+
});
|
|
2022
|
+
if (parentService && parentService !== svc) {
|
|
2023
|
+
const key = `${parentService}→${svc}`;
|
|
2024
|
+
const edge = edgeMap.get(key);
|
|
2025
|
+
if (edge) {
|
|
2026
|
+
edge.callCount++;
|
|
2027
|
+
edge.totalDurationMs += span.durationMs;
|
|
2028
|
+
} else edgeMap.set(key, {
|
|
2029
|
+
callCount: 1,
|
|
2030
|
+
totalDurationMs: span.durationMs
|
|
2031
|
+
});
|
|
2032
|
+
if (!childServices.has(parentService)) childServices.set(parentService, /* @__PURE__ */ new Set());
|
|
2033
|
+
const parentChildren = childServices.get(parentService);
|
|
2034
|
+
if (parentChildren) parentChildren.add(svc);
|
|
2035
|
+
}
|
|
2036
|
+
for (const child of span.children) walk(child, svc);
|
|
2037
|
+
}
|
|
2038
|
+
for (const root of trace.rootSpans) walk(root);
|
|
2039
|
+
const edges = [];
|
|
2040
|
+
for (const [key, meta] of edgeMap) {
|
|
2041
|
+
const [from, to] = key.split("→");
|
|
2042
|
+
if (from && to) edges.push({
|
|
2043
|
+
from,
|
|
2044
|
+
to,
|
|
2045
|
+
...meta
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
return {
|
|
2049
|
+
nodeMap,
|
|
2050
|
+
edges,
|
|
2051
|
+
childServices
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
const NODE_W = 160;
|
|
2055
|
+
const NODE_H = 60;
|
|
2056
|
+
const LAYER_GAP_Y = 100;
|
|
2057
|
+
const NODE_GAP_X = 40;
|
|
2058
|
+
function layoutNodes(nodeMap, edges) {
|
|
2059
|
+
const children = /* @__PURE__ */ new Map();
|
|
2060
|
+
const hasParent = /* @__PURE__ */ new Set();
|
|
2061
|
+
for (const e of edges) {
|
|
2062
|
+
if (!children.has(e.from)) children.set(e.from, /* @__PURE__ */ new Set());
|
|
2063
|
+
const fromChildren = children.get(e.from);
|
|
2064
|
+
if (fromChildren) fromChildren.add(e.to);
|
|
2065
|
+
hasParent.add(e.to);
|
|
2066
|
+
}
|
|
2067
|
+
const roots = [...nodeMap.keys()].filter((s) => !hasParent.has(s));
|
|
2068
|
+
if (roots.length === 0 && nodeMap.size > 0) {
|
|
2069
|
+
const firstKey = nodeMap.keys().next().value;
|
|
2070
|
+
if (firstKey !== void 0) roots.push(firstKey);
|
|
2071
|
+
}
|
|
2072
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
2073
|
+
const enqueueCount = /* @__PURE__ */ new Map();
|
|
2074
|
+
const maxEnqueue = nodeMap.size * 2;
|
|
2075
|
+
const queue = [];
|
|
2076
|
+
for (const r of roots) {
|
|
2077
|
+
layerOf.set(r, 0);
|
|
2078
|
+
queue.push(r);
|
|
2079
|
+
}
|
|
2080
|
+
while (queue.length > 0) {
|
|
2081
|
+
const cur = queue.shift();
|
|
2082
|
+
if (!cur) continue;
|
|
2083
|
+
const curLayer = layerOf.get(cur);
|
|
2084
|
+
if (curLayer === void 0) continue;
|
|
2085
|
+
const kids = children.get(cur);
|
|
2086
|
+
if (!kids) continue;
|
|
2087
|
+
for (const kid of kids) {
|
|
2088
|
+
const prev = layerOf.get(kid);
|
|
2089
|
+
const count = enqueueCount.get(kid) ?? 0;
|
|
2090
|
+
if (prev === void 0 && count < maxEnqueue) {
|
|
2091
|
+
layerOf.set(kid, curLayer + 1);
|
|
2092
|
+
enqueueCount.set(kid, count + 1);
|
|
2093
|
+
queue.push(kid);
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
for (const name of nodeMap.keys()) if (!layerOf.has(name)) layerOf.set(name, 0);
|
|
2098
|
+
const layers = /* @__PURE__ */ new Map();
|
|
2099
|
+
for (const [name, layer] of layerOf) {
|
|
2100
|
+
if (!layers.has(layer)) layers.set(layer, []);
|
|
2101
|
+
const layerNames = layers.get(layer);
|
|
2102
|
+
if (layerNames) layerNames.push(name);
|
|
2103
|
+
}
|
|
2104
|
+
const nodes = [];
|
|
2105
|
+
const totalWidth = Math.max(...Array.from(layers.values()).map((l) => l.length), 1) * (NODE_W + NODE_GAP_X) - NODE_GAP_X;
|
|
2106
|
+
for (const [layer, names] of layers) {
|
|
2107
|
+
const offsetX = (totalWidth - (names.length * (NODE_W + NODE_GAP_X) - NODE_GAP_X)) / 2;
|
|
2108
|
+
names.forEach((name, i) => {
|
|
2109
|
+
const meta = nodeMap.get(name);
|
|
2110
|
+
if (!meta) return;
|
|
2111
|
+
nodes.push({
|
|
2112
|
+
name,
|
|
2113
|
+
spanCount: meta.spanCount,
|
|
2114
|
+
errorCount: meta.errorCount,
|
|
2115
|
+
layer,
|
|
2116
|
+
x: offsetX + i * (NODE_W + NODE_GAP_X),
|
|
2117
|
+
y: layer * (NODE_H + LAYER_GAP_Y)
|
|
2118
|
+
});
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
return nodes;
|
|
2122
|
+
}
|
|
2123
|
+
function GraphView({ trace }) {
|
|
2124
|
+
const { nodes, edges, svgWidth, svgHeight } = (0, react.useMemo)(() => {
|
|
2125
|
+
const { nodeMap, edges } = buildDAG(trace);
|
|
2126
|
+
const nodes = layoutNodes(nodeMap, edges);
|
|
2127
|
+
const maxX = Math.max(...nodes.map((n) => n.x + NODE_W), NODE_W);
|
|
2128
|
+
const maxY = Math.max(...nodes.map((n) => n.y + NODE_H), NODE_H);
|
|
2129
|
+
const padding = 40;
|
|
2130
|
+
return {
|
|
2131
|
+
nodes,
|
|
2132
|
+
edges,
|
|
2133
|
+
svgWidth: maxX + padding * 2,
|
|
2134
|
+
svgHeight: maxY + padding * 2
|
|
2135
|
+
};
|
|
2136
|
+
}, [trace]);
|
|
2137
|
+
const nodeByName = (0, react.useMemo)(() => {
|
|
2138
|
+
const m = /* @__PURE__ */ new Map();
|
|
2139
|
+
for (const n of nodes) m.set(n.name, n);
|
|
2140
|
+
return m;
|
|
2141
|
+
}, [nodes]);
|
|
2142
|
+
const padding = 40;
|
|
2143
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2144
|
+
className: "flex-1 overflow-auto bg-background p-4 flex justify-center",
|
|
2145
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
2146
|
+
viewBox: `0 0 ${svgWidth} ${svgHeight}`,
|
|
2147
|
+
width: svgWidth,
|
|
2148
|
+
height: svgHeight,
|
|
2149
|
+
role: "img",
|
|
2150
|
+
"aria-label": "Service dependency graph",
|
|
2151
|
+
children: [
|
|
2152
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("defs", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("marker", {
|
|
2153
|
+
id: "arrowhead",
|
|
2154
|
+
markerWidth: "10",
|
|
2155
|
+
markerHeight: "7",
|
|
2156
|
+
refX: "9",
|
|
2157
|
+
refY: "3.5",
|
|
2158
|
+
orient: "auto",
|
|
2159
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polygon", {
|
|
2160
|
+
points: "0 0, 10 3.5, 0 7",
|
|
2161
|
+
fill: "#94a3b8"
|
|
2162
|
+
})
|
|
2163
|
+
}) }),
|
|
2164
|
+
edges.map((edge) => {
|
|
2165
|
+
const from = nodeByName.get(edge.from);
|
|
2166
|
+
const to = nodeByName.get(edge.to);
|
|
2167
|
+
if (!from || !to) return null;
|
|
2168
|
+
const x1 = padding + from.x + NODE_W / 2;
|
|
2169
|
+
const y1 = padding + from.y + NODE_H;
|
|
2170
|
+
const x2 = padding + to.x + NODE_W / 2;
|
|
2171
|
+
const y2 = padding + to.y;
|
|
2172
|
+
const midY = (y1 + y2) / 2;
|
|
2173
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
2174
|
+
d: `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`,
|
|
2175
|
+
fill: "none",
|
|
2176
|
+
stroke: "#475569",
|
|
2177
|
+
strokeWidth: 1.5,
|
|
2178
|
+
markerEnd: "url(#arrowhead)"
|
|
2179
|
+
}), edge.callCount > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("text", {
|
|
2180
|
+
x: (x1 + x2) / 2,
|
|
2181
|
+
y: midY - 6,
|
|
2182
|
+
textAnchor: "middle",
|
|
2183
|
+
fontSize: 11,
|
|
2184
|
+
fill: "#94a3b8",
|
|
2185
|
+
children: [edge.callCount, "x"]
|
|
2186
|
+
})] }, `${edge.from}→${edge.to}`);
|
|
2187
|
+
}),
|
|
2188
|
+
nodes.map((node) => {
|
|
2189
|
+
const color = getServiceColor(node.name);
|
|
2190
|
+
const hasError = node.errorCount > 0;
|
|
2191
|
+
const textColor = "#f8fafc";
|
|
2192
|
+
const nx = padding + node.x;
|
|
2193
|
+
const ny = padding + node.y;
|
|
2194
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", { children: [
|
|
2195
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2196
|
+
x: nx,
|
|
2197
|
+
y: ny,
|
|
2198
|
+
width: NODE_W,
|
|
2199
|
+
height: NODE_H,
|
|
2200
|
+
rx: 8,
|
|
2201
|
+
ry: 8,
|
|
2202
|
+
fill: color,
|
|
2203
|
+
stroke: hasError ? "#ef4444" : "none",
|
|
2204
|
+
strokeWidth: hasError ? 2 : 0
|
|
2205
|
+
}),
|
|
2206
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
2207
|
+
x: nx + NODE_W / 2,
|
|
2208
|
+
y: ny + 24,
|
|
2209
|
+
textAnchor: "middle",
|
|
2210
|
+
fontSize: 13,
|
|
2211
|
+
fontWeight: 600,
|
|
2212
|
+
fill: textColor,
|
|
2213
|
+
children: node.name.length > 18 ? node.name.slice(0, 16) + "..." : node.name
|
|
2214
|
+
}),
|
|
2215
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("text", {
|
|
2216
|
+
x: nx + NODE_W / 2,
|
|
2217
|
+
y: ny + 44,
|
|
2218
|
+
textAnchor: "middle",
|
|
2219
|
+
fontSize: 11,
|
|
2220
|
+
fill: textColor,
|
|
2221
|
+
opacity: .85,
|
|
2222
|
+
children: [
|
|
2223
|
+
node.spanCount,
|
|
2224
|
+
" span",
|
|
2225
|
+
node.spanCount !== 1 ? "s" : "",
|
|
2226
|
+
node.errorCount > 0 && ` · ${node.errorCount} err`
|
|
2227
|
+
]
|
|
2228
|
+
})
|
|
2229
|
+
] }, node.name);
|
|
2230
|
+
})
|
|
2231
|
+
]
|
|
2232
|
+
})
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
//#endregion
|
|
2236
|
+
//#region src/components/observability/TraceTimeline/StatisticsView.tsx
|
|
2237
|
+
function computeSelfTime(span) {
|
|
2238
|
+
const childrenTotal = span.children.reduce((sum, child) => sum + child.durationMs, 0);
|
|
2239
|
+
return Math.max(0, span.durationMs - childrenTotal);
|
|
2240
|
+
}
|
|
2241
|
+
function computeStats(trace) {
|
|
2242
|
+
const allFlattened = flattenAllSpans(trace.rootSpans);
|
|
2243
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2244
|
+
for (const { span } of allFlattened) {
|
|
2245
|
+
const key = `${span.serviceName}:${span.name}`;
|
|
2246
|
+
let group = groups.get(key);
|
|
2247
|
+
if (!group) {
|
|
2248
|
+
group = {
|
|
2249
|
+
spans: [],
|
|
2250
|
+
selfTimes: []
|
|
2251
|
+
};
|
|
2252
|
+
groups.set(key, group);
|
|
2253
|
+
}
|
|
2254
|
+
group.spans.push(span);
|
|
2255
|
+
group.selfTimes.push(computeSelfTime(span));
|
|
2256
|
+
}
|
|
2257
|
+
const stats = [];
|
|
2258
|
+
for (const [key, { spans, selfTimes }] of groups) {
|
|
2259
|
+
const durations = spans.map((s) => s.durationMs);
|
|
2260
|
+
const count = spans.length;
|
|
2261
|
+
const totalDuration = durations.reduce((a, b) => a + b, 0);
|
|
2262
|
+
const selfTimeTotal = selfTimes.reduce((a, b) => a + b, 0);
|
|
2263
|
+
const firstSpan = spans[0];
|
|
2264
|
+
if (!firstSpan) continue;
|
|
2265
|
+
stats.push({
|
|
2266
|
+
key,
|
|
2267
|
+
serviceName: firstSpan.serviceName,
|
|
2268
|
+
spanName: firstSpan.name,
|
|
2269
|
+
count,
|
|
2270
|
+
totalDuration,
|
|
2271
|
+
avgDuration: totalDuration / count,
|
|
2272
|
+
minDuration: Math.min(...durations),
|
|
2273
|
+
maxDuration: Math.max(...durations),
|
|
2274
|
+
selfTimeTotal,
|
|
2275
|
+
selfTimeAvg: selfTimeTotal / count,
|
|
2276
|
+
selfTimeMin: Math.min(...selfTimes),
|
|
2277
|
+
selfTimeMax: Math.max(...selfTimes)
|
|
2278
|
+
});
|
|
2279
|
+
}
|
|
2280
|
+
return stats;
|
|
2281
|
+
}
|
|
2282
|
+
function getSortValue(stat, field) {
|
|
2283
|
+
switch (field) {
|
|
2284
|
+
case "name": return stat.key.toLowerCase();
|
|
2285
|
+
case "count": return stat.count;
|
|
2286
|
+
case "total": return stat.totalDuration;
|
|
2287
|
+
case "avg": return stat.avgDuration;
|
|
2288
|
+
case "min": return stat.minDuration;
|
|
2289
|
+
case "max": return stat.maxDuration;
|
|
2290
|
+
case "selfTotal": return stat.selfTimeTotal;
|
|
2291
|
+
case "selfAvg": return stat.selfTimeAvg;
|
|
2292
|
+
case "selfMin": return stat.selfTimeMin;
|
|
2293
|
+
case "selfMax": return stat.selfTimeMax;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const COLUMNS = [
|
|
2297
|
+
{
|
|
2298
|
+
label: "Name",
|
|
2299
|
+
field: "name"
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
label: "Count",
|
|
2303
|
+
field: "count"
|
|
2304
|
+
},
|
|
2305
|
+
{
|
|
2306
|
+
label: "Total",
|
|
2307
|
+
field: "total"
|
|
2308
|
+
},
|
|
2309
|
+
{
|
|
2310
|
+
label: "Avg",
|
|
2311
|
+
field: "avg"
|
|
2312
|
+
},
|
|
2313
|
+
{
|
|
2314
|
+
label: "Min",
|
|
2315
|
+
field: "min"
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
label: "Max",
|
|
2319
|
+
field: "max"
|
|
2320
|
+
},
|
|
2321
|
+
{
|
|
2322
|
+
label: "ST Total",
|
|
2323
|
+
field: "selfTotal"
|
|
2324
|
+
},
|
|
2325
|
+
{
|
|
2326
|
+
label: "ST Avg",
|
|
2327
|
+
field: "selfAvg"
|
|
2328
|
+
},
|
|
2329
|
+
{
|
|
2330
|
+
label: "ST Min",
|
|
2331
|
+
field: "selfMin"
|
|
2332
|
+
},
|
|
2333
|
+
{
|
|
2334
|
+
label: "ST Max",
|
|
2335
|
+
field: "selfMax"
|
|
2336
|
+
}
|
|
2337
|
+
];
|
|
2338
|
+
function StatisticsView({ trace }) {
|
|
2339
|
+
const [sortField, setSortField] = (0, react.useState)("total");
|
|
2340
|
+
const [sortAsc, setSortAsc] = (0, react.useState)(false);
|
|
2341
|
+
const stats = (0, react.useMemo)(() => computeStats(trace), [trace]);
|
|
2342
|
+
const sorted = (0, react.useMemo)(() => {
|
|
2343
|
+
const copy = [...stats];
|
|
2344
|
+
copy.sort((a, b) => {
|
|
2345
|
+
const aVal = getSortValue(a, sortField);
|
|
2346
|
+
const bVal = getSortValue(b, sortField);
|
|
2347
|
+
let cmp;
|
|
2348
|
+
if (typeof aVal === "string" && typeof bVal === "string") cmp = aVal.localeCompare(bVal);
|
|
2349
|
+
else if (typeof aVal === "number" && typeof bVal === "number") cmp = aVal - bVal;
|
|
2350
|
+
else cmp = 0;
|
|
2351
|
+
return sortAsc ? cmp : -cmp;
|
|
2352
|
+
});
|
|
2353
|
+
return copy;
|
|
2354
|
+
}, [
|
|
2355
|
+
stats,
|
|
2356
|
+
sortField,
|
|
2357
|
+
sortAsc
|
|
2358
|
+
]);
|
|
2359
|
+
const handleSort = (field) => {
|
|
2360
|
+
if (sortField === field) setSortAsc((p) => !p);
|
|
2361
|
+
else {
|
|
2362
|
+
setSortField(field);
|
|
2363
|
+
setSortAsc(false);
|
|
2364
|
+
}
|
|
2365
|
+
};
|
|
2366
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2367
|
+
className: "flex-1 overflow-auto p-2",
|
|
2368
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
2369
|
+
className: "w-full text-sm border-collapse",
|
|
2370
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tr", {
|
|
2371
|
+
className: "border-b border-border",
|
|
2372
|
+
children: COLUMNS.map((col) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("th", {
|
|
2373
|
+
className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground cursor-pointer select-none hover:text-foreground whitespace-nowrap",
|
|
2374
|
+
onClick: () => handleSort(col.field),
|
|
2375
|
+
children: [
|
|
2376
|
+
col.label,
|
|
2377
|
+
" ",
|
|
2378
|
+
sortField === col.field ? sortAsc ? "▲" : "▼" : ""
|
|
2379
|
+
]
|
|
2380
|
+
}, col.field))
|
|
2381
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: sorted.map((stat, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
2382
|
+
className: `border-b border-border/50 ${i % 2 === 0 ? "bg-background" : "bg-muted/30"}`,
|
|
2383
|
+
children: [
|
|
2384
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("td", {
|
|
2385
|
+
className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
|
|
2386
|
+
children: [
|
|
2387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2388
|
+
className: "text-muted-foreground",
|
|
2389
|
+
children: stat.serviceName
|
|
2390
|
+
}),
|
|
2391
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2392
|
+
className: "text-muted-foreground/50",
|
|
2393
|
+
children: ":"
|
|
2394
|
+
}),
|
|
2395
|
+
" ",
|
|
2396
|
+
stat.spanName
|
|
2397
|
+
]
|
|
2398
|
+
}),
|
|
2399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2400
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2401
|
+
children: stat.count
|
|
2402
|
+
}),
|
|
2403
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2404
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2405
|
+
children: formatDuration(stat.totalDuration)
|
|
2406
|
+
}),
|
|
2407
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2408
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2409
|
+
children: formatDuration(stat.avgDuration)
|
|
2410
|
+
}),
|
|
2411
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2412
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2413
|
+
children: formatDuration(stat.minDuration)
|
|
2414
|
+
}),
|
|
2415
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2416
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2417
|
+
children: formatDuration(stat.maxDuration)
|
|
2418
|
+
}),
|
|
2419
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2420
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2421
|
+
children: formatDuration(stat.selfTimeTotal)
|
|
2422
|
+
}),
|
|
2423
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2424
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2425
|
+
children: formatDuration(stat.selfTimeAvg)
|
|
2426
|
+
}),
|
|
2427
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2428
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2429
|
+
children: formatDuration(stat.selfTimeMin)
|
|
2430
|
+
}),
|
|
2431
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
2432
|
+
className: "px-3 py-1.5 text-foreground tabular-nums",
|
|
2433
|
+
children: formatDuration(stat.selfTimeMax)
|
|
2434
|
+
})
|
|
2435
|
+
]
|
|
2436
|
+
}, stat.key)) })]
|
|
2437
|
+
})
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
//#endregion
|
|
2441
|
+
//#region src/components/observability/TraceTimeline/FlamegraphView.tsx
|
|
2442
|
+
const ROW_HEIGHT = 24;
|
|
2443
|
+
const MIN_WIDTH = 1;
|
|
2444
|
+
const LABEL_MIN_WIDTH = 40;
|
|
2445
|
+
function findSpanById(rootSpans, spanId) {
|
|
2446
|
+
for (const root of rootSpans) {
|
|
2447
|
+
if (root.spanId === spanId) return root;
|
|
2448
|
+
const found = findSpanById(root.children, spanId);
|
|
2449
|
+
if (found) return found;
|
|
2450
|
+
}
|
|
2451
|
+
return null;
|
|
2452
|
+
}
|
|
2453
|
+
function getAncestorPath(rootSpans, targetId) {
|
|
2454
|
+
const path = [];
|
|
2455
|
+
function walk(span, ancestors) {
|
|
2456
|
+
if (span.spanId === targetId) {
|
|
2457
|
+
path.push(...ancestors, span);
|
|
2458
|
+
return true;
|
|
2459
|
+
}
|
|
2460
|
+
for (const child of span.children) if (walk(child, [...ancestors, span])) return true;
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
for (const root of rootSpans) if (walk(root, [])) break;
|
|
2464
|
+
return path;
|
|
2465
|
+
}
|
|
2466
|
+
function FlamegraphView({ trace, onSpanClick, selectedSpanId }) {
|
|
2467
|
+
const [zoomSpanId, setZoomSpanId] = (0, react.useState)(null);
|
|
2468
|
+
const [tooltip, setTooltip] = (0, react.useState)(null);
|
|
2469
|
+
const zoomRoot = (0, react.useMemo)(() => {
|
|
2470
|
+
if (!zoomSpanId) return null;
|
|
2471
|
+
return findSpanById(trace.rootSpans, zoomSpanId);
|
|
2472
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2473
|
+
const breadcrumbs = (0, react.useMemo)(() => {
|
|
2474
|
+
if (!zoomSpanId) return [];
|
|
2475
|
+
return getAncestorPath(trace.rootSpans, zoomSpanId);
|
|
2476
|
+
}, [trace.rootSpans, zoomSpanId]);
|
|
2477
|
+
const viewRoots = zoomRoot ? [zoomRoot] : trace.rootSpans;
|
|
2478
|
+
const viewMinTime = zoomRoot ? zoomRoot.startTimeUnixMs : trace.minTimeMs;
|
|
2479
|
+
const viewDuration = (zoomRoot ? zoomRoot.endTimeUnixMs : trace.maxTimeMs) - viewMinTime;
|
|
2480
|
+
const flatSpans = (0, react.useMemo)(() => flattenAllSpans(viewRoots).map((fs) => ({
|
|
2481
|
+
span: fs.span,
|
|
2482
|
+
depth: fs.level
|
|
2483
|
+
})), [viewRoots]);
|
|
2484
|
+
const maxDepth = (0, react.useMemo)(() => flatSpans.reduce((max, fs) => Math.max(max, fs.depth), 0) + 1, [flatSpans]);
|
|
2485
|
+
const svgWidth = 1200;
|
|
2486
|
+
const svgHeight = maxDepth * ROW_HEIGHT;
|
|
2487
|
+
const handleClick = (0, react.useCallback)((span) => {
|
|
2488
|
+
onSpanClick?.(span);
|
|
2489
|
+
setZoomSpanId(span.spanId);
|
|
2490
|
+
}, [onSpanClick]);
|
|
2491
|
+
const handleZoomOut = (0, react.useCallback)((spanId) => {
|
|
2492
|
+
setZoomSpanId(spanId);
|
|
2493
|
+
}, []);
|
|
2494
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2495
|
+
className: "flex-1 overflow-auto p-2",
|
|
2496
|
+
children: [
|
|
2497
|
+
breadcrumbs.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2498
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground mb-2 flex-wrap",
|
|
2499
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2500
|
+
className: "hover:text-foreground underline",
|
|
2501
|
+
onClick: () => handleZoomOut(null),
|
|
2502
|
+
children: "root"
|
|
2503
|
+
}), breadcrumbs.map((bc, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2504
|
+
className: "flex items-center gap-1",
|
|
2505
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
2506
|
+
className: "text-muted-foreground/50",
|
|
2507
|
+
children: ">"
|
|
2508
|
+
}), i < breadcrumbs.length - 1 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
2509
|
+
className: "hover:text-foreground underline",
|
|
2510
|
+
onClick: () => handleZoomOut(bc.spanId),
|
|
2511
|
+
children: [
|
|
2512
|
+
bc.serviceName,
|
|
2513
|
+
": ",
|
|
2514
|
+
bc.name
|
|
2515
|
+
]
|
|
2516
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
2517
|
+
className: "text-foreground",
|
|
2518
|
+
children: [
|
|
2519
|
+
bc.serviceName,
|
|
2520
|
+
": ",
|
|
2521
|
+
bc.name
|
|
2522
|
+
]
|
|
2523
|
+
})]
|
|
2524
|
+
}, bc.spanId))]
|
|
2525
|
+
}),
|
|
2526
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2527
|
+
className: "overflow-x-auto",
|
|
2528
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
2529
|
+
width: svgWidth,
|
|
2530
|
+
height: svgHeight,
|
|
2531
|
+
className: "block",
|
|
2532
|
+
onMouseLeave: () => setTooltip(null),
|
|
2533
|
+
children: flatSpans.map(({ span, depth }) => {
|
|
2534
|
+
const x = viewDuration > 0 ? (span.startTimeUnixMs - viewMinTime) / viewDuration * svgWidth : 0;
|
|
2535
|
+
const w = viewDuration > 0 ? Math.max(MIN_WIDTH, span.durationMs / viewDuration * svgWidth) : svgWidth;
|
|
2536
|
+
const y = depth * ROW_HEIGHT;
|
|
2537
|
+
const color = getServiceColor(span.serviceName);
|
|
2538
|
+
const isSelected = span.spanId === selectedSpanId;
|
|
2539
|
+
const showLabel = w >= LABEL_MIN_WIDTH;
|
|
2540
|
+
const label = `${span.serviceName}: ${span.name}`;
|
|
2541
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
|
|
2542
|
+
className: "cursor-pointer",
|
|
2543
|
+
onClick: () => handleClick(span),
|
|
2544
|
+
onMouseEnter: (e) => setTooltip({
|
|
2545
|
+
span,
|
|
2546
|
+
x: e.clientX,
|
|
2547
|
+
y: e.clientY
|
|
2548
|
+
}),
|
|
2549
|
+
onMouseMove: (e) => setTooltip((prev) => prev ? {
|
|
2550
|
+
...prev,
|
|
2551
|
+
x: e.clientX,
|
|
2552
|
+
y: e.clientY
|
|
2553
|
+
} : null),
|
|
2554
|
+
onMouseLeave: () => setTooltip(null),
|
|
2555
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
|
|
2556
|
+
x,
|
|
2557
|
+
y,
|
|
2558
|
+
width: w,
|
|
2559
|
+
height: ROW_HEIGHT - 1,
|
|
2560
|
+
fill: color,
|
|
2561
|
+
opacity: .85,
|
|
2562
|
+
rx: 2,
|
|
2563
|
+
stroke: isSelected ? "#ffffff" : "transparent",
|
|
2564
|
+
strokeWidth: isSelected ? 2 : 0,
|
|
2565
|
+
className: "hover:opacity-100"
|
|
2566
|
+
}), showLabel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
|
|
2567
|
+
x: x + 4,
|
|
2568
|
+
y: y + ROW_HEIGHT / 2 + 1,
|
|
2569
|
+
dominantBaseline: "middle",
|
|
2570
|
+
fill: "#ffffff",
|
|
2571
|
+
fontSize: 11,
|
|
2572
|
+
fontFamily: "monospace",
|
|
2573
|
+
clipPath: `inset(0 0 0 0)`,
|
|
2574
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tspan", { children: label.length > w / 7 ? label.slice(0, Math.floor(w / 7) - 1) + "…" : label })
|
|
2575
|
+
})]
|
|
2576
|
+
}, span.spanId);
|
|
2577
|
+
})
|
|
2578
|
+
})
|
|
2579
|
+
}),
|
|
2580
|
+
tooltip && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2581
|
+
className: "fixed z-50 pointer-events-none bg-popover border border-border rounded px-3 py-2 text-xs shadow-lg",
|
|
2582
|
+
style: {
|
|
2583
|
+
left: tooltip.x + 12,
|
|
2584
|
+
top: tooltip.y + 12
|
|
2585
|
+
},
|
|
2586
|
+
children: [
|
|
2587
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2588
|
+
className: "font-medium text-foreground",
|
|
2589
|
+
children: tooltip.span.name
|
|
2590
|
+
}),
|
|
2591
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2592
|
+
className: "text-muted-foreground",
|
|
2593
|
+
children: tooltip.span.serviceName
|
|
2594
|
+
}),
|
|
2595
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2596
|
+
className: "text-foreground mt-1",
|
|
2597
|
+
children: formatDuration(tooltip.span.durationMs)
|
|
2598
|
+
})
|
|
2599
|
+
]
|
|
2600
|
+
})
|
|
2601
|
+
]
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
//#endregion
|
|
2605
|
+
//#region src/components/observability/TraceTimeline/Minimap.tsx
|
|
2606
|
+
/**
|
|
2607
|
+
* Minimap - Compressed overview of all spans with a draggable viewport.
|
|
2608
|
+
*/
|
|
2609
|
+
const MINIMAP_HEIGHT = 40;
|
|
2610
|
+
const SPAN_HEIGHT = 2;
|
|
2611
|
+
const SPAN_GAP = 1;
|
|
2612
|
+
const MIN_VIEWPORT_WIDTH = .02;
|
|
2613
|
+
const HANDLE_WIDTH = 6;
|
|
2614
|
+
function Minimap({ trace, viewStart, viewEnd, onViewChange }) {
|
|
2615
|
+
const containerRef = (0, react.useRef)(null);
|
|
2616
|
+
const dragRef = (0, react.useRef)(null);
|
|
2617
|
+
const cleanupRef = (0, react.useRef)(null);
|
|
2618
|
+
(0, react.useEffect)(() => {
|
|
2619
|
+
return () => {
|
|
2620
|
+
cleanupRef.current?.();
|
|
2621
|
+
};
|
|
2622
|
+
}, []);
|
|
2623
|
+
const allSpans = (0, react.useMemo)(() => flattenAllSpans(trace.rootSpans), [trace.rootSpans]);
|
|
2624
|
+
const traceDuration = trace.maxTimeMs - trace.minTimeMs;
|
|
2625
|
+
const getFraction = (0, react.useCallback)((clientX) => {
|
|
2626
|
+
const el = containerRef.current;
|
|
2627
|
+
if (!el) return 0;
|
|
2628
|
+
const rect = el.getBoundingClientRect();
|
|
2629
|
+
if (!rect.width) return 0;
|
|
2630
|
+
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
|
2631
|
+
}, []);
|
|
2632
|
+
const clampView = (0, react.useCallback)((start, end) => {
|
|
2633
|
+
let s = Math.max(0, Math.min(1 - MIN_VIEWPORT_WIDTH, start));
|
|
2634
|
+
let e = Math.max(s + MIN_VIEWPORT_WIDTH, Math.min(1, end));
|
|
2635
|
+
if (e > 1) {
|
|
2636
|
+
e = 1;
|
|
2637
|
+
s = Math.max(0, e - Math.max(MIN_VIEWPORT_WIDTH, end - start));
|
|
2638
|
+
}
|
|
2639
|
+
return [s, e];
|
|
2640
|
+
}, []);
|
|
2641
|
+
const handleMouseDown = (0, react.useCallback)((e, mode) => {
|
|
2642
|
+
e.preventDefault();
|
|
2643
|
+
e.stopPropagation();
|
|
2644
|
+
cleanupRef.current?.();
|
|
2645
|
+
dragRef.current = {
|
|
2646
|
+
mode,
|
|
2647
|
+
startX: e.clientX,
|
|
2648
|
+
origViewStart: viewStart,
|
|
2649
|
+
origViewEnd: viewEnd
|
|
2650
|
+
};
|
|
2651
|
+
const handleMouseMove = (ev) => {
|
|
2652
|
+
const drag = dragRef.current;
|
|
2653
|
+
if (!drag || !containerRef.current) return;
|
|
2654
|
+
const rect = containerRef.current.getBoundingClientRect();
|
|
2655
|
+
if (!rect.width) return;
|
|
2656
|
+
const deltaFrac = (ev.clientX - drag.startX) / rect.width;
|
|
2657
|
+
let newStart;
|
|
2658
|
+
let newEnd;
|
|
2659
|
+
if (drag.mode === "pan") {
|
|
2660
|
+
const width = drag.origViewEnd - drag.origViewStart;
|
|
2661
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2662
|
+
newEnd = newStart + width;
|
|
2663
|
+
if (newStart < 0) {
|
|
2664
|
+
newStart = 0;
|
|
2665
|
+
newEnd = width;
|
|
2666
|
+
}
|
|
2667
|
+
if (newEnd > 1) {
|
|
2668
|
+
newEnd = 1;
|
|
2669
|
+
newStart = 1 - width;
|
|
2670
|
+
}
|
|
2671
|
+
} else if (drag.mode === "resize-left") {
|
|
2672
|
+
newStart = drag.origViewStart + deltaFrac;
|
|
2673
|
+
newEnd = drag.origViewEnd;
|
|
2674
|
+
} else {
|
|
2675
|
+
newStart = drag.origViewStart;
|
|
2676
|
+
newEnd = drag.origViewEnd + deltaFrac;
|
|
2677
|
+
}
|
|
2678
|
+
const [s, e] = clampView(newStart, newEnd);
|
|
2679
|
+
onViewChange(s, e);
|
|
2680
|
+
};
|
|
2681
|
+
const handleMouseUp = () => {
|
|
2682
|
+
dragRef.current = null;
|
|
2683
|
+
cleanupRef.current = null;
|
|
2684
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
2685
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
2686
|
+
};
|
|
2687
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
2688
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
2689
|
+
cleanupRef.current = handleMouseUp;
|
|
2690
|
+
}, [
|
|
2691
|
+
viewStart,
|
|
2692
|
+
viewEnd,
|
|
2693
|
+
onViewChange,
|
|
2694
|
+
clampView
|
|
2695
|
+
]);
|
|
2696
|
+
const handleBackgroundClick = (0, react.useCallback)((e) => {
|
|
2697
|
+
if (dragRef.current) return;
|
|
2698
|
+
if (e.target !== e.currentTarget) return;
|
|
2699
|
+
const frac = getFraction(e.clientX);
|
|
2700
|
+
const half = (viewEnd - viewStart) / 2;
|
|
2701
|
+
const [s, eVal] = clampView(frac - half, frac + half);
|
|
2702
|
+
onViewChange(s, eVal);
|
|
2703
|
+
}, [
|
|
2704
|
+
viewStart,
|
|
2705
|
+
viewEnd,
|
|
2706
|
+
onViewChange,
|
|
2707
|
+
getFraction,
|
|
2708
|
+
clampView
|
|
2709
|
+
]);
|
|
2710
|
+
const handleKeyDown = (0, react.useCallback)((e) => {
|
|
2711
|
+
const step = .05;
|
|
2712
|
+
const width = viewEnd - viewStart;
|
|
2713
|
+
let newStart;
|
|
2714
|
+
if (e.key === "ArrowLeft" || e.key === "ArrowUp") newStart = viewStart - step;
|
|
2715
|
+
else if (e.key === "ArrowRight" || e.key === "ArrowDown") newStart = viewStart + step;
|
|
2716
|
+
else return;
|
|
2717
|
+
e.preventDefault();
|
|
2718
|
+
const [s, eVal] = clampView(newStart, newStart + width);
|
|
2719
|
+
onViewChange(s, eVal);
|
|
2720
|
+
}, [
|
|
2721
|
+
viewStart,
|
|
2722
|
+
viewEnd,
|
|
2723
|
+
onViewChange,
|
|
2724
|
+
clampView
|
|
2725
|
+
]);
|
|
2726
|
+
const viewStartPct = viewStart * 100;
|
|
2727
|
+
const viewEndPct = viewEnd * 100;
|
|
2728
|
+
const viewWidthPct = viewEndPct - viewStartPct;
|
|
2729
|
+
const totalRows = allSpans.length;
|
|
2730
|
+
const availableHeight = MINIMAP_HEIGHT - 4;
|
|
2731
|
+
const rowHeight = totalRows > 0 ? Math.min(SPAN_HEIGHT + SPAN_GAP, availableHeight / totalRows) : SPAN_HEIGHT;
|
|
2732
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2733
|
+
ref: containerRef,
|
|
2734
|
+
className: "relative w-full border-b border-border bg-muted/30 select-none",
|
|
2735
|
+
style: { height: MINIMAP_HEIGHT },
|
|
2736
|
+
onClick: handleBackgroundClick,
|
|
2737
|
+
onKeyDown: handleKeyDown,
|
|
2738
|
+
role: "slider",
|
|
2739
|
+
tabIndex: 0,
|
|
2740
|
+
"aria-label": "Trace minimap viewport",
|
|
2741
|
+
"aria-valuemin": 0,
|
|
2742
|
+
"aria-valuemax": 100,
|
|
2743
|
+
"aria-valuenow": Math.round(viewStartPct),
|
|
2744
|
+
children: [
|
|
2745
|
+
traceDuration > 0 && allSpans.map(({ span }, i) => {
|
|
2746
|
+
const left = (span.startTimeUnixMs - trace.minTimeMs) / traceDuration * 100;
|
|
2747
|
+
const width = Math.max(.2, span.durationMs / traceDuration * 100);
|
|
2748
|
+
const color = getSpanBarColor(span.serviceName, span.status === "ERROR");
|
|
2749
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2750
|
+
className: "absolute pointer-events-none",
|
|
2751
|
+
style: {
|
|
2752
|
+
left: `${left}%`,
|
|
2753
|
+
width: `${width}%`,
|
|
2754
|
+
top: 2 + i * rowHeight,
|
|
2755
|
+
height: Math.max(1, rowHeight - SPAN_GAP),
|
|
2756
|
+
backgroundColor: color,
|
|
2757
|
+
opacity: .8,
|
|
2758
|
+
borderRadius: 1
|
|
2759
|
+
}
|
|
2760
|
+
}, span.spanId);
|
|
2761
|
+
}),
|
|
2762
|
+
viewStartPct > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2763
|
+
className: "absolute top-0 left-0 h-full bg-black/30 pointer-events-none",
|
|
2764
|
+
style: { width: `${viewStartPct}%` }
|
|
2765
|
+
}),
|
|
2766
|
+
viewEndPct < 100 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2767
|
+
className: "absolute top-0 h-full bg-black/30 pointer-events-none",
|
|
2768
|
+
style: {
|
|
2769
|
+
left: `${viewEndPct}%`,
|
|
2770
|
+
right: 0
|
|
2771
|
+
}
|
|
2772
|
+
}),
|
|
2773
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2774
|
+
className: "absolute top-0 h-full border border-blue-500/50 bg-blue-500/10 cursor-grab active:cursor-grabbing",
|
|
2775
|
+
style: {
|
|
2776
|
+
left: `${viewStartPct}%`,
|
|
2777
|
+
width: `${viewWidthPct}%`
|
|
2778
|
+
},
|
|
2779
|
+
onMouseDown: (e) => handleMouseDown(e, "pan"),
|
|
2780
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2781
|
+
className: "absolute top-0 left-0 h-full cursor-ew-resize z-10",
|
|
2782
|
+
style: {
|
|
2783
|
+
width: HANDLE_WIDTH,
|
|
2784
|
+
marginLeft: -HANDLE_WIDTH / 2
|
|
2785
|
+
},
|
|
2786
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-left")
|
|
2787
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2788
|
+
className: "absolute top-0 right-0 h-full cursor-ew-resize z-10",
|
|
2789
|
+
style: {
|
|
2790
|
+
width: HANDLE_WIDTH,
|
|
2791
|
+
marginRight: -HANDLE_WIDTH / 2
|
|
2792
|
+
},
|
|
2793
|
+
onMouseDown: (e) => handleMouseDown(e, "resize-right")
|
|
2794
|
+
})]
|
|
2795
|
+
})
|
|
2796
|
+
]
|
|
2797
|
+
});
|
|
2798
|
+
}
|
|
1875
2799
|
//#endregion
|
|
1876
2800
|
//#region src/components/observability/TraceTimeline/index.tsx
|
|
1877
2801
|
/**
|
|
@@ -1960,25 +2884,43 @@ function isSpanAncestorOf(potentialAncestor, descendantId, flattenedSpans) {
|
|
|
1960
2884
|
}
|
|
1961
2885
|
return false;
|
|
1962
2886
|
}
|
|
1963
|
-
function
|
|
2887
|
+
function collectServices(rootSpans) {
|
|
2888
|
+
const set = /* @__PURE__ */ new Set();
|
|
2889
|
+
function walk(span) {
|
|
2890
|
+
set.add(span.serviceName);
|
|
2891
|
+
span.children.forEach(walk);
|
|
2892
|
+
}
|
|
2893
|
+
rootSpans.forEach(walk);
|
|
2894
|
+
return Array.from(set).sort();
|
|
2895
|
+
}
|
|
2896
|
+
function TraceTimeline({ rows, onSpanClick, onSpanDeselect, selectedSpanId: externalSelectedSpanId, isLoading, error, view: externalView, onViewChange, uiFind: externalUiFind, onUiFindChange, viewStart: externalViewStart, viewEnd: externalViewEnd, onViewRangeChange }) {
|
|
1964
2897
|
useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
|
|
1965
2898
|
const [collapsedIds, setCollapsedIds] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
1966
2899
|
const [internalSelectedSpanId, setInternalSelectedSpanId] = (0, react.useState)(null);
|
|
1967
2900
|
const [hoveredSpanId, setHoveredSpanId] = (0, react.useState)(null);
|
|
2901
|
+
const [internalView, setInternalView] = (0, react.useState)("timeline");
|
|
2902
|
+
const [internalUiFind, setInternalUiFind] = (0, react.useState)("");
|
|
2903
|
+
const [currentMatchIndex, setCurrentMatchIndex] = (0, react.useState)(0);
|
|
2904
|
+
const [headerCollapsed, setHeaderCollapsed] = (0, react.useState)(false);
|
|
2905
|
+
const [internalViewStart, setInternalViewStart] = (0, react.useState)(0);
|
|
2906
|
+
const [internalViewEnd, setInternalViewEnd] = (0, react.useState)(1);
|
|
1968
2907
|
const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
|
|
2908
|
+
const viewStart = externalViewStart ?? internalViewStart;
|
|
2909
|
+
const viewEnd = externalViewEnd ?? internalViewEnd;
|
|
2910
|
+
const activeView = externalView ?? internalView;
|
|
2911
|
+
const uiFind = externalUiFind ?? internalUiFind;
|
|
1969
2912
|
const scrollRef = (0, react.useRef)(null);
|
|
1970
2913
|
const announcementRef = (0, react.useRef)(null);
|
|
1971
2914
|
const parsedTrace = (0, react.useMemo)(() => buildTrace(rows), [rows]);
|
|
2915
|
+
const services = (0, react.useMemo)(() => parsedTrace ? collectServices(parsedTrace.rootSpans) : [], [parsedTrace]);
|
|
1972
2916
|
const flattenedSpans = (0, react.useMemo)(() => {
|
|
1973
2917
|
if (!parsedTrace) return [];
|
|
1974
2918
|
return flattenTree(parsedTrace.rootSpans, collapsedIds);
|
|
1975
2919
|
}, [parsedTrace, collapsedIds]);
|
|
1976
|
-
const
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
overscan: 5
|
|
1981
|
-
});
|
|
2920
|
+
const matchingIndices = (0, react.useMemo)(() => {
|
|
2921
|
+
if (!uiFind) return [];
|
|
2922
|
+
return flattenedSpans.map((item, idx) => spanMatchesSearch(item.span, uiFind) ? idx : -1).filter((idx) => idx !== -1);
|
|
2923
|
+
}, [flattenedSpans, uiFind]);
|
|
1982
2924
|
const handleToggleCollapse = (spanId) => {
|
|
1983
2925
|
setCollapsedIds((prev) => {
|
|
1984
2926
|
const next = new Set(prev);
|
|
@@ -1987,11 +2929,22 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
1987
2929
|
return next;
|
|
1988
2930
|
});
|
|
1989
2931
|
};
|
|
2932
|
+
const handleDeselect = (0, react.useCallback)(() => {
|
|
2933
|
+
setInternalSelectedSpanId(null);
|
|
2934
|
+
onSpanDeselect?.();
|
|
2935
|
+
}, [onSpanDeselect]);
|
|
1990
2936
|
const handleSpanClick = (0, react.useCallback)((span) => {
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2937
|
+
if (selectedSpanId === span.spanId) handleDeselect();
|
|
2938
|
+
else {
|
|
2939
|
+
setInternalSelectedSpanId(span.spanId);
|
|
2940
|
+
onSpanClick?.(span);
|
|
2941
|
+
if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
|
|
2942
|
+
}
|
|
2943
|
+
}, [
|
|
2944
|
+
onSpanClick,
|
|
2945
|
+
selectedSpanId,
|
|
2946
|
+
handleDeselect
|
|
2947
|
+
]);
|
|
1995
2948
|
const handleExpandAll = (0, react.useCallback)(() => {
|
|
1996
2949
|
setCollapsedIds(/* @__PURE__ */ new Set());
|
|
1997
2950
|
}, []);
|
|
@@ -2036,27 +2989,81 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2036
2989
|
if (collapse) setCollapsedIds((prev) => new Set([...prev, selectedItem.span.spanId]));
|
|
2037
2990
|
else setCollapsedIds((prev) => {
|
|
2038
2991
|
const next = new Set(prev);
|
|
2039
|
-
next.delete(selectedItem.span.spanId);
|
|
2040
|
-
return next;
|
|
2041
|
-
});
|
|
2042
|
-
}, [selectedSpanId, flattenedSpans]);
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2992
|
+
next.delete(selectedItem.span.spanId);
|
|
2993
|
+
return next;
|
|
2994
|
+
});
|
|
2995
|
+
}, [selectedSpanId, flattenedSpans]);
|
|
2996
|
+
const handleViewChange = (0, react.useCallback)((view) => {
|
|
2997
|
+
if (onViewChange) onViewChange(view);
|
|
2998
|
+
else setInternalView(view);
|
|
2999
|
+
}, [onViewChange]);
|
|
3000
|
+
const handleUiFindChange = (0, react.useCallback)((value) => {
|
|
3001
|
+
if (onUiFindChange) onUiFindChange(value);
|
|
3002
|
+
else setInternalUiFind(value);
|
|
3003
|
+
setCurrentMatchIndex(0);
|
|
3004
|
+
}, [onUiFindChange]);
|
|
3005
|
+
const handleViewRangeChange = (0, react.useCallback)((start, end) => {
|
|
3006
|
+
if (onViewRangeChange) onViewRangeChange(start, end);
|
|
3007
|
+
else {
|
|
3008
|
+
setInternalViewStart(start);
|
|
3009
|
+
setInternalViewEnd(end);
|
|
3010
|
+
}
|
|
3011
|
+
}, [onViewRangeChange]);
|
|
3012
|
+
const scrollToSpan = (0, react.useCallback)((spanId) => {
|
|
3013
|
+
(scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`))?.scrollIntoView({
|
|
3014
|
+
block: "center",
|
|
2051
3015
|
behavior: "smooth"
|
|
2052
3016
|
});
|
|
3017
|
+
}, []);
|
|
3018
|
+
const handleSearchNext = (0, react.useCallback)(() => {
|
|
3019
|
+
if (matchingIndices.length === 0) return;
|
|
3020
|
+
const next = (currentMatchIndex + 1) % matchingIndices.length;
|
|
3021
|
+
setCurrentMatchIndex(next);
|
|
3022
|
+
const idx = matchingIndices[next];
|
|
3023
|
+
if (idx !== void 0) {
|
|
3024
|
+
const item = flattenedSpans[idx];
|
|
3025
|
+
if (item) {
|
|
3026
|
+
handleSpanClick(item.span);
|
|
3027
|
+
scrollToSpan(item.span.spanId);
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
2053
3030
|
}, [
|
|
2054
|
-
|
|
3031
|
+
matchingIndices,
|
|
3032
|
+
currentMatchIndex,
|
|
2055
3033
|
flattenedSpans,
|
|
2056
|
-
|
|
3034
|
+
handleSpanClick,
|
|
3035
|
+
scrollToSpan
|
|
3036
|
+
]);
|
|
3037
|
+
const handleSearchPrev = (0, react.useCallback)(() => {
|
|
3038
|
+
if (matchingIndices.length === 0) return;
|
|
3039
|
+
const prev = (currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
|
|
3040
|
+
setCurrentMatchIndex(prev);
|
|
3041
|
+
const idx = matchingIndices[prev];
|
|
3042
|
+
if (idx !== void 0) {
|
|
3043
|
+
const item = flattenedSpans[idx];
|
|
3044
|
+
if (item) {
|
|
3045
|
+
handleSpanClick(item.span);
|
|
3046
|
+
scrollToSpan(item.span.spanId);
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
}, [
|
|
3050
|
+
matchingIndices,
|
|
3051
|
+
currentMatchIndex,
|
|
3052
|
+
flattenedSpans,
|
|
3053
|
+
handleSpanClick,
|
|
3054
|
+
scrollToSpan
|
|
2057
3055
|
]);
|
|
3056
|
+
(0, react.useEffect)(() => {
|
|
3057
|
+
if (!selectedSpanId) return;
|
|
3058
|
+
scrollToSpan(selectedSpanId);
|
|
3059
|
+
}, [selectedSpanId, scrollToSpan]);
|
|
2058
3060
|
(0, react.useEffect)(() => {
|
|
2059
3061
|
const handleKeyDown = (e) => {
|
|
3062
|
+
if (e.key === "Escape" && selectedSpanId) {
|
|
3063
|
+
e.preventDefault();
|
|
3064
|
+
handleDeselect();
|
|
3065
|
+
return;
|
|
3066
|
+
}
|
|
2060
3067
|
if (!(scrollRef.current?.parentElement)?.contains(document.activeElement)) return;
|
|
2061
3068
|
switch (e.key) {
|
|
2062
3069
|
case "ArrowUp":
|
|
@@ -2079,10 +3086,7 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2079
3086
|
e.preventDefault();
|
|
2080
3087
|
handleCollapseExpand(false);
|
|
2081
3088
|
break;
|
|
2082
|
-
case "Escape":
|
|
2083
|
-
e.preventDefault();
|
|
2084
|
-
handleDeselect();
|
|
2085
|
-
break;
|
|
3089
|
+
case "Escape": break;
|
|
2086
3090
|
case "Enter":
|
|
2087
3091
|
if (selectedSpanId) {
|
|
2088
3092
|
e.preventDefault();
|
|
@@ -2150,10 +3154,9 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2150
3154
|
})
|
|
2151
3155
|
});
|
|
2152
3156
|
const totalDurationMs = parsedTrace.maxTimeMs - parsedTrace.minTimeMs;
|
|
2153
|
-
|
|
2154
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3157
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2155
3158
|
className: "flex h-full bg-background",
|
|
2156
|
-
children:
|
|
3159
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2157
3160
|
className: "flex flex-col flex-1 min-w-0",
|
|
2158
3161
|
children: [
|
|
2159
3162
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -2163,39 +3166,58 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2163
3166
|
"aria-live": "polite",
|
|
2164
3167
|
"aria-atomic": "true"
|
|
2165
3168
|
}),
|
|
2166
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceHeader, {
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
3169
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceHeader, {
|
|
3170
|
+
trace: parsedTrace,
|
|
3171
|
+
services,
|
|
3172
|
+
onHeaderToggle: () => setHeaderCollapsed((p) => !p),
|
|
3173
|
+
isCollapsed: headerCollapsed
|
|
3174
|
+
}),
|
|
3175
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ViewTabs, {
|
|
3176
|
+
activeView,
|
|
3177
|
+
onChange: handleViewChange
|
|
3178
|
+
}),
|
|
3179
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanSearch, {
|
|
3180
|
+
value: uiFind,
|
|
3181
|
+
onChange: handleUiFindChange,
|
|
3182
|
+
matchCount: matchingIndices.length,
|
|
3183
|
+
currentMatch: currentMatchIndex,
|
|
3184
|
+
onPrev: handleSearchPrev,
|
|
3185
|
+
onNext: handleSearchNext
|
|
3186
|
+
}),
|
|
3187
|
+
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, {
|
|
3188
|
+
trace: parsedTrace,
|
|
3189
|
+
onSpanClick: handleSpanClick,
|
|
3190
|
+
selectedSpanId: selectedSpanId ?? void 0
|
|
3191
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
3192
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Minimap, {
|
|
3193
|
+
trace: parsedTrace,
|
|
3194
|
+
viewStart,
|
|
3195
|
+
viewEnd,
|
|
3196
|
+
onViewChange: handleViewRangeChange
|
|
3197
|
+
}),
|
|
3198
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(TimeRuler, {
|
|
3199
|
+
totalDurationMs: totalDurationMs * (viewEnd - viewStart),
|
|
3200
|
+
leftColumnWidth: "24rem",
|
|
3201
|
+
offsetMs: totalDurationMs * viewStart
|
|
3202
|
+
}),
|
|
3203
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3204
|
+
ref: scrollRef,
|
|
3205
|
+
className: "flex-1 overflow-auto outline-none",
|
|
3206
|
+
role: "tree",
|
|
3207
|
+
"aria-label": "Trace timeline",
|
|
3208
|
+
tabIndex: 0,
|
|
3209
|
+
children: flattenedSpans.map((item) => {
|
|
2182
3210
|
const { span, level } = item;
|
|
2183
3211
|
const isCollapsed = collapsedIds.has(span.spanId);
|
|
2184
3212
|
const isSelected = span.spanId === selectedSpanId;
|
|
2185
3213
|
const isHovered = span.spanId === hoveredSpanId;
|
|
2186
3214
|
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, {
|
|
3215
|
+
const viewRange = viewEnd - viewStart;
|
|
3216
|
+
const relativeStart = (calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs) - viewStart) / viewRange;
|
|
3217
|
+
const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs) / viewRange;
|
|
3218
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
3219
|
+
"data-span-id": span.spanId,
|
|
3220
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanRow, {
|
|
2199
3221
|
span,
|
|
2200
3222
|
level,
|
|
2201
3223
|
isCollapsed,
|
|
@@ -2207,34 +3229,30 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2207
3229
|
onClick: () => handleSpanClick(span),
|
|
2208
3230
|
onToggleCollapse: () => handleToggleCollapse(span.spanId),
|
|
2209
3231
|
onMouseEnter: () => setHoveredSpanId(span.spanId),
|
|
2210
|
-
onMouseLeave: () => setHoveredSpanId(null)
|
|
2211
|
-
|
|
3232
|
+
onMouseLeave: () => setHoveredSpanId(null),
|
|
3233
|
+
uiFind: uiFind || void 0
|
|
3234
|
+
}), isSelected && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SpanDetailInline, {
|
|
3235
|
+
span,
|
|
3236
|
+
traceStartMs: parsedTrace.minTimeMs
|
|
3237
|
+
})]
|
|
2212
3238
|
}, span.spanId);
|
|
2213
3239
|
})
|
|
2214
3240
|
})
|
|
2215
|
-
})
|
|
3241
|
+
] })
|
|
2216
3242
|
]
|
|
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
|
-
})]
|
|
3243
|
+
})
|
|
2225
3244
|
});
|
|
2226
3245
|
}
|
|
2227
|
-
|
|
2228
3246
|
//#endregion
|
|
2229
3247
|
//#region src/components/observability/TraceDetail/index.tsx
|
|
2230
|
-
function TraceDetail({
|
|
3248
|
+
function TraceDetail({ traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onSpanDeselect, onBack }) {
|
|
2231
3249
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2232
3250
|
className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
|
|
2233
3251
|
children: [
|
|
2234
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
3252
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
2235
3253
|
onClick: onBack,
|
|
2236
3254
|
className: "hover:text-foreground transition-colors",
|
|
2237
|
-
children:
|
|
3255
|
+
children: "Traces"
|
|
2238
3256
|
}),
|
|
2239
3257
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "/" }),
|
|
2240
3258
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
@@ -2247,10 +3265,10 @@ function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId,
|
|
|
2247
3265
|
isLoading,
|
|
2248
3266
|
error,
|
|
2249
3267
|
selectedSpanId,
|
|
2250
|
-
onSpanClick
|
|
3268
|
+
onSpanClick,
|
|
3269
|
+
onSpanDeselect
|
|
2251
3270
|
})] });
|
|
2252
3271
|
}
|
|
2253
|
-
|
|
2254
3272
|
//#endregion
|
|
2255
3273
|
//#region src/components/observability/LogTimeline/LogRow.tsx
|
|
2256
3274
|
function formatTimestamp(timeMs) {
|
|
@@ -2376,7 +3394,6 @@ const LogRow = (0, react.memo)(function LogRow({ log, isSelected, onClick, searc
|
|
|
2376
3394
|
]
|
|
2377
3395
|
});
|
|
2378
3396
|
});
|
|
2379
|
-
|
|
2380
3397
|
//#endregion
|
|
2381
3398
|
//#region src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx
|
|
2382
3399
|
function AttributesTab({ log }) {
|
|
@@ -2409,7 +3426,6 @@ function AttributesTab({ log }) {
|
|
|
2409
3426
|
})
|
|
2410
3427
|
});
|
|
2411
3428
|
}
|
|
2412
|
-
|
|
2413
3429
|
//#endregion
|
|
2414
3430
|
//#region src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx
|
|
2415
3431
|
function JsonTreeView({ data, level = 0 }) {
|
|
@@ -2497,7 +3513,6 @@ function formatPrimitiveValue(value) {
|
|
|
2497
3513
|
if (typeof value === "number") return String(value);
|
|
2498
3514
|
return String(value);
|
|
2499
3515
|
}
|
|
2500
|
-
|
|
2501
3516
|
//#endregion
|
|
2502
3517
|
//#region src/components/observability/LogTimeline/LogDetailPane/index.tsx
|
|
2503
3518
|
function LogDetailPane({ log, onClose, onTraceLinkClick, initialTab = "message", wordWrap = true }) {
|
|
@@ -2709,7 +3724,6 @@ function getSeverityColor(severity) {
|
|
|
2709
3724
|
bg: "bg-gray-50 dark:bg-gray-800/20"
|
|
2710
3725
|
};
|
|
2711
3726
|
}
|
|
2712
|
-
|
|
2713
3727
|
//#endregion
|
|
2714
3728
|
//#region src/components/observability/LogTimeline/shortcuts.ts
|
|
2715
3729
|
const LOG_VIEWER_SHORTCUTS = {
|
|
@@ -2761,7 +3775,6 @@ const LOG_VIEWER_SHORTCUTS = {
|
|
|
2761
3775
|
}
|
|
2762
3776
|
]
|
|
2763
3777
|
};
|
|
2764
|
-
|
|
2765
3778
|
//#endregion
|
|
2766
3779
|
//#region src/components/observability/LogTimeline/index.tsx
|
|
2767
3780
|
/**
|
|
@@ -2967,7 +3980,7 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
2967
3980
|
(0, react.useEffect)(() => {
|
|
2968
3981
|
const handleKeyDown = (e) => {
|
|
2969
3982
|
const isFormField = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement;
|
|
2970
|
-
if (isFormField && e.key === "Escape") {
|
|
3983
|
+
if (isFormField && e.key === "Escape" && e.target instanceof HTMLElement) {
|
|
2971
3984
|
e.target.blur();
|
|
2972
3985
|
return;
|
|
2973
3986
|
}
|
|
@@ -3208,7 +4221,6 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
|
|
|
3208
4221
|
})]
|
|
3209
4222
|
});
|
|
3210
4223
|
}
|
|
3211
|
-
|
|
3212
4224
|
//#endregion
|
|
3213
4225
|
//#region src/components/observability/LogTimeline/LogFilter.tsx
|
|
3214
4226
|
/**
|
|
@@ -3309,7 +4321,7 @@ function MultiSelect({ options, selected, onChange, testId }) {
|
|
|
3309
4321
|
(0, react.useEffect)(() => {
|
|
3310
4322
|
if (!dropOpen) return;
|
|
3311
4323
|
const handler = (e) => {
|
|
3312
|
-
if (ref.current && !ref.current.contains(e.target)) setDropOpen(false);
|
|
4324
|
+
if (ref.current && e.target instanceof Node && !ref.current.contains(e.target)) setDropOpen(false);
|
|
3313
4325
|
};
|
|
3314
4326
|
document.addEventListener("mousedown", handler);
|
|
3315
4327
|
return () => document.removeEventListener("mousedown", handler);
|
|
@@ -3760,7 +4772,6 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
|
|
|
3760
4772
|
})]
|
|
3761
4773
|
});
|
|
3762
4774
|
}
|
|
3763
|
-
|
|
3764
4775
|
//#endregion
|
|
3765
4776
|
//#region src/components/observability/utils/lttb.ts
|
|
3766
4777
|
function triangleArea(p1, p2, p3) {
|
|
@@ -3826,7 +4837,27 @@ function downsampleLTTB(data, targetPoints) {
|
|
|
3826
4837
|
if (lastPoint) sampled.push(lastPoint);
|
|
3827
4838
|
return sampled;
|
|
3828
4839
|
}
|
|
3829
|
-
|
|
4840
|
+
//#endregion
|
|
4841
|
+
//#region src/components/observability/shared/TooltipEntryList.tsx
|
|
4842
|
+
function TooltipEntryList({ payload, displayLabelMap, formatValue }) {
|
|
4843
|
+
return payload.map((entry, i) => {
|
|
4844
|
+
const dataKey = entry.dataKey;
|
|
4845
|
+
const value = entry.value;
|
|
4846
|
+
if (typeof dataKey !== "string" || typeof value !== "number") return null;
|
|
4847
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4848
|
+
className: "text-sm",
|
|
4849
|
+
style: { color: entry.color },
|
|
4850
|
+
children: [
|
|
4851
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
4852
|
+
className: "font-medium",
|
|
4853
|
+
children: [displayLabelMap.get(dataKey) ?? dataKey, ":"]
|
|
4854
|
+
}),
|
|
4855
|
+
" ",
|
|
4856
|
+
formatValue(value)
|
|
4857
|
+
]
|
|
4858
|
+
}, i);
|
|
4859
|
+
});
|
|
4860
|
+
}
|
|
3830
4861
|
//#endregion
|
|
3831
4862
|
//#region src/components/observability/utils/units.ts
|
|
3832
4863
|
const BYTE_SCALES = [
|
|
@@ -4001,7 +5032,6 @@ function formatDisplayValue(value, scale) {
|
|
|
4001
5032
|
function formatOtelValue(value, unit) {
|
|
4002
5033
|
return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
|
|
4003
5034
|
}
|
|
4004
|
-
|
|
4005
5035
|
//#endregion
|
|
4006
5036
|
//#region src/components/observability/MetricTimeSeries/index.tsx
|
|
4007
5037
|
/**
|
|
@@ -4039,7 +5069,14 @@ function buildMetrics(rows) {
|
|
|
4039
5069
|
for (const row of rows) {
|
|
4040
5070
|
const name = row.MetricName ?? "unknown";
|
|
4041
5071
|
const type = row.MetricType;
|
|
4042
|
-
|
|
5072
|
+
let value;
|
|
5073
|
+
if (type === "Gauge" || type === "Sum") value = "Value" in row ? row.Value : void 0;
|
|
5074
|
+
else if (type === "Histogram" || type === "ExponentialHistogram" || type === "Summary") {
|
|
5075
|
+
const sum = "Sum" in row ? row.Sum : void 0;
|
|
5076
|
+
const count = "Count" in row ? row.Count : void 0;
|
|
5077
|
+
if (sum != null && count != null && count > 0) value = sum / count;
|
|
5078
|
+
}
|
|
5079
|
+
if (value === void 0) continue;
|
|
4043
5080
|
if (!metricMap.has(name)) metricMap.set(name, /* @__PURE__ */ new Map());
|
|
4044
5081
|
if (!metricMeta.has(name)) metricMeta.set(name, {
|
|
4045
5082
|
description: row.MetricDescription ?? "",
|
|
@@ -4058,8 +5095,6 @@ function buildMetrics(rows) {
|
|
|
4058
5095
|
dataPoints: []
|
|
4059
5096
|
});
|
|
4060
5097
|
}
|
|
4061
|
-
if (!("Value" in row)) continue;
|
|
4062
|
-
const value = row.Value;
|
|
4063
5098
|
const timestamp = parseInt(row.TimeUnix, 10) / 1e6;
|
|
4064
5099
|
seriesMap.get(seriesKey).dataPoints.push({
|
|
4065
5100
|
timestamp,
|
|
@@ -4305,18 +5340,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
|
|
|
4305
5340
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4306
5341
|
className: "text-gray-400 text-xs mb-2",
|
|
4307
5342
|
children: formatTime(typeof label === "number" ? label : Number(label))
|
|
4308
|
-
}),
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
className: "font-medium",
|
|
4314
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4315
|
-
}),
|
|
4316
|
-
" ",
|
|
4317
|
-
formatValue(entry.value)
|
|
4318
|
-
]
|
|
4319
|
-
}, i))]
|
|
5343
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipEntryList, {
|
|
5344
|
+
payload,
|
|
5345
|
+
displayLabelMap,
|
|
5346
|
+
formatValue
|
|
5347
|
+
})]
|
|
4320
5348
|
});
|
|
4321
5349
|
}
|
|
4322
5350
|
function MetricLoadingSkeleton({ height = 400 }) {
|
|
@@ -4363,7 +5391,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
|
|
|
4363
5391
|
})
|
|
4364
5392
|
});
|
|
4365
5393
|
}
|
|
4366
|
-
|
|
4367
5394
|
//#endregion
|
|
4368
5395
|
//#region src/components/observability/MetricHistogram/index.tsx
|
|
4369
5396
|
/**
|
|
@@ -4377,6 +5404,9 @@ const COLORS = [
|
|
|
4377
5404
|
"#00C49F",
|
|
4378
5405
|
"#0088FE"
|
|
4379
5406
|
];
|
|
5407
|
+
function isBucketData(v) {
|
|
5408
|
+
return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
|
|
5409
|
+
}
|
|
4380
5410
|
const defaultFormatBucketLabel = (bound, index, bounds) => {
|
|
4381
5411
|
if (index === 0) return `≤${bound}`;
|
|
4382
5412
|
if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
|
|
@@ -4419,7 +5449,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
|
|
|
4419
5449
|
};
|
|
4420
5450
|
buckets.push(bucket);
|
|
4421
5451
|
}
|
|
4422
|
-
|
|
5452
|
+
const prev = bucket[seriesName];
|
|
5453
|
+
bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
|
|
4423
5454
|
}
|
|
4424
5455
|
}
|
|
4425
5456
|
buckets.sort((a, b) => a.lowerBound - b.lowerBound);
|
|
@@ -4556,25 +5587,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
|
|
|
4556
5587
|
}
|
|
4557
5588
|
function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
|
|
4558
5589
|
if (!active || !payload?.length) return null;
|
|
4559
|
-
const
|
|
4560
|
-
if (!
|
|
5590
|
+
const raw = payload[0]?.payload;
|
|
5591
|
+
if (!isBucketData(raw)) return null;
|
|
4561
5592
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4562
5593
|
className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
|
|
4563
5594
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4564
5595
|
className: "text-gray-300 text-sm font-medium mb-2",
|
|
4565
|
-
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(
|
|
4566
|
-
}),
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
className: "font-medium",
|
|
4572
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4573
|
-
}),
|
|
4574
|
-
" ",
|
|
4575
|
-
formatValue(entry.value)
|
|
4576
|
-
]
|
|
4577
|
-
}, i))]
|
|
5596
|
+
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(raw.lowerBound, boundsScale)} – ${raw.upperBound === Infinity ? "∞" : formatDisplayValue(raw.upperBound, boundsScale)}` : raw.bucket]
|
|
5597
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipEntryList, {
|
|
5598
|
+
payload,
|
|
5599
|
+
displayLabelMap,
|
|
5600
|
+
formatValue
|
|
5601
|
+
})]
|
|
4578
5602
|
});
|
|
4579
5603
|
}
|
|
4580
5604
|
function HistogramLoadingSkeleton({ height = 400 }) {
|
|
@@ -4614,7 +5638,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
|
|
|
4614
5638
|
})
|
|
4615
5639
|
});
|
|
4616
5640
|
}
|
|
4617
|
-
|
|
4618
5641
|
//#endregion
|
|
4619
5642
|
//#region src/components/observability/MetricStat/index.tsx
|
|
4620
5643
|
/**
|
|
@@ -4807,7 +5830,6 @@ function TrendIndicator({ direction, value }) {
|
|
|
4807
5830
|
children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
|
|
4808
5831
|
});
|
|
4809
5832
|
}
|
|
4810
|
-
|
|
4811
5833
|
//#endregion
|
|
4812
5834
|
//#region src/components/observability/MetricTable/index.tsx
|
|
4813
5835
|
/**
|
|
@@ -4947,7 +5969,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
|
|
|
4947
5969
|
})]
|
|
4948
5970
|
});
|
|
4949
5971
|
}
|
|
4950
|
-
|
|
4951
5972
|
//#endregion
|
|
4952
5973
|
//#region src/lib/renderer.tsx
|
|
4953
5974
|
/**
|
|
@@ -5052,7 +6073,6 @@ function Renderer({ tree, registry, fallback }) {
|
|
|
5052
6073
|
fallback
|
|
5053
6074
|
});
|
|
5054
6075
|
}
|
|
5055
|
-
|
|
5056
6076
|
//#endregion
|
|
5057
6077
|
//#region src/lib/catalog.ts
|
|
5058
6078
|
const dashboardCatalog = createCatalog({
|
|
@@ -5252,8 +6272,7 @@ const dashboardCatalog = createCatalog({
|
|
|
5252
6272
|
}
|
|
5253
6273
|
}
|
|
5254
6274
|
});
|
|
5255
|
-
|
|
5256
|
-
|
|
6275
|
+
Object.keys(dashboardCatalog.components);
|
|
5257
6276
|
//#endregion
|
|
5258
6277
|
//#region src/components/dashboard/Badge/index.tsx
|
|
5259
6278
|
function Badge({ element }) {
|
|
@@ -5277,7 +6296,6 @@ function Badge({ element }) {
|
|
|
5277
6296
|
children: text
|
|
5278
6297
|
});
|
|
5279
6298
|
}
|
|
5280
|
-
|
|
5281
6299
|
//#endregion
|
|
5282
6300
|
//#region src/components/dashboard/Card/index.tsx
|
|
5283
6301
|
function Card({ element, children }) {
|
|
@@ -5318,7 +6336,6 @@ function Card({ element, children }) {
|
|
|
5318
6336
|
})]
|
|
5319
6337
|
});
|
|
5320
6338
|
}
|
|
5321
|
-
|
|
5322
6339
|
//#endregion
|
|
5323
6340
|
//#region src/components/dashboard/Divider/index.tsx
|
|
5324
6341
|
function Divider({ element }) {
|
|
@@ -5356,7 +6373,6 @@ function Divider({ element }) {
|
|
|
5356
6373
|
margin: "16px 0"
|
|
5357
6374
|
} });
|
|
5358
6375
|
}
|
|
5359
|
-
|
|
5360
6376
|
//#endregion
|
|
5361
6377
|
//#region src/components/dashboard/Empty/index.tsx
|
|
5362
6378
|
function Empty({ element, onAction }) {
|
|
@@ -5399,7 +6415,6 @@ function Empty({ element, onAction }) {
|
|
|
5399
6415
|
]
|
|
5400
6416
|
});
|
|
5401
6417
|
}
|
|
5402
|
-
|
|
5403
6418
|
//#endregion
|
|
5404
6419
|
//#region src/components/dashboard/Grid/index.tsx
|
|
5405
6420
|
function Grid({ element, children }) {
|
|
@@ -5417,7 +6432,6 @@ function Grid({ element, children }) {
|
|
|
5417
6432
|
children
|
|
5418
6433
|
});
|
|
5419
6434
|
}
|
|
5420
|
-
|
|
5421
6435
|
//#endregion
|
|
5422
6436
|
//#region src/components/dashboard/Heading/index.tsx
|
|
5423
6437
|
function Heading({ element }) {
|
|
@@ -5436,7 +6450,6 @@ function Heading({ element }) {
|
|
|
5436
6450
|
children: text
|
|
5437
6451
|
});
|
|
5438
6452
|
}
|
|
5439
|
-
|
|
5440
6453
|
//#endregion
|
|
5441
6454
|
//#region src/components/dashboard/Stack/index.tsx
|
|
5442
6455
|
function Stack({ element, children }) {
|
|
@@ -5460,7 +6473,6 @@ function Stack({ element, children }) {
|
|
|
5460
6473
|
children
|
|
5461
6474
|
});
|
|
5462
6475
|
}
|
|
5463
|
-
|
|
5464
6476
|
//#endregion
|
|
5465
6477
|
//#region src/components/dashboard/Text/index.tsx
|
|
5466
6478
|
function Text({ element }) {
|
|
@@ -5479,7 +6491,6 @@ function Text({ element }) {
|
|
|
5479
6491
|
children: content
|
|
5480
6492
|
});
|
|
5481
6493
|
}
|
|
5482
|
-
|
|
5483
6494
|
//#endregion
|
|
5484
6495
|
//#region src/components/observability/renderers/OtelLogTimeline.tsx
|
|
5485
6496
|
function OtelLogTimeline(props) {
|
|
@@ -5491,13 +6502,16 @@ function OtelLogTimeline(props) {
|
|
|
5491
6502
|
children: "No data source"
|
|
5492
6503
|
});
|
|
5493
6504
|
const response = props.data;
|
|
5494
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
5495
|
-
|
|
5496
|
-
|
|
5497
|
-
|
|
6505
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6506
|
+
style: { height: props.element.props.height ?? 600 },
|
|
6507
|
+
className: "flex flex-col min-h-0",
|
|
6508
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogTimeline, {
|
|
6509
|
+
rows: response?.data ?? [],
|
|
6510
|
+
isLoading: props.loading,
|
|
6511
|
+
error: props.error ?? void 0
|
|
6512
|
+
})
|
|
5498
6513
|
});
|
|
5499
6514
|
}
|
|
5500
|
-
|
|
5501
6515
|
//#endregion
|
|
5502
6516
|
//#region src/components/observability/renderers/OtelMetricDiscovery.tsx
|
|
5503
6517
|
const TYPE_ORDER = {
|
|
@@ -5575,7 +6589,6 @@ function OtelMetricDiscovery(props) {
|
|
|
5575
6589
|
})
|
|
5576
6590
|
});
|
|
5577
6591
|
}
|
|
5578
|
-
|
|
5579
6592
|
//#endregion
|
|
5580
6593
|
//#region src/components/observability/renderers/OtelMetricHistogram.tsx
|
|
5581
6594
|
function OtelMetricHistogram(props) {
|
|
@@ -5596,7 +6609,6 @@ function OtelMetricHistogram(props) {
|
|
|
5596
6609
|
unit: props.element.props.unit ?? void 0
|
|
5597
6610
|
});
|
|
5598
6611
|
}
|
|
5599
|
-
|
|
5600
6612
|
//#endregion
|
|
5601
6613
|
//#region src/components/observability/renderers/OtelMetricStat.tsx
|
|
5602
6614
|
function OtelMetricStat(props) {
|
|
@@ -5617,7 +6629,6 @@ function OtelMetricStat(props) {
|
|
|
5617
6629
|
formatValue: formatOtelValue
|
|
5618
6630
|
});
|
|
5619
6631
|
}
|
|
5620
|
-
|
|
5621
6632
|
//#endregion
|
|
5622
6633
|
//#region src/components/observability/renderers/OtelMetricTable.tsx
|
|
5623
6634
|
function OtelMetricTable(props) {
|
|
@@ -5636,7 +6647,6 @@ function OtelMetricTable(props) {
|
|
|
5636
6647
|
maxRows: props.element.props.maxRows ?? 100
|
|
5637
6648
|
});
|
|
5638
6649
|
}
|
|
5639
|
-
|
|
5640
6650
|
//#endregion
|
|
5641
6651
|
//#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
|
|
5642
6652
|
function OtelMetricTimeSeries(props) {
|
|
@@ -5658,7 +6668,6 @@ function OtelMetricTimeSeries(props) {
|
|
|
5658
6668
|
unit: props.element.props.unit ?? void 0
|
|
5659
6669
|
});
|
|
5660
6670
|
}
|
|
5661
|
-
|
|
5662
6671
|
//#endregion
|
|
5663
6672
|
//#region src/components/observability/renderers/OtelTraceDetail.tsx
|
|
5664
6673
|
function OtelTraceDetail(props) {
|
|
@@ -5670,19 +6679,15 @@ function OtelTraceDetail(props) {
|
|
|
5670
6679
|
children: "No data source"
|
|
5671
6680
|
});
|
|
5672
6681
|
const rows = props.data?.data ?? [];
|
|
5673
|
-
const
|
|
5674
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
5675
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
6682
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
5676
6683
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetail, {
|
|
5677
6684
|
rows,
|
|
5678
6685
|
isLoading: props.loading,
|
|
5679
6686
|
error: props.error ?? void 0,
|
|
5680
|
-
service,
|
|
5681
6687
|
traceId,
|
|
5682
6688
|
onBack: () => {}
|
|
5683
6689
|
});
|
|
5684
6690
|
}
|
|
5685
|
-
|
|
5686
6691
|
//#endregion
|
|
5687
6692
|
//#region src/components/observability/DynamicDashboard/index.tsx
|
|
5688
6693
|
const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
|
|
@@ -5708,24 +6713,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
|
|
|
5708
6713
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricsRenderer, { tree: uiTree })
|
|
5709
6714
|
});
|
|
5710
6715
|
}
|
|
5711
|
-
|
|
6716
|
+
//#endregion
|
|
6717
|
+
//#region src/components/observability/TraceComparison/index.tsx
|
|
6718
|
+
function computeTraceStats(rows) {
|
|
6719
|
+
if (rows.length === 0) return {
|
|
6720
|
+
durationMs: 0,
|
|
6721
|
+
spanCount: 0
|
|
6722
|
+
};
|
|
6723
|
+
let minTs = Infinity;
|
|
6724
|
+
let maxEnd = -Infinity;
|
|
6725
|
+
for (const row of rows) {
|
|
6726
|
+
const startMs = parseInt(row.Timestamp, 10) / 1e6;
|
|
6727
|
+
const endMs = startMs + (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6728
|
+
minTs = Math.min(minTs, startMs);
|
|
6729
|
+
maxEnd = Math.max(maxEnd, endMs);
|
|
6730
|
+
}
|
|
6731
|
+
return {
|
|
6732
|
+
durationMs: maxEnd - minTs,
|
|
6733
|
+
spanCount: rows.length
|
|
6734
|
+
};
|
|
6735
|
+
}
|
|
6736
|
+
function collectSignatures(rows) {
|
|
6737
|
+
const map = /* @__PURE__ */ new Map();
|
|
6738
|
+
for (const row of rows) {
|
|
6739
|
+
const key = `${row.ServiceName ?? "unknown"}::${row.SpanName ?? ""}`;
|
|
6740
|
+
const durMs = (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
|
|
6741
|
+
const existing = map.get(key);
|
|
6742
|
+
if (existing) {
|
|
6743
|
+
existing.count++;
|
|
6744
|
+
existing.totalDurationMs += durMs;
|
|
6745
|
+
} else map.set(key, {
|
|
6746
|
+
count: 1,
|
|
6747
|
+
totalDurationMs: durMs
|
|
6748
|
+
});
|
|
6749
|
+
}
|
|
6750
|
+
return map;
|
|
6751
|
+
}
|
|
6752
|
+
function computeDiff(rowsA, rowsB) {
|
|
6753
|
+
const sigA = collectSignatures(rowsA);
|
|
6754
|
+
const sigB = collectSignatures(rowsB);
|
|
6755
|
+
const allKeys = new Set([...sigA.keys(), ...sigB.keys()]);
|
|
6756
|
+
const result = [];
|
|
6757
|
+
for (const key of allKeys) {
|
|
6758
|
+
const [serviceName = "unknown", spanName = ""] = key.split("::");
|
|
6759
|
+
const a = sigA.get(key);
|
|
6760
|
+
const b = sigB.get(key);
|
|
6761
|
+
const countA = a?.count ?? 0;
|
|
6762
|
+
const countB = b?.count ?? 0;
|
|
6763
|
+
const avgA = a ? a.totalDurationMs / a.count : 0;
|
|
6764
|
+
const avgB = b ? b.totalDurationMs / b.count : 0;
|
|
6765
|
+
result.push({
|
|
6766
|
+
serviceName,
|
|
6767
|
+
spanName,
|
|
6768
|
+
countA,
|
|
6769
|
+
countB,
|
|
6770
|
+
avgDurationA: avgA,
|
|
6771
|
+
avgDurationB: avgB,
|
|
6772
|
+
deltaMs: avgB - avgA
|
|
6773
|
+
});
|
|
6774
|
+
}
|
|
6775
|
+
return result.sort((a, b) => {
|
|
6776
|
+
const aShared = a.countA > 0 && a.countB > 0;
|
|
6777
|
+
if (aShared !== (b.countA > 0 && b.countB > 0)) return aShared ? 1 : -1;
|
|
6778
|
+
return Math.abs(b.deltaMs) - Math.abs(a.deltaMs);
|
|
6779
|
+
});
|
|
6780
|
+
}
|
|
6781
|
+
function formatDelta(ms) {
|
|
6782
|
+
return `${ms > 0 ? "+" : ""}${formatDuration(ms)}`;
|
|
6783
|
+
}
|
|
6784
|
+
function TraceComparison({ traceIdA, traceIdB, onBack }) {
|
|
6785
|
+
const dsA = (0, react.useMemo)(() => ({
|
|
6786
|
+
method: "getTrace",
|
|
6787
|
+
params: { traceId: traceIdA }
|
|
6788
|
+
}), [traceIdA]);
|
|
6789
|
+
const dsB = (0, react.useMemo)(() => ({
|
|
6790
|
+
method: "getTrace",
|
|
6791
|
+
params: { traceId: traceIdB }
|
|
6792
|
+
}), [traceIdB]);
|
|
6793
|
+
const { data: rowsA, loading: loadingA, error: errorA } = useKopaiData(dsA);
|
|
6794
|
+
const { data: rowsB, loading: loadingB, error: errorB } = useKopaiData(dsB);
|
|
6795
|
+
const statsA = (0, react.useMemo)(() => computeTraceStats(rowsA ?? []), [rowsA]);
|
|
6796
|
+
const statsB = (0, react.useMemo)(() => computeTraceStats(rowsB ?? []), [rowsB]);
|
|
6797
|
+
const diff = (0, react.useMemo)(() => computeDiff(rowsA ?? [], rowsB ?? []), [rowsA, rowsB]);
|
|
6798
|
+
const durationDelta = statsB.durationMs - statsA.durationMs;
|
|
6799
|
+
const spanDelta = statsB.spanCount - statsA.spanCount;
|
|
6800
|
+
const isLoading = loadingA || loadingB;
|
|
6801
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6802
|
+
className: "flex flex-col gap-4",
|
|
6803
|
+
children: [
|
|
6804
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6805
|
+
className: "flex items-center justify-between bg-background border border-border rounded-lg p-4",
|
|
6806
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6807
|
+
className: "flex items-center gap-4",
|
|
6808
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
6809
|
+
onClick: onBack,
|
|
6810
|
+
className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
|
|
6811
|
+
children: "← Back"
|
|
6812
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6813
|
+
className: "flex items-center gap-6 text-sm",
|
|
6814
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6815
|
+
className: "text-muted-foreground mr-1",
|
|
6816
|
+
children: "A:"
|
|
6817
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6818
|
+
className: "font-mono text-xs text-foreground",
|
|
6819
|
+
children: [traceIdA.slice(0, 16), "..."]
|
|
6820
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6821
|
+
className: "text-muted-foreground mr-1",
|
|
6822
|
+
children: "B:"
|
|
6823
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6824
|
+
className: "font-mono text-xs text-foreground",
|
|
6825
|
+
children: [traceIdB.slice(0, 16), "..."]
|
|
6826
|
+
})] })]
|
|
6827
|
+
})]
|
|
6828
|
+
}), !isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6829
|
+
className: "flex items-center gap-6 text-sm",
|
|
6830
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6831
|
+
className: "text-muted-foreground mr-1",
|
|
6832
|
+
children: "Duration delta:"
|
|
6833
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6834
|
+
className: durationDelta > 0 ? "text-red-400" : durationDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6835
|
+
children: formatDelta(durationDelta)
|
|
6836
|
+
})] }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6837
|
+
className: "text-muted-foreground mr-1",
|
|
6838
|
+
children: "Span count delta:"
|
|
6839
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6840
|
+
className: spanDelta > 0 ? "text-red-400" : spanDelta < 0 ? "text-green-400" : "text-foreground",
|
|
6841
|
+
children: spanDelta > 0 ? `+${spanDelta}` : String(spanDelta)
|
|
6842
|
+
})] })]
|
|
6843
|
+
})]
|
|
6844
|
+
}),
|
|
6845
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6846
|
+
className: "grid grid-cols-2 gap-4",
|
|
6847
|
+
style: { height: "50vh" },
|
|
6848
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6849
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6850
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceTimeline, {
|
|
6851
|
+
rows: rowsA ?? [],
|
|
6852
|
+
isLoading: loadingA,
|
|
6853
|
+
error: errorA ?? void 0
|
|
6854
|
+
})
|
|
6855
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6856
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6857
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceTimeline, {
|
|
6858
|
+
rows: rowsB ?? [],
|
|
6859
|
+
isLoading: loadingB,
|
|
6860
|
+
error: errorB ?? void 0
|
|
6861
|
+
})
|
|
6862
|
+
})]
|
|
6863
|
+
}),
|
|
6864
|
+
!isLoading && diff.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6865
|
+
className: "border border-border rounded-lg overflow-hidden",
|
|
6866
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6867
|
+
className: "px-4 py-3 border-b border-border bg-background",
|
|
6868
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
6869
|
+
className: "text-sm font-medium text-foreground",
|
|
6870
|
+
children: "Structural Diff"
|
|
6871
|
+
})
|
|
6872
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
6873
|
+
className: "overflow-x-auto",
|
|
6874
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("table", {
|
|
6875
|
+
className: "w-full text-sm",
|
|
6876
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("thead", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
6877
|
+
className: "border-b border-border bg-muted/30",
|
|
6878
|
+
children: [
|
|
6879
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6880
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6881
|
+
children: "Service"
|
|
6882
|
+
}),
|
|
6883
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6884
|
+
className: "text-left px-4 py-2 text-muted-foreground font-medium",
|
|
6885
|
+
children: "Span"
|
|
6886
|
+
}),
|
|
6887
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6888
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6889
|
+
children: "Count A"
|
|
6890
|
+
}),
|
|
6891
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6892
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6893
|
+
children: "Count B"
|
|
6894
|
+
}),
|
|
6895
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6896
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6897
|
+
children: "Avg Dur A"
|
|
6898
|
+
}),
|
|
6899
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6900
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6901
|
+
children: "Avg Dur B"
|
|
6902
|
+
}),
|
|
6903
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("th", {
|
|
6904
|
+
className: "text-right px-4 py-2 text-muted-foreground font-medium",
|
|
6905
|
+
children: "Delta"
|
|
6906
|
+
})
|
|
6907
|
+
]
|
|
6908
|
+
}) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("tbody", { children: diff.map((row) => {
|
|
6909
|
+
const onlyA = row.countA > 0 && row.countB === 0;
|
|
6910
|
+
const onlyB = row.countA === 0 && row.countB > 0;
|
|
6911
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("tr", {
|
|
6912
|
+
className: `border-b border-border/50 ${onlyA ? "bg-red-500/5" : onlyB ? "bg-green-500/5" : ""}`,
|
|
6913
|
+
children: [
|
|
6914
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6915
|
+
className: "px-4 py-1.5 text-foreground",
|
|
6916
|
+
children: row.serviceName
|
|
6917
|
+
}),
|
|
6918
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6919
|
+
className: "px-4 py-1.5 font-mono text-xs text-foreground",
|
|
6920
|
+
children: row.spanName
|
|
6921
|
+
}),
|
|
6922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6923
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6924
|
+
children: row.countA || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6925
|
+
className: "text-muted-foreground",
|
|
6926
|
+
children: "-"
|
|
6927
|
+
})
|
|
6928
|
+
}),
|
|
6929
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6930
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6931
|
+
children: row.countB || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6932
|
+
className: "text-muted-foreground",
|
|
6933
|
+
children: "-"
|
|
6934
|
+
})
|
|
6935
|
+
}),
|
|
6936
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6937
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6938
|
+
children: row.countA > 0 ? formatDuration(row.avgDurationA) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6939
|
+
className: "text-muted-foreground",
|
|
6940
|
+
children: "-"
|
|
6941
|
+
})
|
|
6942
|
+
}),
|
|
6943
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6944
|
+
className: "px-4 py-1.5 text-right text-foreground",
|
|
6945
|
+
children: row.countB > 0 ? formatDuration(row.avgDurationB) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6946
|
+
className: "text-muted-foreground",
|
|
6947
|
+
children: "-"
|
|
6948
|
+
})
|
|
6949
|
+
}),
|
|
6950
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("td", {
|
|
6951
|
+
className: "px-4 py-1.5 text-right",
|
|
6952
|
+
children: row.countA > 0 && row.countB > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6953
|
+
className: row.deltaMs > 0 ? "text-red-400" : row.deltaMs < 0 ? "text-green-400" : "text-foreground",
|
|
6954
|
+
children: formatDelta(row.deltaMs)
|
|
6955
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6956
|
+
className: onlyA ? "text-red-400" : "text-green-400",
|
|
6957
|
+
children: onlyA ? "removed" : "added"
|
|
6958
|
+
})
|
|
6959
|
+
})
|
|
6960
|
+
]
|
|
6961
|
+
}, `${row.serviceName}::${row.spanName}`);
|
|
6962
|
+
}) })]
|
|
6963
|
+
})
|
|
6964
|
+
})]
|
|
6965
|
+
})
|
|
6966
|
+
]
|
|
6967
|
+
});
|
|
6968
|
+
}
|
|
5712
6969
|
//#endregion
|
|
5713
6970
|
//#region src/components/observability/ServiceList/shortcuts.ts
|
|
5714
6971
|
const SERVICES_SHORTCUTS = {
|
|
5715
|
-
name: "
|
|
6972
|
+
name: "Traces",
|
|
5716
6973
|
shortcuts: [{
|
|
5717
6974
|
keys: ["Backspace"],
|
|
5718
6975
|
description: "Go back"
|
|
5719
6976
|
}]
|
|
5720
6977
|
};
|
|
5721
|
-
|
|
5722
6978
|
//#endregion
|
|
5723
6979
|
//#region src/pages/observability.tsx
|
|
5724
6980
|
const TABS = [
|
|
5725
6981
|
{
|
|
5726
6982
|
key: "services",
|
|
5727
|
-
label: "
|
|
5728
|
-
shortcutKey: "
|
|
6983
|
+
label: "Traces",
|
|
6984
|
+
shortcutKey: "T"
|
|
5729
6985
|
},
|
|
5730
6986
|
{
|
|
5731
6987
|
key: "logs",
|
|
@@ -5745,20 +7001,53 @@ function readURLState() {
|
|
|
5745
7001
|
const span = params.get("span");
|
|
5746
7002
|
const dashboardId = params.get("dashboardId");
|
|
5747
7003
|
const rawTab = params.get("tab");
|
|
7004
|
+
const tab = service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services";
|
|
7005
|
+
const rawLimit = params.get("limit");
|
|
7006
|
+
const limit = rawLimit ? parseInt(rawLimit, 10) : null;
|
|
5748
7007
|
return {
|
|
5749
|
-
tab
|
|
7008
|
+
tab,
|
|
5750
7009
|
service,
|
|
7010
|
+
operation: params.get("operation"),
|
|
7011
|
+
tags: params.get("tags"),
|
|
7012
|
+
lookback: params.get("lookback"),
|
|
7013
|
+
tsMin: params.get("tsMin"),
|
|
7014
|
+
tsMax: params.get("tsMax"),
|
|
7015
|
+
minDuration: params.get("minDuration"),
|
|
7016
|
+
maxDuration: params.get("maxDuration"),
|
|
7017
|
+
limit: limit !== null && !isNaN(limit) ? limit : null,
|
|
7018
|
+
sort: params.get("sort"),
|
|
5751
7019
|
trace,
|
|
5752
7020
|
span,
|
|
7021
|
+
view: params.get("view"),
|
|
7022
|
+
uiFind: params.get("uiFind"),
|
|
7023
|
+
compare: params.get("compare"),
|
|
7024
|
+
viewStart: params.get("viewStart"),
|
|
7025
|
+
viewEnd: params.get("viewEnd"),
|
|
5753
7026
|
dashboardId
|
|
5754
7027
|
};
|
|
5755
7028
|
}
|
|
5756
7029
|
function pushURLState(state, { replace = false } = {}) {
|
|
5757
7030
|
const params = new URLSearchParams();
|
|
5758
7031
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
5759
|
-
if (state.
|
|
5760
|
-
|
|
5761
|
-
|
|
7032
|
+
if (state.tab === "services") {
|
|
7033
|
+
if (state.service) params.set("service", state.service);
|
|
7034
|
+
if (state.operation) params.set("operation", state.operation);
|
|
7035
|
+
if (state.tags) params.set("tags", state.tags);
|
|
7036
|
+
if (state.lookback) params.set("lookback", state.lookback);
|
|
7037
|
+
if (state.tsMin) params.set("tsMin", state.tsMin);
|
|
7038
|
+
if (state.tsMax) params.set("tsMax", state.tsMax);
|
|
7039
|
+
if (state.minDuration) params.set("minDuration", state.minDuration);
|
|
7040
|
+
if (state.maxDuration) params.set("maxDuration", state.maxDuration);
|
|
7041
|
+
if (state.limit != null && state.limit !== 20) params.set("limit", String(state.limit));
|
|
7042
|
+
if (state.sort) params.set("sort", state.sort);
|
|
7043
|
+
if (state.trace) params.set("trace", state.trace);
|
|
7044
|
+
if (state.span) params.set("span", state.span);
|
|
7045
|
+
if (state.view) params.set("view", state.view);
|
|
7046
|
+
if (state.uiFind) params.set("uiFind", state.uiFind);
|
|
7047
|
+
if (state.compare) params.set("compare", state.compare);
|
|
7048
|
+
if (state.viewStart) params.set("viewStart", state.viewStart);
|
|
7049
|
+
if (state.viewEnd) params.set("viewEnd", state.viewEnd);
|
|
7050
|
+
}
|
|
5762
7051
|
const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
|
|
5763
7052
|
if (dashboardId) params.set("dashboardId", dashboardId);
|
|
5764
7053
|
const qs = params.toString();
|
|
@@ -5775,8 +7064,22 @@ let _cachedSearch = "";
|
|
|
5775
7064
|
let _cachedState = {
|
|
5776
7065
|
tab: "services",
|
|
5777
7066
|
service: null,
|
|
7067
|
+
operation: null,
|
|
7068
|
+
tags: null,
|
|
7069
|
+
lookback: null,
|
|
7070
|
+
tsMin: null,
|
|
7071
|
+
tsMax: null,
|
|
7072
|
+
minDuration: null,
|
|
7073
|
+
maxDuration: null,
|
|
7074
|
+
limit: null,
|
|
7075
|
+
sort: null,
|
|
5778
7076
|
trace: null,
|
|
5779
7077
|
span: null,
|
|
7078
|
+
view: null,
|
|
7079
|
+
uiFind: null,
|
|
7080
|
+
compare: null,
|
|
7081
|
+
viewStart: null,
|
|
7082
|
+
viewEnd: null,
|
|
5780
7083
|
dashboardId: null
|
|
5781
7084
|
};
|
|
5782
7085
|
function getURLSnapshot() {
|
|
@@ -5886,6 +7189,26 @@ function parseDuration(input) {
|
|
|
5886
7189
|
s: 1e9
|
|
5887
7190
|
}[unit]));
|
|
5888
7191
|
}
|
|
7192
|
+
function parseLogfmt(str) {
|
|
7193
|
+
const result = {};
|
|
7194
|
+
const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
|
|
7195
|
+
let m;
|
|
7196
|
+
while ((m = re.exec(str)) !== null) {
|
|
7197
|
+
const key = m[1];
|
|
7198
|
+
if (key) result[key] = m[2] ?? m[3] ?? "";
|
|
7199
|
+
}
|
|
7200
|
+
return result;
|
|
7201
|
+
}
|
|
7202
|
+
const LOOKBACK_MS = {
|
|
7203
|
+
"5m": 5 * 6e4,
|
|
7204
|
+
"15m": 15 * 6e4,
|
|
7205
|
+
"30m": 30 * 6e4,
|
|
7206
|
+
"1h": 60 * 6e4,
|
|
7207
|
+
"2h": 120 * 6e4,
|
|
7208
|
+
"6h": 360 * 6e4,
|
|
7209
|
+
"12h": 720 * 6e4,
|
|
7210
|
+
"24h": 1440 * 6e4
|
|
7211
|
+
};
|
|
5889
7212
|
function LogsTab() {
|
|
5890
7213
|
const [initState] = (0, react.useState)(() => readLogFilters());
|
|
5891
7214
|
const [filters, setFilters] = (0, react.useState)(initState.filters);
|
|
@@ -5945,187 +7268,146 @@ function LogsTab() {
|
|
|
5945
7268
|
})]
|
|
5946
7269
|
});
|
|
5947
7270
|
}
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
sortOrder: "DESC"
|
|
5953
|
-
}
|
|
5954
|
-
};
|
|
5955
|
-
function ServiceListView({ onSelect }) {
|
|
5956
|
-
const { data, loading, error } = useKopaiData(SERVICES_DS);
|
|
5957
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServiceList, {
|
|
5958
|
-
services: (0, react.useMemo)(() => {
|
|
5959
|
-
if (!data?.data) return [];
|
|
5960
|
-
const names = /* @__PURE__ */ new Set();
|
|
5961
|
-
for (const row of data.data) names.add(row.ServiceName ?? "unknown");
|
|
5962
|
-
return Array.from(names).sort().map((name) => ({ name }));
|
|
5963
|
-
}, [data]),
|
|
5964
|
-
isLoading: loading,
|
|
5965
|
-
error: error ?? void 0,
|
|
5966
|
-
onSelect
|
|
5967
|
-
});
|
|
5968
|
-
}
|
|
5969
|
-
function TraceSearchView({ service, onBack, onSelectTrace }) {
|
|
5970
|
-
const [ds, setDs] = (0, react.useState)(() => ({
|
|
5971
|
-
method: "searchTracesPage",
|
|
5972
|
-
params: {
|
|
5973
|
-
serviceName: service,
|
|
5974
|
-
limit: 20,
|
|
5975
|
-
sortOrder: "DESC"
|
|
5976
|
-
}
|
|
5977
|
-
}));
|
|
5978
|
-
const handleSearch = (0, react.useCallback)((filters) => {
|
|
7271
|
+
function TraceSearchView({ onSelectTrace, onCompare }) {
|
|
7272
|
+
const urlState = useURLState();
|
|
7273
|
+
const service = urlState.service;
|
|
7274
|
+
const ds = (0, react.useMemo)(() => {
|
|
5979
7275
|
const params = {
|
|
5980
|
-
|
|
5981
|
-
limit: filters.limit,
|
|
7276
|
+
limit: urlState.limit ?? 20,
|
|
5982
7277
|
sortOrder: "DESC"
|
|
5983
7278
|
};
|
|
5984
|
-
if (
|
|
5985
|
-
if (
|
|
5986
|
-
if (
|
|
5987
|
-
const
|
|
7279
|
+
if (service) params.serviceName = service;
|
|
7280
|
+
if (urlState.operation) params.spanName = urlState.operation;
|
|
7281
|
+
if (urlState.lookback) {
|
|
7282
|
+
const ms = LOOKBACK_MS[urlState.lookback];
|
|
7283
|
+
if (ms) params.timestampMin = String((Date.now() - ms) * 1e6);
|
|
7284
|
+
}
|
|
7285
|
+
if (urlState.tsMin) params.timestampMin = urlState.tsMin;
|
|
7286
|
+
if (urlState.tsMax) params.timestampMax = urlState.tsMax;
|
|
7287
|
+
if (urlState.minDuration) {
|
|
7288
|
+
const parsed = parseDuration(urlState.minDuration);
|
|
5988
7289
|
if (parsed) params.durationMin = parsed;
|
|
5989
7290
|
}
|
|
5990
|
-
if (
|
|
5991
|
-
const parsed = parseDuration(
|
|
7291
|
+
if (urlState.maxDuration) {
|
|
7292
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
5992
7293
|
if (parsed) params.durationMax = parsed;
|
|
5993
7294
|
}
|
|
5994
|
-
|
|
5995
|
-
|
|
7295
|
+
if (urlState.tags) {
|
|
7296
|
+
const tagMap = parseLogfmt(urlState.tags);
|
|
7297
|
+
if (Object.keys(tagMap).length > 0) params.tags = tagMap;
|
|
7298
|
+
}
|
|
7299
|
+
return {
|
|
7300
|
+
method: "searchTraceSummariesPage",
|
|
5996
7301
|
params
|
|
7302
|
+
};
|
|
7303
|
+
}, [
|
|
7304
|
+
service,
|
|
7305
|
+
urlState.operation,
|
|
7306
|
+
urlState.lookback,
|
|
7307
|
+
urlState.tsMin,
|
|
7308
|
+
urlState.tsMax,
|
|
7309
|
+
urlState.minDuration,
|
|
7310
|
+
urlState.maxDuration,
|
|
7311
|
+
urlState.limit,
|
|
7312
|
+
urlState.tags
|
|
7313
|
+
]);
|
|
7314
|
+
const handleSearch = (0, react.useCallback)((filters) => {
|
|
7315
|
+
pushURLState({
|
|
7316
|
+
tab: "services",
|
|
7317
|
+
service: filters.service ?? service,
|
|
7318
|
+
operation: filters.operation ?? null,
|
|
7319
|
+
tags: filters.tags ?? null,
|
|
7320
|
+
lookback: filters.lookback ?? null,
|
|
7321
|
+
minDuration: filters.minDuration ?? null,
|
|
7322
|
+
maxDuration: filters.maxDuration ?? null,
|
|
7323
|
+
limit: filters.limit
|
|
5997
7324
|
});
|
|
5998
7325
|
}, [service]);
|
|
5999
7326
|
const { data, loading, error } = useKopaiData(ds);
|
|
6000
|
-
const
|
|
6001
|
-
const
|
|
6002
|
-
(0, react.
|
|
6003
|
-
|
|
6004
|
-
|
|
6005
|
-
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
const ac = new AbortController();
|
|
6009
|
-
Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
|
|
6010
|
-
if (!ac.signal.aborted) {
|
|
6011
|
-
const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
6012
|
-
setFullTraces(new Map(entries));
|
|
6013
|
-
}
|
|
6014
|
-
}).catch((err) => {
|
|
6015
|
-
if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
|
|
6016
|
-
});
|
|
6017
|
-
return () => ac.abort();
|
|
6018
|
-
}, [data, client]);
|
|
6019
|
-
const operations = (0, react.useMemo)(() => {
|
|
7327
|
+
const { data: servicesData } = useKopaiData((0, react.useMemo)(() => ({ method: "getServices" }), []));
|
|
7328
|
+
const _services = servicesData?.services ?? [];
|
|
7329
|
+
const { data: opsData } = useKopaiData((0, react.useMemo)(() => service ? {
|
|
7330
|
+
method: "getOperations",
|
|
7331
|
+
params: { serviceName: service }
|
|
7332
|
+
} : void 0, [service]));
|
|
7333
|
+
const operations = opsData?.operations ?? [];
|
|
7334
|
+
const traces = (0, react.useMemo)(() => {
|
|
6020
7335
|
if (!data?.data) return [];
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
7336
|
+
return data.data.map((row) => ({
|
|
7337
|
+
traceId: row.traceId,
|
|
7338
|
+
rootSpanName: row.rootSpanName,
|
|
7339
|
+
serviceName: row.rootServiceName,
|
|
7340
|
+
durationMs: parseInt(row.durationNs, 10) / 1e6,
|
|
7341
|
+
statusCode: row.errorCount > 0 ? "ERROR" : "OK",
|
|
7342
|
+
timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
|
|
7343
|
+
spanCount: row.spanCount,
|
|
7344
|
+
services: row.services,
|
|
7345
|
+
errorCount: row.errorCount
|
|
7346
|
+
}));
|
|
6024
7347
|
}, [data]);
|
|
6025
7348
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearch, {
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6030
|
-
for (const row of data.data) {
|
|
6031
|
-
const tid = row.TraceId;
|
|
6032
|
-
if (!grouped.has(tid)) grouped.set(tid, []);
|
|
6033
|
-
grouped.get(tid).push(row);
|
|
6034
|
-
}
|
|
6035
|
-
return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
|
|
6036
|
-
const spans = fullTraces.get(traceId) ?? searchSpans;
|
|
6037
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
|
|
6038
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
6039
|
-
const svcMap = /* @__PURE__ */ new Map();
|
|
6040
|
-
let errorCount = 0;
|
|
6041
|
-
for (const s of spans) {
|
|
6042
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
6043
|
-
const entry = svcMap.get(svcName) ?? {
|
|
6044
|
-
count: 0,
|
|
6045
|
-
hasError: false
|
|
6046
|
-
};
|
|
6047
|
-
entry.count++;
|
|
6048
|
-
if (s.StatusCode === "ERROR") {
|
|
6049
|
-
entry.hasError = true;
|
|
6050
|
-
errorCount++;
|
|
6051
|
-
}
|
|
6052
|
-
svcMap.set(svcName, entry);
|
|
6053
|
-
}
|
|
6054
|
-
const services = Array.from(svcMap.entries()).map(([name, v]) => ({
|
|
6055
|
-
name,
|
|
6056
|
-
count: v.count,
|
|
6057
|
-
hasError: v.hasError
|
|
6058
|
-
})).sort((a, b) => b.count - a.count);
|
|
6059
|
-
return {
|
|
6060
|
-
traceId,
|
|
6061
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
6062
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
6063
|
-
durationMs: durationNs / 1e6,
|
|
6064
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
6065
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
6066
|
-
spanCount: spans.length,
|
|
6067
|
-
services,
|
|
6068
|
-
errorCount
|
|
6069
|
-
};
|
|
6070
|
-
});
|
|
6071
|
-
}, [data, fullTraces]),
|
|
7349
|
+
services: _services,
|
|
7350
|
+
service: service ?? "",
|
|
7351
|
+
traces,
|
|
6072
7352
|
operations,
|
|
6073
7353
|
isLoading: loading,
|
|
6074
7354
|
error: error ?? void 0,
|
|
6075
7355
|
onSelectTrace,
|
|
6076
|
-
|
|
7356
|
+
onCompare,
|
|
6077
7357
|
onSearch: handleSearch
|
|
6078
7358
|
});
|
|
6079
7359
|
}
|
|
6080
|
-
function TraceDetailView({
|
|
7360
|
+
function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
|
|
6081
7361
|
const { data, loading, error } = useKopaiData((0, react.useMemo)(() => ({
|
|
6082
7362
|
method: "getTrace",
|
|
6083
7363
|
params: { traceId }
|
|
6084
7364
|
}), [traceId]));
|
|
6085
7365
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetail, {
|
|
6086
|
-
service,
|
|
6087
7366
|
traceId,
|
|
6088
7367
|
rows: data ?? [],
|
|
6089
7368
|
isLoading: loading,
|
|
6090
7369
|
error: error ?? void 0,
|
|
6091
7370
|
selectedSpanId: selectedSpanId ?? void 0,
|
|
6092
7371
|
onSpanClick: (span) => onSelectSpan(span.spanId),
|
|
7372
|
+
onSpanDeselect: onDeselectSpan,
|
|
6093
7373
|
onBack
|
|
6094
7374
|
});
|
|
6095
7375
|
}
|
|
6096
|
-
function ServicesTab({
|
|
7376
|
+
function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
|
|
6097
7377
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
6098
|
-
const
|
|
6099
|
-
|
|
6100
|
-
const backToTraceListRef = (0, react.useRef)(onBackToTraceList);
|
|
6101
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
7378
|
+
const backRef = (0, react.useRef)(onBack);
|
|
7379
|
+
backRef.current = onBack;
|
|
6102
7380
|
(0, react.useEffect)(() => {
|
|
6103
7381
|
const handleKeyDown = (e) => {
|
|
6104
7382
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
|
6105
7383
|
if (e.key === "Backspace") {
|
|
6106
7384
|
e.preventDefault();
|
|
6107
|
-
|
|
6108
|
-
else if (selectedService) backToServicesRef.current();
|
|
7385
|
+
backRef.current();
|
|
6109
7386
|
}
|
|
6110
7387
|
};
|
|
6111
7388
|
window.addEventListener("keydown", handleKeyDown);
|
|
6112
7389
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
6113
|
-
}, [
|
|
6114
|
-
if (
|
|
6115
|
-
|
|
7390
|
+
}, []);
|
|
7391
|
+
if (compareParam) {
|
|
7392
|
+
const [traceIdA, traceIdB] = compareParam.split(",");
|
|
7393
|
+
if (traceIdA && traceIdB) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceComparison, {
|
|
7394
|
+
traceIdA,
|
|
7395
|
+
traceIdB,
|
|
7396
|
+
onBack
|
|
7397
|
+
});
|
|
7398
|
+
}
|
|
7399
|
+
if (selectedTraceId) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetailView, {
|
|
6116
7400
|
traceId: selectedTraceId,
|
|
6117
7401
|
selectedSpanId,
|
|
6118
7402
|
onSelectSpan,
|
|
6119
|
-
|
|
7403
|
+
onDeselectSpan,
|
|
7404
|
+
onBack
|
|
6120
7405
|
});
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
|
|
6124
|
-
onSelectTrace
|
|
7406
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearchView, {
|
|
7407
|
+
onSelectTrace,
|
|
7408
|
+
onCompare
|
|
6125
7409
|
});
|
|
6126
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServiceListView, { onSelect: onSelectService });
|
|
6127
7410
|
}
|
|
6128
|
-
const DASHBOARDS_API_BASE = "/dashboards";
|
|
6129
7411
|
const METRICS_TREE = {
|
|
6130
7412
|
root: "root",
|
|
6131
7413
|
elements: {
|
|
@@ -6186,14 +7468,12 @@ const METRICS_TREE = {
|
|
|
6186
7468
|
}
|
|
6187
7469
|
}
|
|
6188
7470
|
};
|
|
6189
|
-
function useDashboardTree(dashboardId) {
|
|
7471
|
+
function useDashboardTree(client, dashboardId) {
|
|
6190
7472
|
const { data, isFetching, error } = (0, _tanstack_react_query.useQuery)({
|
|
6191
7473
|
queryKey: ["dashboard-tree", dashboardId],
|
|
6192
7474
|
queryFn: async ({ signal }) => {
|
|
6193
|
-
const
|
|
6194
|
-
|
|
6195
|
-
const json = await res.json();
|
|
6196
|
-
const parsed = observabilityCatalog.uiTreeSchema.safeParse(json.uiTree);
|
|
7475
|
+
const dashboard = await client.getDashboard(dashboardId, { signal });
|
|
7476
|
+
const parsed = observabilityCatalog.uiTreeSchema.safeParse(dashboard.uiTree);
|
|
6197
7477
|
if (!parsed.success) {
|
|
6198
7478
|
const issue = parsed.error.issues[0];
|
|
6199
7479
|
const path = issue?.path.length ? issue.path.join(".") + ": " : "";
|
|
@@ -6212,7 +7492,7 @@ function useDashboardTree(dashboardId) {
|
|
|
6212
7492
|
function MetricsTab() {
|
|
6213
7493
|
const kopaiClient = useKopaiSDK();
|
|
6214
7494
|
const { dashboardId } = useURLState();
|
|
6215
|
-
const { loading, error, tree } = useDashboardTree(dashboardId);
|
|
7495
|
+
const { loading, error, tree } = useDashboardTree(kopaiClient, dashboardId);
|
|
6216
7496
|
if (loading) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6217
7497
|
className: "text-muted-foreground text-sm",
|
|
6218
7498
|
children: "Loading dashboard..."
|
|
@@ -6233,40 +7513,56 @@ function getDefaultClient() {
|
|
|
6233
7513
|
}
|
|
6234
7514
|
function ObservabilityPage({ client }) {
|
|
6235
7515
|
const activeClient = client ?? getDefaultClient();
|
|
6236
|
-
const { tab: activeTab,
|
|
7516
|
+
const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
|
|
6237
7517
|
const handleTabChange = (0, react.useCallback)((tab) => {
|
|
6238
7518
|
pushURLState({ tab });
|
|
6239
7519
|
}, []);
|
|
6240
|
-
const handleSelectService = (0, react.useCallback)((service) => {
|
|
6241
|
-
pushURLState({
|
|
6242
|
-
tab: "services",
|
|
6243
|
-
service
|
|
6244
|
-
});
|
|
6245
|
-
}, []);
|
|
6246
7520
|
const handleSelectTrace = (0, react.useCallback)((traceId) => {
|
|
6247
7521
|
pushURLState({
|
|
7522
|
+
...readURLState(),
|
|
6248
7523
|
tab: "services",
|
|
6249
|
-
service: selectedService,
|
|
6250
7524
|
trace: traceId
|
|
6251
7525
|
});
|
|
6252
|
-
}, [
|
|
7526
|
+
}, []);
|
|
6253
7527
|
const handleSelectSpan = (0, react.useCallback)((spanId) => {
|
|
6254
7528
|
pushURLState({
|
|
7529
|
+
...readURLState(),
|
|
6255
7530
|
tab: "services",
|
|
6256
|
-
service: selectedService,
|
|
6257
|
-
trace: selectedTraceId,
|
|
6258
7531
|
span: spanId
|
|
6259
7532
|
}, { replace: true });
|
|
6260
|
-
}, [selectedService, selectedTraceId]);
|
|
6261
|
-
const handleBackToServices = (0, react.useCallback)(() => {
|
|
6262
|
-
pushURLState({ tab: "services" });
|
|
6263
7533
|
}, []);
|
|
6264
|
-
const
|
|
7534
|
+
const handleDeselectSpan = (0, react.useCallback)(() => {
|
|
7535
|
+
pushURLState({
|
|
7536
|
+
...readURLState(),
|
|
7537
|
+
span: null
|
|
7538
|
+
}, { replace: true });
|
|
7539
|
+
}, []);
|
|
7540
|
+
const handleCompare = (0, react.useCallback)((traceIds) => {
|
|
7541
|
+
pushURLState({
|
|
7542
|
+
...readURLState(),
|
|
7543
|
+
tab: "services",
|
|
7544
|
+
trace: null,
|
|
7545
|
+
span: null,
|
|
7546
|
+
view: null,
|
|
7547
|
+
uiFind: null,
|
|
7548
|
+
viewStart: null,
|
|
7549
|
+
viewEnd: null,
|
|
7550
|
+
compare: traceIds.join(",")
|
|
7551
|
+
});
|
|
7552
|
+
}, []);
|
|
7553
|
+
const handleBack = (0, react.useCallback)(() => {
|
|
6265
7554
|
pushURLState({
|
|
7555
|
+
...readURLState(),
|
|
6266
7556
|
tab: "services",
|
|
6267
|
-
|
|
7557
|
+
trace: null,
|
|
7558
|
+
span: null,
|
|
7559
|
+
view: null,
|
|
7560
|
+
uiFind: null,
|
|
7561
|
+
viewStart: null,
|
|
7562
|
+
viewEnd: null,
|
|
7563
|
+
compare: null
|
|
6268
7564
|
});
|
|
6269
|
-
}, [
|
|
7565
|
+
}, []);
|
|
6270
7566
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KopaiSDKProvider, {
|
|
6271
7567
|
client: activeClient,
|
|
6272
7568
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyboardShortcutsProvider, {
|
|
@@ -6281,21 +7577,20 @@ function ObservabilityPage({ client }) {
|
|
|
6281
7577
|
}),
|
|
6282
7578
|
activeTab === "logs" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogsTab, {}),
|
|
6283
7579
|
activeTab === "services" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServicesTab, {
|
|
6284
|
-
selectedService,
|
|
6285
7580
|
selectedTraceId,
|
|
6286
7581
|
selectedSpanId,
|
|
6287
|
-
|
|
7582
|
+
compareParam,
|
|
6288
7583
|
onSelectTrace: handleSelectTrace,
|
|
6289
7584
|
onSelectSpan: handleSelectSpan,
|
|
6290
|
-
|
|
6291
|
-
|
|
7585
|
+
onDeselectSpan: handleDeselectSpan,
|
|
7586
|
+
onBack: handleBack,
|
|
7587
|
+
onCompare: handleCompare
|
|
6292
7588
|
}),
|
|
6293
7589
|
activeTab === "metrics" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricsTab, {})
|
|
6294
7590
|
] })
|
|
6295
7591
|
})
|
|
6296
7592
|
});
|
|
6297
7593
|
}
|
|
6298
|
-
|
|
6299
7594
|
//#endregion
|
|
6300
7595
|
//#region src/lib/generate-prompt-instructions.ts
|
|
6301
7596
|
function formatPropType(prop) {
|
|
@@ -6425,7 +7720,6 @@ ${JSON.stringify(unifiedSchema)}
|
|
|
6425
7720
|
|
|
6426
7721
|
${JSON.stringify(exampleElements)}`;
|
|
6427
7722
|
}
|
|
6428
|
-
|
|
6429
7723
|
//#endregion
|
|
6430
7724
|
exports.KopaiSDKProvider = KopaiSDKProvider;
|
|
6431
7725
|
exports.ObservabilityPage = ObservabilityPage;
|
|
@@ -6434,4 +7728,4 @@ exports.createCatalog = createCatalog;
|
|
|
6434
7728
|
exports.createRendererFromCatalog = createRendererFromCatalog;
|
|
6435
7729
|
exports.generatePromptInstructions = generatePromptInstructions;
|
|
6436
7730
|
exports.observabilityCatalog = observabilityCatalog;
|
|
6437
|
-
exports.useKopaiSDK = useKopaiSDK;
|
|
7731
|
+
exports.useKopaiSDK = useKopaiSDK;
|