@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.
Files changed (49) hide show
  1. package/dist/index.cjs +2427 -1139
  2. package/dist/index.d.cts +36 -7
  3. package/dist/index.d.cts.map +1 -1
  4. package/dist/index.d.mts +36 -7
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +2376 -1082
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +13 -13
  9. package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
  10. package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
  11. package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
  12. package/src/components/observability/LogTimeline/index.tsx +6 -2
  13. package/src/components/observability/MetricHistogram/index.tsx +20 -19
  14. package/src/components/observability/MetricTimeSeries/index.tsx +8 -9
  15. package/src/components/observability/ServiceList/shortcuts.ts +1 -1
  16. package/src/components/observability/TraceComparison/index.tsx +332 -0
  17. package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
  18. package/src/components/observability/TraceDetail/index.tsx +4 -3
  19. package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
  20. package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
  21. package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
  22. package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
  23. package/src/components/observability/TraceSearch/index.tsx +211 -218
  24. package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
  25. package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
  26. package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
  27. package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
  28. package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
  29. package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
  30. package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
  31. package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
  32. package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
  33. package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
  34. package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
  35. package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
  36. package/src/components/observability/TraceTimeline/index.tsx +254 -110
  37. package/src/components/observability/index.ts +15 -0
  38. package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
  39. package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
  40. package/src/components/observability/utils/flatten-tree.ts +15 -0
  41. package/src/components/observability/utils/time.ts +9 -0
  42. package/src/hooks/use-kopai-data.test.ts +3 -0
  43. package/src/hooks/use-kopai-data.ts +11 -0
  44. package/src/hooks/use-live-logs.test.ts +3 -0
  45. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
  46. package/src/lib/component-catalog.ts +15 -0
  47. package/src/pages/observability.test.tsx +5 -0
  48. package/src/pages/observability.tsx +314 -235
  49. package/src/providers/kopai-provider.tsx +3 -0
package/dist/index.cjs CHANGED
@@ -1,4 +1,4 @@
1
- Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
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
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
12
- key = keys[i];
13
- if (!__hasOwnProp.call(to, key) && key !== except) {
14
- __defProp(to, key, {
15
- get: ((k) => from[k]).bind(null, key),
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
- const catalogConfigSchema = zod.z.object({
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/index.tsx
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
- ms: 5 * 6e4
542
+ value: "5m"
563
543
  },
564
544
  {
565
545
  label: "Last 15 Minutes",
566
- ms: 15 * 6e4
546
+ value: "15m"
567
547
  },
568
548
  {
569
549
  label: "Last 30 Minutes",
570
- ms: 30 * 6e4
550
+ value: "30m"
571
551
  },
572
552
  {
573
553
  label: "Last 1 Hour",
574
- ms: 60 * 6e4
554
+ value: "1h"
575
555
  },
576
556
  {
577
557
  label: "Last 2 Hours",
578
- ms: 120 * 6e4
558
+ value: "2h"
579
559
  },
580
560
  {
581
561
  label: "Last 6 Hours",
582
- ms: 360 * 6e4
562
+ value: "6h"
583
563
  },
584
564
  {
585
565
  label: "Last 12 Hours",
586
- ms: 720 * 6e4
566
+ value: "12h"
587
567
  },
588
568
  {
589
569
  label: "Last 24 Hours",
590
- ms: 1440 * 6e4
570
+ value: "24h"
591
571
  }
592
572
  ];
593
- function TraceSearch({ service, traces, operations = [], isLoading, error, onSelectTrace, onBack, onSearch }) {
594
- const [operation, setOperation] = (0, react.useState)("all");
595
- const [lookbackIdx, setLookbackIdx] = (0, react.useState)(-1);
596
- const [minDuration, setMinDuration] = (0, react.useState)("");
597
- const [maxDuration, setMaxDuration] = (0, react.useState)("");
598
- const [limit, setLimit] = (0, react.useState)(20);
599
- const [filtersOpen, setFiltersOpen] = (0, react.useState)(true);
600
- const handleFindTraces = () => {
601
- onSearch?.({
602
- operation: operation !== "all" ? operation : void 0,
603
- lookbackMs: lookbackIdx >= 0 ? LOOKBACK_OPTIONS$1[lookbackIdx].ms : void 0,
604
- minDuration: minDuration || void 0,
605
- maxDuration: maxDuration || void 0,
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", { children: [
610
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
611
- className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
612
- children: [
613
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
614
- onClick: onBack,
615
- className: "hover:text-foreground transition-colors",
616
- children: "Services"
617
- }),
618
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "/" }),
619
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
620
- className: "text-foreground",
621
- children: service
622
- })
623
- ]
624
- }),
625
- onSearch && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
626
- className: "border border-border rounded-lg mb-4",
627
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
628
- onClick: () => setFiltersOpen((v) => !v),
629
- className: "w-full flex items-center justify-between px-4 py-2.5 text-sm font-medium text-foreground hover:bg-muted/30 transition-colors",
630
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: "Filters" }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
631
- className: "text-muted-foreground text-xs",
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
- }), filtersOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
635
- className: "px-4 pb-4 pt-1 border-t border-border space-y-3",
636
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
637
- className: "grid grid-cols-2 md:grid-cols-3 gap-3",
638
- children: [
639
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
640
- className: "space-y-1",
641
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
642
- className: "text-xs text-muted-foreground",
643
- children: "Operation"
644
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("select", {
645
- value: operation,
646
- onChange: (e) => setOperation(e.target.value),
647
- className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
648
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
649
- value: "all",
650
- children: "All"
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
- isLoading && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
727
- className: "flex items-center gap-2 text-muted-foreground py-8",
728
- 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..."]
729
- }),
730
- error && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
731
- className: "text-red-400 py-4",
732
- children: ["Error loading traces: ", error.message]
733
- }),
734
- !isLoading && !error && traces.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
735
- className: "text-muted-foreground py-8",
736
- children: ["No traces found for ", service]
737
- }),
738
- traces.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
739
- className: "space-y-2",
740
- children: traces.map((t) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
741
- onClick: () => onSelectTrace(t.traceId),
742
- className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
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
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
745
- className: "flex items-baseline justify-between gap-2",
746
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
747
- className: "flex items-baseline gap-1.5 min-w-0",
748
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
749
- className: "font-medium text-foreground truncate",
750
- children: [
751
- t.serviceName,
752
- ": ",
753
- t.rootSpanName
754
- ]
755
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
756
- className: "text-xs font-mono text-muted-foreground shrink-0",
757
- children: t.traceId.slice(0, 7)
758
- })]
759
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
760
- className: "text-sm text-foreground/80 shrink-0",
761
- children: formatDuration(t.durationMs)
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.jsxs)("div", {
765
- className: "flex items-center flex-wrap gap-1.5 mt-1.5",
766
- children: [
767
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
768
- className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
769
- children: [
770
- t.spanCount,
771
- " Span",
772
- t.spanCount !== 1 ? "s" : ""
773
- ]
774
- }),
775
- t.errorCount > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
776
- className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
777
- children: [
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)("div", {
800
- className: "text-xs text-muted-foreground mt-1 text-right",
801
- children: formatTimestamp$1(t.timestampMs)
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
- }, t.traceId))
833
+ })
805
834
  })
806
- ] });
835
+ });
807
836
  }
808
-
809
837
  //#endregion
810
- //#region src/components/observability/utils/flatten-tree.ts
811
- function flattenTree(rootSpans, collapsedIds) {
812
- const result = [];
813
- function traverse(span, level) {
814
- result.push({
815
- span,
816
- level
817
- });
818
- if (!collapsedIds.has(span.spanId)) span.children.forEach((child) => traverse(child, level + 1));
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
- rootSpans.forEach((root) => traverse(root, 0));
821
- return result;
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
- function getAllSpanIds(rootSpans) {
824
- const ids = [];
825
- function traverse(span) {
826
- ids.push(span.spanId);
827
- span.children.forEach((child) => traverse(child));
828
- }
829
- rootSpans.forEach((root) => traverse(root));
830
- return ids;
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/TraceTimeline/TraceHeader.tsx
835
- function TraceHeader({ trace }) {
836
- const [copied, setCopied] = (0, react.useState)(false);
837
- const rootSpan = trace.rootSpans[0];
838
- const rootServiceName = rootSpan?.serviceName ?? "unknown";
839
- const rootSpanName = rootSpan?.name ?? "unknown";
840
- const totalDuration = trace.maxTimeMs - trace.minTimeMs;
841
- const handleCopyTraceId = async () => {
842
- try {
843
- await navigator.clipboard.writeText(trace.traceId);
844
- setCopied(true);
845
- setTimeout(() => setCopied(false), 2e3);
846
- } catch (err) {
847
- console.error("Failed to copy trace ID:", err);
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
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
851
- className: "bg-background border-b border-border px-4 py-3",
852
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
853
- className: "flex items-center gap-6 flex-wrap",
854
- children: [
855
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
856
- className: "flex items-center gap-2",
857
- children: [
858
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
859
- className: "text-xs font-semibold text-muted-foreground",
860
- children: "Trace ID:"
861
- }),
862
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
863
- onClick: handleCopyTraceId,
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: "Root:"
879
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
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: "Duration:"
1210
+ children: "Spans:"
902
1211
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
903
1212
  className: "text-sm font-medium text-foreground",
904
- children: formatDuration(totalDuration)
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: "Spans:"
1220
+ children: "Depth:"
912
1221
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
913
1222
  className: "text-sm font-medium text-foreground",
914
- children: trace.totalSpanCount
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: `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`,
973
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
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 getHttpContext(span) {
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 httpContext = getHttpContext(span);
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 text-muted-foreground flex-shrink-0 mr-2",
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/DetailPane/AttributesTab.tsx
1118
- const HTTP_SEMANTIC_CONVENTIONS = new Set([
1119
- "http.method",
1120
- "http.url",
1121
- "http.status_code",
1122
- "http.target",
1123
- "http.host",
1124
- "http.scheme",
1125
- "http.route",
1126
- "http.user_agent",
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
- httpAttributes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("h3", {
1159
- className: "text-sm font-semibold text-foreground mb-3 flex items-center",
1160
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: "w-2 h-2 bg-blue-500 rounded-full mr-2" }), "HTTP Attributes"]
1161
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1162
- className: "space-y-2",
1163
- children: httpAttributes.map(([key, value]) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttributeRow, {
1164
- attrKey: key,
1165
- value,
1166
- highlighted: true
1167
- }, key))
1168
- })] }),
1169
- otherAttributes.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1170
- className: "text-sm font-semibold text-foreground mb-3",
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 AttributeRow({ attrKey, value, highlighted }) {
1193
- const isComplex = isComplexValue(value);
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: `grid grid-cols-[minmax(150px,1fr)_2fr] gap-4 p-2 rounded text-sm ${highlighted ? "bg-blue-50 dark:bg-blue-950 border-l-2 border-blue-500" : "bg-muted"}`,
1197
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1198
- className: `font-mono font-medium break-words ${highlighted ? "text-blue-700 dark:text-blue-300" : "text-foreground"}`,
1199
- title: attrKey,
1200
- children: attrKey
1201
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1202
- className: "break-words",
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: formattedValue
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 handleTabChange = (0, react.useCallback)((tab) => {
1475
- setActiveTab(tab);
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 (err) {
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: "flex flex-col h-full bg-background border-l border-border",
1488
- onKeyDown: (0, react.useCallback)((e) => {
1489
- if (e.key === "Escape") onClose();
1490
- }, [onClose]),
1491
- tabIndex: -1,
1492
- role: "complementary",
1493
- "aria-label": "Span details",
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: "p-4 border-b border-border",
1497
- children: [
1498
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1499
- className: "flex items-center justify-between mb-3",
1500
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
1501
- className: "text-lg font-semibold text-foreground truncate",
1502
- children: "Span Details"
1503
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1504
- onClick: onClose,
1505
- className: "p-1 hover:bg-muted rounded transition-colors",
1506
- "aria-label": "Close detail pane",
1507
- title: "Close (Esc)",
1508
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1509
- className: "w-5 h-5 text-muted-foreground",
1510
- fill: "none",
1511
- stroke: "currentColor",
1512
- viewBox: "0 0 24 24",
1513
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1514
- strokeLinecap: "round",
1515
- strokeLinejoin: "round",
1516
- strokeWidth: 2,
1517
- d: "M6 18L18 6M6 6l12 12"
1518
- })
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
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1523
- className: "mb-2",
1524
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1525
- className: "text-sm font-medium text-foreground truncate",
1526
- title: span.name,
1527
- children: span.name
1528
- })
1529
- }),
1530
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1531
- className: "flex items-center gap-2",
1532
- children: [
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-xs text-muted-foreground",
1535
- children: "Span ID:"
1536
- }),
1537
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
1538
- className: "text-xs font-mono text-foreground bg-muted px-2 py-1 rounded flex-1 truncate",
1539
- title: span.spanId,
1540
- children: span.spanId
1541
- }),
1542
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1543
- onClick: handleCopySpanId,
1544
- className: "p-1 hover:bg-muted rounded transition-colors",
1545
- "aria-label": "Copy span ID",
1546
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
1547
- className: `w-4 h-4 ${copiedId ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`,
1548
- fill: "none",
1549
- stroke: "currentColor",
1550
- viewBox: "0 0 24 24",
1551
- children: copiedId ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1552
- strokeLinecap: "round",
1553
- strokeLinejoin: "round",
1554
- strokeWidth: 2,
1555
- d: "M5 13l4 4L19 7"
1556
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
1557
- strokeLinecap: "round",
1558
- strokeLinejoin: "round",
1559
- strokeWidth: 2,
1560
- d: "M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"
1561
- })
1562
- })
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.jsx)("div", {
1569
- className: "flex border-b border-border",
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
- "attributes",
1574
- "events",
1575
- "links"
1576
- ].map((tab) => {
1577
- const count = tab === "attributes" ? Object.keys(span.attributes).length : tab === "events" ? span.events.length : span.links.length;
1578
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
1579
- role: "tab",
1580
- "aria-selected": activeTab === tab,
1581
- onClick: () => handleTabChange(tab),
1582
- className: `px-4 py-2 text-sm font-medium transition-colors ${activeTab === tab ? "text-blue-600 dark:text-blue-400 border-b-2 border-blue-600 dark:border-blue-400" : "text-muted-foreground hover:text-foreground"}`,
1583
- children: [tab.charAt(0).toUpperCase() + tab.slice(1), count > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1584
- className: "ml-1 text-xs text-muted-foreground",
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
- count,
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
- }, tab);
1592
- })
1604
+ }, i))
1605
+ })
1606
+ ]
1593
1607
  }),
1594
1608
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1595
- className: "flex-1 overflow-auto p-4",
1609
+ className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
1596
1610
  children: [
1597
- activeTab === "attributes" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AttributesTab$1, { span }),
1598
- activeTab === "events" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EventsTab, { span }),
1599
- activeTab === "links" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LinksTab, {
1600
- span,
1601
- onLinkClick
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", "S"],
1739
- description: "Services tab"
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
- const target = e.target;
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 === "S") {
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 TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpanId, isLoading, error }) {
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 virtualizer = (0, _tanstack_react_virtual.useVirtualizer)({
1977
- count: flattenedSpans.length,
1978
- getScrollElement: () => scrollRef.current,
1979
- estimateSize: () => 32,
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
- setInternalSelectedSpanId(span.spanId);
1992
- onSpanClick?.(span);
1993
- if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
1994
- }, [onSpanClick]);
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 handleDeselect = (0, react.useCallback)(() => {
2044
- setInternalSelectedSpanId(null);
2045
- }, []);
2046
- (0, react.useEffect)(() => {
2047
- if (!selectedSpanId) return;
2048
- const selectedIndex = flattenedSpans.findIndex((item) => item.span.spanId === selectedSpanId);
2049
- if (selectedIndex !== -1) virtualizer.scrollToIndex(selectedIndex, {
2050
- align: "center",
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
- selectedSpanId,
3031
+ matchingIndices,
3032
+ currentMatchIndex,
2055
3033
  flattenedSpans,
2056
- virtualizer
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
- const selectedSpan = selectedSpanId && flattenedSpans.length > 0 ? flattenedSpans.find((item) => item.span.spanId === selectedSpanId)?.span : null;
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: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
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, { trace: parsedTrace }),
2167
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2168
- ref: scrollRef,
2169
- className: "flex-1 overflow-auto outline-none",
2170
- role: "tree",
2171
- "aria-label": "Trace timeline",
2172
- tabIndex: 0,
2173
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2174
- style: {
2175
- height: `${virtualizer.getTotalSize()}px`,
2176
- width: "100%",
2177
- position: "relative"
2178
- },
2179
- children: virtualizer.getVirtualItems().map((virtualItem) => {
2180
- const item = flattenedSpans[virtualItem.index];
2181
- if (!item) return null;
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 relativeStart = calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs);
2188
- const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs);
2189
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2190
- style: {
2191
- position: "absolute",
2192
- top: 0,
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
- }), selectedSpan && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
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({ service, traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onBack }) {
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.jsxs)("button", {
3252
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2235
3253
  onClick: onBack,
2236
3254
  className: "hover:text-foreground transition-colors",
2237
- children: ["Services / ", service]
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
- }), payload.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4314
- className: "text-sm",
4315
- style: { color: entry.color },
4316
- children: [
4317
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
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
- bucket[seriesName] = (bucket[seriesName] ?? 0) + count;
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 bucket = payload[0]?.payload;
4565
- if (!bucket) return null;
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(bucket.lowerBound, boundsScale)} – ${bucket.upperBound === Infinity ? "∞" : formatDisplayValue(bucket.upperBound, boundsScale)}` : bucket.bucket]
4571
- }), payload.map((entry, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
4572
- className: "text-sm",
4573
- style: { color: entry.color },
4574
- children: [
4575
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
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
- const componentList = Object.keys(dashboardCatalog.components);
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 firstRow = rows[0];
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: "Services",
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: "Services",
5737
- shortcutKey: "S"
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: service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services",
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.service) params.set("service", state.service);
5769
- if (state.trace) params.set("trace", state.trace);
5770
- if (state.span) params.set("span", state.span);
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
- const SERVICES_DS = {
5958
- method: "searchTracesPage",
5959
- params: {
5960
- limit: 1e3,
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
- serviceName: service,
5990
- limit: filters.limit,
7276
+ limit: urlState.limit ?? 20,
5991
7277
  sortOrder: "DESC"
5992
7278
  };
5993
- if (filters.operation) params.spanName = filters.operation;
5994
- if (filters.lookbackMs) params.timestampMin = String((Date.now() - filters.lookbackMs) * 1e6);
5995
- if (filters.minDuration) {
5996
- const parsed = parseDuration(filters.minDuration);
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 (filters.maxDuration) {
6000
- const parsed = parseDuration(filters.maxDuration);
7291
+ if (urlState.maxDuration) {
7292
+ const parsed = parseDuration(urlState.maxDuration);
6001
7293
  if (parsed) params.durationMax = parsed;
6002
7294
  }
6003
- setDs({
6004
- method: "searchTracesPage",
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 client = useKopaiSDK();
6010
- const [fullTraces, setFullTraces] = (0, react.useState)(() => /* @__PURE__ */ new Map());
6011
- (0, react.useEffect)(() => {
6012
- if (!data?.data?.length) {
6013
- setFullTraces(/* @__PURE__ */ new Map());
6014
- return;
6015
- }
6016
- const traceIds = [...new Set(data.data.map((r) => r.TraceId))];
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
- const set = /* @__PURE__ */ new Set();
6031
- for (const row of data.data) if (row.SpanName) set.add(row.SpanName);
6032
- return Array.from(set).sort();
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
- service,
6036
- traces: (0, react.useMemo)(() => {
6037
- if (!data?.data) return [];
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
- onBack,
7356
+ onCompare,
6086
7357
  onSearch: handleSearch
6087
7358
  });
6088
7359
  }
6089
- function TraceDetailView({ service, traceId, selectedSpanId, onSelectSpan, onBack }) {
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({ selectedService, selectedTraceId, selectedSpanId, onSelectService, onSelectTrace, onSelectSpan, onBackToServices, onBackToTraceList }) {
7376
+ function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
6106
7377
  useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
6107
- const backToServicesRef = (0, react.useRef)(onBackToServices);
6108
- backToServicesRef.current = onBackToServices;
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
- if (selectedTraceId && selectedService) backToTraceListRef.current();
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
- }, [selectedService, selectedTraceId]);
6123
- if (selectedTraceId && selectedService) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceDetailView, {
6124
- service: selectedService,
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
- onBack: onBackToTraceList
7403
+ onDeselectSpan,
7404
+ onBack
6129
7405
  });
6130
- if (selectedService) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(TraceSearchView, {
6131
- service: selectedService,
6132
- onBack: onBackToServices,
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, service: selectedService, trace: selectedTraceId, span: selectedSpanId } = useURLState();
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
- }, [selectedService]);
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 handleBackToTraceList = (0, react.useCallback)(() => {
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
- service: selectedService
7557
+ trace: null,
7558
+ span: null,
7559
+ view: null,
7560
+ uiFind: null,
7561
+ viewStart: null,
7562
+ viewEnd: null,
7563
+ compare: null
6274
7564
  });
6275
- }, [selectedService]);
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
- onSelectService: handleSelectService,
7582
+ compareParam,
6294
7583
  onSelectTrace: handleSelectTrace,
6295
7584
  onSelectSpan: handleSelectSpan,
6296
- onBackToServices: handleBackToServices,
6297
- onBackToTraceList: handleBackToTraceList
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;