@kopai/ui 0.8.0 → 0.10.0

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