@kopai/ui 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2427 -1139
- package/dist/index.d.cts +36 -7
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +36 -7
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2376 -1082
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -13
- package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
- package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
- package/src/components/observability/LogTimeline/index.tsx +6 -2
- package/src/components/observability/MetricHistogram/index.tsx +20 -19
- package/src/components/observability/MetricTimeSeries/index.tsx +8 -9
- package/src/components/observability/ServiceList/shortcuts.ts +1 -1
- package/src/components/observability/TraceComparison/index.tsx +332 -0
- package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
- package/src/components/observability/TraceDetail/index.tsx +4 -3
- package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
- package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
- package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
- package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
- package/src/components/observability/TraceSearch/index.tsx +211 -218
- package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
- package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
- package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
- package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
- package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
- package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
- package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
- package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
- package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
- package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
- package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
- package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
- package/src/components/observability/TraceTimeline/index.tsx +254 -110
- package/src/components/observability/index.ts +15 -0
- package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
- package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
- package/src/components/observability/utils/flatten-tree.ts +15 -0
- package/src/components/observability/utils/time.ts +9 -0
- package/src/hooks/use-kopai-data.test.ts +3 -0
- package/src/hooks/use-kopai-data.ts +11 -0
- package/src/hooks/use-live-logs.test.ts +3 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
- package/src/lib/component-catalog.ts +15 -0
- package/src/pages/observability.test.tsx +5 -0
- package/src/pages/observability.tsx +314 -235
- package/src/providers/kopai-provider.tsx +3 -0
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
|
}, []);
|
|
@@ -2040,23 +2993,77 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
|
|
|
2040
2993
|
return next;
|
|
2041
2994
|
});
|
|
2042
2995
|
}, [selectedSpanId, flattenedSpans]);
|
|
2043
|
-
const
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
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
|
|
2057
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
|
|
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
|
/**
|
|
@@ -4310,18 +5340,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
|
|
|
4310
5340
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
4311
5341
|
className: "text-gray-400 text-xs mb-2",
|
|
4312
5342
|
children: formatTime(typeof label === "number" ? label : Number(label))
|
|
4313
|
-
}),
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
className: "font-medium",
|
|
4319
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4320
|
-
}),
|
|
4321
|
-
" ",
|
|
4322
|
-
formatValue(entry.value)
|
|
4323
|
-
]
|
|
4324
|
-
}, i))]
|
|
5343
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TooltipEntryList, {
|
|
5344
|
+
payload,
|
|
5345
|
+
displayLabelMap,
|
|
5346
|
+
formatValue
|
|
5347
|
+
})]
|
|
4325
5348
|
});
|
|
4326
5349
|
}
|
|
4327
5350
|
function MetricLoadingSkeleton({ height = 400 }) {
|
|
@@ -4368,7 +5391,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
|
|
|
4368
5391
|
})
|
|
4369
5392
|
});
|
|
4370
5393
|
}
|
|
4371
|
-
|
|
4372
5394
|
//#endregion
|
|
4373
5395
|
//#region src/components/observability/MetricHistogram/index.tsx
|
|
4374
5396
|
/**
|
|
@@ -4382,6 +5404,9 @@ const COLORS = [
|
|
|
4382
5404
|
"#00C49F",
|
|
4383
5405
|
"#0088FE"
|
|
4384
5406
|
];
|
|
5407
|
+
function isBucketData(v) {
|
|
5408
|
+
return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
|
|
5409
|
+
}
|
|
4385
5410
|
const defaultFormatBucketLabel = (bound, index, bounds) => {
|
|
4386
5411
|
if (index === 0) return `≤${bound}`;
|
|
4387
5412
|
if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
|
|
@@ -4424,7 +5449,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
|
|
|
4424
5449
|
};
|
|
4425
5450
|
buckets.push(bucket);
|
|
4426
5451
|
}
|
|
4427
|
-
|
|
5452
|
+
const prev = bucket[seriesName];
|
|
5453
|
+
bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
|
|
4428
5454
|
}
|
|
4429
5455
|
}
|
|
4430
5456
|
buckets.sort((a, b) => a.lowerBound - b.lowerBound);
|
|
@@ -4561,25 +5587,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
|
|
|
4561
5587
|
}
|
|
4562
5588
|
function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
|
|
4563
5589
|
if (!active || !payload?.length) return null;
|
|
4564
|
-
const
|
|
4565
|
-
if (!
|
|
5590
|
+
const raw = payload[0]?.payload;
|
|
5591
|
+
if (!isBucketData(raw)) return null;
|
|
4566
5592
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
4567
5593
|
className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
|
|
4568
5594
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
|
|
4569
5595
|
className: "text-gray-300 text-sm font-medium mb-2",
|
|
4570
|
-
children: ["Bucket: ", boundsScale ? `${formatDisplayValue(
|
|
4571
|
-
}),
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
4575
|
-
|
|
4576
|
-
className: "font-medium",
|
|
4577
|
-
children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
|
|
4578
|
-
}),
|
|
4579
|
-
" ",
|
|
4580
|
-
formatValue(entry.value)
|
|
4581
|
-
]
|
|
4582
|
-
}, i))]
|
|
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
|
+
})]
|
|
4583
5602
|
});
|
|
4584
5603
|
}
|
|
4585
5604
|
function HistogramLoadingSkeleton({ height = 400 }) {
|
|
@@ -4619,7 +5638,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
|
|
|
4619
5638
|
})
|
|
4620
5639
|
});
|
|
4621
5640
|
}
|
|
4622
|
-
|
|
4623
5641
|
//#endregion
|
|
4624
5642
|
//#region src/components/observability/MetricStat/index.tsx
|
|
4625
5643
|
/**
|
|
@@ -4812,7 +5830,6 @@ function TrendIndicator({ direction, value }) {
|
|
|
4812
5830
|
children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
|
|
4813
5831
|
});
|
|
4814
5832
|
}
|
|
4815
|
-
|
|
4816
5833
|
//#endregion
|
|
4817
5834
|
//#region src/components/observability/MetricTable/index.tsx
|
|
4818
5835
|
/**
|
|
@@ -4952,7 +5969,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
|
|
|
4952
5969
|
})]
|
|
4953
5970
|
});
|
|
4954
5971
|
}
|
|
4955
|
-
|
|
4956
5972
|
//#endregion
|
|
4957
5973
|
//#region src/lib/renderer.tsx
|
|
4958
5974
|
/**
|
|
@@ -5057,7 +6073,6 @@ function Renderer({ tree, registry, fallback }) {
|
|
|
5057
6073
|
fallback
|
|
5058
6074
|
});
|
|
5059
6075
|
}
|
|
5060
|
-
|
|
5061
6076
|
//#endregion
|
|
5062
6077
|
//#region src/lib/catalog.ts
|
|
5063
6078
|
const dashboardCatalog = createCatalog({
|
|
@@ -5257,8 +6272,7 @@ const dashboardCatalog = createCatalog({
|
|
|
5257
6272
|
}
|
|
5258
6273
|
}
|
|
5259
6274
|
});
|
|
5260
|
-
|
|
5261
|
-
|
|
6275
|
+
Object.keys(dashboardCatalog.components);
|
|
5262
6276
|
//#endregion
|
|
5263
6277
|
//#region src/components/dashboard/Badge/index.tsx
|
|
5264
6278
|
function Badge({ element }) {
|
|
@@ -5282,7 +6296,6 @@ function Badge({ element }) {
|
|
|
5282
6296
|
children: text
|
|
5283
6297
|
});
|
|
5284
6298
|
}
|
|
5285
|
-
|
|
5286
6299
|
//#endregion
|
|
5287
6300
|
//#region src/components/dashboard/Card/index.tsx
|
|
5288
6301
|
function Card({ element, children }) {
|
|
@@ -5323,7 +6336,6 @@ function Card({ element, children }) {
|
|
|
5323
6336
|
})]
|
|
5324
6337
|
});
|
|
5325
6338
|
}
|
|
5326
|
-
|
|
5327
6339
|
//#endregion
|
|
5328
6340
|
//#region src/components/dashboard/Divider/index.tsx
|
|
5329
6341
|
function Divider({ element }) {
|
|
@@ -5361,7 +6373,6 @@ function Divider({ element }) {
|
|
|
5361
6373
|
margin: "16px 0"
|
|
5362
6374
|
} });
|
|
5363
6375
|
}
|
|
5364
|
-
|
|
5365
6376
|
//#endregion
|
|
5366
6377
|
//#region src/components/dashboard/Empty/index.tsx
|
|
5367
6378
|
function Empty({ element, onAction }) {
|
|
@@ -5404,7 +6415,6 @@ function Empty({ element, onAction }) {
|
|
|
5404
6415
|
]
|
|
5405
6416
|
});
|
|
5406
6417
|
}
|
|
5407
|
-
|
|
5408
6418
|
//#endregion
|
|
5409
6419
|
//#region src/components/dashboard/Grid/index.tsx
|
|
5410
6420
|
function Grid({ element, children }) {
|
|
@@ -5422,7 +6432,6 @@ function Grid({ element, children }) {
|
|
|
5422
6432
|
children
|
|
5423
6433
|
});
|
|
5424
6434
|
}
|
|
5425
|
-
|
|
5426
6435
|
//#endregion
|
|
5427
6436
|
//#region src/components/dashboard/Heading/index.tsx
|
|
5428
6437
|
function Heading({ element }) {
|
|
@@ -5441,7 +6450,6 @@ function Heading({ element }) {
|
|
|
5441
6450
|
children: text
|
|
5442
6451
|
});
|
|
5443
6452
|
}
|
|
5444
|
-
|
|
5445
6453
|
//#endregion
|
|
5446
6454
|
//#region src/components/dashboard/Stack/index.tsx
|
|
5447
6455
|
function Stack({ element, children }) {
|
|
@@ -5465,7 +6473,6 @@ function Stack({ element, children }) {
|
|
|
5465
6473
|
children
|
|
5466
6474
|
});
|
|
5467
6475
|
}
|
|
5468
|
-
|
|
5469
6476
|
//#endregion
|
|
5470
6477
|
//#region src/components/dashboard/Text/index.tsx
|
|
5471
6478
|
function Text({ element }) {
|
|
@@ -5484,7 +6491,6 @@ function Text({ element }) {
|
|
|
5484
6491
|
children: content
|
|
5485
6492
|
});
|
|
5486
6493
|
}
|
|
5487
|
-
|
|
5488
6494
|
//#endregion
|
|
5489
6495
|
//#region src/components/observability/renderers/OtelLogTimeline.tsx
|
|
5490
6496
|
function OtelLogTimeline(props) {
|
|
@@ -5506,7 +6512,6 @@ function OtelLogTimeline(props) {
|
|
|
5506
6512
|
})
|
|
5507
6513
|
});
|
|
5508
6514
|
}
|
|
5509
|
-
|
|
5510
6515
|
//#endregion
|
|
5511
6516
|
//#region src/components/observability/renderers/OtelMetricDiscovery.tsx
|
|
5512
6517
|
const TYPE_ORDER = {
|
|
@@ -5584,7 +6589,6 @@ function OtelMetricDiscovery(props) {
|
|
|
5584
6589
|
})
|
|
5585
6590
|
});
|
|
5586
6591
|
}
|
|
5587
|
-
|
|
5588
6592
|
//#endregion
|
|
5589
6593
|
//#region src/components/observability/renderers/OtelMetricHistogram.tsx
|
|
5590
6594
|
function OtelMetricHistogram(props) {
|
|
@@ -5605,7 +6609,6 @@ function OtelMetricHistogram(props) {
|
|
|
5605
6609
|
unit: props.element.props.unit ?? void 0
|
|
5606
6610
|
});
|
|
5607
6611
|
}
|
|
5608
|
-
|
|
5609
6612
|
//#endregion
|
|
5610
6613
|
//#region src/components/observability/renderers/OtelMetricStat.tsx
|
|
5611
6614
|
function OtelMetricStat(props) {
|
|
@@ -5626,7 +6629,6 @@ function OtelMetricStat(props) {
|
|
|
5626
6629
|
formatValue: formatOtelValue
|
|
5627
6630
|
});
|
|
5628
6631
|
}
|
|
5629
|
-
|
|
5630
6632
|
//#endregion
|
|
5631
6633
|
//#region src/components/observability/renderers/OtelMetricTable.tsx
|
|
5632
6634
|
function OtelMetricTable(props) {
|
|
@@ -5645,7 +6647,6 @@ function OtelMetricTable(props) {
|
|
|
5645
6647
|
maxRows: props.element.props.maxRows ?? 100
|
|
5646
6648
|
});
|
|
5647
6649
|
}
|
|
5648
|
-
|
|
5649
6650
|
//#endregion
|
|
5650
6651
|
//#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
|
|
5651
6652
|
function OtelMetricTimeSeries(props) {
|
|
@@ -5667,7 +6668,6 @@ function OtelMetricTimeSeries(props) {
|
|
|
5667
6668
|
unit: props.element.props.unit ?? void 0
|
|
5668
6669
|
});
|
|
5669
6670
|
}
|
|
5670
|
-
|
|
5671
6671
|
//#endregion
|
|
5672
6672
|
//#region src/components/observability/renderers/OtelTraceDetail.tsx
|
|
5673
6673
|
function OtelTraceDetail(props) {
|
|
@@ -5679,19 +6679,15 @@ function OtelTraceDetail(props) {
|
|
|
5679
6679
|
children: "No data source"
|
|
5680
6680
|
});
|
|
5681
6681
|
const rows = props.data?.data ?? [];
|
|
5682
|
-
const
|
|
5683
|
-
const service = firstRow?.ServiceName ?? "unknown";
|
|
5684
|
-
const traceId = firstRow?.TraceId ?? "";
|
|
6682
|
+
const traceId = rows[0]?.TraceId ?? "";
|
|
5685
6683
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetail, {
|
|
5686
6684
|
rows,
|
|
5687
6685
|
isLoading: props.loading,
|
|
5688
6686
|
error: props.error ?? void 0,
|
|
5689
|
-
service,
|
|
5690
6687
|
traceId,
|
|
5691
6688
|
onBack: () => {}
|
|
5692
6689
|
});
|
|
5693
6690
|
}
|
|
5694
|
-
|
|
5695
6691
|
//#endregion
|
|
5696
6692
|
//#region src/components/observability/DynamicDashboard/index.tsx
|
|
5697
6693
|
const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
|
|
@@ -5717,24 +6713,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
|
|
|
5717
6713
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricsRenderer, { tree: uiTree })
|
|
5718
6714
|
});
|
|
5719
6715
|
}
|
|
5720
|
-
|
|
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
|
+
}
|
|
5721
6969
|
//#endregion
|
|
5722
6970
|
//#region src/components/observability/ServiceList/shortcuts.ts
|
|
5723
6971
|
const SERVICES_SHORTCUTS = {
|
|
5724
|
-
name: "
|
|
6972
|
+
name: "Traces",
|
|
5725
6973
|
shortcuts: [{
|
|
5726
6974
|
keys: ["Backspace"],
|
|
5727
6975
|
description: "Go back"
|
|
5728
6976
|
}]
|
|
5729
6977
|
};
|
|
5730
|
-
|
|
5731
6978
|
//#endregion
|
|
5732
6979
|
//#region src/pages/observability.tsx
|
|
5733
6980
|
const TABS = [
|
|
5734
6981
|
{
|
|
5735
6982
|
key: "services",
|
|
5736
|
-
label: "
|
|
5737
|
-
shortcutKey: "
|
|
6983
|
+
label: "Traces",
|
|
6984
|
+
shortcutKey: "T"
|
|
5738
6985
|
},
|
|
5739
6986
|
{
|
|
5740
6987
|
key: "logs",
|
|
@@ -5754,20 +7001,53 @@ function readURLState() {
|
|
|
5754
7001
|
const span = params.get("span");
|
|
5755
7002
|
const dashboardId = params.get("dashboardId");
|
|
5756
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;
|
|
5757
7007
|
return {
|
|
5758
|
-
tab
|
|
7008
|
+
tab,
|
|
5759
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"),
|
|
5760
7019
|
trace,
|
|
5761
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"),
|
|
5762
7026
|
dashboardId
|
|
5763
7027
|
};
|
|
5764
7028
|
}
|
|
5765
7029
|
function pushURLState(state, { replace = false } = {}) {
|
|
5766
7030
|
const params = new URLSearchParams();
|
|
5767
7031
|
if (state.tab !== "services") params.set("tab", state.tab);
|
|
5768
|
-
if (state.
|
|
5769
|
-
|
|
5770
|
-
|
|
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
|
+
}
|
|
5771
7051
|
const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
|
|
5772
7052
|
if (dashboardId) params.set("dashboardId", dashboardId);
|
|
5773
7053
|
const qs = params.toString();
|
|
@@ -5784,8 +7064,22 @@ let _cachedSearch = "";
|
|
|
5784
7064
|
let _cachedState = {
|
|
5785
7065
|
tab: "services",
|
|
5786
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,
|
|
5787
7076
|
trace: null,
|
|
5788
7077
|
span: null,
|
|
7078
|
+
view: null,
|
|
7079
|
+
uiFind: null,
|
|
7080
|
+
compare: null,
|
|
7081
|
+
viewStart: null,
|
|
7082
|
+
viewEnd: null,
|
|
5789
7083
|
dashboardId: null
|
|
5790
7084
|
};
|
|
5791
7085
|
function getURLSnapshot() {
|
|
@@ -5895,6 +7189,26 @@ function parseDuration(input) {
|
|
|
5895
7189
|
s: 1e9
|
|
5896
7190
|
}[unit]));
|
|
5897
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
|
+
};
|
|
5898
7212
|
function LogsTab() {
|
|
5899
7213
|
const [initState] = (0, react.useState)(() => readLogFilters());
|
|
5900
7214
|
const [filters, setFilters] = (0, react.useState)(initState.filters);
|
|
@@ -5954,185 +7268,145 @@ function LogsTab() {
|
|
|
5954
7268
|
})]
|
|
5955
7269
|
});
|
|
5956
7270
|
}
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
sortOrder: "DESC"
|
|
5962
|
-
}
|
|
5963
|
-
};
|
|
5964
|
-
function ServiceListView({ onSelect }) {
|
|
5965
|
-
const { data, loading, error } = useKopaiData(SERVICES_DS);
|
|
5966
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServiceList, {
|
|
5967
|
-
services: (0, react.useMemo)(() => {
|
|
5968
|
-
if (!data?.data) return [];
|
|
5969
|
-
const names = /* @__PURE__ */ new Set();
|
|
5970
|
-
for (const row of data.data) names.add(row.ServiceName ?? "unknown");
|
|
5971
|
-
return Array.from(names).sort().map((name) => ({ name }));
|
|
5972
|
-
}, [data]),
|
|
5973
|
-
isLoading: loading,
|
|
5974
|
-
error: error ?? void 0,
|
|
5975
|
-
onSelect
|
|
5976
|
-
});
|
|
5977
|
-
}
|
|
5978
|
-
function TraceSearchView({ service, onBack, onSelectTrace }) {
|
|
5979
|
-
const [ds, setDs] = (0, react.useState)(() => ({
|
|
5980
|
-
method: "searchTracesPage",
|
|
5981
|
-
params: {
|
|
5982
|
-
serviceName: service,
|
|
5983
|
-
limit: 20,
|
|
5984
|
-
sortOrder: "DESC"
|
|
5985
|
-
}
|
|
5986
|
-
}));
|
|
5987
|
-
const handleSearch = (0, react.useCallback)((filters) => {
|
|
7271
|
+
function TraceSearchView({ onSelectTrace, onCompare }) {
|
|
7272
|
+
const urlState = useURLState();
|
|
7273
|
+
const service = urlState.service;
|
|
7274
|
+
const ds = (0, react.useMemo)(() => {
|
|
5988
7275
|
const params = {
|
|
5989
|
-
|
|
5990
|
-
limit: filters.limit,
|
|
7276
|
+
limit: urlState.limit ?? 20,
|
|
5991
7277
|
sortOrder: "DESC"
|
|
5992
7278
|
};
|
|
5993
|
-
if (
|
|
5994
|
-
if (
|
|
5995
|
-
if (
|
|
5996
|
-
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);
|
|
5997
7289
|
if (parsed) params.durationMin = parsed;
|
|
5998
7290
|
}
|
|
5999
|
-
if (
|
|
6000
|
-
const parsed = parseDuration(
|
|
7291
|
+
if (urlState.maxDuration) {
|
|
7292
|
+
const parsed = parseDuration(urlState.maxDuration);
|
|
6001
7293
|
if (parsed) params.durationMax = parsed;
|
|
6002
7294
|
}
|
|
6003
|
-
|
|
6004
|
-
|
|
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",
|
|
6005
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
|
|
6006
7324
|
});
|
|
6007
7325
|
}, [service]);
|
|
6008
7326
|
const { data, loading, error } = useKopaiData(ds);
|
|
6009
|
-
const
|
|
6010
|
-
const
|
|
6011
|
-
(0, react.
|
|
6012
|
-
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
const ac = new AbortController();
|
|
6018
|
-
Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
|
|
6019
|
-
if (!ac.signal.aborted) {
|
|
6020
|
-
const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
|
|
6021
|
-
setFullTraces(new Map(entries));
|
|
6022
|
-
}
|
|
6023
|
-
}).catch((err) => {
|
|
6024
|
-
if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
|
|
6025
|
-
});
|
|
6026
|
-
return () => ac.abort();
|
|
6027
|
-
}, [data, client]);
|
|
6028
|
-
const operations = (0, react.useMemo)(() => {
|
|
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)(() => {
|
|
6029
7335
|
if (!data?.data) return [];
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
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
|
+
}));
|
|
6033
7347
|
}, [data]);
|
|
6034
7348
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearch, {
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
const grouped = /* @__PURE__ */ new Map();
|
|
6039
|
-
for (const row of data.data) {
|
|
6040
|
-
const tid = row.TraceId;
|
|
6041
|
-
if (!grouped.has(tid)) grouped.set(tid, []);
|
|
6042
|
-
grouped.get(tid).push(row);
|
|
6043
|
-
}
|
|
6044
|
-
return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
|
|
6045
|
-
const spans = fullTraces.get(traceId) ?? searchSpans;
|
|
6046
|
-
const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
|
|
6047
|
-
const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
|
|
6048
|
-
const svcMap = /* @__PURE__ */ new Map();
|
|
6049
|
-
let errorCount = 0;
|
|
6050
|
-
for (const s of spans) {
|
|
6051
|
-
const svcName = s.ServiceName ?? "unknown";
|
|
6052
|
-
const entry = svcMap.get(svcName) ?? {
|
|
6053
|
-
count: 0,
|
|
6054
|
-
hasError: false
|
|
6055
|
-
};
|
|
6056
|
-
entry.count++;
|
|
6057
|
-
if (s.StatusCode === "ERROR") {
|
|
6058
|
-
entry.hasError = true;
|
|
6059
|
-
errorCount++;
|
|
6060
|
-
}
|
|
6061
|
-
svcMap.set(svcName, entry);
|
|
6062
|
-
}
|
|
6063
|
-
const services = Array.from(svcMap.entries()).map(([name, v]) => ({
|
|
6064
|
-
name,
|
|
6065
|
-
count: v.count,
|
|
6066
|
-
hasError: v.hasError
|
|
6067
|
-
})).sort((a, b) => b.count - a.count);
|
|
6068
|
-
return {
|
|
6069
|
-
traceId,
|
|
6070
|
-
rootSpanName: root.SpanName ?? "unknown",
|
|
6071
|
-
serviceName: root.ServiceName ?? "unknown",
|
|
6072
|
-
durationMs: durationNs / 1e6,
|
|
6073
|
-
statusCode: root.StatusCode ?? "UNSET",
|
|
6074
|
-
timestampMs: parseInt(root.Timestamp, 10) / 1e6,
|
|
6075
|
-
spanCount: spans.length,
|
|
6076
|
-
services,
|
|
6077
|
-
errorCount
|
|
6078
|
-
};
|
|
6079
|
-
});
|
|
6080
|
-
}, [data, fullTraces]),
|
|
7349
|
+
services: _services,
|
|
7350
|
+
service: service ?? "",
|
|
7351
|
+
traces,
|
|
6081
7352
|
operations,
|
|
6082
7353
|
isLoading: loading,
|
|
6083
7354
|
error: error ?? void 0,
|
|
6084
7355
|
onSelectTrace,
|
|
6085
|
-
|
|
7356
|
+
onCompare,
|
|
6086
7357
|
onSearch: handleSearch
|
|
6087
7358
|
});
|
|
6088
7359
|
}
|
|
6089
|
-
function TraceDetailView({
|
|
7360
|
+
function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
|
|
6090
7361
|
const { data, loading, error } = useKopaiData((0, react.useMemo)(() => ({
|
|
6091
7362
|
method: "getTrace",
|
|
6092
7363
|
params: { traceId }
|
|
6093
7364
|
}), [traceId]));
|
|
6094
7365
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetail, {
|
|
6095
|
-
service,
|
|
6096
7366
|
traceId,
|
|
6097
7367
|
rows: data ?? [],
|
|
6098
7368
|
isLoading: loading,
|
|
6099
7369
|
error: error ?? void 0,
|
|
6100
7370
|
selectedSpanId: selectedSpanId ?? void 0,
|
|
6101
7371
|
onSpanClick: (span) => onSelectSpan(span.spanId),
|
|
7372
|
+
onSpanDeselect: onDeselectSpan,
|
|
6102
7373
|
onBack
|
|
6103
7374
|
});
|
|
6104
7375
|
}
|
|
6105
|
-
function ServicesTab({
|
|
7376
|
+
function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
|
|
6106
7377
|
useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
|
|
6107
|
-
const
|
|
6108
|
-
|
|
6109
|
-
const backToTraceListRef = (0, react.useRef)(onBackToTraceList);
|
|
6110
|
-
backToTraceListRef.current = onBackToTraceList;
|
|
7378
|
+
const backRef = (0, react.useRef)(onBack);
|
|
7379
|
+
backRef.current = onBack;
|
|
6111
7380
|
(0, react.useEffect)(() => {
|
|
6112
7381
|
const handleKeyDown = (e) => {
|
|
6113
7382
|
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
|
|
6114
7383
|
if (e.key === "Backspace") {
|
|
6115
7384
|
e.preventDefault();
|
|
6116
|
-
|
|
6117
|
-
else if (selectedService) backToServicesRef.current();
|
|
7385
|
+
backRef.current();
|
|
6118
7386
|
}
|
|
6119
7387
|
};
|
|
6120
7388
|
window.addEventListener("keydown", handleKeyDown);
|
|
6121
7389
|
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
6122
|
-
}, [
|
|
6123
|
-
if (
|
|
6124
|
-
|
|
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, {
|
|
6125
7400
|
traceId: selectedTraceId,
|
|
6126
7401
|
selectedSpanId,
|
|
6127
7402
|
onSelectSpan,
|
|
6128
|
-
|
|
7403
|
+
onDeselectSpan,
|
|
7404
|
+
onBack
|
|
6129
7405
|
});
|
|
6130
|
-
|
|
6131
|
-
|
|
6132
|
-
|
|
6133
|
-
onSelectTrace
|
|
7406
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearchView, {
|
|
7407
|
+
onSelectTrace,
|
|
7408
|
+
onCompare
|
|
6134
7409
|
});
|
|
6135
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServiceListView, { onSelect: onSelectService });
|
|
6136
7410
|
}
|
|
6137
7411
|
const METRICS_TREE = {
|
|
6138
7412
|
root: "root",
|
|
@@ -6239,40 +7513,56 @@ function getDefaultClient() {
|
|
|
6239
7513
|
}
|
|
6240
7514
|
function ObservabilityPage({ client }) {
|
|
6241
7515
|
const activeClient = client ?? getDefaultClient();
|
|
6242
|
-
const { tab: activeTab,
|
|
7516
|
+
const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
|
|
6243
7517
|
const handleTabChange = (0, react.useCallback)((tab) => {
|
|
6244
7518
|
pushURLState({ tab });
|
|
6245
7519
|
}, []);
|
|
6246
|
-
const handleSelectService = (0, react.useCallback)((service) => {
|
|
6247
|
-
pushURLState({
|
|
6248
|
-
tab: "services",
|
|
6249
|
-
service
|
|
6250
|
-
});
|
|
6251
|
-
}, []);
|
|
6252
7520
|
const handleSelectTrace = (0, react.useCallback)((traceId) => {
|
|
6253
7521
|
pushURLState({
|
|
7522
|
+
...readURLState(),
|
|
6254
7523
|
tab: "services",
|
|
6255
|
-
service: selectedService,
|
|
6256
7524
|
trace: traceId
|
|
6257
7525
|
});
|
|
6258
|
-
}, [
|
|
7526
|
+
}, []);
|
|
6259
7527
|
const handleSelectSpan = (0, react.useCallback)((spanId) => {
|
|
6260
7528
|
pushURLState({
|
|
7529
|
+
...readURLState(),
|
|
6261
7530
|
tab: "services",
|
|
6262
|
-
service: selectedService,
|
|
6263
|
-
trace: selectedTraceId,
|
|
6264
7531
|
span: spanId
|
|
6265
7532
|
}, { replace: true });
|
|
6266
|
-
}, [selectedService, selectedTraceId]);
|
|
6267
|
-
const handleBackToServices = (0, react.useCallback)(() => {
|
|
6268
|
-
pushURLState({ tab: "services" });
|
|
6269
7533
|
}, []);
|
|
6270
|
-
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)(() => {
|
|
6271
7554
|
pushURLState({
|
|
7555
|
+
...readURLState(),
|
|
6272
7556
|
tab: "services",
|
|
6273
|
-
|
|
7557
|
+
trace: null,
|
|
7558
|
+
span: null,
|
|
7559
|
+
view: null,
|
|
7560
|
+
uiFind: null,
|
|
7561
|
+
viewStart: null,
|
|
7562
|
+
viewEnd: null,
|
|
7563
|
+
compare: null
|
|
6274
7564
|
});
|
|
6275
|
-
}, [
|
|
7565
|
+
}, []);
|
|
6276
7566
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KopaiSDKProvider, {
|
|
6277
7567
|
client: activeClient,
|
|
6278
7568
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyboardShortcutsProvider, {
|
|
@@ -6287,21 +7577,20 @@ function ObservabilityPage({ client }) {
|
|
|
6287
7577
|
}),
|
|
6288
7578
|
activeTab === "logs" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LogsTab, {}),
|
|
6289
7579
|
activeTab === "services" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ServicesTab, {
|
|
6290
|
-
selectedService,
|
|
6291
7580
|
selectedTraceId,
|
|
6292
7581
|
selectedSpanId,
|
|
6293
|
-
|
|
7582
|
+
compareParam,
|
|
6294
7583
|
onSelectTrace: handleSelectTrace,
|
|
6295
7584
|
onSelectSpan: handleSelectSpan,
|
|
6296
|
-
|
|
6297
|
-
|
|
7585
|
+
onDeselectSpan: handleDeselectSpan,
|
|
7586
|
+
onBack: handleBack,
|
|
7587
|
+
onCompare: handleCompare
|
|
6298
7588
|
}),
|
|
6299
7589
|
activeTab === "metrics" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MetricsTab, {})
|
|
6300
7590
|
] })
|
|
6301
7591
|
})
|
|
6302
7592
|
});
|
|
6303
7593
|
}
|
|
6304
|
-
|
|
6305
7594
|
//#endregion
|
|
6306
7595
|
//#region src/lib/generate-prompt-instructions.ts
|
|
6307
7596
|
function formatPropType(prop) {
|
|
@@ -6431,7 +7720,6 @@ ${JSON.stringify(unifiedSchema)}
|
|
|
6431
7720
|
|
|
6432
7721
|
${JSON.stringify(exampleElements)}`;
|
|
6433
7722
|
}
|
|
6434
|
-
|
|
6435
7723
|
//#endregion
|
|
6436
7724
|
exports.KopaiSDKProvider = KopaiSDKProvider;
|
|
6437
7725
|
exports.ObservabilityPage = ObservabilityPage;
|
|
@@ -6440,4 +7728,4 @@ exports.createCatalog = createCatalog;
|
|
|
6440
7728
|
exports.createRendererFromCatalog = createRendererFromCatalog;
|
|
6441
7729
|
exports.generatePromptInstructions = generatePromptInstructions;
|
|
6442
7730
|
exports.observabilityCatalog = observabilityCatalog;
|
|
6443
|
-
exports.useKopaiSDK = useKopaiSDK;
|
|
7731
|
+
exports.useKopaiSDK = useKopaiSDK;
|