@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.mjs CHANGED
@@ -2,12 +2,11 @@ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRe
2
2
  import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { KopaiClient } from "@kopai/sdk";
5
- import z$1, { z } from "zod";
5
+ import { z } from "zod";
6
6
  import { dataFilterSchemas } from "@kopai/core";
7
- import { useVirtualizer } from "@tanstack/react-virtual";
7
+ import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from "recharts";
8
8
  import { createPortal } from "react-dom";
9
- import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
10
-
9
+ import { useVirtualizer } from "@tanstack/react-virtual";
11
10
  //#region src/providers/kopai-provider.tsx
12
11
  const KopaiSDKContext = createContext(null);
13
12
  const queryClient = new QueryClient({ defaultOptions: { queries: {
@@ -29,16 +28,25 @@ function useKopaiSDK() {
29
28
  if (!ctx) throw new Error("useKopaiSDK must be used within KopaiSDKProvider");
30
29
  return ctx.client;
31
30
  }
32
-
33
31
  //#endregion
34
32
  //#region src/hooks/use-kopai-data.ts
35
33
  function fetchForDataSource(client, dataSource, signal) {
36
34
  switch (dataSource.method) {
37
35
  case "searchTracesPage": return client.searchTracesPage(dataSource.params, { signal });
38
36
  case "searchLogsPage": return client.searchLogsPage(dataSource.params, { signal });
39
- case "searchMetricsPage": return client.searchMetricsPage(dataSource.params, { signal });
37
+ case "searchMetricsPage": {
38
+ const params = dataSource.params;
39
+ if (params.aggregate) return client.searchAggregatedMetrics({
40
+ ...params,
41
+ aggregate: params.aggregate
42
+ }, { signal });
43
+ return client.searchMetricsPage(params, { signal });
44
+ }
40
45
  case "getTrace": return client.getTrace(dataSource.params.traceId, { signal });
41
46
  case "discoverMetrics": return client.discoverMetrics({ signal });
47
+ case "getServices": return client.getServices({ signal });
48
+ case "getOperations": return client.getOperations(dataSource.params.serviceName, { signal });
49
+ case "searchTraceSummariesPage": return client.searchTraceSummariesPage(dataSource.params, { signal });
42
50
  default: {
43
51
  const exhaustiveCheck = dataSource;
44
52
  throw new Error(`Unknown method: ${exhaustiveCheck.method}`);
@@ -64,7 +72,6 @@ function useKopaiData(dataSource) {
64
72
  refetch
65
73
  };
66
74
  }
67
-
68
75
  //#endregion
69
76
  //#region src/lib/log-buffer.ts
70
77
  function logKey(row) {
@@ -116,7 +123,6 @@ var LogBuffer = class {
116
123
  this.keys.clear();
117
124
  }
118
125
  };
119
-
120
126
  //#endregion
121
127
  //#region src/hooks/use-live-logs.ts
122
128
  function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = true }) {
@@ -171,7 +177,6 @@ function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = tr
171
177
  setLive
172
178
  };
173
179
  }
174
-
175
180
  //#endregion
176
181
  //#region src/lib/component-catalog.ts
177
182
  const dataSourceSchema = z.discriminatedUnion("method", [
@@ -199,6 +204,21 @@ const dataSourceSchema = z.discriminatedUnion("method", [
199
204
  method: z.literal("discoverMetrics"),
200
205
  params: z.object({}).optional(),
201
206
  refetchIntervalMs: z.number().optional()
207
+ }),
208
+ z.object({
209
+ method: z.literal("getServices"),
210
+ params: z.object({}).optional(),
211
+ refetchIntervalMs: z.number().optional()
212
+ }),
213
+ z.object({
214
+ method: z.literal("getOperations"),
215
+ params: z.object({ serviceName: z.string() }),
216
+ refetchIntervalMs: z.number().optional()
217
+ }),
218
+ z.object({
219
+ method: z.literal("searchTraceSummariesPage"),
220
+ params: dataFilterSchemas.traceSummariesFilterSchema,
221
+ refetchIntervalMs: z.number().optional()
202
222
  })
203
223
  ]);
204
224
  const componentDefinitionSchema = z.object({
@@ -206,7 +226,7 @@ const componentDefinitionSchema = z.object({
206
226
  description: z.string().describe("Component description to be displayed by the prompt generator"),
207
227
  props: z.unknown()
208
228
  }).describe("All options and properties necessary to render the React component with renderer");
209
- const catalogConfigSchema = z.object({
229
+ z.object({
210
230
  name: z.string().describe("catalog name"),
211
231
  components: z.record(z.string().describe("React component name"), componentDefinitionSchema)
212
232
  });
@@ -253,7 +273,6 @@ function createCatalog(catalogConfig) {
253
273
  uiTreeSchema
254
274
  };
255
275
  }
256
-
257
276
  //#endregion
258
277
  //#region src/lib/observability-catalog.ts
259
278
  const observabilityCatalog = createCatalog({
@@ -412,7 +431,6 @@ const observabilityCatalog = createCatalog({
412
431
  }
413
432
  }
414
433
  });
415
-
416
434
  //#endregion
417
435
  //#region src/components/observability/TabBar/index.tsx
418
436
  function renderLabel(label, shortcutKey) {
@@ -441,7 +459,6 @@ function TabBar({ tabs, active, onChange }) {
441
459
  }, t.key))
442
460
  });
443
461
  }
444
-
445
462
  //#endregion
446
463
  //#region src/components/observability/utils/colors.ts
447
464
  /**
@@ -461,38 +478,6 @@ function getSpanBarColor(serviceName, isError) {
461
478
  if (isError) return ERROR_COLOR;
462
479
  return getServiceColor(serviceName);
463
480
  }
464
-
465
- //#endregion
466
- //#region src/components/observability/ServiceList/index.tsx
467
- function ServiceList({ services, isLoading, error, onSelect }) {
468
- if (isLoading) return /* @__PURE__ */ jsxs("div", {
469
- className: "flex items-center gap-2 text-muted-foreground py-8",
470
- children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading services..."]
471
- });
472
- if (error) return /* @__PURE__ */ jsxs("div", {
473
- className: "text-red-400 py-4",
474
- children: ["Error loading services: ", error.message]
475
- });
476
- if (services.length === 0) return /* @__PURE__ */ jsx("div", {
477
- className: "text-muted-foreground py-8",
478
- children: "No services found"
479
- });
480
- return /* @__PURE__ */ jsx("div", {
481
- className: "space-y-1",
482
- children: services.map((svc) => /* @__PURE__ */ jsx("button", {
483
- onClick: () => onSelect(svc.name),
484
- 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",
485
- children: /* @__PURE__ */ jsxs("span", {
486
- className: "flex items-center gap-2 font-medium text-foreground",
487
- children: [/* @__PURE__ */ jsx("span", {
488
- className: "inline-block w-2.5 h-2.5 rounded-full shrink-0",
489
- style: { backgroundColor: getServiceColor(svc.name) }
490
- }), svc.name]
491
- })
492
- }, svc.name))
493
- });
494
- }
495
-
496
481
  //#endregion
497
482
  //#region src/components/observability/utils/time.ts
498
483
  /**
@@ -514,6 +499,10 @@ function formatTimestamp$1(timestampMs) {
514
499
  timeZoneName: "short"
515
500
  });
516
501
  }
502
+ function formatRelativeTime$1(eventTimeMs, spanStartMs) {
503
+ const relativeMs = eventTimeMs - spanStartMs;
504
+ return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
505
+ }
517
506
  function calculateRelativeTime(timeMs, minTimeMs, maxTimeMs) {
518
507
  const totalDuration = maxTimeMs - minTimeMs;
519
508
  if (totalDuration === 0) return 0;
@@ -523,259 +512,556 @@ function calculateRelativeDuration(durationMs, totalDurationMs) {
523
512
  if (totalDurationMs === 0) return 0;
524
513
  return durationMs / totalDurationMs;
525
514
  }
526
-
527
515
  //#endregion
528
- //#region src/components/observability/TraceSearch/index.tsx
516
+ //#region src/components/observability/TraceSearch/SearchForm.tsx
517
+ /**
518
+ * SearchForm - Jaeger-style sidebar search form for trace filtering.
519
+ * Owns its own form state; parent only receives values on submit.
520
+ */
529
521
  const LOOKBACK_OPTIONS$1 = [
530
522
  {
531
523
  label: "Last 5 Minutes",
532
- ms: 5 * 6e4
524
+ value: "5m"
533
525
  },
534
526
  {
535
527
  label: "Last 15 Minutes",
536
- ms: 15 * 6e4
528
+ value: "15m"
537
529
  },
538
530
  {
539
531
  label: "Last 30 Minutes",
540
- ms: 30 * 6e4
532
+ value: "30m"
541
533
  },
542
534
  {
543
535
  label: "Last 1 Hour",
544
- ms: 60 * 6e4
536
+ value: "1h"
545
537
  },
546
538
  {
547
539
  label: "Last 2 Hours",
548
- ms: 120 * 6e4
540
+ value: "2h"
549
541
  },
550
542
  {
551
543
  label: "Last 6 Hours",
552
- ms: 360 * 6e4
544
+ value: "6h"
553
545
  },
554
546
  {
555
547
  label: "Last 12 Hours",
556
- ms: 720 * 6e4
548
+ value: "12h"
557
549
  },
558
550
  {
559
551
  label: "Last 24 Hours",
560
- ms: 1440 * 6e4
552
+ value: "24h"
561
553
  }
562
554
  ];
563
- function TraceSearch({ service, traces, operations = [], isLoading, error, onSelectTrace, onBack, onSearch }) {
564
- const [operation, setOperation] = useState("all");
565
- const [lookbackIdx, setLookbackIdx] = useState(-1);
566
- const [minDuration, setMinDuration] = useState("");
567
- const [maxDuration, setMaxDuration] = useState("");
568
- const [limit, setLimit] = useState(20);
569
- const [filtersOpen, setFiltersOpen] = useState(true);
570
- const handleFindTraces = () => {
571
- onSearch?.({
572
- operation: operation !== "all" ? operation : void 0,
573
- lookbackMs: lookbackIdx >= 0 ? LOOKBACK_OPTIONS$1[lookbackIdx].ms : void 0,
574
- minDuration: minDuration || void 0,
575
- maxDuration: maxDuration || void 0,
555
+ const inputClass = "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground";
556
+ function SearchForm({ services, operations, initialValues, onSubmit, isLoading }) {
557
+ const [service, setService] = useState(initialValues?.service ?? "");
558
+ const [operation, setOperation] = useState(initialValues?.operation ?? "");
559
+ const [tags, setTags] = useState(initialValues?.tags ?? "");
560
+ const [lookback, setLookback] = useState(initialValues?.lookback ?? "");
561
+ const [minDuration, setMinDuration] = useState(initialValues?.minDuration ?? "");
562
+ const [maxDuration, setMaxDuration] = useState(initialValues?.maxDuration ?? "");
563
+ const [limit, setLimit] = useState(initialValues?.limit ?? 20);
564
+ useEffect(() => {
565
+ if (initialValues?.service != null) setService(initialValues.service);
566
+ }, [initialValues?.service]);
567
+ const handleSubmit = () => {
568
+ onSubmit({
569
+ service,
570
+ operation,
571
+ tags,
572
+ lookback,
573
+ minDuration,
574
+ maxDuration,
576
575
  limit
577
576
  });
578
577
  };
579
- return /* @__PURE__ */ jsxs("div", { children: [
580
- /* @__PURE__ */ jsxs("div", {
581
- className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
582
- children: [
583
- /* @__PURE__ */ jsx("button", {
584
- onClick: onBack,
585
- className: "hover:text-foreground transition-colors",
586
- children: "Services"
587
- }),
588
- /* @__PURE__ */ jsx("span", { children: "/" }),
589
- /* @__PURE__ */ jsx("span", {
590
- className: "text-foreground",
591
- children: service
592
- })
593
- ]
594
- }),
595
- onSearch && /* @__PURE__ */ jsxs("div", {
596
- className: "border border-border rounded-lg mb-4",
597
- children: [/* @__PURE__ */ jsxs("button", {
598
- onClick: () => setFiltersOpen((v) => !v),
599
- 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",
600
- children: [/* @__PURE__ */ jsx("span", { children: "Filters" }), /* @__PURE__ */ jsx("span", {
601
- className: "text-muted-foreground text-xs",
602
- children: filtersOpen ? "▲" : "▼"
578
+ return /* @__PURE__ */ jsxs("div", {
579
+ className: "space-y-4",
580
+ children: [
581
+ /* @__PURE__ */ jsx("h3", {
582
+ className: "text-sm font-semibold text-foreground uppercase tracking-wider",
583
+ children: "Search"
584
+ }),
585
+ /* @__PURE__ */ jsxs("label", {
586
+ className: "block space-y-1",
587
+ children: [/* @__PURE__ */ jsx("span", {
588
+ className: "text-xs text-muted-foreground",
589
+ children: "Service"
590
+ }), /* @__PURE__ */ jsxs("select", {
591
+ value: service,
592
+ onChange: (e) => setService(e.target.value),
593
+ className: inputClass,
594
+ children: [/* @__PURE__ */ jsx("option", {
595
+ value: "",
596
+ children: "All Services"
597
+ }), services.map((s) => /* @__PURE__ */ jsx("option", {
598
+ value: s,
599
+ children: s
600
+ }, s))]
603
601
  })]
604
- }), filtersOpen && /* @__PURE__ */ jsxs("div", {
605
- className: "px-4 pb-4 pt-1 border-t border-border space-y-3",
606
- children: [/* @__PURE__ */ jsxs("div", {
607
- className: "grid grid-cols-2 md:grid-cols-3 gap-3",
608
- children: [
609
- /* @__PURE__ */ jsxs("label", {
610
- className: "space-y-1",
611
- children: [/* @__PURE__ */ jsx("span", {
612
- className: "text-xs text-muted-foreground",
613
- children: "Operation"
614
- }), /* @__PURE__ */ jsxs("select", {
615
- value: operation,
616
- onChange: (e) => setOperation(e.target.value),
617
- className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
618
- children: [/* @__PURE__ */ jsx("option", {
619
- value: "all",
620
- children: "All"
621
- }), operations.map((op) => /* @__PURE__ */ jsx("option", {
622
- value: op,
623
- children: op
624
- }, op))]
625
- })]
626
- }),
627
- /* @__PURE__ */ jsxs("label", {
628
- className: "space-y-1",
629
- children: [/* @__PURE__ */ jsx("span", {
630
- className: "text-xs text-muted-foreground",
631
- children: "Lookback"
632
- }), /* @__PURE__ */ jsxs("select", {
633
- value: lookbackIdx,
634
- onChange: (e) => setLookbackIdx(Number(e.target.value)),
635
- className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
636
- children: [/* @__PURE__ */ jsx("option", {
637
- value: -1,
638
- children: "All time"
639
- }), LOOKBACK_OPTIONS$1.map((opt, i) => /* @__PURE__ */ jsx("option", {
640
- value: i,
641
- children: opt.label
642
- }, i))]
643
- })]
644
- }),
645
- /* @__PURE__ */ jsxs("label", {
646
- className: "space-y-1",
647
- children: [/* @__PURE__ */ jsx("span", {
648
- className: "text-xs text-muted-foreground",
649
- children: "Limit"
650
- }), /* @__PURE__ */ jsx("input", {
651
- type: "number",
652
- min: 1,
653
- max: 1e3,
654
- value: limit,
655
- onChange: (e) => {
656
- const n = Number(e.target.value);
657
- setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
658
- },
659
- className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground"
660
- })]
661
- }),
662
- /* @__PURE__ */ jsxs("label", {
663
- className: "space-y-1",
664
- children: [/* @__PURE__ */ jsx("span", {
665
- className: "text-xs text-muted-foreground",
666
- children: "Min Duration"
667
- }), /* @__PURE__ */ jsx("input", {
668
- type: "text",
669
- placeholder: "e.g. 100ms",
670
- value: minDuration,
671
- onChange: (e) => setMinDuration(e.target.value),
672
- className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
673
- })]
674
- }),
675
- /* @__PURE__ */ jsxs("label", {
676
- className: "space-y-1",
677
- children: [/* @__PURE__ */ jsx("span", {
678
- className: "text-xs text-muted-foreground",
679
- children: "Max Duration"
680
- }), /* @__PURE__ */ jsx("input", {
681
- type: "text",
682
- placeholder: "e.g. 5s",
683
- value: maxDuration,
684
- onChange: (e) => setMaxDuration(e.target.value),
685
- className: "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground/50"
686
- })]
687
- })
688
- ]
689
- }), /* @__PURE__ */ jsx("button", {
690
- onClick: handleFindTraces,
691
- className: "px-4 py-1.5 text-sm font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
692
- children: "Find Traces"
602
+ }),
603
+ /* @__PURE__ */ jsxs("label", {
604
+ className: "block space-y-1",
605
+ children: [/* @__PURE__ */ jsx("span", {
606
+ className: "text-xs text-muted-foreground",
607
+ children: "Operation"
608
+ }), /* @__PURE__ */ jsxs("select", {
609
+ value: operation,
610
+ onChange: (e) => setOperation(e.target.value),
611
+ className: inputClass,
612
+ children: [/* @__PURE__ */ jsx("option", {
613
+ value: "",
614
+ children: "All Operations"
615
+ }), operations.map((op) => /* @__PURE__ */ jsx("option", {
616
+ value: op,
617
+ children: op
618
+ }, op))]
693
619
  })]
694
- })]
695
- }),
696
- isLoading && /* @__PURE__ */ jsxs("div", {
697
- className: "flex items-center gap-2 text-muted-foreground py-8",
698
- children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading traces..."]
699
- }),
700
- error && /* @__PURE__ */ jsxs("div", {
701
- className: "text-red-400 py-4",
702
- children: ["Error loading traces: ", error.message]
703
- }),
704
- !isLoading && !error && traces.length === 0 && /* @__PURE__ */ jsxs("div", {
705
- className: "text-muted-foreground py-8",
706
- children: ["No traces found for ", service]
707
- }),
708
- traces.length > 0 && /* @__PURE__ */ jsx("div", {
709
- className: "space-y-2",
710
- children: traces.map((t) => /* @__PURE__ */ jsxs("div", {
711
- onClick: () => onSelectTrace(t.traceId),
712
- className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
620
+ }),
621
+ /* @__PURE__ */ jsxs("label", {
622
+ className: "block space-y-1",
623
+ children: [/* @__PURE__ */ jsx("span", {
624
+ className: "text-xs text-muted-foreground",
625
+ children: "Tags"
626
+ }), /* @__PURE__ */ jsx("textarea", {
627
+ value: tags,
628
+ onChange: (e) => setTags(e.target.value),
629
+ placeholder: "key=value key2=\"quoted value\"",
630
+ rows: 3,
631
+ className: `${inputClass} placeholder:text-muted-foreground/50 resize-y`
632
+ })]
633
+ }),
634
+ /* @__PURE__ */ jsxs("label", {
635
+ className: "block space-y-1",
636
+ children: [/* @__PURE__ */ jsx("span", {
637
+ className: "text-xs text-muted-foreground",
638
+ children: "Lookback"
639
+ }), /* @__PURE__ */ jsxs("select", {
640
+ value: lookback,
641
+ onChange: (e) => setLookback(e.target.value),
642
+ className: inputClass,
643
+ children: [/* @__PURE__ */ jsx("option", {
644
+ value: "",
645
+ children: "All time"
646
+ }), LOOKBACK_OPTIONS$1.map((opt) => /* @__PURE__ */ jsx("option", {
647
+ value: opt.value,
648
+ children: opt.label
649
+ }, opt.value))]
650
+ })]
651
+ }),
652
+ /* @__PURE__ */ jsxs("div", {
653
+ className: "grid grid-cols-2 gap-2",
654
+ children: [/* @__PURE__ */ jsxs("label", {
655
+ className: "block space-y-1",
656
+ children: [/* @__PURE__ */ jsx("span", {
657
+ className: "text-xs text-muted-foreground",
658
+ children: "Min Duration"
659
+ }), /* @__PURE__ */ jsx("input", {
660
+ type: "text",
661
+ placeholder: "e.g. 100ms",
662
+ value: minDuration,
663
+ onChange: (e) => setMinDuration(e.target.value),
664
+ className: `${inputClass} placeholder:text-muted-foreground/50`
665
+ })]
666
+ }), /* @__PURE__ */ jsxs("label", {
667
+ className: "block space-y-1",
668
+ children: [/* @__PURE__ */ jsx("span", {
669
+ className: "text-xs text-muted-foreground",
670
+ children: "Max Duration"
671
+ }), /* @__PURE__ */ jsx("input", {
672
+ type: "text",
673
+ placeholder: "e.g. 5s",
674
+ value: maxDuration,
675
+ onChange: (e) => setMaxDuration(e.target.value),
676
+ className: `${inputClass} placeholder:text-muted-foreground/50`
677
+ })]
678
+ })]
679
+ }),
680
+ /* @__PURE__ */ jsxs("label", {
681
+ className: "block space-y-1",
682
+ children: [/* @__PURE__ */ jsx("span", {
683
+ className: "text-xs text-muted-foreground",
684
+ children: "Limit"
685
+ }), /* @__PURE__ */ jsx("input", {
686
+ type: "number",
687
+ min: 1,
688
+ max: 1e3,
689
+ value: limit,
690
+ onChange: (e) => {
691
+ const n = Number(e.target.value);
692
+ setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
693
+ },
694
+ className: inputClass
695
+ })]
696
+ }),
697
+ /* @__PURE__ */ jsx("button", {
698
+ onClick: handleSubmit,
699
+ disabled: isLoading,
700
+ 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",
701
+ children: isLoading ? "Searching..." : "Find Traces"
702
+ })
703
+ ]
704
+ });
705
+ }
706
+ //#endregion
707
+ //#region src/components/observability/TraceSearch/ScatterPlot.tsx
708
+ /**
709
+ * ScatterPlot - Scatter chart showing trace duration vs timestamp.
710
+ */
711
+ function CustomTooltip$1({ active, payload }) {
712
+ if (!active || !payload?.[0]) return null;
713
+ const d = payload[0].payload;
714
+ return /* @__PURE__ */ jsxs("div", {
715
+ className: "bg-background border border-border rounded px-3 py-2 text-xs shadow-lg",
716
+ children: [
717
+ /* @__PURE__ */ jsxs("div", {
718
+ className: "font-medium text-foreground",
713
719
  children: [
714
- /* @__PURE__ */ jsxs("div", {
715
- className: "flex items-baseline justify-between gap-2",
716
- children: [/* @__PURE__ */ jsxs("div", {
717
- className: "flex items-baseline gap-1.5 min-w-0",
718
- children: [/* @__PURE__ */ jsxs("span", {
719
- className: "font-medium text-foreground truncate",
720
- children: [
721
- t.serviceName,
722
- ": ",
723
- t.rootSpanName
724
- ]
725
- }), /* @__PURE__ */ jsx("span", {
726
- className: "text-xs font-mono text-muted-foreground shrink-0",
727
- children: t.traceId.slice(0, 7)
728
- })]
729
- }), /* @__PURE__ */ jsx("span", {
730
- className: "text-sm text-foreground/80 shrink-0",
731
- children: formatDuration(t.durationMs)
732
- })]
720
+ d.serviceName,
721
+ ": ",
722
+ d.rootSpanName
723
+ ]
724
+ }),
725
+ /* @__PURE__ */ jsxs("div", {
726
+ className: "text-muted-foreground mt-1",
727
+ children: [
728
+ d.spanCount,
729
+ " span",
730
+ d.spanCount !== 1 ? "s" : "",
731
+ " ·",
732
+ " ",
733
+ formatDuration(d.y)
734
+ ]
735
+ }),
736
+ /* @__PURE__ */ jsx("div", {
737
+ className: "text-muted-foreground",
738
+ children: formatTimestamp$1(d.x)
739
+ })
740
+ ]
741
+ });
742
+ }
743
+ function ScatterPlot({ traces, onSelectTrace }) {
744
+ const data = useMemo(() => traces.map((t) => ({
745
+ x: t.timestampMs,
746
+ y: t.durationMs,
747
+ traceId: t.traceId,
748
+ serviceName: t.serviceName,
749
+ rootSpanName: t.rootSpanName,
750
+ spanCount: t.spanCount,
751
+ hasError: t.errorCount > 0
752
+ })), [traces]);
753
+ const handleClick = useCallback((entry) => {
754
+ const payload = entry?.payload;
755
+ if (payload?.traceId) onSelectTrace(payload.traceId);
756
+ }, [onSelectTrace]);
757
+ if (traces.length === 0) return null;
758
+ return /* @__PURE__ */ jsx("div", {
759
+ className: "border border-border rounded-lg p-4 bg-background",
760
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
761
+ width: "100%",
762
+ height: 200,
763
+ children: /* @__PURE__ */ jsxs(ScatterChart, {
764
+ margin: {
765
+ top: 8,
766
+ right: 8,
767
+ bottom: 4,
768
+ left: 0
769
+ },
770
+ children: [
771
+ /* @__PURE__ */ jsx(CartesianGrid, {
772
+ strokeDasharray: "3 3",
773
+ stroke: "hsl(var(--border))",
774
+ opacity: .4
733
775
  }),
734
- /* @__PURE__ */ jsxs("div", {
735
- className: "flex items-center flex-wrap gap-1.5 mt-1.5",
736
- children: [
737
- /* @__PURE__ */ jsxs("span", {
738
- className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
739
- children: [
740
- t.spanCount,
741
- " Span",
742
- t.spanCount !== 1 ? "s" : ""
743
- ]
744
- }),
745
- t.errorCount > 0 && /* @__PURE__ */ jsxs("span", {
746
- className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
747
- children: [
748
- t.errorCount,
749
- " Error",
750
- t.errorCount !== 1 ? "s" : ""
751
- ]
752
- }),
753
- t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
754
- className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
755
- style: {
756
- backgroundColor: `${getServiceColor(svc.name)}20`,
757
- color: getServiceColor(svc.name)
758
- },
759
- children: [
760
- svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
761
- svc.name,
762
- " (",
763
- svc.count,
764
- ")"
765
- ]
766
- }, svc.name))
767
- ]
776
+ /* @__PURE__ */ jsx(XAxis, {
777
+ dataKey: "x",
778
+ type: "number",
779
+ domain: ["dataMin", "dataMax"],
780
+ tickFormatter: (v) => {
781
+ const d = new Date(v);
782
+ return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
783
+ },
784
+ tick: {
785
+ fontSize: 11,
786
+ fill: "hsl(var(--muted-foreground))"
787
+ },
788
+ stroke: "hsl(var(--border))",
789
+ name: "Time"
768
790
  }),
769
- /* @__PURE__ */ jsx("div", {
770
- className: "text-xs text-muted-foreground mt-1 text-right",
771
- children: formatTimestamp$1(t.timestampMs)
791
+ /* @__PURE__ */ jsx(YAxis, {
792
+ dataKey: "y",
793
+ type: "number",
794
+ tickFormatter: (v) => formatDuration(v),
795
+ tick: {
796
+ fontSize: 11,
797
+ fill: "hsl(var(--muted-foreground))"
798
+ },
799
+ stroke: "hsl(var(--border))",
800
+ name: "Duration",
801
+ width: 70
802
+ }),
803
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip$1, {}) }),
804
+ /* @__PURE__ */ jsx(Scatter, {
805
+ data,
806
+ onClick: handleClick,
807
+ cursor: "pointer",
808
+ children: data.map((point, i) => /* @__PURE__ */ jsx(Cell, {
809
+ fill: point.hasError ? "#ef4444" : getServiceColor(point.serviceName),
810
+ stroke: point.hasError ? "#ef4444" : "none",
811
+ strokeWidth: point.hasError ? 2 : 0
812
+ }, i))
772
813
  })
773
814
  ]
774
- }, t.traceId))
815
+ })
775
816
  })
776
- ] });
817
+ });
818
+ }
819
+ //#endregion
820
+ //#region src/components/observability/TraceSearch/SortDropdown.tsx
821
+ const SORT_OPTIONS = [
822
+ {
823
+ value: "recent",
824
+ label: "Most Recent"
825
+ },
826
+ {
827
+ value: "longest",
828
+ label: "Longest First"
829
+ },
830
+ {
831
+ value: "shortest",
832
+ label: "Shortest First"
833
+ },
834
+ {
835
+ value: "mostSpans",
836
+ label: "Most Spans"
837
+ },
838
+ {
839
+ value: "leastSpans",
840
+ label: "Least Spans"
841
+ }
842
+ ];
843
+ function SortDropdown({ value, onChange }) {
844
+ return /* @__PURE__ */ jsx("select", {
845
+ value,
846
+ onChange: (e) => onChange(e.target.value),
847
+ className: "bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
848
+ children: SORT_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", {
849
+ value: opt.value,
850
+ children: opt.label
851
+ }, opt.value))
852
+ });
853
+ }
854
+ //#endregion
855
+ //#region src/components/observability/TraceSearch/DurationBar.tsx
856
+ /**
857
+ * DurationBar - Horizontal bar showing relative trace duration.
858
+ */
859
+ function DurationBar({ durationMs, maxDurationMs, color }) {
860
+ const rawPct = maxDurationMs > 0 ? durationMs / maxDurationMs * 100 : 0;
861
+ return /* @__PURE__ */ jsxs("div", {
862
+ className: "flex items-center gap-2",
863
+ children: [/* @__PURE__ */ jsx("div", {
864
+ className: "flex-1 h-2 bg-muted/30 rounded overflow-hidden",
865
+ children: /* @__PURE__ */ jsx("div", {
866
+ className: "h-full rounded",
867
+ style: {
868
+ width: `${durationMs <= 0 ? 0 : Math.min(Math.max(rawPct, 1), 100)}%`,
869
+ backgroundColor: color,
870
+ opacity: .7
871
+ }
872
+ })
873
+ }), /* @__PURE__ */ jsx("span", {
874
+ className: "text-xs text-foreground/80 shrink-0 w-16 text-right font-mono",
875
+ children: formatDuration(durationMs)
876
+ })]
877
+ });
878
+ }
879
+ //#endregion
880
+ //#region src/components/observability/TraceSearch/index.tsx
881
+ function sortTraces(traces, sort) {
882
+ const sorted = [...traces];
883
+ switch (sort) {
884
+ case "longest": return sorted.sort((a, b) => b.durationMs - a.durationMs);
885
+ case "shortest": return sorted.sort((a, b) => a.durationMs - b.durationMs);
886
+ case "mostSpans": return sorted.sort((a, b) => b.spanCount - a.spanCount);
887
+ case "leastSpans": return sorted.sort((a, b) => a.spanCount - b.spanCount);
888
+ default: return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
889
+ }
890
+ }
891
+ function TraceSearch({ services = [], service, operations = [], traces, isLoading, error, onSelectTrace, onSearch, onCompare, sort: controlledSort, onSortChange }) {
892
+ const [internalSort, setInternalSort] = useState("recent");
893
+ const currentSort = controlledSort ?? internalSort;
894
+ const handleSortChange = (s) => {
895
+ if (onSortChange) onSortChange(s);
896
+ else setInternalSort(s);
897
+ };
898
+ const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
899
+ const toggleSelected = (traceId) => {
900
+ setSelected((prev) => {
901
+ const next = new Set(prev);
902
+ if (next.has(traceId)) next.delete(traceId);
903
+ else {
904
+ if (next.size >= 2) return prev;
905
+ next.add(traceId);
906
+ }
907
+ return next;
908
+ });
909
+ };
910
+ const handleFormSubmit = (values) => {
911
+ onSearch?.({
912
+ service: values.service || void 0,
913
+ operation: values.operation || void 0,
914
+ tags: values.tags || void 0,
915
+ lookback: values.lookback || void 0,
916
+ minDuration: values.minDuration || void 0,
917
+ maxDuration: values.maxDuration || void 0,
918
+ limit: values.limit
919
+ });
920
+ };
921
+ const sortedTraces = useMemo(() => sortTraces(traces, currentSort), [traces, currentSort]);
922
+ const maxDurationMs = useMemo(() => Math.max(...traces.map((t) => t.durationMs), 0), [traces]);
923
+ const selectedArr = Array.from(selected);
924
+ return /* @__PURE__ */ jsxs("div", {
925
+ className: "flex gap-6 min-h-0",
926
+ children: [onSearch && /* @__PURE__ */ jsx("div", {
927
+ className: "w-72 shrink-0 border border-border rounded-lg p-4 self-start",
928
+ children: /* @__PURE__ */ jsx(SearchForm, {
929
+ services,
930
+ operations,
931
+ initialValues: { service },
932
+ onSubmit: handleFormSubmit,
933
+ isLoading
934
+ })
935
+ }), /* @__PURE__ */ jsxs("div", {
936
+ className: "flex-1 min-w-0 space-y-4",
937
+ children: [
938
+ traces.length > 0 && /* @__PURE__ */ jsx(ScatterPlot, {
939
+ traces,
940
+ onSelectTrace
941
+ }),
942
+ /* @__PURE__ */ jsxs("div", {
943
+ className: "flex items-center justify-between gap-2",
944
+ children: [/* @__PURE__ */ jsxs("span", {
945
+ className: "text-sm text-muted-foreground",
946
+ children: [
947
+ traces.length,
948
+ " Trace",
949
+ traces.length !== 1 ? "s" : ""
950
+ ]
951
+ }), /* @__PURE__ */ jsxs("div", {
952
+ className: "flex items-center gap-2",
953
+ children: [onCompare && selected.size === 2 && /* @__PURE__ */ jsx("button", {
954
+ onClick: () => onCompare(selectedArr),
955
+ className: "px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
956
+ children: "Compare"
957
+ }), /* @__PURE__ */ jsx(SortDropdown, {
958
+ value: currentSort,
959
+ onChange: handleSortChange
960
+ })]
961
+ })]
962
+ }),
963
+ isLoading && /* @__PURE__ */ jsxs("div", {
964
+ className: "flex items-center gap-2 text-muted-foreground py-8",
965
+ children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading traces..."]
966
+ }),
967
+ error && /* @__PURE__ */ jsxs("div", {
968
+ className: "text-red-400 py-4",
969
+ children: ["Error loading traces: ", error.message]
970
+ }),
971
+ !isLoading && !error && traces.length === 0 && /* @__PURE__ */ jsx("div", {
972
+ className: "text-muted-foreground py-8",
973
+ children: "No traces found"
974
+ }),
975
+ sortedTraces.length > 0 && /* @__PURE__ */ jsx("div", {
976
+ className: "space-y-2",
977
+ children: sortedTraces.map((t) => /* @__PURE__ */ jsx("div", {
978
+ className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
979
+ children: /* @__PURE__ */ jsxs("div", {
980
+ className: "flex items-center gap-2",
981
+ children: [onCompare && /* @__PURE__ */ jsx("input", {
982
+ type: "checkbox",
983
+ checked: selected.has(t.traceId),
984
+ onChange: () => toggleSelected(t.traceId),
985
+ onClick: (e) => e.stopPropagation(),
986
+ className: "shrink-0",
987
+ disabled: !selected.has(t.traceId) && selected.size >= 2
988
+ }), /* @__PURE__ */ jsxs("div", {
989
+ className: "flex-1 min-w-0",
990
+ onClick: () => onSelectTrace(t.traceId),
991
+ children: [
992
+ /* @__PURE__ */ jsx("div", {
993
+ className: "flex items-baseline justify-between gap-2",
994
+ children: /* @__PURE__ */ jsxs("div", {
995
+ className: "flex items-baseline gap-1.5 min-w-0",
996
+ children: [/* @__PURE__ */ jsxs("span", {
997
+ className: "font-medium text-foreground truncate",
998
+ children: [
999
+ t.serviceName,
1000
+ ": ",
1001
+ t.rootSpanName
1002
+ ]
1003
+ }), /* @__PURE__ */ jsx("span", {
1004
+ className: "text-xs font-mono text-muted-foreground shrink-0",
1005
+ children: t.traceId.slice(0, 7)
1006
+ })]
1007
+ })
1008
+ }),
1009
+ /* @__PURE__ */ jsx("div", {
1010
+ className: "mt-1.5",
1011
+ children: /* @__PURE__ */ jsx(DurationBar, {
1012
+ durationMs: t.durationMs,
1013
+ maxDurationMs,
1014
+ color: getServiceColor(t.serviceName)
1015
+ })
1016
+ }),
1017
+ /* @__PURE__ */ jsxs("div", {
1018
+ className: "flex items-center flex-wrap gap-1.5 mt-1.5",
1019
+ children: [
1020
+ /* @__PURE__ */ jsxs("span", {
1021
+ className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
1022
+ children: [
1023
+ t.spanCount,
1024
+ " Span",
1025
+ t.spanCount !== 1 ? "s" : ""
1026
+ ]
1027
+ }),
1028
+ t.errorCount > 0 && /* @__PURE__ */ jsxs("span", {
1029
+ className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
1030
+ children: [
1031
+ t.errorCount,
1032
+ " Error",
1033
+ t.errorCount !== 1 ? "s" : ""
1034
+ ]
1035
+ }),
1036
+ t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
1037
+ className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
1038
+ style: {
1039
+ backgroundColor: `${getServiceColor(svc.name)}20`,
1040
+ color: getServiceColor(svc.name)
1041
+ },
1042
+ children: [
1043
+ svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
1044
+ svc.name,
1045
+ " (",
1046
+ svc.count,
1047
+ ")"
1048
+ ]
1049
+ }, svc.name))
1050
+ ]
1051
+ }),
1052
+ /* @__PURE__ */ jsx("div", {
1053
+ className: "text-xs text-muted-foreground mt-1 text-right",
1054
+ children: formatTimestamp$1(t.timestampMs)
1055
+ })
1056
+ ]
1057
+ })]
1058
+ })
1059
+ }, t.traceId))
1060
+ })
1061
+ ]
1062
+ })]
1063
+ });
777
1064
  }
778
-
779
1065
  //#endregion
780
1066
  //#region src/components/observability/utils/flatten-tree.ts
781
1067
  function flattenTree(rootSpans, collapsedIds) {
@@ -790,6 +1076,17 @@ function flattenTree(rootSpans, collapsedIds) {
790
1076
  rootSpans.forEach((root) => traverse(root, 0));
791
1077
  return result;
792
1078
  }
1079
+ /** Flatten all spans (ignoring collapse state) with depth. */
1080
+ function flattenAllSpans(rootSpans) {
1081
+ return flattenTree(rootSpans, /* @__PURE__ */ new Set());
1082
+ }
1083
+ function spanMatchesSearch(span, query) {
1084
+ const q = query.toLowerCase();
1085
+ if (span.name.toLowerCase().includes(q)) return true;
1086
+ if (span.serviceName.toLowerCase().includes(q)) return true;
1087
+ for (const val of Object.values(span.attributes)) if (String(val).toLowerCase().includes(q)) return true;
1088
+ return false;
1089
+ }
793
1090
  function getAllSpanIds(rootSpans) {
794
1091
  const ids = [];
795
1092
  function traverse(span) {
@@ -799,15 +1096,26 @@ function getAllSpanIds(rootSpans) {
799
1096
  rootSpans.forEach((root) => traverse(root));
800
1097
  return ids;
801
1098
  }
802
-
803
1099
  //#endregion
804
1100
  //#region src/components/observability/TraceTimeline/TraceHeader.tsx
805
- function TraceHeader({ trace }) {
1101
+ function computeMaxDepth(spans) {
1102
+ let max = 0;
1103
+ function walk(nodes, depth) {
1104
+ for (const node of nodes) {
1105
+ if (depth > max) max = depth;
1106
+ walk(node.children, depth + 1);
1107
+ }
1108
+ }
1109
+ walk(spans, 1);
1110
+ return max;
1111
+ }
1112
+ function TraceHeader({ trace, services = [], onHeaderToggle, isCollapsed = false }) {
806
1113
  const [copied, setCopied] = useState(false);
807
1114
  const rootSpan = trace.rootSpans[0];
808
1115
  const rootServiceName = rootSpan?.serviceName ?? "unknown";
809
1116
  const rootSpanName = rootSpan?.name ?? "unknown";
810
1117
  const totalDuration = trace.maxTimeMs - trace.minTimeMs;
1118
+ const maxDepth = computeMaxDepth(trace.rootSpans);
811
1119
  const handleCopyTraceId = async () => {
812
1120
  try {
813
1121
  await navigator.clipboard.writeText(trace.traceId);
@@ -817,9 +1125,35 @@ function TraceHeader({ trace }) {
817
1125
  console.error("Failed to copy trace ID:", err);
818
1126
  }
819
1127
  };
820
- return /* @__PURE__ */ jsx("div", {
1128
+ return /* @__PURE__ */ jsxs("div", {
821
1129
  className: "bg-background border-b border-border px-4 py-3",
822
- children: /* @__PURE__ */ jsxs("div", {
1130
+ children: [/* @__PURE__ */ jsxs("div", {
1131
+ className: "flex items-center gap-2 mb-1",
1132
+ children: [onHeaderToggle && /* @__PURE__ */ jsx("button", {
1133
+ onClick: onHeaderToggle,
1134
+ className: "p-0.5 text-muted-foreground hover:text-foreground",
1135
+ "aria-label": isCollapsed ? "Expand header" : "Collapse header",
1136
+ children: /* @__PURE__ */ jsx("svg", {
1137
+ className: `w-4 h-4 transition-transform ${isCollapsed ? "-rotate-90" : ""}`,
1138
+ fill: "none",
1139
+ stroke: "currentColor",
1140
+ viewBox: "0 0 24 24",
1141
+ children: /* @__PURE__ */ jsx("path", {
1142
+ strokeLinecap: "round",
1143
+ strokeLinejoin: "round",
1144
+ strokeWidth: 2,
1145
+ d: "M19 9l-7 7-7-7"
1146
+ })
1147
+ })
1148
+ }), /* @__PURE__ */ jsxs("span", {
1149
+ className: "text-sm font-semibold text-foreground",
1150
+ children: [
1151
+ rootServiceName,
1152
+ ": ",
1153
+ rootSpanName
1154
+ ]
1155
+ })]
1156
+ }), !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
823
1157
  className: "flex items-center gap-6 flex-wrap",
824
1158
  children: [
825
1159
  /* @__PURE__ */ jsxs("div", {
@@ -845,43 +1179,30 @@ function TraceHeader({ trace }) {
845
1179
  className: "flex items-center gap-2",
846
1180
  children: [/* @__PURE__ */ jsx("span", {
847
1181
  className: "text-xs font-semibold text-muted-foreground",
848
- children: "Root:"
849
- }), /* @__PURE__ */ jsxs("span", {
850
- className: "text-sm",
851
- children: [
852
- /* @__PURE__ */ jsx("span", {
853
- className: "text-muted-foreground",
854
- children: rootServiceName
855
- }),
856
- /* @__PURE__ */ jsx("span", {
857
- className: "mx-1 text-muted-foreground/70",
858
- children: "/"
859
- }),
860
- /* @__PURE__ */ jsx("span", {
861
- className: "font-medium text-foreground",
862
- children: rootSpanName
863
- })
864
- ]
1182
+ children: "Duration:"
1183
+ }), /* @__PURE__ */ jsx("span", {
1184
+ className: "text-sm font-medium text-foreground",
1185
+ children: formatDuration(totalDuration)
865
1186
  })]
866
1187
  }),
867
1188
  /* @__PURE__ */ jsxs("div", {
868
1189
  className: "flex items-center gap-2",
869
1190
  children: [/* @__PURE__ */ jsx("span", {
870
1191
  className: "text-xs font-semibold text-muted-foreground",
871
- children: "Duration:"
1192
+ children: "Spans:"
872
1193
  }), /* @__PURE__ */ jsx("span", {
873
1194
  className: "text-sm font-medium text-foreground",
874
- children: formatDuration(totalDuration)
1195
+ children: trace.totalSpanCount
875
1196
  })]
876
1197
  }),
877
1198
  /* @__PURE__ */ jsxs("div", {
878
1199
  className: "flex items-center gap-2",
879
1200
  children: [/* @__PURE__ */ jsx("span", {
880
1201
  className: "text-xs font-semibold text-muted-foreground",
881
- children: "Spans:"
1202
+ children: "Depth:"
882
1203
  }), /* @__PURE__ */ jsx("span", {
883
1204
  className: "text-sm font-medium text-foreground",
884
- children: trace.totalSpanCount
1205
+ children: maxDepth
885
1206
  })]
886
1207
  }),
887
1208
  /* @__PURE__ */ jsxs("div", {
@@ -895,10 +1216,21 @@ function TraceHeader({ trace }) {
895
1216
  })]
896
1217
  })
897
1218
  ]
898
- })
1219
+ }), services.length > 0 && /* @__PURE__ */ jsx("div", {
1220
+ className: "flex items-center gap-3 mt-2 flex-wrap",
1221
+ children: services.map((svc) => /* @__PURE__ */ jsxs("div", {
1222
+ className: "flex items-center gap-1.5",
1223
+ children: [/* @__PURE__ */ jsx("span", {
1224
+ className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
1225
+ style: { backgroundColor: getServiceColor(svc) }
1226
+ }), /* @__PURE__ */ jsx("span", {
1227
+ className: "text-xs text-muted-foreground",
1228
+ children: svc
1229
+ })]
1230
+ }, svc))
1231
+ })] })]
899
1232
  });
900
1233
  }
901
-
902
1234
  //#endregion
903
1235
  //#region src/components/observability/TraceTimeline/Tooltip.tsx
904
1236
  function Tooltip$1({ content, children }) {
@@ -928,7 +1260,6 @@ function Tooltip$1({ content, children }) {
928
1260
  children: content
929
1261
  }), document.body)] });
930
1262
  }
931
-
932
1263
  //#endregion
933
1264
  //#region src/components/observability/TraceTimeline/TimelineBar.tsx
934
1265
  function TimelineBar({ span, relativeStart, relativeDuration }) {
@@ -936,45 +1267,48 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
936
1267
  const barColor = getSpanBarColor(span.serviceName, isError);
937
1268
  const leftPercent = relativeStart * 100;
938
1269
  const widthPercent = Math.max(.2, relativeDuration * 100);
1270
+ const isWide = widthPercent > 8;
1271
+ const tooltipText = `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`;
1272
+ const durationLabel = formatDuration(span.durationMs);
939
1273
  return /* @__PURE__ */ jsx("div", {
940
1274
  className: "relative h-full",
941
1275
  children: /* @__PURE__ */ jsx(Tooltip$1, {
942
- content: `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`,
943
- children: /* @__PURE__ */ jsx("div", {
1276
+ content: tooltipText,
1277
+ children: /* @__PURE__ */ jsxs("div", {
944
1278
  className: "absolute inset-0",
945
- children: /* @__PURE__ */ jsx("div", {
946
- className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity",
1279
+ children: [/* @__PURE__ */ jsx("div", {
1280
+ className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity flex items-center",
947
1281
  style: {
948
1282
  left: `${leftPercent}%`,
949
1283
  width: `max(2px, ${widthPercent}%)`,
950
1284
  backgroundColor: barColor
951
- }
952
- })
1285
+ },
1286
+ children: isWide && /* @__PURE__ */ jsx("span", {
1287
+ className: "text-[10px] font-mono text-white px-1 truncate",
1288
+ children: durationLabel
1289
+ })
1290
+ }), !isWide && /* @__PURE__ */ jsx("span", {
1291
+ className: "absolute top-1/2 -translate-y-1/2 text-[10px] font-mono text-muted-foreground whitespace-nowrap",
1292
+ style: { left: `calc(${leftPercent + widthPercent}% + 4px)` },
1293
+ children: durationLabel
1294
+ })]
953
1295
  })
954
1296
  })
955
1297
  });
956
1298
  }
957
-
958
1299
  //#endregion
959
1300
  //#region src/components/observability/TraceTimeline/SpanRow.tsx
960
- function getHttpContext(span) {
961
- const attrs = span.attributes;
962
- const method = attrs["http.method"];
963
- const url = attrs["http.url"] || attrs["http.target"];
964
- const statusCode = attrs["http.status_code"];
965
- if (!method && !url) return null;
966
- const parts = [];
967
- if (method) parts.push(String(method));
968
- if (url) parts.push(String(url));
969
- if (statusCode) parts.push(`[${statusCode}]`);
970
- return parts.join(" ");
971
- }
972
- const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave }) {
1301
+ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave, uiFind }) {
973
1302
  const hasChildren = span.children.length > 0;
974
1303
  const isError = span.status === "ERROR";
975
- const httpContext = getHttpContext(span);
1304
+ const serviceColor = getServiceColor(span.serviceName);
1305
+ const isDimmed = uiFind ? !spanMatchesSearch(span, uiFind) : false;
976
1306
  return /* @__PURE__ */ jsxs("div", {
977
1307
  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" : ""}`,
1308
+ style: {
1309
+ borderLeft: `3px solid ${serviceColor}`,
1310
+ opacity: isDimmed ? .4 : 1
1311
+ },
978
1312
  onClick,
979
1313
  onMouseEnter,
980
1314
  onMouseLeave,
@@ -1029,7 +1363,8 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
1029
1363
  })
1030
1364
  }),
1031
1365
  /* @__PURE__ */ jsx("span", {
1032
- className: "text-xs text-muted-foreground flex-shrink-0 mr-2",
1366
+ className: "text-xs flex-shrink-0 mr-2 font-medium",
1367
+ style: { color: serviceColor },
1033
1368
  children: span.serviceName
1034
1369
  }),
1035
1370
  /* @__PURE__ */ jsx("span", {
@@ -1044,10 +1379,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
1044
1379
  ")"
1045
1380
  ]
1046
1381
  }),
1047
- httpContext && /* @__PURE__ */ jsx("span", {
1048
- className: "text-xs text-muted-foreground truncate ml-2 flex-shrink-0 max-w-xs",
1049
- children: httpContext
1050
- }),
1051
1382
  /* @__PURE__ */ jsx("span", {
1052
1383
  className: "text-xs text-muted-foreground flex-shrink-0 ml-2",
1053
1384
  children: formatDuration(span.durationMs)
@@ -1063,7 +1394,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
1063
1394
  })]
1064
1395
  });
1065
1396
  });
1066
-
1067
1397
  //#endregion
1068
1398
  //#region src/components/observability/utils/attributes.ts
1069
1399
  function formatAttributeValue(value) {
@@ -1082,570 +1412,271 @@ function formatSeriesLabel(labels) {
1082
1412
  function isComplexValue(value) {
1083
1413
  return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
1084
1414
  }
1085
-
1086
1415
  //#endregion
1087
- //#region src/components/observability/TraceTimeline/DetailPane/AttributesTab.tsx
1088
- const HTTP_SEMANTIC_CONVENTIONS = new Set([
1089
- "http.method",
1090
- "http.url",
1091
- "http.status_code",
1092
- "http.target",
1093
- "http.host",
1094
- "http.scheme",
1095
- "http.route",
1096
- "http.user_agent",
1097
- "http.request_content_length",
1098
- "http.response_content_length"
1099
- ]);
1100
- function AttributesTab$1({ span }) {
1101
- const { httpAttributes, otherAttributes, resourceAttributes } = useMemo(() => {
1102
- const http = [];
1103
- const other = [];
1104
- const resource = [];
1105
- if (span.attributes) Object.entries(span.attributes).forEach(([key, value]) => {
1106
- if (HTTP_SEMANTIC_CONVENTIONS.has(key)) http.push([key, value]);
1107
- else other.push([key, value]);
1108
- });
1109
- if (span.resourceAttributes) Object.entries(span.resourceAttributes).forEach(([key, value]) => {
1110
- resource.push([key, value]);
1111
- });
1112
- http.sort(([a], [b]) => a.localeCompare(b));
1113
- other.sort(([a], [b]) => a.localeCompare(b));
1114
- resource.sort(([a], [b]) => a.localeCompare(b));
1115
- return {
1116
- httpAttributes: http,
1117
- otherAttributes: other,
1118
- resourceAttributes: resource
1119
- };
1120
- }, [span]);
1121
- if (!(httpAttributes.length > 0 || otherAttributes.length > 0 || resourceAttributes.length > 0)) return /* @__PURE__ */ jsx("div", {
1122
- className: "text-sm text-muted-foreground text-center py-8",
1123
- children: "No attributes available"
1124
- });
1125
- return /* @__PURE__ */ jsxs("div", {
1126
- className: "space-y-6",
1416
+ //#region src/components/observability/TraceTimeline/SpanDetailInline.tsx
1417
+ function CollapsibleSection({ title, count, children }) {
1418
+ const [open, setOpen] = useState(false);
1419
+ if (count === 0) return null;
1420
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
1421
+ className: "flex items-center gap-1 text-xs font-medium text-foreground hover:text-blue-600 dark:hover:text-blue-400 py-1",
1422
+ onClick: (e) => {
1423
+ e.stopPropagation();
1424
+ setOpen((p) => !p);
1425
+ },
1127
1426
  children: [
1128
- httpAttributes.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsxs("h3", {
1129
- className: "text-sm font-semibold text-foreground mb-3 flex items-center",
1130
- children: [/* @__PURE__ */ jsx("span", { className: "w-2 h-2 bg-blue-500 rounded-full mr-2" }), "HTTP Attributes"]
1131
- }), /* @__PURE__ */ jsx("div", {
1132
- className: "space-y-2",
1133
- children: httpAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
1134
- attrKey: key,
1135
- value,
1136
- highlighted: true
1137
- }, key))
1138
- })] }),
1139
- otherAttributes.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
1140
- className: "text-sm font-semibold text-foreground mb-3",
1141
- children: "Span Attributes"
1142
- }), /* @__PURE__ */ jsx("div", {
1143
- className: "space-y-2",
1144
- children: otherAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
1145
- attrKey: key,
1146
- value
1147
- }, key))
1148
- })] }),
1149
- resourceAttributes.length > 0 && /* @__PURE__ */ jsxs("section", { children: [/* @__PURE__ */ jsx("h3", {
1150
- className: "text-sm font-semibold text-foreground mb-3",
1151
- children: "Resource Attributes"
1152
- }), /* @__PURE__ */ jsx("div", {
1153
- className: "space-y-2",
1154
- children: resourceAttributes.map(([key, value]) => /* @__PURE__ */ jsx(AttributeRow, {
1155
- attrKey: key,
1156
- value
1157
- }, key))
1158
- })] })
1427
+ /* @__PURE__ */ jsx("span", {
1428
+ className: "w-3 text-center",
1429
+ children: open ? "" : ""
1430
+ }),
1431
+ title,
1432
+ /* @__PURE__ */ jsxs("span", {
1433
+ className: "text-muted-foreground",
1434
+ children: [
1435
+ "(",
1436
+ count,
1437
+ ")"
1438
+ ]
1439
+ })
1159
1440
  ]
1160
- });
1441
+ }), open && /* @__PURE__ */ jsx("div", {
1442
+ className: "ml-4 mt-1 space-y-1",
1443
+ children
1444
+ })] });
1161
1445
  }
1162
- function AttributeRow({ attrKey, value, highlighted }) {
1163
- const isComplex = isComplexValue(value);
1164
- const formattedValue = formatAttributeValue(value);
1446
+ function KeyValueRow({ k, v }) {
1447
+ const formatted = formatAttributeValue(v);
1165
1448
  return /* @__PURE__ */ jsxs("div", {
1166
- 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"}`,
1167
- children: [/* @__PURE__ */ jsx("div", {
1168
- className: `font-mono font-medium break-words ${highlighted ? "text-blue-700 dark:text-blue-300" : "text-foreground"}`,
1169
- title: attrKey,
1170
- children: attrKey
1171
- }), /* @__PURE__ */ jsx("div", {
1172
- className: "break-words",
1173
- children: isComplex ? /* @__PURE__ */ jsx("pre", {
1174
- className: "text-xs text-foreground bg-background p-2 rounded border border-border overflow-x-auto",
1175
- children: formattedValue
1176
- }) : /* @__PURE__ */ jsx("span", {
1449
+ className: "flex gap-2 text-xs font-mono py-0.5",
1450
+ children: [
1451
+ /* @__PURE__ */ jsx("span", {
1452
+ className: "text-muted-foreground flex-shrink-0",
1453
+ children: k
1454
+ }),
1455
+ /* @__PURE__ */ jsx("span", {
1177
1456
  className: "text-foreground",
1178
- children: formattedValue
1457
+ children: "="
1458
+ }),
1459
+ /* @__PURE__ */ jsx("span", {
1460
+ className: "text-foreground break-all",
1461
+ children: formatted
1179
1462
  })
1180
- })]
1463
+ ]
1181
1464
  });
1182
1465
  }
1183
-
1184
- //#endregion
1185
- //#region src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx
1186
- function formatRelativeTime$1(eventTimeMs, spanStartMs) {
1187
- const relativeMs = eventTimeMs - spanStartMs;
1188
- return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
1189
- }
1190
- function EventsTab({ span }) {
1191
- const [expandedEvents, setExpandedEvents] = useState(/* @__PURE__ */ new Set());
1192
- const toggleEventExpanded = (index) => {
1193
- setExpandedEvents((prev) => {
1194
- const next = new Set(prev);
1195
- if (next.has(index)) next.delete(index);
1196
- else next.add(index);
1197
- return next;
1198
- });
1199
- };
1200
- if (!span.events || span.events.length === 0) return /* @__PURE__ */ jsx("div", {
1201
- className: "text-sm text-muted-foreground text-center py-8",
1202
- children: "No events available"
1203
- });
1204
- return /* @__PURE__ */ jsx("div", {
1205
- className: "space-y-3",
1206
- children: span.events.map((event, index) => {
1207
- const isExpanded = expandedEvents.has(index);
1208
- const hasAttributes = event.attributes && Object.keys(event.attributes).length > 0;
1209
- const relativeTime = formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs);
1210
- return /* @__PURE__ */ jsxs("div", {
1211
- className: "border border-border rounded-lg overflow-hidden",
1466
+ function SpanDetailInline({ span, traceStartMs }) {
1467
+ const [copiedId, setCopiedId] = useState(false);
1468
+ const serviceColor = getServiceColor(span.serviceName);
1469
+ const relativeStartMs = span.startTimeUnixMs - traceStartMs;
1470
+ const handleCopy = useCallback(async () => {
1471
+ try {
1472
+ await navigator.clipboard.writeText(span.spanId);
1473
+ setCopiedId(true);
1474
+ setTimeout(() => setCopiedId(false), 2e3);
1475
+ } catch {}
1476
+ }, [span.spanId]);
1477
+ const spanAttrs = Object.entries(span.attributes).sort(([a], [b]) => a.localeCompare(b));
1478
+ const resourceAttrs = Object.entries(span.resourceAttributes).sort(([a], [b]) => a.localeCompare(b));
1479
+ return /* @__PURE__ */ jsxs("div", {
1480
+ className: "border-b border-border bg-muted/50 px-4 py-3",
1481
+ style: { borderLeft: `3px solid ${serviceColor}` },
1482
+ onClick: (e) => e.stopPropagation(),
1483
+ children: [
1484
+ /* @__PURE__ */ jsxs("div", {
1485
+ className: "mb-2",
1486
+ children: [/* @__PURE__ */ jsx("div", {
1487
+ className: "text-sm font-medium text-foreground",
1488
+ children: span.name
1489
+ }), /* @__PURE__ */ jsxs("div", {
1490
+ className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground mt-1",
1491
+ children: [
1492
+ /* @__PURE__ */ jsxs("span", { children: ["Service: ", /* @__PURE__ */ jsx("span", {
1493
+ className: "text-foreground",
1494
+ children: span.serviceName
1495
+ })] }),
1496
+ /* @__PURE__ */ jsxs("span", { children: [
1497
+ "Duration:",
1498
+ " ",
1499
+ /* @__PURE__ */ jsx("span", {
1500
+ className: "text-foreground",
1501
+ children: formatDuration(span.durationMs)
1502
+ })
1503
+ ] }),
1504
+ /* @__PURE__ */ jsxs("span", { children: [
1505
+ "Start:",
1506
+ " ",
1507
+ /* @__PURE__ */ jsx("span", {
1508
+ className: "text-foreground",
1509
+ children: formatDuration(relativeStartMs)
1510
+ })
1511
+ ] }),
1512
+ /* @__PURE__ */ jsxs("span", { children: ["Kind: ", /* @__PURE__ */ jsx("span", {
1513
+ className: "text-foreground",
1514
+ children: span.kind
1515
+ })] }),
1516
+ span.status !== "UNSET" && /* @__PURE__ */ jsxs("span", { children: [
1517
+ "Status:",
1518
+ " ",
1519
+ /* @__PURE__ */ jsx("span", {
1520
+ className: span.status === "ERROR" ? "text-red-500" : "text-foreground",
1521
+ children: span.status
1522
+ })
1523
+ ] })
1524
+ ]
1525
+ })]
1526
+ }),
1527
+ /* @__PURE__ */ jsxs("div", {
1528
+ className: "space-y-1",
1212
1529
  children: [
1213
- /* @__PURE__ */ jsx("div", {
1214
- className: "bg-muted p-3",
1215
- children: /* @__PURE__ */ jsxs("div", {
1216
- className: "flex items-start justify-between gap-2",
1530
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1531
+ title: "Tags",
1532
+ count: spanAttrs.length,
1533
+ children: spanAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
1534
+ k,
1535
+ v
1536
+ }, k))
1537
+ }),
1538
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1539
+ title: "Process",
1540
+ count: resourceAttrs.length,
1541
+ children: resourceAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
1542
+ k,
1543
+ v
1544
+ }, k))
1545
+ }),
1546
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1547
+ title: "Events",
1548
+ count: span.events.length,
1549
+ children: span.events.map((event, i) => /* @__PURE__ */ jsxs("div", {
1550
+ className: "text-xs border-l-2 border-border pl-2 py-1.5 space-y-0.5",
1217
1551
  children: [/* @__PURE__ */ jsxs("div", {
1218
- className: "flex-1 min-w-0",
1219
- children: [/* @__PURE__ */ jsx("div", {
1220
- className: "font-medium text-sm text-foreground truncate",
1552
+ className: "flex items-center gap-2",
1553
+ children: [/* @__PURE__ */ jsx("span", {
1554
+ className: "font-mono text-muted-foreground flex-shrink-0",
1555
+ children: formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs)
1556
+ }), /* @__PURE__ */ jsx("span", {
1557
+ className: "font-medium text-foreground",
1221
1558
  children: event.name
1222
- }), /* @__PURE__ */ jsxs("div", {
1223
- className: "text-xs text-muted-foreground mt-1 font-mono",
1224
- children: [relativeTime, " from span start"]
1225
1559
  })]
1226
- }), hasAttributes && /* @__PURE__ */ jsx("button", {
1227
- onClick: () => toggleEventExpanded(index),
1228
- className: "p-1 hover:bg-muted/80 rounded transition-colors",
1229
- "aria-label": isExpanded ? "Collapse attributes" : "Expand attributes",
1230
- "aria-expanded": isExpanded,
1231
- children: /* @__PURE__ */ jsx("svg", {
1232
- className: `w-4 h-4 text-muted-foreground transition-transform ${isExpanded ? "rotate-180" : ""}`,
1233
- fill: "none",
1234
- stroke: "currentColor",
1235
- viewBox: "0 0 24 24",
1236
- children: /* @__PURE__ */ jsx("path", {
1237
- strokeLinecap: "round",
1238
- strokeLinejoin: "round",
1239
- strokeWidth: 2,
1240
- d: "M19 9l-7 7-7-7"
1241
- })
1242
- })
1243
- })]
1244
- })
1560
+ }), Object.entries(event.attributes).map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
1561
+ k,
1562
+ v
1563
+ }, k))]
1564
+ }, i))
1245
1565
  }),
1246
- hasAttributes && isExpanded && /* @__PURE__ */ jsxs("div", {
1247
- className: "p-3 bg-background border-t border-border",
1248
- children: [/* @__PURE__ */ jsx("div", {
1249
- className: "text-xs font-semibold text-foreground mb-2",
1250
- children: "Attributes"
1251
- }), /* @__PURE__ */ jsx("div", {
1252
- className: "space-y-2",
1253
- children: Object.entries(event.attributes).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
1254
- className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
1255
- children: [/* @__PURE__ */ jsx("div", {
1256
- className: "font-mono font-medium text-foreground break-words",
1257
- children: key
1258
- }), /* @__PURE__ */ jsx("div", {
1259
- className: "text-foreground break-words",
1260
- children: typeof value === "object" ? /* @__PURE__ */ jsx("pre", {
1261
- className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
1262
- children: formatAttributeValue(value)
1263
- }) : /* @__PURE__ */ jsx("span", { children: formatAttributeValue(value) })
1264
- })]
1265
- }, key))
1266
- })]
1566
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1567
+ title: "Links",
1568
+ count: span.links.length,
1569
+ children: span.links.map((link, i) => /* @__PURE__ */ jsxs("div", {
1570
+ className: "text-xs font-mono py-0.5",
1571
+ children: [
1572
+ /* @__PURE__ */ jsx("span", {
1573
+ className: "text-muted-foreground",
1574
+ children: "trace:"
1575
+ }),
1576
+ " ",
1577
+ link.traceId,
1578
+ " ",
1579
+ /* @__PURE__ */ jsx("span", {
1580
+ className: "text-muted-foreground",
1581
+ children: "span:"
1582
+ }),
1583
+ " ",
1584
+ link.spanId
1585
+ ]
1586
+ }, i))
1587
+ })
1588
+ ]
1589
+ }),
1590
+ /* @__PURE__ */ jsxs("div", {
1591
+ className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
1592
+ children: [
1593
+ /* @__PURE__ */ jsx("span", {
1594
+ className: "text-xs text-muted-foreground",
1595
+ children: "SpanID:"
1267
1596
  }),
1268
- !hasAttributes && /* @__PURE__ */ jsx("div", {
1269
- className: "px-3 pb-3 text-xs text-muted-foreground italic",
1270
- children: "No attributes"
1597
+ /* @__PURE__ */ jsx("code", {
1598
+ className: "text-xs font-mono text-foreground",
1599
+ children: span.spanId
1600
+ }),
1601
+ /* @__PURE__ */ jsx("button", {
1602
+ onClick: handleCopy,
1603
+ className: "text-xs text-muted-foreground hover:text-foreground",
1604
+ "aria-label": "Copy span ID",
1605
+ children: copiedId ? "✓" : "Copy"
1271
1606
  })
1272
1607
  ]
1273
- }, index);
1274
- })
1608
+ })
1609
+ ]
1275
1610
  });
1276
1611
  }
1277
-
1278
1612
  //#endregion
1279
- //#region src/components/observability/TraceTimeline/DetailPane/LinksTab.tsx
1280
- function truncateId(id) {
1281
- return id.length > 8 ? `${id.substring(0, 8)}...` : id;
1282
- }
1283
- function LinksTab({ span, onLinkClick }) {
1284
- const [expandedLinks, setExpandedLinks] = useState(/* @__PURE__ */ new Set());
1285
- const [copiedId, setCopiedId] = useState(null);
1286
- const toggleLinkExpanded = (index) => {
1287
- setExpandedLinks((prev) => {
1288
- const next = new Set(prev);
1289
- if (next.has(index)) next.delete(index);
1290
- else next.add(index);
1291
- return next;
1292
- });
1293
- };
1294
- const copyToClipboard = async (text, type, index) => {
1295
- try {
1296
- await navigator.clipboard.writeText(text);
1297
- setCopiedId(`${type}-${index}-${text}`);
1298
- setTimeout(() => setCopiedId(null), 2e3);
1299
- } catch (err) {
1300
- console.error("Failed to copy:", err);
1301
- }
1302
- };
1303
- if (!span.links || span.links.length === 0) return /* @__PURE__ */ jsx("div", {
1304
- className: "text-sm text-muted-foreground text-center py-8",
1305
- children: "No links available"
1306
- });
1307
- return /* @__PURE__ */ jsx("div", {
1308
- className: "space-y-3",
1309
- children: span.links.map((link, index) => {
1310
- const isExpanded = expandedLinks.has(index);
1311
- const hasAttributes = link.attributes && Object.keys(link.attributes).length > 0;
1312
- return /* @__PURE__ */ jsxs("div", {
1313
- className: "border border-border rounded-lg overflow-hidden",
1314
- children: [/* @__PURE__ */ jsxs("div", {
1315
- className: "bg-muted p-3",
1316
- children: [
1317
- /* @__PURE__ */ jsxs("div", {
1318
- className: "mb-2",
1319
- children: [/* @__PURE__ */ jsx("div", {
1320
- className: "text-xs font-semibold text-muted-foreground mb-1",
1321
- children: "Trace ID"
1322
- }), /* @__PURE__ */ jsxs("div", {
1323
- className: "flex items-center gap-2",
1324
- children: [/* @__PURE__ */ jsx("code", {
1325
- className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
1326
- title: link.traceId,
1327
- children: truncateId(link.traceId)
1328
- }), /* @__PURE__ */ jsx("button", {
1329
- onClick: () => copyToClipboard(link.traceId, "trace", index),
1330
- className: "p-1 hover:bg-muted/80 rounded transition-colors",
1331
- "aria-label": "Copy trace ID",
1332
- children: /* @__PURE__ */ jsx("svg", {
1333
- className: `w-4 h-4 ${copiedId === `trace-${index}-${link.traceId}` ? "text-green-600" : "text-muted-foreground"}`,
1334
- fill: "none",
1335
- stroke: "currentColor",
1336
- viewBox: "0 0 24 24",
1337
- children: copiedId === `trace-${index}-${link.traceId}` ? /* @__PURE__ */ jsx("path", {
1338
- strokeLinecap: "round",
1339
- strokeLinejoin: "round",
1340
- strokeWidth: 2,
1341
- d: "M5 13l4 4L19 7"
1342
- }) : /* @__PURE__ */ jsx("path", {
1343
- strokeLinecap: "round",
1344
- strokeLinejoin: "round",
1345
- strokeWidth: 2,
1346
- 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"
1347
- })
1348
- })
1349
- })]
1350
- })]
1351
- }),
1352
- /* @__PURE__ */ jsxs("div", {
1353
- className: "mb-2",
1354
- children: [/* @__PURE__ */ jsx("div", {
1355
- className: "text-xs font-semibold text-muted-foreground mb-1",
1356
- children: "Span ID"
1357
- }), /* @__PURE__ */ jsxs("div", {
1358
- className: "flex items-center gap-2",
1359
- children: [/* @__PURE__ */ jsx("code", {
1360
- className: "text-xs font-mono text-foreground bg-background px-2 py-1 rounded border border-border flex-1 truncate",
1361
- title: link.spanId,
1362
- children: truncateId(link.spanId)
1363
- }), /* @__PURE__ */ jsx("button", {
1364
- onClick: () => copyToClipboard(link.spanId, "span", index),
1365
- className: "p-1 hover:bg-muted/80 rounded transition-colors",
1366
- "aria-label": "Copy span ID",
1367
- children: /* @__PURE__ */ jsx("svg", {
1368
- className: `w-4 h-4 ${copiedId === `span-${index}-${link.spanId}` ? "text-green-600" : "text-muted-foreground"}`,
1369
- fill: "none",
1370
- stroke: "currentColor",
1371
- viewBox: "0 0 24 24",
1372
- children: copiedId === `span-${index}-${link.spanId}` ? /* @__PURE__ */ jsx("path", {
1373
- strokeLinecap: "round",
1374
- strokeLinejoin: "round",
1375
- strokeWidth: 2,
1376
- d: "M5 13l4 4L19 7"
1377
- }) : /* @__PURE__ */ jsx("path", {
1378
- strokeLinecap: "round",
1379
- strokeLinejoin: "round",
1380
- strokeWidth: 2,
1381
- 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"
1382
- })
1383
- })
1384
- })]
1385
- })]
1386
- }),
1387
- onLinkClick && /* @__PURE__ */ jsx("button", {
1388
- onClick: () => onLinkClick(link.traceId, link.spanId),
1389
- 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",
1390
- children: "Navigate to Span"
1391
- }),
1392
- hasAttributes && /* @__PURE__ */ jsxs("button", {
1393
- onClick: () => toggleLinkExpanded(index),
1394
- 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",
1395
- "aria-expanded": isExpanded,
1396
- children: [/* @__PURE__ */ jsxs("span", { children: [
1397
- isExpanded ? "Hide" : "Show",
1398
- " Attributes (",
1399
- Object.keys(link.attributes).length,
1400
- ")"
1401
- ] }), /* @__PURE__ */ jsx("svg", {
1402
- className: `w-3 h-3 transition-transform ${isExpanded ? "rotate-180" : ""}`,
1403
- fill: "none",
1404
- stroke: "currentColor",
1405
- viewBox: "0 0 24 24",
1406
- children: /* @__PURE__ */ jsx("path", {
1407
- strokeLinecap: "round",
1408
- strokeLinejoin: "round",
1409
- strokeWidth: 2,
1410
- d: "M19 9l-7 7-7-7"
1411
- })
1412
- })]
1413
- })
1414
- ]
1415
- }), hasAttributes && isExpanded && /* @__PURE__ */ jsx("div", {
1416
- className: "p-3 bg-background border-t border-border",
1417
- children: /* @__PURE__ */ jsx("div", {
1418
- className: "space-y-2",
1419
- children: Object.entries(link.attributes).map(([key, value]) => /* @__PURE__ */ jsxs("div", {
1420
- className: "grid grid-cols-[minmax(100px,1fr)_2fr] gap-3 text-xs",
1421
- children: [/* @__PURE__ */ jsx("div", {
1422
- className: "font-mono font-medium text-foreground break-words",
1423
- children: key
1424
- }), /* @__PURE__ */ jsx("div", {
1425
- className: "text-foreground break-words",
1426
- children: typeof value === "object" ? /* @__PURE__ */ jsx("pre", {
1427
- className: "text-xs bg-muted p-2 rounded border border-border overflow-x-auto",
1428
- children: formatAttributeValue(value)
1429
- }) : /* @__PURE__ */ jsx("span", { children: formatAttributeValue(value) })
1430
- })]
1431
- }, key))
1432
- })
1433
- })]
1434
- }, index);
1435
- })
1436
- });
1437
- }
1438
-
1439
- //#endregion
1440
- //#region src/components/observability/TraceTimeline/DetailPane/index.tsx
1441
- function DetailPane({ span, onClose, onLinkClick, initialTab = "attributes" }) {
1442
- const [activeTab, setActiveTab] = useState(initialTab);
1443
- const [copiedId, setCopiedId] = useState(false);
1444
- const handleTabChange = useCallback((tab) => {
1445
- setActiveTab(tab);
1446
- }, []);
1447
- const handleCopySpanId = useCallback(async () => {
1448
- try {
1449
- await navigator.clipboard.writeText(span.spanId);
1450
- setCopiedId(true);
1451
- setTimeout(() => setCopiedId(false), 2e3);
1452
- } catch (err) {
1453
- console.error("Failed to copy span ID:", err);
1454
- }
1455
- }, [span.spanId]);
1456
- return /* @__PURE__ */ jsxs("div", {
1457
- className: "flex flex-col h-full bg-background border-l border-border",
1458
- onKeyDown: useCallback((e) => {
1459
- if (e.key === "Escape") onClose();
1460
- }, [onClose]),
1461
- tabIndex: -1,
1462
- role: "complementary",
1463
- "aria-label": "Span details",
1464
- children: [
1465
- /* @__PURE__ */ jsxs("div", {
1466
- className: "p-4 border-b border-border",
1467
- children: [
1468
- /* @__PURE__ */ jsxs("div", {
1469
- className: "flex items-center justify-between mb-3",
1470
- children: [/* @__PURE__ */ jsx("h2", {
1471
- className: "text-lg font-semibold text-foreground truncate",
1472
- children: "Span Details"
1473
- }), /* @__PURE__ */ jsx("button", {
1474
- onClick: onClose,
1475
- className: "p-1 hover:bg-muted rounded transition-colors",
1476
- "aria-label": "Close detail pane",
1477
- title: "Close (Esc)",
1478
- children: /* @__PURE__ */ jsx("svg", {
1479
- className: "w-5 h-5 text-muted-foreground",
1480
- fill: "none",
1481
- stroke: "currentColor",
1482
- viewBox: "0 0 24 24",
1483
- children: /* @__PURE__ */ jsx("path", {
1484
- strokeLinecap: "round",
1485
- strokeLinejoin: "round",
1486
- strokeWidth: 2,
1487
- d: "M6 18L18 6M6 6l12 12"
1488
- })
1489
- })
1490
- })]
1491
- }),
1492
- /* @__PURE__ */ jsx("div", {
1493
- className: "mb-2",
1494
- children: /* @__PURE__ */ jsx("div", {
1495
- className: "text-sm font-medium text-foreground truncate",
1496
- title: span.name,
1497
- children: span.name
1498
- })
1499
- }),
1500
- /* @__PURE__ */ jsxs("div", {
1501
- className: "flex items-center gap-2",
1502
- children: [
1503
- /* @__PURE__ */ jsx("span", {
1504
- className: "text-xs text-muted-foreground",
1505
- children: "Span ID:"
1506
- }),
1507
- /* @__PURE__ */ jsx("code", {
1508
- className: "text-xs font-mono text-foreground bg-muted px-2 py-1 rounded flex-1 truncate",
1509
- title: span.spanId,
1510
- children: span.spanId
1511
- }),
1512
- /* @__PURE__ */ jsx("button", {
1513
- onClick: handleCopySpanId,
1514
- className: "p-1 hover:bg-muted rounded transition-colors",
1515
- "aria-label": "Copy span ID",
1516
- children: /* @__PURE__ */ jsx("svg", {
1517
- className: `w-4 h-4 ${copiedId ? "text-green-600 dark:text-green-400" : "text-muted-foreground"}`,
1518
- fill: "none",
1519
- stroke: "currentColor",
1520
- viewBox: "0 0 24 24",
1521
- children: copiedId ? /* @__PURE__ */ jsx("path", {
1522
- strokeLinecap: "round",
1523
- strokeLinejoin: "round",
1524
- strokeWidth: 2,
1525
- d: "M5 13l4 4L19 7"
1526
- }) : /* @__PURE__ */ jsx("path", {
1527
- strokeLinecap: "round",
1528
- strokeLinejoin: "round",
1529
- strokeWidth: 2,
1530
- 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"
1531
- })
1532
- })
1533
- })
1534
- ]
1535
- })
1536
- ]
1537
- }),
1538
- /* @__PURE__ */ jsx("div", {
1539
- className: "flex border-b border-border",
1540
- role: "tablist",
1541
- "aria-label": "Span detail tabs",
1542
- children: [
1543
- "attributes",
1544
- "events",
1545
- "links"
1546
- ].map((tab) => {
1547
- const count = tab === "attributes" ? Object.keys(span.attributes).length : tab === "events" ? span.events.length : span.links.length;
1548
- return /* @__PURE__ */ jsxs("button", {
1549
- role: "tab",
1550
- "aria-selected": activeTab === tab,
1551
- onClick: () => handleTabChange(tab),
1552
- 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"}`,
1553
- children: [tab.charAt(0).toUpperCase() + tab.slice(1), count > 0 && /* @__PURE__ */ jsxs("span", {
1554
- className: "ml-1 text-xs text-muted-foreground",
1555
- children: [
1556
- "(",
1557
- count,
1558
- ")"
1559
- ]
1560
- })]
1561
- }, tab);
1562
- })
1563
- }),
1564
- /* @__PURE__ */ jsxs("div", {
1565
- className: "flex-1 overflow-auto p-4",
1566
- children: [
1567
- activeTab === "attributes" && /* @__PURE__ */ jsx(AttributesTab$1, { span }),
1568
- activeTab === "events" && /* @__PURE__ */ jsx(EventsTab, { span }),
1569
- activeTab === "links" && /* @__PURE__ */ jsx(LinksTab, {
1570
- span,
1571
- onLinkClick
1572
- })
1573
- ]
1574
- })
1575
- ]
1576
- });
1577
- }
1578
-
1579
- //#endregion
1580
- //#region src/components/observability/shared/LoadingSkeleton.tsx
1581
- function LoadingSkeleton() {
1582
- return /* @__PURE__ */ jsxs("div", {
1583
- className: "flex flex-col h-full bg-background animate-pulse",
1584
- children: [/* @__PURE__ */ jsxs("div", {
1585
- className: "border-b border-border p-4",
1586
- children: [/* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-1/4 mb-3" }), /* @__PURE__ */ jsxs("div", {
1587
- className: "flex gap-4",
1588
- children: [
1589
- /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-32" }),
1590
- /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-24" }),
1591
- /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-20" })
1592
- ]
1593
- })]
1594
- }), /* @__PURE__ */ jsx("div", {
1595
- className: "flex-1 p-4 space-y-2",
1596
- children: Array.from({ length: 15 }).map((_, i) => /* @__PURE__ */ jsxs("div", {
1597
- className: "flex items-start gap-3",
1598
- children: [
1599
- /* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-32" }),
1600
- /* @__PURE__ */ jsx("div", {
1601
- className: "h-4 rounded w-16",
1602
- style: {
1603
- backgroundColor: i % 4 === 0 ? "#ef4444" : i % 4 === 1 ? "#f97316" : i % 4 === 2 ? "#3b82f6" : "#6b7280",
1604
- opacity: .3
1605
- }
1606
- }),
1607
- /* @__PURE__ */ jsx("div", {
1608
- className: "h-4 bg-muted rounded",
1609
- style: { width: `${80 + i * 7 % 40}px` }
1610
- }),
1611
- /* @__PURE__ */ jsx("div", {
1612
- className: "h-4 bg-muted/80 rounded flex-1",
1613
- style: { maxWidth: `${300 + i * 13 % 200}px` }
1614
- })
1615
- ]
1616
- }, i))
1617
- })]
1618
- });
1619
- }
1620
-
1621
- //#endregion
1622
- //#region src/components/KeyboardShortcuts/context.ts
1623
- const noop = () => {};
1624
- const KeyboardShortcutsContext = createContext({
1625
- register: noop,
1626
- unregister: noop
1627
- });
1628
- function useRegisterShortcuts(id, group) {
1629
- const { register, unregister } = useContext(KeyboardShortcutsContext);
1630
- useEffect(() => {
1631
- register(id, group);
1632
- return () => unregister(id);
1633
- }, [
1634
- id,
1635
- group,
1636
- register,
1637
- unregister
1638
- ]);
1639
- }
1640
-
1641
- //#endregion
1642
- //#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
1643
- function ShortcutsHelpDialog({ open, onClose, groups }) {
1644
- if (!open) return null;
1645
- const handleKeyDown = (e) => {
1646
- if (e.key === "Escape") {
1647
- e.stopPropagation();
1648
- onClose();
1613
+ //#region src/components/observability/shared/LoadingSkeleton.tsx
1614
+ function LoadingSkeleton() {
1615
+ return /* @__PURE__ */ jsxs("div", {
1616
+ className: "flex flex-col h-full bg-background animate-pulse",
1617
+ children: [/* @__PURE__ */ jsxs("div", {
1618
+ className: "border-b border-border p-4",
1619
+ children: [/* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-1/4 mb-3" }), /* @__PURE__ */ jsxs("div", {
1620
+ className: "flex gap-4",
1621
+ children: [
1622
+ /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-32" }),
1623
+ /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-24" }),
1624
+ /* @__PURE__ */ jsx("div", { className: "h-3 bg-muted rounded w-20" })
1625
+ ]
1626
+ })]
1627
+ }), /* @__PURE__ */ jsx("div", {
1628
+ className: "flex-1 p-4 space-y-2",
1629
+ children: Array.from({ length: 15 }).map((_, i) => /* @__PURE__ */ jsxs("div", {
1630
+ className: "flex items-start gap-3",
1631
+ children: [
1632
+ /* @__PURE__ */ jsx("div", { className: "h-4 bg-muted rounded w-32" }),
1633
+ /* @__PURE__ */ jsx("div", {
1634
+ className: "h-4 rounded w-16",
1635
+ style: {
1636
+ backgroundColor: i % 4 === 0 ? "#ef4444" : i % 4 === 1 ? "#f97316" : i % 4 === 2 ? "#3b82f6" : "#6b7280",
1637
+ opacity: .3
1638
+ }
1639
+ }),
1640
+ /* @__PURE__ */ jsx("div", {
1641
+ className: "h-4 bg-muted rounded",
1642
+ style: { width: `${80 + i * 7 % 40}px` }
1643
+ }),
1644
+ /* @__PURE__ */ jsx("div", {
1645
+ className: "h-4 bg-muted/80 rounded flex-1",
1646
+ style: { maxWidth: `${300 + i * 13 % 200}px` }
1647
+ })
1648
+ ]
1649
+ }, i))
1650
+ })]
1651
+ });
1652
+ }
1653
+ //#endregion
1654
+ //#region src/components/KeyboardShortcuts/context.ts
1655
+ const noop = () => {};
1656
+ const KeyboardShortcutsContext = createContext({
1657
+ register: noop,
1658
+ unregister: noop
1659
+ });
1660
+ function useRegisterShortcuts(id, group) {
1661
+ const { register, unregister } = useContext(KeyboardShortcutsContext);
1662
+ useEffect(() => {
1663
+ register(id, group);
1664
+ return () => unregister(id);
1665
+ }, [
1666
+ id,
1667
+ group,
1668
+ register,
1669
+ unregister
1670
+ ]);
1671
+ }
1672
+ //#endregion
1673
+ //#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
1674
+ function ShortcutsHelpDialog({ open, onClose, groups }) {
1675
+ if (!open) return null;
1676
+ const handleKeyDown = (e) => {
1677
+ if (e.key === "Escape") {
1678
+ e.stopPropagation();
1679
+ onClose();
1649
1680
  }
1650
1681
  };
1651
1682
  return /* @__PURE__ */ jsx("div", {
@@ -1694,154 +1725,1059 @@ function ShortcutsHelpDialog({ open, onClose, groups }) {
1694
1725
  })
1695
1726
  });
1696
1727
  }
1697
-
1698
1728
  //#endregion
1699
- //#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
1700
- const GENERAL_GROUP = {
1701
- name: "General",
1702
- shortcuts: [
1703
- {
1704
- keys: ["Shift", "?"],
1705
- description: "Toggle shortcuts help"
1706
- },
1707
- {
1708
- keys: ["Shift", "S"],
1709
- description: "Services tab"
1710
- },
1711
- {
1712
- keys: ["Shift", "L"],
1713
- description: "Logs tab"
1714
- },
1715
- {
1716
- keys: ["Shift", "M"],
1717
- description: "Metrics tab"
1718
- }
1719
- ]
1720
- };
1721
- function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLogs, onNavigateMetrics }) {
1722
- const [registry, setRegistry] = useState(() => /* @__PURE__ */ new Map());
1723
- const [isOpen, setIsOpen] = useState(false);
1724
- const register = useCallback((id, group) => {
1725
- setRegistry((prev) => {
1726
- const next = new Map(prev);
1727
- next.set(id, group);
1728
- return next;
1729
- });
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", "T"],
1739
+ description: "Traces 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] = useState(() => /* @__PURE__ */ new Map());
1753
+ const [isOpen, setIsOpen] = useState(false);
1754
+ const register = useCallback((id, group) => {
1755
+ setRegistry((prev) => {
1756
+ const next = new Map(prev);
1757
+ next.set(id, group);
1758
+ return next;
1759
+ });
1760
+ }, []);
1761
+ const unregister = useCallback((id) => {
1762
+ setRegistry((prev) => {
1763
+ const next = new Map(prev);
1764
+ next.delete(id);
1765
+ return next;
1766
+ });
1767
+ }, []);
1768
+ useEffect(() => {
1769
+ function handleKeyDown(e) {
1770
+ if (!(e.target instanceof HTMLElement)) return;
1771
+ if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || e.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 === "T") {
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
+ }
1797
+ }
1798
+ document.addEventListener("keydown", handleKeyDown);
1799
+ return () => document.removeEventListener("keydown", handleKeyDown);
1800
+ }, [
1801
+ isOpen,
1802
+ onNavigateServices,
1803
+ onNavigateLogs,
1804
+ onNavigateMetrics
1805
+ ]);
1806
+ const groups = useMemo(() => {
1807
+ return [GENERAL_GROUP, ...registry.values()];
1808
+ }, [registry]);
1809
+ const contextValue = useMemo(() => ({
1810
+ register,
1811
+ unregister
1812
+ }), [register, unregister]);
1813
+ return /* @__PURE__ */ jsxs(KeyboardShortcutsContext.Provider, {
1814
+ value: contextValue,
1815
+ children: [children, /* @__PURE__ */ jsx(ShortcutsHelpDialog, {
1816
+ open: isOpen,
1817
+ onClose: () => setIsOpen(false),
1818
+ groups
1819
+ })]
1820
+ });
1821
+ }
1822
+ //#endregion
1823
+ //#region src/components/observability/TraceTimeline/shortcuts.ts
1824
+ const TRACE_VIEWER_SHORTCUTS = {
1825
+ name: "Trace Viewer",
1826
+ shortcuts: [
1827
+ {
1828
+ keys: ["↑/K"],
1829
+ description: "Previous span"
1830
+ },
1831
+ {
1832
+ keys: ["↓/J"],
1833
+ description: "Next span"
1834
+ },
1835
+ {
1836
+ keys: ["←"],
1837
+ description: "Collapse span"
1838
+ },
1839
+ {
1840
+ keys: ["→"],
1841
+ description: "Expand span"
1842
+ },
1843
+ {
1844
+ keys: ["Enter"],
1845
+ description: "Focus detail pane"
1846
+ },
1847
+ {
1848
+ keys: ["C"],
1849
+ description: "Copy span name"
1850
+ },
1851
+ {
1852
+ keys: ["Esc"],
1853
+ description: "Deselect span"
1854
+ },
1855
+ {
1856
+ keys: [
1857
+ "Ctrl",
1858
+ "Shift",
1859
+ "E"
1860
+ ],
1861
+ description: "Expand all"
1862
+ },
1863
+ {
1864
+ keys: [
1865
+ "Ctrl",
1866
+ "Shift",
1867
+ "C"
1868
+ ],
1869
+ description: "Collapse all"
1870
+ }
1871
+ ]
1872
+ };
1873
+ //#endregion
1874
+ //#region src/components/observability/TraceTimeline/TimeRuler.tsx
1875
+ const TICK_COUNT = 5;
1876
+ function TimeRuler({ totalDurationMs, leftColumnWidth, offsetMs = 0 }) {
1877
+ const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => {
1878
+ const fraction = i / TICK_COUNT;
1879
+ return {
1880
+ label: formatDuration(offsetMs + totalDurationMs * fraction),
1881
+ percent: fraction * 100
1882
+ };
1883
+ });
1884
+ return /* @__PURE__ */ jsxs("div", {
1885
+ className: "flex border-b border-border bg-background",
1886
+ children: [/* @__PURE__ */ jsx("div", {
1887
+ className: "flex-shrink-0",
1888
+ style: { width: leftColumnWidth }
1889
+ }), /* @__PURE__ */ jsx("div", {
1890
+ className: "flex-1 relative h-6 px-2",
1891
+ children: ticks.map((tick) => /* @__PURE__ */ jsxs("div", {
1892
+ className: "absolute top-0 h-full flex flex-col justify-end",
1893
+ style: { left: `${tick.percent}%` },
1894
+ children: [/* @__PURE__ */ jsx("div", { className: "h-2 border-l border-muted-foreground/40" }), /* @__PURE__ */ jsx("span", {
1895
+ className: "text-[10px] text-muted-foreground font-mono -translate-x-1/2 absolute bottom-0 whitespace-nowrap",
1896
+ style: {
1897
+ left: 0,
1898
+ transform: tick.percent === 100 ? "translateX(-100%)" : tick.percent === 0 ? "none" : "translateX(-50%)"
1899
+ },
1900
+ children: tick.label
1901
+ })]
1902
+ }, tick.percent))
1903
+ })]
1904
+ });
1905
+ }
1906
+ //#endregion
1907
+ //#region src/components/observability/TraceTimeline/SpanSearch.tsx
1908
+ function SpanSearch({ value, onChange, matchCount, currentMatch, onPrev, onNext }) {
1909
+ return /* @__PURE__ */ jsxs("div", {
1910
+ className: "flex items-center gap-1 px-2 py-1 border-b border-border bg-background",
1911
+ children: [/* @__PURE__ */ jsx("input", {
1912
+ type: "text",
1913
+ placeholder: "Find...",
1914
+ value,
1915
+ onChange: (e) => onChange(e.target.value),
1916
+ className: "bg-muted text-foreground text-sm px-2 py-0.5 rounded border border-border outline-none focus:border-blue-500 w-48"
1917
+ }), value && /* @__PURE__ */ jsxs(Fragment, { children: [
1918
+ /* @__PURE__ */ jsx("span", {
1919
+ className: "text-xs text-muted-foreground whitespace-nowrap",
1920
+ children: matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : "0 matches"
1921
+ }),
1922
+ /* @__PURE__ */ jsx("button", {
1923
+ onClick: onPrev,
1924
+ disabled: matchCount === 0,
1925
+ className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
1926
+ "aria-label": "Previous match",
1927
+ children: /* @__PURE__ */ jsx("svg", {
1928
+ className: "w-3.5 h-3.5",
1929
+ fill: "none",
1930
+ stroke: "currentColor",
1931
+ viewBox: "0 0 24 24",
1932
+ children: /* @__PURE__ */ jsx("path", {
1933
+ strokeLinecap: "round",
1934
+ strokeLinejoin: "round",
1935
+ strokeWidth: 2,
1936
+ d: "M5 15l7-7 7 7"
1937
+ })
1938
+ })
1939
+ }),
1940
+ /* @__PURE__ */ jsx("button", {
1941
+ onClick: onNext,
1942
+ disabled: matchCount === 0,
1943
+ className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
1944
+ "aria-label": "Next match",
1945
+ children: /* @__PURE__ */ jsx("svg", {
1946
+ className: "w-3.5 h-3.5",
1947
+ fill: "none",
1948
+ stroke: "currentColor",
1949
+ viewBox: "0 0 24 24",
1950
+ children: /* @__PURE__ */ jsx("path", {
1951
+ strokeLinecap: "round",
1952
+ strokeLinejoin: "round",
1953
+ strokeWidth: 2,
1954
+ d: "M19 9l-7 7-7-7"
1955
+ })
1956
+ })
1957
+ })
1958
+ ] })]
1959
+ });
1960
+ }
1961
+ //#endregion
1962
+ //#region src/components/observability/TraceTimeline/ViewTabs.tsx
1963
+ const VIEWS = [
1964
+ "timeline",
1965
+ "graph",
1966
+ "statistics",
1967
+ "flamegraph"
1968
+ ];
1969
+ const VIEW_LABELS = {
1970
+ timeline: "Timeline",
1971
+ graph: "Graph",
1972
+ statistics: "Statistics",
1973
+ flamegraph: "Flamegraph"
1974
+ };
1975
+ function ViewTabs({ activeView, onChange }) {
1976
+ return /* @__PURE__ */ jsx("div", {
1977
+ className: "flex border-b border-border bg-background",
1978
+ children: VIEWS.map((view) => /* @__PURE__ */ jsx("button", {
1979
+ onClick: () => onChange(view),
1980
+ 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"}`,
1981
+ children: VIEW_LABELS[view]
1982
+ }, view))
1983
+ });
1984
+ }
1985
+ //#endregion
1986
+ //#region src/components/observability/TraceTimeline/GraphView.tsx
1987
+ /**
1988
+ * GraphView - SVG-based DAG showing service dependencies within a trace.
1989
+ */
1990
+ function buildDAG(trace) {
1991
+ const nodeMap = /* @__PURE__ */ new Map();
1992
+ const edgeMap = /* @__PURE__ */ new Map();
1993
+ const childServices = /* @__PURE__ */ new Map();
1994
+ function walk(span, parentService) {
1995
+ const svc = span.serviceName;
1996
+ const existing = nodeMap.get(svc);
1997
+ if (existing) {
1998
+ existing.spanCount++;
1999
+ if (span.status === "ERROR") existing.errorCount++;
2000
+ } else nodeMap.set(svc, {
2001
+ spanCount: 1,
2002
+ errorCount: span.status === "ERROR" ? 1 : 0
2003
+ });
2004
+ if (parentService && parentService !== svc) {
2005
+ const key = `${parentService}→${svc}`;
2006
+ const edge = edgeMap.get(key);
2007
+ if (edge) {
2008
+ edge.callCount++;
2009
+ edge.totalDurationMs += span.durationMs;
2010
+ } else edgeMap.set(key, {
2011
+ callCount: 1,
2012
+ totalDurationMs: span.durationMs
2013
+ });
2014
+ if (!childServices.has(parentService)) childServices.set(parentService, /* @__PURE__ */ new Set());
2015
+ const parentChildren = childServices.get(parentService);
2016
+ if (parentChildren) parentChildren.add(svc);
2017
+ }
2018
+ for (const child of span.children) walk(child, svc);
2019
+ }
2020
+ for (const root of trace.rootSpans) walk(root);
2021
+ const edges = [];
2022
+ for (const [key, meta] of edgeMap) {
2023
+ const [from, to] = key.split("→");
2024
+ if (from && to) edges.push({
2025
+ from,
2026
+ to,
2027
+ ...meta
2028
+ });
2029
+ }
2030
+ return {
2031
+ nodeMap,
2032
+ edges,
2033
+ childServices
2034
+ };
2035
+ }
2036
+ const NODE_W = 160;
2037
+ const NODE_H = 60;
2038
+ const LAYER_GAP_Y = 100;
2039
+ const NODE_GAP_X = 40;
2040
+ function layoutNodes(nodeMap, edges) {
2041
+ const children = /* @__PURE__ */ new Map();
2042
+ const hasParent = /* @__PURE__ */ new Set();
2043
+ for (const e of edges) {
2044
+ if (!children.has(e.from)) children.set(e.from, /* @__PURE__ */ new Set());
2045
+ const fromChildren = children.get(e.from);
2046
+ if (fromChildren) fromChildren.add(e.to);
2047
+ hasParent.add(e.to);
2048
+ }
2049
+ const roots = [...nodeMap.keys()].filter((s) => !hasParent.has(s));
2050
+ if (roots.length === 0 && nodeMap.size > 0) {
2051
+ const firstKey = nodeMap.keys().next().value;
2052
+ if (firstKey !== void 0) roots.push(firstKey);
2053
+ }
2054
+ const layerOf = /* @__PURE__ */ new Map();
2055
+ const enqueueCount = /* @__PURE__ */ new Map();
2056
+ const maxEnqueue = nodeMap.size * 2;
2057
+ const queue = [];
2058
+ for (const r of roots) {
2059
+ layerOf.set(r, 0);
2060
+ queue.push(r);
2061
+ }
2062
+ while (queue.length > 0) {
2063
+ const cur = queue.shift();
2064
+ if (!cur) continue;
2065
+ const curLayer = layerOf.get(cur);
2066
+ if (curLayer === void 0) continue;
2067
+ const kids = children.get(cur);
2068
+ if (!kids) continue;
2069
+ for (const kid of kids) {
2070
+ const prev = layerOf.get(kid);
2071
+ const count = enqueueCount.get(kid) ?? 0;
2072
+ if (prev === void 0 && count < maxEnqueue) {
2073
+ layerOf.set(kid, curLayer + 1);
2074
+ enqueueCount.set(kid, count + 1);
2075
+ queue.push(kid);
2076
+ }
2077
+ }
2078
+ }
2079
+ for (const name of nodeMap.keys()) if (!layerOf.has(name)) layerOf.set(name, 0);
2080
+ const layers = /* @__PURE__ */ new Map();
2081
+ for (const [name, layer] of layerOf) {
2082
+ if (!layers.has(layer)) layers.set(layer, []);
2083
+ const layerNames = layers.get(layer);
2084
+ if (layerNames) layerNames.push(name);
2085
+ }
2086
+ const nodes = [];
2087
+ const totalWidth = Math.max(...Array.from(layers.values()).map((l) => l.length), 1) * (NODE_W + NODE_GAP_X) - NODE_GAP_X;
2088
+ for (const [layer, names] of layers) {
2089
+ const offsetX = (totalWidth - (names.length * (NODE_W + NODE_GAP_X) - NODE_GAP_X)) / 2;
2090
+ names.forEach((name, i) => {
2091
+ const meta = nodeMap.get(name);
2092
+ if (!meta) return;
2093
+ nodes.push({
2094
+ name,
2095
+ spanCount: meta.spanCount,
2096
+ errorCount: meta.errorCount,
2097
+ layer,
2098
+ x: offsetX + i * (NODE_W + NODE_GAP_X),
2099
+ y: layer * (NODE_H + LAYER_GAP_Y)
2100
+ });
2101
+ });
2102
+ }
2103
+ return nodes;
2104
+ }
2105
+ function GraphView({ trace }) {
2106
+ const { nodes, edges, svgWidth, svgHeight } = useMemo(() => {
2107
+ const { nodeMap, edges } = buildDAG(trace);
2108
+ const nodes = layoutNodes(nodeMap, edges);
2109
+ const maxX = Math.max(...nodes.map((n) => n.x + NODE_W), NODE_W);
2110
+ const maxY = Math.max(...nodes.map((n) => n.y + NODE_H), NODE_H);
2111
+ const padding = 40;
2112
+ return {
2113
+ nodes,
2114
+ edges,
2115
+ svgWidth: maxX + padding * 2,
2116
+ svgHeight: maxY + padding * 2
2117
+ };
2118
+ }, [trace]);
2119
+ const nodeByName = useMemo(() => {
2120
+ const m = /* @__PURE__ */ new Map();
2121
+ for (const n of nodes) m.set(n.name, n);
2122
+ return m;
2123
+ }, [nodes]);
2124
+ const padding = 40;
2125
+ return /* @__PURE__ */ jsx("div", {
2126
+ className: "flex-1 overflow-auto bg-background p-4 flex justify-center",
2127
+ children: /* @__PURE__ */ jsxs("svg", {
2128
+ viewBox: `0 0 ${svgWidth} ${svgHeight}`,
2129
+ width: svgWidth,
2130
+ height: svgHeight,
2131
+ role: "img",
2132
+ "aria-label": "Service dependency graph",
2133
+ children: [
2134
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
2135
+ id: "arrowhead",
2136
+ markerWidth: "10",
2137
+ markerHeight: "7",
2138
+ refX: "9",
2139
+ refY: "3.5",
2140
+ orient: "auto",
2141
+ children: /* @__PURE__ */ jsx("polygon", {
2142
+ points: "0 0, 10 3.5, 0 7",
2143
+ fill: "#94a3b8"
2144
+ })
2145
+ }) }),
2146
+ edges.map((edge) => {
2147
+ const from = nodeByName.get(edge.from);
2148
+ const to = nodeByName.get(edge.to);
2149
+ if (!from || !to) return null;
2150
+ const x1 = padding + from.x + NODE_W / 2;
2151
+ const y1 = padding + from.y + NODE_H;
2152
+ const x2 = padding + to.x + NODE_W / 2;
2153
+ const y2 = padding + to.y;
2154
+ const midY = (y1 + y2) / 2;
2155
+ return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
2156
+ d: `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`,
2157
+ fill: "none",
2158
+ stroke: "#475569",
2159
+ strokeWidth: 1.5,
2160
+ markerEnd: "url(#arrowhead)"
2161
+ }), edge.callCount > 1 && /* @__PURE__ */ jsxs("text", {
2162
+ x: (x1 + x2) / 2,
2163
+ y: midY - 6,
2164
+ textAnchor: "middle",
2165
+ fontSize: 11,
2166
+ fill: "#94a3b8",
2167
+ children: [edge.callCount, "x"]
2168
+ })] }, `${edge.from}→${edge.to}`);
2169
+ }),
2170
+ nodes.map((node) => {
2171
+ const color = getServiceColor(node.name);
2172
+ const hasError = node.errorCount > 0;
2173
+ const textColor = "#f8fafc";
2174
+ const nx = padding + node.x;
2175
+ const ny = padding + node.y;
2176
+ return /* @__PURE__ */ jsxs("g", { children: [
2177
+ /* @__PURE__ */ jsx("rect", {
2178
+ x: nx,
2179
+ y: ny,
2180
+ width: NODE_W,
2181
+ height: NODE_H,
2182
+ rx: 8,
2183
+ ry: 8,
2184
+ fill: color,
2185
+ stroke: hasError ? "#ef4444" : "none",
2186
+ strokeWidth: hasError ? 2 : 0
2187
+ }),
2188
+ /* @__PURE__ */ jsx("text", {
2189
+ x: nx + NODE_W / 2,
2190
+ y: ny + 24,
2191
+ textAnchor: "middle",
2192
+ fontSize: 13,
2193
+ fontWeight: 600,
2194
+ fill: textColor,
2195
+ children: node.name.length > 18 ? node.name.slice(0, 16) + "..." : node.name
2196
+ }),
2197
+ /* @__PURE__ */ jsxs("text", {
2198
+ x: nx + NODE_W / 2,
2199
+ y: ny + 44,
2200
+ textAnchor: "middle",
2201
+ fontSize: 11,
2202
+ fill: textColor,
2203
+ opacity: .85,
2204
+ children: [
2205
+ node.spanCount,
2206
+ " span",
2207
+ node.spanCount !== 1 ? "s" : "",
2208
+ node.errorCount > 0 && ` · ${node.errorCount} err`
2209
+ ]
2210
+ })
2211
+ ] }, node.name);
2212
+ })
2213
+ ]
2214
+ })
2215
+ });
2216
+ }
2217
+ //#endregion
2218
+ //#region src/components/observability/TraceTimeline/StatisticsView.tsx
2219
+ function computeSelfTime(span) {
2220
+ const childrenTotal = span.children.reduce((sum, child) => sum + child.durationMs, 0);
2221
+ return Math.max(0, span.durationMs - childrenTotal);
2222
+ }
2223
+ function computeStats(trace) {
2224
+ const allFlattened = flattenAllSpans(trace.rootSpans);
2225
+ const groups = /* @__PURE__ */ new Map();
2226
+ for (const { span } of allFlattened) {
2227
+ const key = `${span.serviceName}:${span.name}`;
2228
+ let group = groups.get(key);
2229
+ if (!group) {
2230
+ group = {
2231
+ spans: [],
2232
+ selfTimes: []
2233
+ };
2234
+ groups.set(key, group);
2235
+ }
2236
+ group.spans.push(span);
2237
+ group.selfTimes.push(computeSelfTime(span));
2238
+ }
2239
+ const stats = [];
2240
+ for (const [key, { spans, selfTimes }] of groups) {
2241
+ const durations = spans.map((s) => s.durationMs);
2242
+ const count = spans.length;
2243
+ const totalDuration = durations.reduce((a, b) => a + b, 0);
2244
+ const selfTimeTotal = selfTimes.reduce((a, b) => a + b, 0);
2245
+ const firstSpan = spans[0];
2246
+ if (!firstSpan) continue;
2247
+ stats.push({
2248
+ key,
2249
+ serviceName: firstSpan.serviceName,
2250
+ spanName: firstSpan.name,
2251
+ count,
2252
+ totalDuration,
2253
+ avgDuration: totalDuration / count,
2254
+ minDuration: Math.min(...durations),
2255
+ maxDuration: Math.max(...durations),
2256
+ selfTimeTotal,
2257
+ selfTimeAvg: selfTimeTotal / count,
2258
+ selfTimeMin: Math.min(...selfTimes),
2259
+ selfTimeMax: Math.max(...selfTimes)
2260
+ });
2261
+ }
2262
+ return stats;
2263
+ }
2264
+ function getSortValue(stat, field) {
2265
+ switch (field) {
2266
+ case "name": return stat.key.toLowerCase();
2267
+ case "count": return stat.count;
2268
+ case "total": return stat.totalDuration;
2269
+ case "avg": return stat.avgDuration;
2270
+ case "min": return stat.minDuration;
2271
+ case "max": return stat.maxDuration;
2272
+ case "selfTotal": return stat.selfTimeTotal;
2273
+ case "selfAvg": return stat.selfTimeAvg;
2274
+ case "selfMin": return stat.selfTimeMin;
2275
+ case "selfMax": return stat.selfTimeMax;
2276
+ }
2277
+ }
2278
+ const COLUMNS = [
2279
+ {
2280
+ label: "Name",
2281
+ field: "name"
2282
+ },
2283
+ {
2284
+ label: "Count",
2285
+ field: "count"
2286
+ },
2287
+ {
2288
+ label: "Total",
2289
+ field: "total"
2290
+ },
2291
+ {
2292
+ label: "Avg",
2293
+ field: "avg"
2294
+ },
2295
+ {
2296
+ label: "Min",
2297
+ field: "min"
2298
+ },
2299
+ {
2300
+ label: "Max",
2301
+ field: "max"
2302
+ },
2303
+ {
2304
+ label: "ST Total",
2305
+ field: "selfTotal"
2306
+ },
2307
+ {
2308
+ label: "ST Avg",
2309
+ field: "selfAvg"
2310
+ },
2311
+ {
2312
+ label: "ST Min",
2313
+ field: "selfMin"
2314
+ },
2315
+ {
2316
+ label: "ST Max",
2317
+ field: "selfMax"
2318
+ }
2319
+ ];
2320
+ function StatisticsView({ trace }) {
2321
+ const [sortField, setSortField] = useState("total");
2322
+ const [sortAsc, setSortAsc] = useState(false);
2323
+ const stats = useMemo(() => computeStats(trace), [trace]);
2324
+ const sorted = useMemo(() => {
2325
+ const copy = [...stats];
2326
+ copy.sort((a, b) => {
2327
+ const aVal = getSortValue(a, sortField);
2328
+ const bVal = getSortValue(b, sortField);
2329
+ let cmp;
2330
+ if (typeof aVal === "string" && typeof bVal === "string") cmp = aVal.localeCompare(bVal);
2331
+ else if (typeof aVal === "number" && typeof bVal === "number") cmp = aVal - bVal;
2332
+ else cmp = 0;
2333
+ return sortAsc ? cmp : -cmp;
2334
+ });
2335
+ return copy;
2336
+ }, [
2337
+ stats,
2338
+ sortField,
2339
+ sortAsc
2340
+ ]);
2341
+ const handleSort = (field) => {
2342
+ if (sortField === field) setSortAsc((p) => !p);
2343
+ else {
2344
+ setSortField(field);
2345
+ setSortAsc(false);
2346
+ }
2347
+ };
2348
+ return /* @__PURE__ */ jsx("div", {
2349
+ className: "flex-1 overflow-auto p-2",
2350
+ children: /* @__PURE__ */ jsxs("table", {
2351
+ className: "w-full text-sm border-collapse",
2352
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
2353
+ className: "border-b border-border",
2354
+ children: COLUMNS.map((col) => /* @__PURE__ */ jsxs("th", {
2355
+ className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground cursor-pointer select-none hover:text-foreground whitespace-nowrap",
2356
+ onClick: () => handleSort(col.field),
2357
+ children: [
2358
+ col.label,
2359
+ " ",
2360
+ sortField === col.field ? sortAsc ? "▲" : "▼" : ""
2361
+ ]
2362
+ }, col.field))
2363
+ }) }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((stat, i) => /* @__PURE__ */ jsxs("tr", {
2364
+ className: `border-b border-border/50 ${i % 2 === 0 ? "bg-background" : "bg-muted/30"}`,
2365
+ children: [
2366
+ /* @__PURE__ */ jsxs("td", {
2367
+ className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
2368
+ children: [
2369
+ /* @__PURE__ */ jsx("span", {
2370
+ className: "text-muted-foreground",
2371
+ children: stat.serviceName
2372
+ }),
2373
+ /* @__PURE__ */ jsx("span", {
2374
+ className: "text-muted-foreground/50",
2375
+ children: ":"
2376
+ }),
2377
+ " ",
2378
+ stat.spanName
2379
+ ]
2380
+ }),
2381
+ /* @__PURE__ */ jsx("td", {
2382
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2383
+ children: stat.count
2384
+ }),
2385
+ /* @__PURE__ */ jsx("td", {
2386
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2387
+ children: formatDuration(stat.totalDuration)
2388
+ }),
2389
+ /* @__PURE__ */ jsx("td", {
2390
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2391
+ children: formatDuration(stat.avgDuration)
2392
+ }),
2393
+ /* @__PURE__ */ jsx("td", {
2394
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2395
+ children: formatDuration(stat.minDuration)
2396
+ }),
2397
+ /* @__PURE__ */ jsx("td", {
2398
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2399
+ children: formatDuration(stat.maxDuration)
2400
+ }),
2401
+ /* @__PURE__ */ jsx("td", {
2402
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2403
+ children: formatDuration(stat.selfTimeTotal)
2404
+ }),
2405
+ /* @__PURE__ */ jsx("td", {
2406
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2407
+ children: formatDuration(stat.selfTimeAvg)
2408
+ }),
2409
+ /* @__PURE__ */ jsx("td", {
2410
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2411
+ children: formatDuration(stat.selfTimeMin)
2412
+ }),
2413
+ /* @__PURE__ */ jsx("td", {
2414
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2415
+ children: formatDuration(stat.selfTimeMax)
2416
+ })
2417
+ ]
2418
+ }, stat.key)) })]
2419
+ })
2420
+ });
2421
+ }
2422
+ //#endregion
2423
+ //#region src/components/observability/TraceTimeline/FlamegraphView.tsx
2424
+ const ROW_HEIGHT = 24;
2425
+ const MIN_WIDTH = 1;
2426
+ const LABEL_MIN_WIDTH = 40;
2427
+ function findSpanById(rootSpans, spanId) {
2428
+ for (const root of rootSpans) {
2429
+ if (root.spanId === spanId) return root;
2430
+ const found = findSpanById(root.children, spanId);
2431
+ if (found) return found;
2432
+ }
2433
+ return null;
2434
+ }
2435
+ function getAncestorPath(rootSpans, targetId) {
2436
+ const path = [];
2437
+ function walk(span, ancestors) {
2438
+ if (span.spanId === targetId) {
2439
+ path.push(...ancestors, span);
2440
+ return true;
2441
+ }
2442
+ for (const child of span.children) if (walk(child, [...ancestors, span])) return true;
2443
+ return false;
2444
+ }
2445
+ for (const root of rootSpans) if (walk(root, [])) break;
2446
+ return path;
2447
+ }
2448
+ function FlamegraphView({ trace, onSpanClick, selectedSpanId }) {
2449
+ const [zoomSpanId, setZoomSpanId] = useState(null);
2450
+ const [tooltip, setTooltip] = useState(null);
2451
+ const zoomRoot = useMemo(() => {
2452
+ if (!zoomSpanId) return null;
2453
+ return findSpanById(trace.rootSpans, zoomSpanId);
2454
+ }, [trace.rootSpans, zoomSpanId]);
2455
+ const breadcrumbs = useMemo(() => {
2456
+ if (!zoomSpanId) return [];
2457
+ return getAncestorPath(trace.rootSpans, zoomSpanId);
2458
+ }, [trace.rootSpans, zoomSpanId]);
2459
+ const viewRoots = zoomRoot ? [zoomRoot] : trace.rootSpans;
2460
+ const viewMinTime = zoomRoot ? zoomRoot.startTimeUnixMs : trace.minTimeMs;
2461
+ const viewDuration = (zoomRoot ? zoomRoot.endTimeUnixMs : trace.maxTimeMs) - viewMinTime;
2462
+ const flatSpans = useMemo(() => flattenAllSpans(viewRoots).map((fs) => ({
2463
+ span: fs.span,
2464
+ depth: fs.level
2465
+ })), [viewRoots]);
2466
+ const maxDepth = useMemo(() => flatSpans.reduce((max, fs) => Math.max(max, fs.depth), 0) + 1, [flatSpans]);
2467
+ const svgWidth = 1200;
2468
+ const svgHeight = maxDepth * ROW_HEIGHT;
2469
+ const handleClick = useCallback((span) => {
2470
+ onSpanClick?.(span);
2471
+ setZoomSpanId(span.spanId);
2472
+ }, [onSpanClick]);
2473
+ const handleZoomOut = useCallback((spanId) => {
2474
+ setZoomSpanId(spanId);
2475
+ }, []);
2476
+ return /* @__PURE__ */ jsxs("div", {
2477
+ className: "flex-1 overflow-auto p-2",
2478
+ children: [
2479
+ breadcrumbs.length > 0 && /* @__PURE__ */ jsxs("div", {
2480
+ className: "flex items-center gap-1 text-xs text-muted-foreground mb-2 flex-wrap",
2481
+ children: [/* @__PURE__ */ jsx("button", {
2482
+ className: "hover:text-foreground underline",
2483
+ onClick: () => handleZoomOut(null),
2484
+ children: "root"
2485
+ }), breadcrumbs.map((bc, i) => /* @__PURE__ */ jsxs("span", {
2486
+ className: "flex items-center gap-1",
2487
+ children: [/* @__PURE__ */ jsx("span", {
2488
+ className: "text-muted-foreground/50",
2489
+ children: ">"
2490
+ }), i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxs("button", {
2491
+ className: "hover:text-foreground underline",
2492
+ onClick: () => handleZoomOut(bc.spanId),
2493
+ children: [
2494
+ bc.serviceName,
2495
+ ": ",
2496
+ bc.name
2497
+ ]
2498
+ }) : /* @__PURE__ */ jsxs("span", {
2499
+ className: "text-foreground",
2500
+ children: [
2501
+ bc.serviceName,
2502
+ ": ",
2503
+ bc.name
2504
+ ]
2505
+ })]
2506
+ }, bc.spanId))]
2507
+ }),
2508
+ /* @__PURE__ */ jsx("div", {
2509
+ className: "overflow-x-auto",
2510
+ children: /* @__PURE__ */ jsx("svg", {
2511
+ width: svgWidth,
2512
+ height: svgHeight,
2513
+ className: "block",
2514
+ onMouseLeave: () => setTooltip(null),
2515
+ children: flatSpans.map(({ span, depth }) => {
2516
+ const x = viewDuration > 0 ? (span.startTimeUnixMs - viewMinTime) / viewDuration * svgWidth : 0;
2517
+ const w = viewDuration > 0 ? Math.max(MIN_WIDTH, span.durationMs / viewDuration * svgWidth) : svgWidth;
2518
+ const y = depth * ROW_HEIGHT;
2519
+ const color = getServiceColor(span.serviceName);
2520
+ const isSelected = span.spanId === selectedSpanId;
2521
+ const showLabel = w >= LABEL_MIN_WIDTH;
2522
+ const label = `${span.serviceName}: ${span.name}`;
2523
+ return /* @__PURE__ */ jsxs("g", {
2524
+ className: "cursor-pointer",
2525
+ onClick: () => handleClick(span),
2526
+ onMouseEnter: (e) => setTooltip({
2527
+ span,
2528
+ x: e.clientX,
2529
+ y: e.clientY
2530
+ }),
2531
+ onMouseMove: (e) => setTooltip((prev) => prev ? {
2532
+ ...prev,
2533
+ x: e.clientX,
2534
+ y: e.clientY
2535
+ } : null),
2536
+ onMouseLeave: () => setTooltip(null),
2537
+ children: [/* @__PURE__ */ jsx("rect", {
2538
+ x,
2539
+ y,
2540
+ width: w,
2541
+ height: ROW_HEIGHT - 1,
2542
+ fill: color,
2543
+ opacity: .85,
2544
+ rx: 2,
2545
+ stroke: isSelected ? "#ffffff" : "transparent",
2546
+ strokeWidth: isSelected ? 2 : 0,
2547
+ className: "hover:opacity-100"
2548
+ }), showLabel && /* @__PURE__ */ jsx("text", {
2549
+ x: x + 4,
2550
+ y: y + ROW_HEIGHT / 2 + 1,
2551
+ dominantBaseline: "middle",
2552
+ fill: "#ffffff",
2553
+ fontSize: 11,
2554
+ fontFamily: "monospace",
2555
+ clipPath: `inset(0 0 0 0)`,
2556
+ children: /* @__PURE__ */ jsx("tspan", { children: label.length > w / 7 ? label.slice(0, Math.floor(w / 7) - 1) + "…" : label })
2557
+ })]
2558
+ }, span.spanId);
2559
+ })
2560
+ })
2561
+ }),
2562
+ tooltip && /* @__PURE__ */ jsxs("div", {
2563
+ className: "fixed z-50 pointer-events-none bg-popover border border-border rounded px-3 py-2 text-xs shadow-lg",
2564
+ style: {
2565
+ left: tooltip.x + 12,
2566
+ top: tooltip.y + 12
2567
+ },
2568
+ children: [
2569
+ /* @__PURE__ */ jsx("div", {
2570
+ className: "font-medium text-foreground",
2571
+ children: tooltip.span.name
2572
+ }),
2573
+ /* @__PURE__ */ jsx("div", {
2574
+ className: "text-muted-foreground",
2575
+ children: tooltip.span.serviceName
2576
+ }),
2577
+ /* @__PURE__ */ jsx("div", {
2578
+ className: "text-foreground mt-1",
2579
+ children: formatDuration(tooltip.span.durationMs)
2580
+ })
2581
+ ]
2582
+ })
2583
+ ]
2584
+ });
2585
+ }
2586
+ //#endregion
2587
+ //#region src/components/observability/TraceTimeline/Minimap.tsx
2588
+ /**
2589
+ * Minimap - Compressed overview of all spans with a draggable viewport.
2590
+ */
2591
+ const MINIMAP_HEIGHT = 40;
2592
+ const SPAN_HEIGHT = 2;
2593
+ const SPAN_GAP = 1;
2594
+ const MIN_VIEWPORT_WIDTH = .02;
2595
+ const HANDLE_WIDTH = 6;
2596
+ function Minimap({ trace, viewStart, viewEnd, onViewChange }) {
2597
+ const containerRef = useRef(null);
2598
+ const dragRef = useRef(null);
2599
+ const cleanupRef = useRef(null);
2600
+ useEffect(() => {
2601
+ return () => {
2602
+ cleanupRef.current?.();
2603
+ };
1730
2604
  }, []);
1731
- const unregister = useCallback((id) => {
1732
- setRegistry((prev) => {
1733
- const next = new Map(prev);
1734
- next.delete(id);
1735
- return next;
1736
- });
2605
+ const allSpans = useMemo(() => flattenAllSpans(trace.rootSpans), [trace.rootSpans]);
2606
+ const traceDuration = trace.maxTimeMs - trace.minTimeMs;
2607
+ const getFraction = useCallback((clientX) => {
2608
+ const el = containerRef.current;
2609
+ if (!el) return 0;
2610
+ const rect = el.getBoundingClientRect();
2611
+ if (!rect.width) return 0;
2612
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
1737
2613
  }, []);
1738
- useEffect(() => {
1739
- function handleKeyDown(e) {
1740
- const target = e.target;
1741
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable) return;
1742
- if (e.shiftKey && e.key === "?") {
1743
- e.preventDefault();
1744
- setIsOpen((v) => !v);
1745
- return;
1746
- }
1747
- if (e.key === "Escape" && isOpen) {
1748
- e.preventDefault();
1749
- setIsOpen(false);
1750
- return;
1751
- }
1752
- if (e.shiftKey && e.key === "S") {
1753
- e.preventDefault();
1754
- onNavigateServices();
1755
- return;
1756
- }
1757
- if (e.shiftKey && e.key === "L") {
1758
- e.preventDefault();
1759
- onNavigateLogs();
1760
- return;
1761
- }
1762
- if (e.shiftKey && e.key === "M") {
1763
- e.preventDefault();
1764
- onNavigateMetrics();
1765
- return;
1766
- }
2614
+ const clampView = useCallback((start, end) => {
2615
+ let s = Math.max(0, Math.min(1 - MIN_VIEWPORT_WIDTH, start));
2616
+ let e = Math.max(s + MIN_VIEWPORT_WIDTH, Math.min(1, end));
2617
+ if (e > 1) {
2618
+ e = 1;
2619
+ s = Math.max(0, e - Math.max(MIN_VIEWPORT_WIDTH, end - start));
1767
2620
  }
1768
- document.addEventListener("keydown", handleKeyDown);
1769
- return () => document.removeEventListener("keydown", handleKeyDown);
2621
+ return [s, e];
2622
+ }, []);
2623
+ const handleMouseDown = useCallback((e, mode) => {
2624
+ e.preventDefault();
2625
+ e.stopPropagation();
2626
+ cleanupRef.current?.();
2627
+ dragRef.current = {
2628
+ mode,
2629
+ startX: e.clientX,
2630
+ origViewStart: viewStart,
2631
+ origViewEnd: viewEnd
2632
+ };
2633
+ const handleMouseMove = (ev) => {
2634
+ const drag = dragRef.current;
2635
+ if (!drag || !containerRef.current) return;
2636
+ const rect = containerRef.current.getBoundingClientRect();
2637
+ if (!rect.width) return;
2638
+ const deltaFrac = (ev.clientX - drag.startX) / rect.width;
2639
+ let newStart;
2640
+ let newEnd;
2641
+ if (drag.mode === "pan") {
2642
+ const width = drag.origViewEnd - drag.origViewStart;
2643
+ newStart = drag.origViewStart + deltaFrac;
2644
+ newEnd = newStart + width;
2645
+ if (newStart < 0) {
2646
+ newStart = 0;
2647
+ newEnd = width;
2648
+ }
2649
+ if (newEnd > 1) {
2650
+ newEnd = 1;
2651
+ newStart = 1 - width;
2652
+ }
2653
+ } else if (drag.mode === "resize-left") {
2654
+ newStart = drag.origViewStart + deltaFrac;
2655
+ newEnd = drag.origViewEnd;
2656
+ } else {
2657
+ newStart = drag.origViewStart;
2658
+ newEnd = drag.origViewEnd + deltaFrac;
2659
+ }
2660
+ const [s, e] = clampView(newStart, newEnd);
2661
+ onViewChange(s, e);
2662
+ };
2663
+ const handleMouseUp = () => {
2664
+ dragRef.current = null;
2665
+ cleanupRef.current = null;
2666
+ window.removeEventListener("mousemove", handleMouseMove);
2667
+ window.removeEventListener("mouseup", handleMouseUp);
2668
+ };
2669
+ window.addEventListener("mousemove", handleMouseMove);
2670
+ window.addEventListener("mouseup", handleMouseUp);
2671
+ cleanupRef.current = handleMouseUp;
1770
2672
  }, [
1771
- isOpen,
1772
- onNavigateServices,
1773
- onNavigateLogs,
1774
- onNavigateMetrics
2673
+ viewStart,
2674
+ viewEnd,
2675
+ onViewChange,
2676
+ clampView
1775
2677
  ]);
1776
- const groups = useMemo(() => {
1777
- return [GENERAL_GROUP, ...registry.values()];
1778
- }, [registry]);
1779
- const contextValue = useMemo(() => ({
1780
- register,
1781
- unregister
1782
- }), [register, unregister]);
1783
- return /* @__PURE__ */ jsxs(KeyboardShortcutsContext.Provider, {
1784
- value: contextValue,
1785
- children: [children, /* @__PURE__ */ jsx(ShortcutsHelpDialog, {
1786
- open: isOpen,
1787
- onClose: () => setIsOpen(false),
1788
- groups
1789
- })]
2678
+ const handleBackgroundClick = useCallback((e) => {
2679
+ if (dragRef.current) return;
2680
+ if (e.target !== e.currentTarget) return;
2681
+ const frac = getFraction(e.clientX);
2682
+ const half = (viewEnd - viewStart) / 2;
2683
+ const [s, eVal] = clampView(frac - half, frac + half);
2684
+ onViewChange(s, eVal);
2685
+ }, [
2686
+ viewStart,
2687
+ viewEnd,
2688
+ onViewChange,
2689
+ getFraction,
2690
+ clampView
2691
+ ]);
2692
+ const handleKeyDown = useCallback((e) => {
2693
+ const step = .05;
2694
+ const width = viewEnd - viewStart;
2695
+ let newStart;
2696
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") newStart = viewStart - step;
2697
+ else if (e.key === "ArrowRight" || e.key === "ArrowDown") newStart = viewStart + step;
2698
+ else return;
2699
+ e.preventDefault();
2700
+ const [s, eVal] = clampView(newStart, newStart + width);
2701
+ onViewChange(s, eVal);
2702
+ }, [
2703
+ viewStart,
2704
+ viewEnd,
2705
+ onViewChange,
2706
+ clampView
2707
+ ]);
2708
+ const viewStartPct = viewStart * 100;
2709
+ const viewEndPct = viewEnd * 100;
2710
+ const viewWidthPct = viewEndPct - viewStartPct;
2711
+ const totalRows = allSpans.length;
2712
+ const availableHeight = MINIMAP_HEIGHT - 4;
2713
+ const rowHeight = totalRows > 0 ? Math.min(SPAN_HEIGHT + SPAN_GAP, availableHeight / totalRows) : SPAN_HEIGHT;
2714
+ return /* @__PURE__ */ jsxs("div", {
2715
+ ref: containerRef,
2716
+ className: "relative w-full border-b border-border bg-muted/30 select-none",
2717
+ style: { height: MINIMAP_HEIGHT },
2718
+ onClick: handleBackgroundClick,
2719
+ onKeyDown: handleKeyDown,
2720
+ role: "slider",
2721
+ tabIndex: 0,
2722
+ "aria-label": "Trace minimap viewport",
2723
+ "aria-valuemin": 0,
2724
+ "aria-valuemax": 100,
2725
+ "aria-valuenow": Math.round(viewStartPct),
2726
+ children: [
2727
+ traceDuration > 0 && allSpans.map(({ span }, i) => {
2728
+ const left = (span.startTimeUnixMs - trace.minTimeMs) / traceDuration * 100;
2729
+ const width = Math.max(.2, span.durationMs / traceDuration * 100);
2730
+ const color = getSpanBarColor(span.serviceName, span.status === "ERROR");
2731
+ return /* @__PURE__ */ jsx("div", {
2732
+ className: "absolute pointer-events-none",
2733
+ style: {
2734
+ left: `${left}%`,
2735
+ width: `${width}%`,
2736
+ top: 2 + i * rowHeight,
2737
+ height: Math.max(1, rowHeight - SPAN_GAP),
2738
+ backgroundColor: color,
2739
+ opacity: .8,
2740
+ borderRadius: 1
2741
+ }
2742
+ }, span.spanId);
2743
+ }),
2744
+ viewStartPct > 0 && /* @__PURE__ */ jsx("div", {
2745
+ className: "absolute top-0 left-0 h-full bg-black/30 pointer-events-none",
2746
+ style: { width: `${viewStartPct}%` }
2747
+ }),
2748
+ viewEndPct < 100 && /* @__PURE__ */ jsx("div", {
2749
+ className: "absolute top-0 h-full bg-black/30 pointer-events-none",
2750
+ style: {
2751
+ left: `${viewEndPct}%`,
2752
+ right: 0
2753
+ }
2754
+ }),
2755
+ /* @__PURE__ */ jsxs("div", {
2756
+ className: "absolute top-0 h-full border border-blue-500/50 bg-blue-500/10 cursor-grab active:cursor-grabbing",
2757
+ style: {
2758
+ left: `${viewStartPct}%`,
2759
+ width: `${viewWidthPct}%`
2760
+ },
2761
+ onMouseDown: (e) => handleMouseDown(e, "pan"),
2762
+ children: [/* @__PURE__ */ jsx("div", {
2763
+ className: "absolute top-0 left-0 h-full cursor-ew-resize z-10",
2764
+ style: {
2765
+ width: HANDLE_WIDTH,
2766
+ marginLeft: -HANDLE_WIDTH / 2
2767
+ },
2768
+ onMouseDown: (e) => handleMouseDown(e, "resize-left")
2769
+ }), /* @__PURE__ */ jsx("div", {
2770
+ className: "absolute top-0 right-0 h-full cursor-ew-resize z-10",
2771
+ style: {
2772
+ width: HANDLE_WIDTH,
2773
+ marginRight: -HANDLE_WIDTH / 2
2774
+ },
2775
+ onMouseDown: (e) => handleMouseDown(e, "resize-right")
2776
+ })]
2777
+ })
2778
+ ]
1790
2779
  });
1791
2780
  }
1792
-
1793
- //#endregion
1794
- //#region src/components/observability/TraceTimeline/shortcuts.ts
1795
- const TRACE_VIEWER_SHORTCUTS = {
1796
- name: "Trace Viewer",
1797
- shortcuts: [
1798
- {
1799
- keys: ["↑/K"],
1800
- description: "Previous span"
1801
- },
1802
- {
1803
- keys: ["↓/J"],
1804
- description: "Next span"
1805
- },
1806
- {
1807
- keys: ["←"],
1808
- description: "Collapse span"
1809
- },
1810
- {
1811
- keys: ["→"],
1812
- description: "Expand span"
1813
- },
1814
- {
1815
- keys: ["Enter"],
1816
- description: "Focus detail pane"
1817
- },
1818
- {
1819
- keys: ["C"],
1820
- description: "Copy span name"
1821
- },
1822
- {
1823
- keys: ["Esc"],
1824
- description: "Deselect span"
1825
- },
1826
- {
1827
- keys: [
1828
- "Ctrl",
1829
- "Shift",
1830
- "E"
1831
- ],
1832
- description: "Expand all"
1833
- },
1834
- {
1835
- keys: [
1836
- "Ctrl",
1837
- "Shift",
1838
- "C"
1839
- ],
1840
- description: "Collapse all"
1841
- }
1842
- ]
1843
- };
1844
-
1845
2781
  //#endregion
1846
2782
  //#region src/components/observability/TraceTimeline/index.tsx
1847
2783
  /**
@@ -1930,25 +2866,43 @@ function isSpanAncestorOf(potentialAncestor, descendantId, flattenedSpans) {
1930
2866
  }
1931
2867
  return false;
1932
2868
  }
1933
- function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpanId, isLoading, error }) {
2869
+ function collectServices(rootSpans) {
2870
+ const set = /* @__PURE__ */ new Set();
2871
+ function walk(span) {
2872
+ set.add(span.serviceName);
2873
+ span.children.forEach(walk);
2874
+ }
2875
+ rootSpans.forEach(walk);
2876
+ return Array.from(set).sort();
2877
+ }
2878
+ function TraceTimeline({ rows, onSpanClick, onSpanDeselect, selectedSpanId: externalSelectedSpanId, isLoading, error, view: externalView, onViewChange, uiFind: externalUiFind, onUiFindChange, viewStart: externalViewStart, viewEnd: externalViewEnd, onViewRangeChange }) {
1934
2879
  useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
1935
2880
  const [collapsedIds, setCollapsedIds] = useState(/* @__PURE__ */ new Set());
1936
2881
  const [internalSelectedSpanId, setInternalSelectedSpanId] = useState(null);
1937
2882
  const [hoveredSpanId, setHoveredSpanId] = useState(null);
2883
+ const [internalView, setInternalView] = useState("timeline");
2884
+ const [internalUiFind, setInternalUiFind] = useState("");
2885
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
2886
+ const [headerCollapsed, setHeaderCollapsed] = useState(false);
2887
+ const [internalViewStart, setInternalViewStart] = useState(0);
2888
+ const [internalViewEnd, setInternalViewEnd] = useState(1);
1938
2889
  const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
2890
+ const viewStart = externalViewStart ?? internalViewStart;
2891
+ const viewEnd = externalViewEnd ?? internalViewEnd;
2892
+ const activeView = externalView ?? internalView;
2893
+ const uiFind = externalUiFind ?? internalUiFind;
1939
2894
  const scrollRef = useRef(null);
1940
2895
  const announcementRef = useRef(null);
1941
2896
  const parsedTrace = useMemo(() => buildTrace(rows), [rows]);
2897
+ const services = useMemo(() => parsedTrace ? collectServices(parsedTrace.rootSpans) : [], [parsedTrace]);
1942
2898
  const flattenedSpans = useMemo(() => {
1943
2899
  if (!parsedTrace) return [];
1944
2900
  return flattenTree(parsedTrace.rootSpans, collapsedIds);
1945
2901
  }, [parsedTrace, collapsedIds]);
1946
- const virtualizer = useVirtualizer({
1947
- count: flattenedSpans.length,
1948
- getScrollElement: () => scrollRef.current,
1949
- estimateSize: () => 32,
1950
- overscan: 5
1951
- });
2902
+ const matchingIndices = useMemo(() => {
2903
+ if (!uiFind) return [];
2904
+ return flattenedSpans.map((item, idx) => spanMatchesSearch(item.span, uiFind) ? idx : -1).filter((idx) => idx !== -1);
2905
+ }, [flattenedSpans, uiFind]);
1952
2906
  const handleToggleCollapse = (spanId) => {
1953
2907
  setCollapsedIds((prev) => {
1954
2908
  const next = new Set(prev);
@@ -1957,11 +2911,22 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
1957
2911
  return next;
1958
2912
  });
1959
2913
  };
2914
+ const handleDeselect = useCallback(() => {
2915
+ setInternalSelectedSpanId(null);
2916
+ onSpanDeselect?.();
2917
+ }, [onSpanDeselect]);
1960
2918
  const handleSpanClick = useCallback((span) => {
1961
- setInternalSelectedSpanId(span.spanId);
1962
- onSpanClick?.(span);
1963
- if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
1964
- }, [onSpanClick]);
2919
+ if (selectedSpanId === span.spanId) handleDeselect();
2920
+ else {
2921
+ setInternalSelectedSpanId(span.spanId);
2922
+ onSpanClick?.(span);
2923
+ if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
2924
+ }
2925
+ }, [
2926
+ onSpanClick,
2927
+ selectedSpanId,
2928
+ handleDeselect
2929
+ ]);
1965
2930
  const handleExpandAll = useCallback(() => {
1966
2931
  setCollapsedIds(/* @__PURE__ */ new Set());
1967
2932
  }, []);
@@ -2010,23 +2975,77 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2010
2975
  return next;
2011
2976
  });
2012
2977
  }, [selectedSpanId, flattenedSpans]);
2013
- const handleDeselect = useCallback(() => {
2014
- setInternalSelectedSpanId(null);
2015
- }, []);
2016
- useEffect(() => {
2017
- if (!selectedSpanId) return;
2018
- const selectedIndex = flattenedSpans.findIndex((item) => item.span.spanId === selectedSpanId);
2019
- if (selectedIndex !== -1) virtualizer.scrollToIndex(selectedIndex, {
2020
- align: "center",
2978
+ const handleViewChange = useCallback((view) => {
2979
+ if (onViewChange) onViewChange(view);
2980
+ else setInternalView(view);
2981
+ }, [onViewChange]);
2982
+ const handleUiFindChange = useCallback((value) => {
2983
+ if (onUiFindChange) onUiFindChange(value);
2984
+ else setInternalUiFind(value);
2985
+ setCurrentMatchIndex(0);
2986
+ }, [onUiFindChange]);
2987
+ const handleViewRangeChange = useCallback((start, end) => {
2988
+ if (onViewRangeChange) onViewRangeChange(start, end);
2989
+ else {
2990
+ setInternalViewStart(start);
2991
+ setInternalViewEnd(end);
2992
+ }
2993
+ }, [onViewRangeChange]);
2994
+ const scrollToSpan = useCallback((spanId) => {
2995
+ (scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`))?.scrollIntoView({
2996
+ block: "center",
2021
2997
  behavior: "smooth"
2022
2998
  });
2999
+ }, []);
3000
+ const handleSearchNext = useCallback(() => {
3001
+ if (matchingIndices.length === 0) return;
3002
+ const next = (currentMatchIndex + 1) % matchingIndices.length;
3003
+ setCurrentMatchIndex(next);
3004
+ const idx = matchingIndices[next];
3005
+ if (idx !== void 0) {
3006
+ const item = flattenedSpans[idx];
3007
+ if (item) {
3008
+ handleSpanClick(item.span);
3009
+ scrollToSpan(item.span.spanId);
3010
+ }
3011
+ }
2023
3012
  }, [
2024
- selectedSpanId,
3013
+ matchingIndices,
3014
+ currentMatchIndex,
2025
3015
  flattenedSpans,
2026
- virtualizer
3016
+ handleSpanClick,
3017
+ scrollToSpan
2027
3018
  ]);
3019
+ const handleSearchPrev = useCallback(() => {
3020
+ if (matchingIndices.length === 0) return;
3021
+ const prev = (currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
3022
+ setCurrentMatchIndex(prev);
3023
+ const idx = matchingIndices[prev];
3024
+ if (idx !== void 0) {
3025
+ const item = flattenedSpans[idx];
3026
+ if (item) {
3027
+ handleSpanClick(item.span);
3028
+ scrollToSpan(item.span.spanId);
3029
+ }
3030
+ }
3031
+ }, [
3032
+ matchingIndices,
3033
+ currentMatchIndex,
3034
+ flattenedSpans,
3035
+ handleSpanClick,
3036
+ scrollToSpan
3037
+ ]);
3038
+ useEffect(() => {
3039
+ if (!selectedSpanId) return;
3040
+ scrollToSpan(selectedSpanId);
3041
+ }, [selectedSpanId, scrollToSpan]);
2028
3042
  useEffect(() => {
2029
3043
  const handleKeyDown = (e) => {
3044
+ if (e.key === "Escape" && selectedSpanId) {
3045
+ e.preventDefault();
3046
+ handleDeselect();
3047
+ return;
3048
+ }
2030
3049
  if (!(scrollRef.current?.parentElement)?.contains(document.activeElement)) return;
2031
3050
  switch (e.key) {
2032
3051
  case "ArrowUp":
@@ -2049,10 +3068,7 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2049
3068
  e.preventDefault();
2050
3069
  handleCollapseExpand(false);
2051
3070
  break;
2052
- case "Escape":
2053
- e.preventDefault();
2054
- handleDeselect();
2055
- break;
3071
+ case "Escape": break;
2056
3072
  case "Enter":
2057
3073
  if (selectedSpanId) {
2058
3074
  e.preventDefault();
@@ -2120,10 +3136,9 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2120
3136
  })
2121
3137
  });
2122
3138
  const totalDurationMs = parsedTrace.maxTimeMs - parsedTrace.minTimeMs;
2123
- const selectedSpan = selectedSpanId && flattenedSpans.length > 0 ? flattenedSpans.find((item) => item.span.spanId === selectedSpanId)?.span : null;
2124
- return /* @__PURE__ */ jsxs("div", {
3139
+ return /* @__PURE__ */ jsx("div", {
2125
3140
  className: "flex h-full bg-background",
2126
- children: [/* @__PURE__ */ jsxs("div", {
3141
+ children: /* @__PURE__ */ jsxs("div", {
2127
3142
  className: "flex flex-col flex-1 min-w-0",
2128
3143
  children: [
2129
3144
  /* @__PURE__ */ jsx("div", {
@@ -2133,39 +3148,58 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2133
3148
  "aria-live": "polite",
2134
3149
  "aria-atomic": "true"
2135
3150
  }),
2136
- /* @__PURE__ */ jsx(TraceHeader, { trace: parsedTrace }),
2137
- /* @__PURE__ */ jsx("div", {
2138
- ref: scrollRef,
2139
- className: "flex-1 overflow-auto outline-none",
2140
- role: "tree",
2141
- "aria-label": "Trace timeline",
2142
- tabIndex: 0,
2143
- children: /* @__PURE__ */ jsx("div", {
2144
- style: {
2145
- height: `${virtualizer.getTotalSize()}px`,
2146
- width: "100%",
2147
- position: "relative"
2148
- },
2149
- children: virtualizer.getVirtualItems().map((virtualItem) => {
2150
- const item = flattenedSpans[virtualItem.index];
2151
- if (!item) return null;
3151
+ /* @__PURE__ */ jsx(TraceHeader, {
3152
+ trace: parsedTrace,
3153
+ services,
3154
+ onHeaderToggle: () => setHeaderCollapsed((p) => !p),
3155
+ isCollapsed: headerCollapsed
3156
+ }),
3157
+ /* @__PURE__ */ jsx(ViewTabs, {
3158
+ activeView,
3159
+ onChange: handleViewChange
3160
+ }),
3161
+ /* @__PURE__ */ jsx(SpanSearch, {
3162
+ value: uiFind,
3163
+ onChange: handleUiFindChange,
3164
+ matchCount: matchingIndices.length,
3165
+ currentMatch: currentMatchIndex,
3166
+ onPrev: handleSearchPrev,
3167
+ onNext: handleSearchNext
3168
+ }),
3169
+ activeView === "graph" ? /* @__PURE__ */ jsx(GraphView, { trace: parsedTrace }) : activeView === "statistics" ? /* @__PURE__ */ jsx(StatisticsView, { trace: parsedTrace }) : activeView === "flamegraph" ? /* @__PURE__ */ jsx(FlamegraphView, {
3170
+ trace: parsedTrace,
3171
+ onSpanClick: handleSpanClick,
3172
+ selectedSpanId: selectedSpanId ?? void 0
3173
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3174
+ /* @__PURE__ */ jsx(Minimap, {
3175
+ trace: parsedTrace,
3176
+ viewStart,
3177
+ viewEnd,
3178
+ onViewChange: handleViewRangeChange
3179
+ }),
3180
+ /* @__PURE__ */ jsx(TimeRuler, {
3181
+ totalDurationMs: totalDurationMs * (viewEnd - viewStart),
3182
+ leftColumnWidth: "24rem",
3183
+ offsetMs: totalDurationMs * viewStart
3184
+ }),
3185
+ /* @__PURE__ */ jsx("div", {
3186
+ ref: scrollRef,
3187
+ className: "flex-1 overflow-auto outline-none",
3188
+ role: "tree",
3189
+ "aria-label": "Trace timeline",
3190
+ tabIndex: 0,
3191
+ children: flattenedSpans.map((item) => {
2152
3192
  const { span, level } = item;
2153
3193
  const isCollapsed = collapsedIds.has(span.spanId);
2154
3194
  const isSelected = span.spanId === selectedSpanId;
2155
3195
  const isHovered = span.spanId === hoveredSpanId;
2156
- const isParentOfHovered = hoveredSpanId ? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans) : false;
2157
- const relativeStart = calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs);
2158
- const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs);
2159
- return /* @__PURE__ */ jsx("div", {
2160
- style: {
2161
- position: "absolute",
2162
- top: 0,
2163
- left: 0,
2164
- width: "100%",
2165
- height: `${virtualItem.size}px`,
2166
- transform: `translateY(${virtualItem.start}px)`
2167
- },
2168
- children: /* @__PURE__ */ jsx(SpanRow, {
3196
+ const isParentOfHovered = hoveredSpanId ? isSpanAncestorOf(span, hoveredSpanId, flattenedSpans) : false;
3197
+ const viewRange = viewEnd - viewStart;
3198
+ const relativeStart = (calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs) - viewStart) / viewRange;
3199
+ const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs) / viewRange;
3200
+ return /* @__PURE__ */ jsxs("div", {
3201
+ "data-span-id": span.spanId,
3202
+ children: [/* @__PURE__ */ jsx(SpanRow, {
2169
3203
  span,
2170
3204
  level,
2171
3205
  isCollapsed,
@@ -2177,34 +3211,30 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2177
3211
  onClick: () => handleSpanClick(span),
2178
3212
  onToggleCollapse: () => handleToggleCollapse(span.spanId),
2179
3213
  onMouseEnter: () => setHoveredSpanId(span.spanId),
2180
- onMouseLeave: () => setHoveredSpanId(null)
2181
- })
3214
+ onMouseLeave: () => setHoveredSpanId(null),
3215
+ uiFind: uiFind || void 0
3216
+ }), isSelected && /* @__PURE__ */ jsx(SpanDetailInline, {
3217
+ span,
3218
+ traceStartMs: parsedTrace.minTimeMs
3219
+ })]
2182
3220
  }, span.spanId);
2183
3221
  })
2184
3222
  })
2185
- })
3223
+ ] })
2186
3224
  ]
2187
- }), selectedSpan && /* @__PURE__ */ jsx("div", {
2188
- className: "w-96 h-full flex-shrink-0",
2189
- children: /* @__PURE__ */ jsx(DetailPane, {
2190
- span: selectedSpan,
2191
- onClose: handleDeselect,
2192
- onLinkClick: void 0
2193
- })
2194
- })]
3225
+ })
2195
3226
  });
2196
3227
  }
2197
-
2198
3228
  //#endregion
2199
3229
  //#region src/components/observability/TraceDetail/index.tsx
2200
- function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onBack }) {
3230
+ function TraceDetail({ traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onSpanDeselect, onBack }) {
2201
3231
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
2202
3232
  className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
2203
3233
  children: [
2204
- /* @__PURE__ */ jsxs("button", {
3234
+ /* @__PURE__ */ jsx("button", {
2205
3235
  onClick: onBack,
2206
3236
  className: "hover:text-foreground transition-colors",
2207
- children: ["Services / ", service]
3237
+ children: "Traces"
2208
3238
  }),
2209
3239
  /* @__PURE__ */ jsx("span", { children: "/" }),
2210
3240
  /* @__PURE__ */ jsxs("span", {
@@ -2217,10 +3247,10 @@ function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId,
2217
3247
  isLoading,
2218
3248
  error,
2219
3249
  selectedSpanId,
2220
- onSpanClick
3250
+ onSpanClick,
3251
+ onSpanDeselect
2221
3252
  })] });
2222
3253
  }
2223
-
2224
3254
  //#endregion
2225
3255
  //#region src/components/observability/LogTimeline/LogRow.tsx
2226
3256
  function formatTimestamp(timeMs) {
@@ -2346,7 +3376,6 @@ const LogRow = memo(function LogRow({ log, isSelected, onClick, searchText, rela
2346
3376
  ]
2347
3377
  });
2348
3378
  });
2349
-
2350
3379
  //#endregion
2351
3380
  //#region src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx
2352
3381
  function AttributesTab({ log }) {
@@ -2379,7 +3408,6 @@ function AttributesTab({ log }) {
2379
3408
  })
2380
3409
  });
2381
3410
  }
2382
-
2383
3411
  //#endregion
2384
3412
  //#region src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx
2385
3413
  function JsonTreeView({ data, level = 0 }) {
@@ -2467,7 +3495,6 @@ function formatPrimitiveValue(value) {
2467
3495
  if (typeof value === "number") return String(value);
2468
3496
  return String(value);
2469
3497
  }
2470
-
2471
3498
  //#endregion
2472
3499
  //#region src/components/observability/LogTimeline/LogDetailPane/index.tsx
2473
3500
  function LogDetailPane({ log, onClose, onTraceLinkClick, initialTab = "message", wordWrap = true }) {
@@ -2679,7 +3706,6 @@ function getSeverityColor(severity) {
2679
3706
  bg: "bg-gray-50 dark:bg-gray-800/20"
2680
3707
  };
2681
3708
  }
2682
-
2683
3709
  //#endregion
2684
3710
  //#region src/components/observability/LogTimeline/shortcuts.ts
2685
3711
  const LOG_VIEWER_SHORTCUTS = {
@@ -2731,7 +3757,6 @@ const LOG_VIEWER_SHORTCUTS = {
2731
3757
  }
2732
3758
  ]
2733
3759
  };
2734
-
2735
3760
  //#endregion
2736
3761
  //#region src/components/observability/LogTimeline/index.tsx
2737
3762
  /**
@@ -2937,7 +3962,7 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
2937
3962
  useEffect(() => {
2938
3963
  const handleKeyDown = (e) => {
2939
3964
  const isFormField = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement;
2940
- if (isFormField && e.key === "Escape") {
3965
+ if (isFormField && e.key === "Escape" && e.target instanceof HTMLElement) {
2941
3966
  e.target.blur();
2942
3967
  return;
2943
3968
  }
@@ -3178,7 +4203,6 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
3178
4203
  })]
3179
4204
  });
3180
4205
  }
3181
-
3182
4206
  //#endregion
3183
4207
  //#region src/components/observability/LogTimeline/LogFilter.tsx
3184
4208
  /**
@@ -3279,7 +4303,7 @@ function MultiSelect({ options, selected, onChange, testId }) {
3279
4303
  useEffect(() => {
3280
4304
  if (!dropOpen) return;
3281
4305
  const handler = (e) => {
3282
- if (ref.current && !ref.current.contains(e.target)) setDropOpen(false);
4306
+ if (ref.current && e.target instanceof Node && !ref.current.contains(e.target)) setDropOpen(false);
3283
4307
  };
3284
4308
  document.addEventListener("mousedown", handler);
3285
4309
  return () => document.removeEventListener("mousedown", handler);
@@ -3730,7 +4754,6 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
3730
4754
  })]
3731
4755
  });
3732
4756
  }
3733
-
3734
4757
  //#endregion
3735
4758
  //#region src/components/observability/utils/lttb.ts
3736
4759
  function triangleArea(p1, p2, p3) {
@@ -3796,7 +4819,27 @@ function downsampleLTTB(data, targetPoints) {
3796
4819
  if (lastPoint) sampled.push(lastPoint);
3797
4820
  return sampled;
3798
4821
  }
3799
-
4822
+ //#endregion
4823
+ //#region src/components/observability/shared/TooltipEntryList.tsx
4824
+ function TooltipEntryList({ payload, displayLabelMap, formatValue }) {
4825
+ return payload.map((entry, i) => {
4826
+ const dataKey = entry.dataKey;
4827
+ const value = entry.value;
4828
+ if (typeof dataKey !== "string" || typeof value !== "number") return null;
4829
+ return /* @__PURE__ */ jsxs("p", {
4830
+ className: "text-sm",
4831
+ style: { color: entry.color },
4832
+ children: [
4833
+ /* @__PURE__ */ jsxs("span", {
4834
+ className: "font-medium",
4835
+ children: [displayLabelMap.get(dataKey) ?? dataKey, ":"]
4836
+ }),
4837
+ " ",
4838
+ formatValue(value)
4839
+ ]
4840
+ }, i);
4841
+ });
4842
+ }
3800
4843
  //#endregion
3801
4844
  //#region src/components/observability/utils/units.ts
3802
4845
  const BYTE_SCALES = [
@@ -3971,7 +5014,6 @@ function formatDisplayValue(value, scale) {
3971
5014
  function formatOtelValue(value, unit) {
3972
5015
  return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
3973
5016
  }
3974
-
3975
5017
  //#endregion
3976
5018
  //#region src/components/observability/MetricTimeSeries/index.tsx
3977
5019
  /**
@@ -4280,18 +5322,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
4280
5322
  children: [/* @__PURE__ */ jsx("p", {
4281
5323
  className: "text-gray-400 text-xs mb-2",
4282
5324
  children: formatTime(typeof label === "number" ? label : Number(label))
4283
- }), payload.map((entry, i) => /* @__PURE__ */ jsxs("p", {
4284
- className: "text-sm",
4285
- style: { color: entry.color },
4286
- children: [
4287
- /* @__PURE__ */ jsxs("span", {
4288
- className: "font-medium",
4289
- children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
4290
- }),
4291
- " ",
4292
- formatValue(entry.value)
4293
- ]
4294
- }, i))]
5325
+ }), /* @__PURE__ */ jsx(TooltipEntryList, {
5326
+ payload,
5327
+ displayLabelMap,
5328
+ formatValue
5329
+ })]
4295
5330
  });
4296
5331
  }
4297
5332
  function MetricLoadingSkeleton({ height = 400 }) {
@@ -4338,7 +5373,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
4338
5373
  })
4339
5374
  });
4340
5375
  }
4341
-
4342
5376
  //#endregion
4343
5377
  //#region src/components/observability/MetricHistogram/index.tsx
4344
5378
  /**
@@ -4352,6 +5386,9 @@ const COLORS = [
4352
5386
  "#00C49F",
4353
5387
  "#0088FE"
4354
5388
  ];
5389
+ function isBucketData(v) {
5390
+ return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
5391
+ }
4355
5392
  const defaultFormatBucketLabel = (bound, index, bounds) => {
4356
5393
  if (index === 0) return `≤${bound}`;
4357
5394
  if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
@@ -4394,7 +5431,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
4394
5431
  };
4395
5432
  buckets.push(bucket);
4396
5433
  }
4397
- bucket[seriesName] = (bucket[seriesName] ?? 0) + count;
5434
+ const prev = bucket[seriesName];
5435
+ bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
4398
5436
  }
4399
5437
  }
4400
5438
  buckets.sort((a, b) => a.lowerBound - b.lowerBound);
@@ -4531,25 +5569,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
4531
5569
  }
4532
5570
  function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
4533
5571
  if (!active || !payload?.length) return null;
4534
- const bucket = payload[0]?.payload;
4535
- if (!bucket) return null;
5572
+ const raw = payload[0]?.payload;
5573
+ if (!isBucketData(raw)) return null;
4536
5574
  return /* @__PURE__ */ jsxs("div", {
4537
5575
  className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
4538
5576
  children: [/* @__PURE__ */ jsxs("p", {
4539
5577
  className: "text-gray-300 text-sm font-medium mb-2",
4540
- children: ["Bucket: ", boundsScale ? `${formatDisplayValue(bucket.lowerBound, boundsScale)} – ${bucket.upperBound === Infinity ? "∞" : formatDisplayValue(bucket.upperBound, boundsScale)}` : bucket.bucket]
4541
- }), payload.map((entry, i) => /* @__PURE__ */ jsxs("p", {
4542
- className: "text-sm",
4543
- style: { color: entry.color },
4544
- children: [
4545
- /* @__PURE__ */ jsxs("span", {
4546
- className: "font-medium",
4547
- children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
4548
- }),
4549
- " ",
4550
- formatValue(entry.value)
4551
- ]
4552
- }, i))]
5578
+ children: ["Bucket: ", boundsScale ? `${formatDisplayValue(raw.lowerBound, boundsScale)} – ${raw.upperBound === Infinity ? "∞" : formatDisplayValue(raw.upperBound, boundsScale)}` : raw.bucket]
5579
+ }), /* @__PURE__ */ jsx(TooltipEntryList, {
5580
+ payload,
5581
+ displayLabelMap,
5582
+ formatValue
5583
+ })]
4553
5584
  });
4554
5585
  }
4555
5586
  function HistogramLoadingSkeleton({ height = 400 }) {
@@ -4589,7 +5620,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
4589
5620
  })
4590
5621
  });
4591
5622
  }
4592
-
4593
5623
  //#endregion
4594
5624
  //#region src/components/observability/MetricStat/index.tsx
4595
5625
  /**
@@ -4677,8 +5707,11 @@ function buildStatData(rows) {
4677
5707
  metricName
4678
5708
  };
4679
5709
  }
4680
- 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 }) {
4681
- const { latestValue, unit, timestamp, dataPoints, metricName } = useMemo(() => buildStatData(rows), [rows]);
5710
+ 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 }) {
5711
+ const statData = useMemo(() => buildStatData(rows), [rows]);
5712
+ const latestValue = directValue ?? statData.latestValue;
5713
+ const unit = directUnit ?? statData.unit;
5714
+ const { timestamp, dataPoints, metricName } = statData;
4682
5715
  const sparklineData = useMemo(() => {
4683
5716
  if (!showSparkline || dataPoints.length === 0) return [];
4684
5717
  return dataPoints.slice(-sparklinePoints).map((dp) => ({ value: dp.value }));
@@ -4782,7 +5815,6 @@ function TrendIndicator({ direction, value }) {
4782
5815
  children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
4783
5816
  });
4784
5817
  }
4785
-
4786
5818
  //#endregion
4787
5819
  //#region src/components/observability/MetricTable/index.tsx
4788
5820
  /**
@@ -4922,7 +5954,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
4922
5954
  })]
4923
5955
  });
4924
5956
  }
4925
-
4926
5957
  //#endregion
4927
5958
  //#region src/lib/renderer.tsx
4928
5959
  /**
@@ -5027,7 +6058,6 @@ function Renderer({ tree, registry, fallback }) {
5027
6058
  fallback
5028
6059
  });
5029
6060
  }
5030
-
5031
6061
  //#endregion
5032
6062
  //#region src/lib/catalog.ts
5033
6063
  const dashboardCatalog = createCatalog({
@@ -5227,8 +6257,7 @@ const dashboardCatalog = createCatalog({
5227
6257
  }
5228
6258
  }
5229
6259
  });
5230
- const componentList = Object.keys(dashboardCatalog.components);
5231
-
6260
+ Object.keys(dashboardCatalog.components);
5232
6261
  //#endregion
5233
6262
  //#region src/components/dashboard/Badge/index.tsx
5234
6263
  function Badge({ element }) {
@@ -5252,7 +6281,6 @@ function Badge({ element }) {
5252
6281
  children: text
5253
6282
  });
5254
6283
  }
5255
-
5256
6284
  //#endregion
5257
6285
  //#region src/components/dashboard/Card/index.tsx
5258
6286
  function Card({ element, children }) {
@@ -5293,7 +6321,6 @@ function Card({ element, children }) {
5293
6321
  })]
5294
6322
  });
5295
6323
  }
5296
-
5297
6324
  //#endregion
5298
6325
  //#region src/components/dashboard/Divider/index.tsx
5299
6326
  function Divider({ element }) {
@@ -5331,7 +6358,6 @@ function Divider({ element }) {
5331
6358
  margin: "16px 0"
5332
6359
  } });
5333
6360
  }
5334
-
5335
6361
  //#endregion
5336
6362
  //#region src/components/dashboard/Empty/index.tsx
5337
6363
  function Empty({ element, onAction }) {
@@ -5374,7 +6400,6 @@ function Empty({ element, onAction }) {
5374
6400
  ]
5375
6401
  });
5376
6402
  }
5377
-
5378
6403
  //#endregion
5379
6404
  //#region src/components/dashboard/Grid/index.tsx
5380
6405
  function Grid({ element, children }) {
@@ -5392,7 +6417,6 @@ function Grid({ element, children }) {
5392
6417
  children
5393
6418
  });
5394
6419
  }
5395
-
5396
6420
  //#endregion
5397
6421
  //#region src/components/dashboard/Heading/index.tsx
5398
6422
  function Heading({ element }) {
@@ -5411,7 +6435,6 @@ function Heading({ element }) {
5411
6435
  children: text
5412
6436
  });
5413
6437
  }
5414
-
5415
6438
  //#endregion
5416
6439
  //#region src/components/dashboard/Stack/index.tsx
5417
6440
  function Stack({ element, children }) {
@@ -5435,7 +6458,6 @@ function Stack({ element, children }) {
5435
6458
  children
5436
6459
  });
5437
6460
  }
5438
-
5439
6461
  //#endregion
5440
6462
  //#region src/components/dashboard/Text/index.tsx
5441
6463
  function Text({ element }) {
@@ -5454,7 +6476,6 @@ function Text({ element }) {
5454
6476
  children: content
5455
6477
  });
5456
6478
  }
5457
-
5458
6479
  //#endregion
5459
6480
  //#region src/components/observability/renderers/OtelLogTimeline.tsx
5460
6481
  function OtelLogTimeline(props) {
@@ -5476,7 +6497,6 @@ function OtelLogTimeline(props) {
5476
6497
  })
5477
6498
  });
5478
6499
  }
5479
-
5480
6500
  //#endregion
5481
6501
  //#region src/components/observability/renderers/OtelMetricDiscovery.tsx
5482
6502
  const TYPE_ORDER = {
@@ -5554,7 +6574,6 @@ function OtelMetricDiscovery(props) {
5554
6574
  })
5555
6575
  });
5556
6576
  }
5557
-
5558
6577
  //#endregion
5559
6578
  //#region src/components/observability/renderers/OtelMetricHistogram.tsx
5560
6579
  function OtelMetricHistogram(props) {
@@ -5575,9 +6594,15 @@ function OtelMetricHistogram(props) {
5575
6594
  unit: props.element.props.unit ?? void 0
5576
6595
  });
5577
6596
  }
5578
-
5579
6597
  //#endregion
5580
6598
  //#region src/components/observability/renderers/OtelMetricStat.tsx
6599
+ const EMPTY_ROWS = [];
6600
+ const GROUPED_AGGREGATE_ERROR = /* @__PURE__ */ new Error("MetricStat cannot display grouped aggregates. Remove groupBy or use MetricTable.");
6601
+ function isAggregatedRequest(props) {
6602
+ const ds = props.element.dataSource;
6603
+ if (!ds || ds.method !== "searchMetricsPage" || !ds.params) return false;
6604
+ return !!ds.params.aggregate;
6605
+ }
5581
6606
  function OtelMetricStat(props) {
5582
6607
  if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5583
6608
  style: {
@@ -5586,6 +6611,24 @@ function OtelMetricStat(props) {
5586
6611
  },
5587
6612
  children: "No data source"
5588
6613
  });
6614
+ if (isAggregatedRequest(props)) {
6615
+ const rows = props.data?.data ?? [];
6616
+ if (rows.length > 1) return /* @__PURE__ */ jsx(MetricStat, {
6617
+ rows: EMPTY_ROWS,
6618
+ error: GROUPED_AGGREGATE_ERROR,
6619
+ label: props.element.props.label ?? void 0,
6620
+ formatValue: formatOtelValue
6621
+ });
6622
+ return /* @__PURE__ */ jsx(MetricStat, {
6623
+ rows: EMPTY_ROWS,
6624
+ value: rows[0]?.value,
6625
+ isLoading: props.loading,
6626
+ error: props.error ?? void 0,
6627
+ label: props.element.props.label ?? void 0,
6628
+ showSparkline: false,
6629
+ formatValue: formatOtelValue
6630
+ });
6631
+ }
5589
6632
  const response = props.data;
5590
6633
  return /* @__PURE__ */ jsx(MetricStat, {
5591
6634
  rows: response?.data ?? [],
@@ -5596,7 +6639,6 @@ function OtelMetricStat(props) {
5596
6639
  formatValue: formatOtelValue
5597
6640
  });
5598
6641
  }
5599
-
5600
6642
  //#endregion
5601
6643
  //#region src/components/observability/renderers/OtelMetricTable.tsx
5602
6644
  function OtelMetricTable(props) {
@@ -5615,7 +6657,6 @@ function OtelMetricTable(props) {
5615
6657
  maxRows: props.element.props.maxRows ?? 100
5616
6658
  });
5617
6659
  }
5618
-
5619
6660
  //#endregion
5620
6661
  //#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
5621
6662
  function OtelMetricTimeSeries(props) {
@@ -5637,7 +6678,6 @@ function OtelMetricTimeSeries(props) {
5637
6678
  unit: props.element.props.unit ?? void 0
5638
6679
  });
5639
6680
  }
5640
-
5641
6681
  //#endregion
5642
6682
  //#region src/components/observability/renderers/OtelTraceDetail.tsx
5643
6683
  function OtelTraceDetail(props) {
@@ -5649,19 +6689,15 @@ function OtelTraceDetail(props) {
5649
6689
  children: "No data source"
5650
6690
  });
5651
6691
  const rows = props.data?.data ?? [];
5652
- const firstRow = rows[0];
5653
- const service = firstRow?.ServiceName ?? "unknown";
5654
- const traceId = firstRow?.TraceId ?? "";
6692
+ const traceId = rows[0]?.TraceId ?? "";
5655
6693
  return /* @__PURE__ */ jsx(TraceDetail, {
5656
6694
  rows,
5657
6695
  isLoading: props.loading,
5658
6696
  error: props.error ?? void 0,
5659
- service,
5660
6697
  traceId,
5661
6698
  onBack: () => {}
5662
6699
  });
5663
6700
  }
5664
-
5665
6701
  //#endregion
5666
6702
  //#region src/components/observability/DynamicDashboard/index.tsx
5667
6703
  const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
@@ -5687,24 +6723,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
5687
6723
  children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
5688
6724
  });
5689
6725
  }
5690
-
6726
+ //#endregion
6727
+ //#region src/components/observability/TraceComparison/index.tsx
6728
+ function computeTraceStats(rows) {
6729
+ if (rows.length === 0) return {
6730
+ durationMs: 0,
6731
+ spanCount: 0
6732
+ };
6733
+ let minTs = Infinity;
6734
+ let maxEnd = -Infinity;
6735
+ for (const row of rows) {
6736
+ const startMs = parseInt(row.Timestamp, 10) / 1e6;
6737
+ const endMs = startMs + (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
6738
+ minTs = Math.min(minTs, startMs);
6739
+ maxEnd = Math.max(maxEnd, endMs);
6740
+ }
6741
+ return {
6742
+ durationMs: maxEnd - minTs,
6743
+ spanCount: rows.length
6744
+ };
6745
+ }
6746
+ function collectSignatures(rows) {
6747
+ const map = /* @__PURE__ */ new Map();
6748
+ for (const row of rows) {
6749
+ const key = `${row.ServiceName ?? "unknown"}::${row.SpanName ?? ""}`;
6750
+ const durMs = (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
6751
+ const existing = map.get(key);
6752
+ if (existing) {
6753
+ existing.count++;
6754
+ existing.totalDurationMs += durMs;
6755
+ } else map.set(key, {
6756
+ count: 1,
6757
+ totalDurationMs: durMs
6758
+ });
6759
+ }
6760
+ return map;
6761
+ }
6762
+ function computeDiff(rowsA, rowsB) {
6763
+ const sigA = collectSignatures(rowsA);
6764
+ const sigB = collectSignatures(rowsB);
6765
+ const allKeys = new Set([...sigA.keys(), ...sigB.keys()]);
6766
+ const result = [];
6767
+ for (const key of allKeys) {
6768
+ const [serviceName = "unknown", spanName = ""] = key.split("::");
6769
+ const a = sigA.get(key);
6770
+ const b = sigB.get(key);
6771
+ const countA = a?.count ?? 0;
6772
+ const countB = b?.count ?? 0;
6773
+ const avgA = a ? a.totalDurationMs / a.count : 0;
6774
+ const avgB = b ? b.totalDurationMs / b.count : 0;
6775
+ result.push({
6776
+ serviceName,
6777
+ spanName,
6778
+ countA,
6779
+ countB,
6780
+ avgDurationA: avgA,
6781
+ avgDurationB: avgB,
6782
+ deltaMs: avgB - avgA
6783
+ });
6784
+ }
6785
+ return result.sort((a, b) => {
6786
+ const aShared = a.countA > 0 && a.countB > 0;
6787
+ if (aShared !== (b.countA > 0 && b.countB > 0)) return aShared ? 1 : -1;
6788
+ return Math.abs(b.deltaMs) - Math.abs(a.deltaMs);
6789
+ });
6790
+ }
6791
+ function formatDelta(ms) {
6792
+ return `${ms > 0 ? "+" : ""}${formatDuration(ms)}`;
6793
+ }
6794
+ function TraceComparison({ traceIdA, traceIdB, onBack }) {
6795
+ const dsA = useMemo(() => ({
6796
+ method: "getTrace",
6797
+ params: { traceId: traceIdA }
6798
+ }), [traceIdA]);
6799
+ const dsB = useMemo(() => ({
6800
+ method: "getTrace",
6801
+ params: { traceId: traceIdB }
6802
+ }), [traceIdB]);
6803
+ const { data: rowsA, loading: loadingA, error: errorA } = useKopaiData(dsA);
6804
+ const { data: rowsB, loading: loadingB, error: errorB } = useKopaiData(dsB);
6805
+ const statsA = useMemo(() => computeTraceStats(rowsA ?? []), [rowsA]);
6806
+ const statsB = useMemo(() => computeTraceStats(rowsB ?? []), [rowsB]);
6807
+ const diff = useMemo(() => computeDiff(rowsA ?? [], rowsB ?? []), [rowsA, rowsB]);
6808
+ const durationDelta = statsB.durationMs - statsA.durationMs;
6809
+ const spanDelta = statsB.spanCount - statsA.spanCount;
6810
+ const isLoading = loadingA || loadingB;
6811
+ return /* @__PURE__ */ jsxs("div", {
6812
+ className: "flex flex-col gap-4",
6813
+ children: [
6814
+ /* @__PURE__ */ jsxs("div", {
6815
+ className: "flex items-center justify-between bg-background border border-border rounded-lg p-4",
6816
+ children: [/* @__PURE__ */ jsxs("div", {
6817
+ className: "flex items-center gap-4",
6818
+ children: [/* @__PURE__ */ jsx("button", {
6819
+ onClick: onBack,
6820
+ className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
6821
+ children: "← Back"
6822
+ }), /* @__PURE__ */ jsxs("div", {
6823
+ className: "flex items-center gap-6 text-sm",
6824
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6825
+ className: "text-muted-foreground mr-1",
6826
+ children: "A:"
6827
+ }), /* @__PURE__ */ jsxs("span", {
6828
+ className: "font-mono text-xs text-foreground",
6829
+ children: [traceIdA.slice(0, 16), "..."]
6830
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6831
+ className: "text-muted-foreground mr-1",
6832
+ children: "B:"
6833
+ }), /* @__PURE__ */ jsxs("span", {
6834
+ className: "font-mono text-xs text-foreground",
6835
+ children: [traceIdB.slice(0, 16), "..."]
6836
+ })] })]
6837
+ })]
6838
+ }), !isLoading && /* @__PURE__ */ jsxs("div", {
6839
+ className: "flex items-center gap-6 text-sm",
6840
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6841
+ className: "text-muted-foreground mr-1",
6842
+ children: "Duration delta:"
6843
+ }), /* @__PURE__ */ jsx("span", {
6844
+ className: durationDelta > 0 ? "text-red-400" : durationDelta < 0 ? "text-green-400" : "text-foreground",
6845
+ children: formatDelta(durationDelta)
6846
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6847
+ className: "text-muted-foreground mr-1",
6848
+ children: "Span count delta:"
6849
+ }), /* @__PURE__ */ jsx("span", {
6850
+ className: spanDelta > 0 ? "text-red-400" : spanDelta < 0 ? "text-green-400" : "text-foreground",
6851
+ children: spanDelta > 0 ? `+${spanDelta}` : String(spanDelta)
6852
+ })] })]
6853
+ })]
6854
+ }),
6855
+ /* @__PURE__ */ jsxs("div", {
6856
+ className: "grid grid-cols-2 gap-4",
6857
+ style: { height: "50vh" },
6858
+ children: [/* @__PURE__ */ jsx("div", {
6859
+ className: "border border-border rounded-lg overflow-hidden",
6860
+ children: /* @__PURE__ */ jsx(TraceTimeline, {
6861
+ rows: rowsA ?? [],
6862
+ isLoading: loadingA,
6863
+ error: errorA ?? void 0
6864
+ })
6865
+ }), /* @__PURE__ */ jsx("div", {
6866
+ className: "border border-border rounded-lg overflow-hidden",
6867
+ children: /* @__PURE__ */ jsx(TraceTimeline, {
6868
+ rows: rowsB ?? [],
6869
+ isLoading: loadingB,
6870
+ error: errorB ?? void 0
6871
+ })
6872
+ })]
6873
+ }),
6874
+ !isLoading && diff.length > 0 && /* @__PURE__ */ jsxs("div", {
6875
+ className: "border border-border rounded-lg overflow-hidden",
6876
+ children: [/* @__PURE__ */ jsx("div", {
6877
+ className: "px-4 py-3 border-b border-border bg-background",
6878
+ children: /* @__PURE__ */ jsx("h3", {
6879
+ className: "text-sm font-medium text-foreground",
6880
+ children: "Structural Diff"
6881
+ })
6882
+ }), /* @__PURE__ */ jsx("div", {
6883
+ className: "overflow-x-auto",
6884
+ children: /* @__PURE__ */ jsxs("table", {
6885
+ className: "w-full text-sm",
6886
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
6887
+ className: "border-b border-border bg-muted/30",
6888
+ children: [
6889
+ /* @__PURE__ */ jsx("th", {
6890
+ className: "text-left px-4 py-2 text-muted-foreground font-medium",
6891
+ children: "Service"
6892
+ }),
6893
+ /* @__PURE__ */ jsx("th", {
6894
+ className: "text-left px-4 py-2 text-muted-foreground font-medium",
6895
+ children: "Span"
6896
+ }),
6897
+ /* @__PURE__ */ jsx("th", {
6898
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6899
+ children: "Count A"
6900
+ }),
6901
+ /* @__PURE__ */ jsx("th", {
6902
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6903
+ children: "Count B"
6904
+ }),
6905
+ /* @__PURE__ */ jsx("th", {
6906
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6907
+ children: "Avg Dur A"
6908
+ }),
6909
+ /* @__PURE__ */ jsx("th", {
6910
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6911
+ children: "Avg Dur B"
6912
+ }),
6913
+ /* @__PURE__ */ jsx("th", {
6914
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6915
+ children: "Delta"
6916
+ })
6917
+ ]
6918
+ }) }), /* @__PURE__ */ jsx("tbody", { children: diff.map((row) => {
6919
+ const onlyA = row.countA > 0 && row.countB === 0;
6920
+ const onlyB = row.countA === 0 && row.countB > 0;
6921
+ return /* @__PURE__ */ jsxs("tr", {
6922
+ className: `border-b border-border/50 ${onlyA ? "bg-red-500/5" : onlyB ? "bg-green-500/5" : ""}`,
6923
+ children: [
6924
+ /* @__PURE__ */ jsx("td", {
6925
+ className: "px-4 py-1.5 text-foreground",
6926
+ children: row.serviceName
6927
+ }),
6928
+ /* @__PURE__ */ jsx("td", {
6929
+ className: "px-4 py-1.5 font-mono text-xs text-foreground",
6930
+ children: row.spanName
6931
+ }),
6932
+ /* @__PURE__ */ jsx("td", {
6933
+ className: "px-4 py-1.5 text-right text-foreground",
6934
+ children: row.countA || /* @__PURE__ */ jsx("span", {
6935
+ className: "text-muted-foreground",
6936
+ children: "-"
6937
+ })
6938
+ }),
6939
+ /* @__PURE__ */ jsx("td", {
6940
+ className: "px-4 py-1.5 text-right text-foreground",
6941
+ children: row.countB || /* @__PURE__ */ jsx("span", {
6942
+ className: "text-muted-foreground",
6943
+ children: "-"
6944
+ })
6945
+ }),
6946
+ /* @__PURE__ */ jsx("td", {
6947
+ className: "px-4 py-1.5 text-right text-foreground",
6948
+ children: row.countA > 0 ? formatDuration(row.avgDurationA) : /* @__PURE__ */ jsx("span", {
6949
+ className: "text-muted-foreground",
6950
+ children: "-"
6951
+ })
6952
+ }),
6953
+ /* @__PURE__ */ jsx("td", {
6954
+ className: "px-4 py-1.5 text-right text-foreground",
6955
+ children: row.countB > 0 ? formatDuration(row.avgDurationB) : /* @__PURE__ */ jsx("span", {
6956
+ className: "text-muted-foreground",
6957
+ children: "-"
6958
+ })
6959
+ }),
6960
+ /* @__PURE__ */ jsx("td", {
6961
+ className: "px-4 py-1.5 text-right",
6962
+ children: row.countA > 0 && row.countB > 0 ? /* @__PURE__ */ jsx("span", {
6963
+ className: row.deltaMs > 0 ? "text-red-400" : row.deltaMs < 0 ? "text-green-400" : "text-foreground",
6964
+ children: formatDelta(row.deltaMs)
6965
+ }) : /* @__PURE__ */ jsx("span", {
6966
+ className: onlyA ? "text-red-400" : "text-green-400",
6967
+ children: onlyA ? "removed" : "added"
6968
+ })
6969
+ })
6970
+ ]
6971
+ }, `${row.serviceName}::${row.spanName}`);
6972
+ }) })]
6973
+ })
6974
+ })]
6975
+ })
6976
+ ]
6977
+ });
6978
+ }
5691
6979
  //#endregion
5692
6980
  //#region src/components/observability/ServiceList/shortcuts.ts
5693
6981
  const SERVICES_SHORTCUTS = {
5694
- name: "Services",
6982
+ name: "Traces",
5695
6983
  shortcuts: [{
5696
6984
  keys: ["Backspace"],
5697
6985
  description: "Go back"
5698
6986
  }]
5699
6987
  };
5700
-
5701
6988
  //#endregion
5702
6989
  //#region src/pages/observability.tsx
5703
6990
  const TABS = [
5704
6991
  {
5705
6992
  key: "services",
5706
- label: "Services",
5707
- shortcutKey: "S"
6993
+ label: "Traces",
6994
+ shortcutKey: "T"
5708
6995
  },
5709
6996
  {
5710
6997
  key: "logs",
@@ -5724,20 +7011,53 @@ function readURLState() {
5724
7011
  const span = params.get("span");
5725
7012
  const dashboardId = params.get("dashboardId");
5726
7013
  const rawTab = params.get("tab");
7014
+ const tab = service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services";
7015
+ const rawLimit = params.get("limit");
7016
+ const limit = rawLimit ? parseInt(rawLimit, 10) : null;
5727
7017
  return {
5728
- tab: service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services",
7018
+ tab,
5729
7019
  service,
7020
+ operation: params.get("operation"),
7021
+ tags: params.get("tags"),
7022
+ lookback: params.get("lookback"),
7023
+ tsMin: params.get("tsMin"),
7024
+ tsMax: params.get("tsMax"),
7025
+ minDuration: params.get("minDuration"),
7026
+ maxDuration: params.get("maxDuration"),
7027
+ limit: limit !== null && !isNaN(limit) ? limit : null,
7028
+ sort: params.get("sort"),
5730
7029
  trace,
5731
7030
  span,
7031
+ view: params.get("view"),
7032
+ uiFind: params.get("uiFind"),
7033
+ compare: params.get("compare"),
7034
+ viewStart: params.get("viewStart"),
7035
+ viewEnd: params.get("viewEnd"),
5732
7036
  dashboardId
5733
7037
  };
5734
7038
  }
5735
7039
  function pushURLState(state, { replace = false } = {}) {
5736
7040
  const params = new URLSearchParams();
5737
7041
  if (state.tab !== "services") params.set("tab", state.tab);
5738
- if (state.service) params.set("service", state.service);
5739
- if (state.trace) params.set("trace", state.trace);
5740
- if (state.span) params.set("span", state.span);
7042
+ if (state.tab === "services") {
7043
+ if (state.service) params.set("service", state.service);
7044
+ if (state.operation) params.set("operation", state.operation);
7045
+ if (state.tags) params.set("tags", state.tags);
7046
+ if (state.lookback) params.set("lookback", state.lookback);
7047
+ if (state.tsMin) params.set("tsMin", state.tsMin);
7048
+ if (state.tsMax) params.set("tsMax", state.tsMax);
7049
+ if (state.minDuration) params.set("minDuration", state.minDuration);
7050
+ if (state.maxDuration) params.set("maxDuration", state.maxDuration);
7051
+ if (state.limit != null && state.limit !== 20) params.set("limit", String(state.limit));
7052
+ if (state.sort) params.set("sort", state.sort);
7053
+ if (state.trace) params.set("trace", state.trace);
7054
+ if (state.span) params.set("span", state.span);
7055
+ if (state.view) params.set("view", state.view);
7056
+ if (state.uiFind) params.set("uiFind", state.uiFind);
7057
+ if (state.compare) params.set("compare", state.compare);
7058
+ if (state.viewStart) params.set("viewStart", state.viewStart);
7059
+ if (state.viewEnd) params.set("viewEnd", state.viewEnd);
7060
+ }
5741
7061
  const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
5742
7062
  if (dashboardId) params.set("dashboardId", dashboardId);
5743
7063
  const qs = params.toString();
@@ -5754,8 +7074,22 @@ let _cachedSearch = "";
5754
7074
  let _cachedState = {
5755
7075
  tab: "services",
5756
7076
  service: null,
7077
+ operation: null,
7078
+ tags: null,
7079
+ lookback: null,
7080
+ tsMin: null,
7081
+ tsMax: null,
7082
+ minDuration: null,
7083
+ maxDuration: null,
7084
+ limit: null,
7085
+ sort: null,
5757
7086
  trace: null,
5758
7087
  span: null,
7088
+ view: null,
7089
+ uiFind: null,
7090
+ compare: null,
7091
+ viewStart: null,
7092
+ viewEnd: null,
5759
7093
  dashboardId: null
5760
7094
  };
5761
7095
  function getURLSnapshot() {
@@ -5865,6 +7199,26 @@ function parseDuration(input) {
5865
7199
  s: 1e9
5866
7200
  }[unit]));
5867
7201
  }
7202
+ function parseLogfmt(str) {
7203
+ const result = {};
7204
+ const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
7205
+ let m;
7206
+ while ((m = re.exec(str)) !== null) {
7207
+ const key = m[1];
7208
+ if (key) result[key] = m[2] ?? m[3] ?? "";
7209
+ }
7210
+ return result;
7211
+ }
7212
+ const LOOKBACK_MS = {
7213
+ "5m": 5 * 6e4,
7214
+ "15m": 15 * 6e4,
7215
+ "30m": 30 * 6e4,
7216
+ "1h": 60 * 6e4,
7217
+ "2h": 120 * 6e4,
7218
+ "6h": 360 * 6e4,
7219
+ "12h": 720 * 6e4,
7220
+ "24h": 1440 * 6e4
7221
+ };
5868
7222
  function LogsTab() {
5869
7223
  const [initState] = useState(() => readLogFilters());
5870
7224
  const [filters, setFilters] = useState(initState.filters);
@@ -5924,185 +7278,145 @@ function LogsTab() {
5924
7278
  })]
5925
7279
  });
5926
7280
  }
5927
- const SERVICES_DS = {
5928
- method: "searchTracesPage",
5929
- params: {
5930
- limit: 1e3,
5931
- sortOrder: "DESC"
5932
- }
5933
- };
5934
- function ServiceListView({ onSelect }) {
5935
- const { data, loading, error } = useKopaiData(SERVICES_DS);
5936
- return /* @__PURE__ */ jsx(ServiceList, {
5937
- services: useMemo(() => {
5938
- if (!data?.data) return [];
5939
- const names = /* @__PURE__ */ new Set();
5940
- for (const row of data.data) names.add(row.ServiceName ?? "unknown");
5941
- return Array.from(names).sort().map((name) => ({ name }));
5942
- }, [data]),
5943
- isLoading: loading,
5944
- error: error ?? void 0,
5945
- onSelect
5946
- });
5947
- }
5948
- function TraceSearchView({ service, onBack, onSelectTrace }) {
5949
- const [ds, setDs] = useState(() => ({
5950
- method: "searchTracesPage",
5951
- params: {
5952
- serviceName: service,
5953
- limit: 20,
5954
- sortOrder: "DESC"
5955
- }
5956
- }));
5957
- const handleSearch = useCallback((filters) => {
7281
+ function TraceSearchView({ onSelectTrace, onCompare }) {
7282
+ const urlState = useURLState();
7283
+ const service = urlState.service;
7284
+ const ds = useMemo(() => {
5958
7285
  const params = {
5959
- serviceName: service,
5960
- limit: filters.limit,
7286
+ limit: urlState.limit ?? 20,
5961
7287
  sortOrder: "DESC"
5962
7288
  };
5963
- if (filters.operation) params.spanName = filters.operation;
5964
- if (filters.lookbackMs) params.timestampMin = String((Date.now() - filters.lookbackMs) * 1e6);
5965
- if (filters.minDuration) {
5966
- const parsed = parseDuration(filters.minDuration);
7289
+ if (service) params.serviceName = service;
7290
+ if (urlState.operation) params.spanName = urlState.operation;
7291
+ if (urlState.lookback) {
7292
+ const ms = LOOKBACK_MS[urlState.lookback];
7293
+ if (ms) params.timestampMin = String((Date.now() - ms) * 1e6);
7294
+ }
7295
+ if (urlState.tsMin) params.timestampMin = urlState.tsMin;
7296
+ if (urlState.tsMax) params.timestampMax = urlState.tsMax;
7297
+ if (urlState.minDuration) {
7298
+ const parsed = parseDuration(urlState.minDuration);
5967
7299
  if (parsed) params.durationMin = parsed;
5968
7300
  }
5969
- if (filters.maxDuration) {
5970
- const parsed = parseDuration(filters.maxDuration);
7301
+ if (urlState.maxDuration) {
7302
+ const parsed = parseDuration(urlState.maxDuration);
5971
7303
  if (parsed) params.durationMax = parsed;
5972
7304
  }
5973
- setDs({
5974
- method: "searchTracesPage",
7305
+ if (urlState.tags) {
7306
+ const tagMap = parseLogfmt(urlState.tags);
7307
+ if (Object.keys(tagMap).length > 0) params.tags = tagMap;
7308
+ }
7309
+ return {
7310
+ method: "searchTraceSummariesPage",
5975
7311
  params
7312
+ };
7313
+ }, [
7314
+ service,
7315
+ urlState.operation,
7316
+ urlState.lookback,
7317
+ urlState.tsMin,
7318
+ urlState.tsMax,
7319
+ urlState.minDuration,
7320
+ urlState.maxDuration,
7321
+ urlState.limit,
7322
+ urlState.tags
7323
+ ]);
7324
+ const handleSearch = useCallback((filters) => {
7325
+ pushURLState({
7326
+ tab: "services",
7327
+ service: filters.service ?? service,
7328
+ operation: filters.operation ?? null,
7329
+ tags: filters.tags ?? null,
7330
+ lookback: filters.lookback ?? null,
7331
+ minDuration: filters.minDuration ?? null,
7332
+ maxDuration: filters.maxDuration ?? null,
7333
+ limit: filters.limit
5976
7334
  });
5977
7335
  }, [service]);
5978
7336
  const { data, loading, error } = useKopaiData(ds);
5979
- const client = useKopaiSDK();
5980
- const [fullTraces, setFullTraces] = useState(() => /* @__PURE__ */ new Map());
5981
- useEffect(() => {
5982
- if (!data?.data?.length) {
5983
- setFullTraces(/* @__PURE__ */ new Map());
5984
- return;
5985
- }
5986
- const traceIds = [...new Set(data.data.map((r) => r.TraceId))];
5987
- const ac = new AbortController();
5988
- Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
5989
- if (!ac.signal.aborted) {
5990
- const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
5991
- setFullTraces(new Map(entries));
5992
- }
5993
- }).catch((err) => {
5994
- if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
5995
- });
5996
- return () => ac.abort();
5997
- }, [data, client]);
5998
- const operations = useMemo(() => {
7337
+ const { data: servicesData } = useKopaiData(useMemo(() => ({ method: "getServices" }), []));
7338
+ const _services = servicesData?.services ?? [];
7339
+ const { data: opsData } = useKopaiData(useMemo(() => service ? {
7340
+ method: "getOperations",
7341
+ params: { serviceName: service }
7342
+ } : void 0, [service]));
7343
+ const operations = opsData?.operations ?? [];
7344
+ const traces = useMemo(() => {
5999
7345
  if (!data?.data) return [];
6000
- const set = /* @__PURE__ */ new Set();
6001
- for (const row of data.data) if (row.SpanName) set.add(row.SpanName);
6002
- return Array.from(set).sort();
7346
+ return data.data.map((row) => ({
7347
+ traceId: row.traceId,
7348
+ rootSpanName: row.rootSpanName,
7349
+ serviceName: row.rootServiceName,
7350
+ durationMs: parseInt(row.durationNs, 10) / 1e6,
7351
+ statusCode: row.errorCount > 0 ? "ERROR" : "OK",
7352
+ timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
7353
+ spanCount: row.spanCount,
7354
+ services: row.services,
7355
+ errorCount: row.errorCount
7356
+ }));
6003
7357
  }, [data]);
6004
7358
  return /* @__PURE__ */ jsx(TraceSearch, {
6005
- service,
6006
- traces: useMemo(() => {
6007
- if (!data?.data) return [];
6008
- const grouped = /* @__PURE__ */ new Map();
6009
- for (const row of data.data) {
6010
- const tid = row.TraceId;
6011
- if (!grouped.has(tid)) grouped.set(tid, []);
6012
- grouped.get(tid).push(row);
6013
- }
6014
- return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
6015
- const spans = fullTraces.get(traceId) ?? searchSpans;
6016
- const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
6017
- const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
6018
- const svcMap = /* @__PURE__ */ new Map();
6019
- let errorCount = 0;
6020
- for (const s of spans) {
6021
- const svcName = s.ServiceName ?? "unknown";
6022
- const entry = svcMap.get(svcName) ?? {
6023
- count: 0,
6024
- hasError: false
6025
- };
6026
- entry.count++;
6027
- if (s.StatusCode === "ERROR") {
6028
- entry.hasError = true;
6029
- errorCount++;
6030
- }
6031
- svcMap.set(svcName, entry);
6032
- }
6033
- const services = Array.from(svcMap.entries()).map(([name, v]) => ({
6034
- name,
6035
- count: v.count,
6036
- hasError: v.hasError
6037
- })).sort((a, b) => b.count - a.count);
6038
- return {
6039
- traceId,
6040
- rootSpanName: root.SpanName ?? "unknown",
6041
- serviceName: root.ServiceName ?? "unknown",
6042
- durationMs: durationNs / 1e6,
6043
- statusCode: root.StatusCode ?? "UNSET",
6044
- timestampMs: parseInt(root.Timestamp, 10) / 1e6,
6045
- spanCount: spans.length,
6046
- services,
6047
- errorCount
6048
- };
6049
- });
6050
- }, [data, fullTraces]),
7359
+ services: _services,
7360
+ service: service ?? "",
7361
+ traces,
6051
7362
  operations,
6052
7363
  isLoading: loading,
6053
7364
  error: error ?? void 0,
6054
7365
  onSelectTrace,
6055
- onBack,
7366
+ onCompare,
6056
7367
  onSearch: handleSearch
6057
7368
  });
6058
7369
  }
6059
- function TraceDetailView({ service, traceId, selectedSpanId, onSelectSpan, onBack }) {
7370
+ function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
6060
7371
  const { data, loading, error } = useKopaiData(useMemo(() => ({
6061
7372
  method: "getTrace",
6062
7373
  params: { traceId }
6063
7374
  }), [traceId]));
6064
7375
  return /* @__PURE__ */ jsx(TraceDetail, {
6065
- service,
6066
7376
  traceId,
6067
7377
  rows: data ?? [],
6068
7378
  isLoading: loading,
6069
7379
  error: error ?? void 0,
6070
7380
  selectedSpanId: selectedSpanId ?? void 0,
6071
7381
  onSpanClick: (span) => onSelectSpan(span.spanId),
7382
+ onSpanDeselect: onDeselectSpan,
6072
7383
  onBack
6073
7384
  });
6074
7385
  }
6075
- function ServicesTab({ selectedService, selectedTraceId, selectedSpanId, onSelectService, onSelectTrace, onSelectSpan, onBackToServices, onBackToTraceList }) {
7386
+ function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
6076
7387
  useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
6077
- const backToServicesRef = useRef(onBackToServices);
6078
- backToServicesRef.current = onBackToServices;
6079
- const backToTraceListRef = useRef(onBackToTraceList);
6080
- backToTraceListRef.current = onBackToTraceList;
7388
+ const backRef = useRef(onBack);
7389
+ backRef.current = onBack;
6081
7390
  useEffect(() => {
6082
7391
  const handleKeyDown = (e) => {
6083
7392
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
6084
7393
  if (e.key === "Backspace") {
6085
7394
  e.preventDefault();
6086
- if (selectedTraceId && selectedService) backToTraceListRef.current();
6087
- else if (selectedService) backToServicesRef.current();
7395
+ backRef.current();
6088
7396
  }
6089
7397
  };
6090
7398
  window.addEventListener("keydown", handleKeyDown);
6091
7399
  return () => window.removeEventListener("keydown", handleKeyDown);
6092
- }, [selectedService, selectedTraceId]);
6093
- if (selectedTraceId && selectedService) return /* @__PURE__ */ jsx(TraceDetailView, {
6094
- service: selectedService,
7400
+ }, []);
7401
+ if (compareParam) {
7402
+ const [traceIdA, traceIdB] = compareParam.split(",");
7403
+ if (traceIdA && traceIdB) return /* @__PURE__ */ jsx(TraceComparison, {
7404
+ traceIdA,
7405
+ traceIdB,
7406
+ onBack
7407
+ });
7408
+ }
7409
+ if (selectedTraceId) return /* @__PURE__ */ jsx(TraceDetailView, {
6095
7410
  traceId: selectedTraceId,
6096
7411
  selectedSpanId,
6097
7412
  onSelectSpan,
6098
- onBack: onBackToTraceList
7413
+ onDeselectSpan,
7414
+ onBack
6099
7415
  });
6100
- if (selectedService) return /* @__PURE__ */ jsx(TraceSearchView, {
6101
- service: selectedService,
6102
- onBack: onBackToServices,
6103
- onSelectTrace
7416
+ return /* @__PURE__ */ jsx(TraceSearchView, {
7417
+ onSelectTrace,
7418
+ onCompare
6104
7419
  });
6105
- return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
6106
7420
  }
6107
7421
  const METRICS_TREE = {
6108
7422
  root: "root",
@@ -6112,6 +7426,9 @@ const METRICS_TREE = {
6112
7426
  type: "Stack",
6113
7427
  children: [
6114
7428
  "heading",
7429
+ "ingestion-heading",
7430
+ "ingestion-grid",
7431
+ "discovery-heading",
6115
7432
  "description",
6116
7433
  "discovery-card"
6117
7434
  ],
@@ -6132,6 +7449,96 @@ const METRICS_TREE = {
6132
7449
  level: "h2"
6133
7450
  }
6134
7451
  },
7452
+ "ingestion-heading": {
7453
+ key: "ingestion-heading",
7454
+ type: "Heading",
7455
+ children: [],
7456
+ parentKey: "root",
7457
+ props: {
7458
+ text: "OTEL Ingestion",
7459
+ level: "h3"
7460
+ }
7461
+ },
7462
+ "ingestion-grid": {
7463
+ key: "ingestion-grid",
7464
+ type: "Grid",
7465
+ children: ["card-bytes", "card-requests"],
7466
+ parentKey: "root",
7467
+ props: {
7468
+ columns: 2,
7469
+ gap: "md"
7470
+ }
7471
+ },
7472
+ "card-bytes": {
7473
+ key: "card-bytes",
7474
+ type: "Card",
7475
+ children: ["stat-bytes"],
7476
+ parentKey: "ingestion-grid",
7477
+ props: {
7478
+ title: "Total Bytes Ingested",
7479
+ description: null,
7480
+ padding: null
7481
+ }
7482
+ },
7483
+ "stat-bytes": {
7484
+ key: "stat-bytes",
7485
+ type: "MetricStat",
7486
+ children: [],
7487
+ parentKey: "card-bytes",
7488
+ dataSource: {
7489
+ method: "searchMetricsPage",
7490
+ params: {
7491
+ metricType: "Sum",
7492
+ metricName: "kopai.ingestion.bytes",
7493
+ aggregate: "sum"
7494
+ },
7495
+ refetchIntervalMs: 1e4
7496
+ },
7497
+ props: {
7498
+ label: "Bytes",
7499
+ showSparkline: false
7500
+ }
7501
+ },
7502
+ "card-requests": {
7503
+ key: "card-requests",
7504
+ type: "Card",
7505
+ children: ["stat-requests"],
7506
+ parentKey: "ingestion-grid",
7507
+ props: {
7508
+ title: "Total Requests",
7509
+ description: null,
7510
+ padding: null
7511
+ }
7512
+ },
7513
+ "stat-requests": {
7514
+ key: "stat-requests",
7515
+ type: "MetricStat",
7516
+ children: [],
7517
+ parentKey: "card-requests",
7518
+ dataSource: {
7519
+ method: "searchMetricsPage",
7520
+ params: {
7521
+ metricType: "Sum",
7522
+ metricName: "kopai.ingestion.requests",
7523
+ aggregate: "sum"
7524
+ },
7525
+ refetchIntervalMs: 1e4
7526
+ },
7527
+ props: {
7528
+ label: "Requests",
7529
+ showSparkline: false
7530
+ }
7531
+ },
7532
+ "discovery-heading": {
7533
+ key: "discovery-heading",
7534
+ type: "Heading",
7535
+ children: [],
7536
+ parentKey: "root",
7537
+ props: {
7538
+ text: "Discovered Metrics",
7539
+ level: "h3"
7540
+ }
7541
+ },
6135
7542
  description: {
6136
7543
  key: "description",
6137
7544
  type: "Text",
@@ -6209,40 +7616,56 @@ function getDefaultClient() {
6209
7616
  }
6210
7617
  function ObservabilityPage({ client }) {
6211
7618
  const activeClient = client ?? getDefaultClient();
6212
- const { tab: activeTab, service: selectedService, trace: selectedTraceId, span: selectedSpanId } = useURLState();
7619
+ const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
6213
7620
  const handleTabChange = useCallback((tab) => {
6214
7621
  pushURLState({ tab });
6215
7622
  }, []);
6216
- const handleSelectService = useCallback((service) => {
6217
- pushURLState({
6218
- tab: "services",
6219
- service
6220
- });
6221
- }, []);
6222
7623
  const handleSelectTrace = useCallback((traceId) => {
6223
7624
  pushURLState({
7625
+ ...readURLState(),
6224
7626
  tab: "services",
6225
- service: selectedService,
6226
7627
  trace: traceId
6227
7628
  });
6228
- }, [selectedService]);
7629
+ }, []);
6229
7630
  const handleSelectSpan = useCallback((spanId) => {
6230
7631
  pushURLState({
7632
+ ...readURLState(),
6231
7633
  tab: "services",
6232
- service: selectedService,
6233
- trace: selectedTraceId,
6234
7634
  span: spanId
6235
7635
  }, { replace: true });
6236
- }, [selectedService, selectedTraceId]);
6237
- const handleBackToServices = useCallback(() => {
6238
- pushURLState({ tab: "services" });
6239
7636
  }, []);
6240
- const handleBackToTraceList = useCallback(() => {
7637
+ const handleDeselectSpan = useCallback(() => {
7638
+ pushURLState({
7639
+ ...readURLState(),
7640
+ span: null
7641
+ }, { replace: true });
7642
+ }, []);
7643
+ const handleCompare = useCallback((traceIds) => {
6241
7644
  pushURLState({
7645
+ ...readURLState(),
6242
7646
  tab: "services",
6243
- service: selectedService
7647
+ trace: null,
7648
+ span: null,
7649
+ view: null,
7650
+ uiFind: null,
7651
+ viewStart: null,
7652
+ viewEnd: null,
7653
+ compare: traceIds.join(",")
6244
7654
  });
6245
- }, [selectedService]);
7655
+ }, []);
7656
+ const handleBack = useCallback(() => {
7657
+ pushURLState({
7658
+ ...readURLState(),
7659
+ tab: "services",
7660
+ trace: null,
7661
+ span: null,
7662
+ view: null,
7663
+ uiFind: null,
7664
+ viewStart: null,
7665
+ viewEnd: null,
7666
+ compare: null
7667
+ });
7668
+ }, []);
6246
7669
  return /* @__PURE__ */ jsx(KopaiSDKProvider, {
6247
7670
  client: activeClient,
6248
7671
  children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
@@ -6257,21 +7680,20 @@ function ObservabilityPage({ client }) {
6257
7680
  }),
6258
7681
  activeTab === "logs" && /* @__PURE__ */ jsx(LogsTab, {}),
6259
7682
  activeTab === "services" && /* @__PURE__ */ jsx(ServicesTab, {
6260
- selectedService,
6261
7683
  selectedTraceId,
6262
7684
  selectedSpanId,
6263
- onSelectService: handleSelectService,
7685
+ compareParam,
6264
7686
  onSelectTrace: handleSelectTrace,
6265
7687
  onSelectSpan: handleSelectSpan,
6266
- onBackToServices: handleBackToServices,
6267
- onBackToTraceList: handleBackToTraceList
7688
+ onDeselectSpan: handleDeselectSpan,
7689
+ onBack: handleBack,
7690
+ onCompare: handleCompare
6268
7691
  }),
6269
7692
  activeTab === "metrics" && /* @__PURE__ */ jsx(MetricsTab, {})
6270
7693
  ] })
6271
7694
  })
6272
7695
  });
6273
7696
  }
6274
-
6275
7697
  //#endregion
6276
7698
  //#region src/lib/generate-prompt-instructions.ts
6277
7699
  function formatPropType(prop) {
@@ -6401,7 +7823,7 @@ ${JSON.stringify(unifiedSchema)}
6401
7823
 
6402
7824
  ${JSON.stringify(exampleElements)}`;
6403
7825
  }
6404
-
6405
7826
  //#endregion
6406
7827
  export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
7828
+
6407
7829
  //# sourceMappingURL=index.mjs.map