@kopai/ui 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/index.cjs +2427 -1139
  2. package/dist/index.d.cts +36 -7
  3. package/dist/index.d.cts.map +1 -1
  4. package/dist/index.d.mts +36 -7
  5. package/dist/index.d.mts.map +1 -1
  6. package/dist/index.mjs +2376 -1082
  7. package/dist/index.mjs.map +1 -1
  8. package/package.json +13 -13
  9. package/src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx +7 -7
  10. package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +5 -0
  11. package/src/components/observability/LogTimeline/LogFilter.tsx +5 -1
  12. package/src/components/observability/LogTimeline/index.tsx +6 -2
  13. package/src/components/observability/MetricHistogram/index.tsx +20 -19
  14. package/src/components/observability/MetricTimeSeries/index.tsx +8 -9
  15. package/src/components/observability/ServiceList/shortcuts.ts +1 -1
  16. package/src/components/observability/TraceComparison/index.tsx +332 -0
  17. package/src/components/observability/TraceDetail/TraceDetail.stories.tsx +0 -4
  18. package/src/components/observability/TraceDetail/index.tsx +4 -3
  19. package/src/components/observability/TraceSearch/DurationBar.tsx +38 -0
  20. package/src/components/observability/TraceSearch/ScatterPlot.tsx +135 -0
  21. package/src/components/observability/TraceSearch/SearchForm.tsx +195 -0
  22. package/src/components/observability/TraceSearch/SortDropdown.tsx +32 -0
  23. package/src/components/observability/TraceSearch/index.tsx +211 -218
  24. package/src/components/observability/TraceTimeline/DetailPane/EventsTab.tsx +1 -7
  25. package/src/components/observability/TraceTimeline/FlamegraphView.tsx +232 -0
  26. package/src/components/observability/TraceTimeline/GraphView.tsx +322 -0
  27. package/src/components/observability/TraceTimeline/Minimap.tsx +260 -0
  28. package/src/components/observability/TraceTimeline/SpanDetailInline.tsx +184 -0
  29. package/src/components/observability/TraceTimeline/SpanRow.tsx +14 -24
  30. package/src/components/observability/TraceTimeline/SpanSearch.tsx +76 -0
  31. package/src/components/observability/TraceTimeline/StatisticsView.tsx +223 -0
  32. package/src/components/observability/TraceTimeline/TimeRuler.tsx +54 -0
  33. package/src/components/observability/TraceTimeline/TimelineBar.tsx +18 -2
  34. package/src/components/observability/TraceTimeline/TraceHeader.tsx +116 -51
  35. package/src/components/observability/TraceTimeline/ViewTabs.tsx +34 -0
  36. package/src/components/observability/TraceTimeline/index.tsx +254 -110
  37. package/src/components/observability/index.ts +15 -0
  38. package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
  39. package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
  40. package/src/components/observability/utils/flatten-tree.ts +15 -0
  41. package/src/components/observability/utils/time.ts +9 -0
  42. package/src/hooks/use-kopai-data.test.ts +3 -0
  43. package/src/hooks/use-kopai-data.ts +11 -0
  44. package/src/hooks/use-live-logs.test.ts +3 -0
  45. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
  46. package/src/lib/component-catalog.ts +15 -0
  47. package/src/pages/observability.test.tsx +5 -0
  48. package/src/pages/observability.tsx +314 -235
  49. package/src/providers/kopai-provider.tsx +3 -0
package/dist/index.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,7 +28,6 @@ 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) {
@@ -39,6 +37,9 @@ function fetchForDataSource(client, dataSource, signal) {
39
37
  case "searchMetricsPage": return client.searchMetricsPage(dataSource.params, { signal });
40
38
  case "getTrace": return client.getTrace(dataSource.params.traceId, { signal });
41
39
  case "discoverMetrics": return client.discoverMetrics({ signal });
40
+ case "getServices": return client.getServices({ signal });
41
+ case "getOperations": return client.getOperations(dataSource.params.serviceName, { signal });
42
+ case "searchTraceSummariesPage": return client.searchTraceSummariesPage(dataSource.params, { signal });
42
43
  default: {
43
44
  const exhaustiveCheck = dataSource;
44
45
  throw new Error(`Unknown method: ${exhaustiveCheck.method}`);
@@ -64,7 +65,6 @@ function useKopaiData(dataSource) {
64
65
  refetch
65
66
  };
66
67
  }
67
-
68
68
  //#endregion
69
69
  //#region src/lib/log-buffer.ts
70
70
  function logKey(row) {
@@ -116,7 +116,6 @@ var LogBuffer = class {
116
116
  this.keys.clear();
117
117
  }
118
118
  };
119
-
120
119
  //#endregion
121
120
  //#region src/hooks/use-live-logs.ts
122
121
  function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = true }) {
@@ -171,7 +170,6 @@ function useLiveLogs({ params, pollIntervalMs = 3e3, maxLogs = 1e3, enabled = tr
171
170
  setLive
172
171
  };
173
172
  }
174
-
175
173
  //#endregion
176
174
  //#region src/lib/component-catalog.ts
177
175
  const dataSourceSchema = z.discriminatedUnion("method", [
@@ -199,6 +197,21 @@ const dataSourceSchema = z.discriminatedUnion("method", [
199
197
  method: z.literal("discoverMetrics"),
200
198
  params: z.object({}).optional(),
201
199
  refetchIntervalMs: z.number().optional()
200
+ }),
201
+ z.object({
202
+ method: z.literal("getServices"),
203
+ params: z.object({}).optional(),
204
+ refetchIntervalMs: z.number().optional()
205
+ }),
206
+ z.object({
207
+ method: z.literal("getOperations"),
208
+ params: z.object({ serviceName: z.string() }),
209
+ refetchIntervalMs: z.number().optional()
210
+ }),
211
+ z.object({
212
+ method: z.literal("searchTraceSummariesPage"),
213
+ params: dataFilterSchemas.traceSummariesFilterSchema,
214
+ refetchIntervalMs: z.number().optional()
202
215
  })
203
216
  ]);
204
217
  const componentDefinitionSchema = z.object({
@@ -206,7 +219,7 @@ const componentDefinitionSchema = z.object({
206
219
  description: z.string().describe("Component description to be displayed by the prompt generator"),
207
220
  props: z.unknown()
208
221
  }).describe("All options and properties necessary to render the React component with renderer");
209
- const catalogConfigSchema = z.object({
222
+ z.object({
210
223
  name: z.string().describe("catalog name"),
211
224
  components: z.record(z.string().describe("React component name"), componentDefinitionSchema)
212
225
  });
@@ -253,7 +266,6 @@ function createCatalog(catalogConfig) {
253
266
  uiTreeSchema
254
267
  };
255
268
  }
256
-
257
269
  //#endregion
258
270
  //#region src/lib/observability-catalog.ts
259
271
  const observabilityCatalog = createCatalog({
@@ -412,7 +424,6 @@ const observabilityCatalog = createCatalog({
412
424
  }
413
425
  }
414
426
  });
415
-
416
427
  //#endregion
417
428
  //#region src/components/observability/TabBar/index.tsx
418
429
  function renderLabel(label, shortcutKey) {
@@ -441,7 +452,6 @@ function TabBar({ tabs, active, onChange }) {
441
452
  }, t.key))
442
453
  });
443
454
  }
444
-
445
455
  //#endregion
446
456
  //#region src/components/observability/utils/colors.ts
447
457
  /**
@@ -461,38 +471,6 @@ function getSpanBarColor(serviceName, isError) {
461
471
  if (isError) return ERROR_COLOR;
462
472
  return getServiceColor(serviceName);
463
473
  }
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
474
  //#endregion
497
475
  //#region src/components/observability/utils/time.ts
498
476
  /**
@@ -514,6 +492,10 @@ function formatTimestamp$1(timestampMs) {
514
492
  timeZoneName: "short"
515
493
  });
516
494
  }
495
+ function formatRelativeTime$1(eventTimeMs, spanStartMs) {
496
+ const relativeMs = eventTimeMs - spanStartMs;
497
+ return `${relativeMs < 0 ? "-" : "+"}${formatDuration(Math.abs(relativeMs))}`;
498
+ }
517
499
  function calculateRelativeTime(timeMs, minTimeMs, maxTimeMs) {
518
500
  const totalDuration = maxTimeMs - minTimeMs;
519
501
  if (totalDuration === 0) return 0;
@@ -523,259 +505,556 @@ function calculateRelativeDuration(durationMs, totalDurationMs) {
523
505
  if (totalDurationMs === 0) return 0;
524
506
  return durationMs / totalDurationMs;
525
507
  }
526
-
527
508
  //#endregion
528
- //#region src/components/observability/TraceSearch/index.tsx
509
+ //#region src/components/observability/TraceSearch/SearchForm.tsx
510
+ /**
511
+ * SearchForm - Jaeger-style sidebar search form for trace filtering.
512
+ * Owns its own form state; parent only receives values on submit.
513
+ */
529
514
  const LOOKBACK_OPTIONS$1 = [
530
515
  {
531
516
  label: "Last 5 Minutes",
532
- ms: 5 * 6e4
517
+ value: "5m"
533
518
  },
534
519
  {
535
520
  label: "Last 15 Minutes",
536
- ms: 15 * 6e4
521
+ value: "15m"
537
522
  },
538
523
  {
539
524
  label: "Last 30 Minutes",
540
- ms: 30 * 6e4
525
+ value: "30m"
541
526
  },
542
527
  {
543
528
  label: "Last 1 Hour",
544
- ms: 60 * 6e4
529
+ value: "1h"
545
530
  },
546
531
  {
547
532
  label: "Last 2 Hours",
548
- ms: 120 * 6e4
533
+ value: "2h"
549
534
  },
550
535
  {
551
536
  label: "Last 6 Hours",
552
- ms: 360 * 6e4
537
+ value: "6h"
553
538
  },
554
539
  {
555
540
  label: "Last 12 Hours",
556
- ms: 720 * 6e4
541
+ value: "12h"
557
542
  },
558
543
  {
559
544
  label: "Last 24 Hours",
560
- ms: 1440 * 6e4
545
+ value: "24h"
561
546
  }
562
547
  ];
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,
548
+ const inputClass = "w-full bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground";
549
+ function SearchForm({ services, operations, initialValues, onSubmit, isLoading }) {
550
+ const [service, setService] = useState(initialValues?.service ?? "");
551
+ const [operation, setOperation] = useState(initialValues?.operation ?? "");
552
+ const [tags, setTags] = useState(initialValues?.tags ?? "");
553
+ const [lookback, setLookback] = useState(initialValues?.lookback ?? "");
554
+ const [minDuration, setMinDuration] = useState(initialValues?.minDuration ?? "");
555
+ const [maxDuration, setMaxDuration] = useState(initialValues?.maxDuration ?? "");
556
+ const [limit, setLimit] = useState(initialValues?.limit ?? 20);
557
+ useEffect(() => {
558
+ if (initialValues?.service != null) setService(initialValues.service);
559
+ }, [initialValues?.service]);
560
+ const handleSubmit = () => {
561
+ onSubmit({
562
+ service,
563
+ operation,
564
+ tags,
565
+ lookback,
566
+ minDuration,
567
+ maxDuration,
576
568
  limit
577
569
  });
578
570
  };
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 ? "▲" : "▼"
571
+ return /* @__PURE__ */ jsxs("div", {
572
+ className: "space-y-4",
573
+ children: [
574
+ /* @__PURE__ */ jsx("h3", {
575
+ className: "text-sm font-semibold text-foreground uppercase tracking-wider",
576
+ children: "Search"
577
+ }),
578
+ /* @__PURE__ */ jsxs("label", {
579
+ className: "block space-y-1",
580
+ children: [/* @__PURE__ */ jsx("span", {
581
+ className: "text-xs text-muted-foreground",
582
+ children: "Service"
583
+ }), /* @__PURE__ */ jsxs("select", {
584
+ value: service,
585
+ onChange: (e) => setService(e.target.value),
586
+ className: inputClass,
587
+ children: [/* @__PURE__ */ jsx("option", {
588
+ value: "",
589
+ children: "All Services"
590
+ }), services.map((s) => /* @__PURE__ */ jsx("option", {
591
+ value: s,
592
+ children: s
593
+ }, s))]
603
594
  })]
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"
595
+ }),
596
+ /* @__PURE__ */ jsxs("label", {
597
+ className: "block space-y-1",
598
+ children: [/* @__PURE__ */ jsx("span", {
599
+ className: "text-xs text-muted-foreground",
600
+ children: "Operation"
601
+ }), /* @__PURE__ */ jsxs("select", {
602
+ value: operation,
603
+ onChange: (e) => setOperation(e.target.value),
604
+ className: inputClass,
605
+ children: [/* @__PURE__ */ jsx("option", {
606
+ value: "",
607
+ children: "All Operations"
608
+ }), operations.map((op) => /* @__PURE__ */ jsx("option", {
609
+ value: op,
610
+ children: op
611
+ }, op))]
693
612
  })]
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",
613
+ }),
614
+ /* @__PURE__ */ jsxs("label", {
615
+ className: "block space-y-1",
616
+ children: [/* @__PURE__ */ jsx("span", {
617
+ className: "text-xs text-muted-foreground",
618
+ children: "Tags"
619
+ }), /* @__PURE__ */ jsx("textarea", {
620
+ value: tags,
621
+ onChange: (e) => setTags(e.target.value),
622
+ placeholder: "key=value key2=\"quoted value\"",
623
+ rows: 3,
624
+ className: `${inputClass} placeholder:text-muted-foreground/50 resize-y`
625
+ })]
626
+ }),
627
+ /* @__PURE__ */ jsxs("label", {
628
+ className: "block space-y-1",
629
+ children: [/* @__PURE__ */ jsx("span", {
630
+ className: "text-xs text-muted-foreground",
631
+ children: "Lookback"
632
+ }), /* @__PURE__ */ jsxs("select", {
633
+ value: lookback,
634
+ onChange: (e) => setLookback(e.target.value),
635
+ className: inputClass,
636
+ children: [/* @__PURE__ */ jsx("option", {
637
+ value: "",
638
+ children: "All time"
639
+ }), LOOKBACK_OPTIONS$1.map((opt) => /* @__PURE__ */ jsx("option", {
640
+ value: opt.value,
641
+ children: opt.label
642
+ }, opt.value))]
643
+ })]
644
+ }),
645
+ /* @__PURE__ */ jsxs("div", {
646
+ className: "grid grid-cols-2 gap-2",
647
+ children: [/* @__PURE__ */ jsxs("label", {
648
+ className: "block space-y-1",
649
+ children: [/* @__PURE__ */ jsx("span", {
650
+ className: "text-xs text-muted-foreground",
651
+ children: "Min Duration"
652
+ }), /* @__PURE__ */ jsx("input", {
653
+ type: "text",
654
+ placeholder: "e.g. 100ms",
655
+ value: minDuration,
656
+ onChange: (e) => setMinDuration(e.target.value),
657
+ className: `${inputClass} placeholder:text-muted-foreground/50`
658
+ })]
659
+ }), /* @__PURE__ */ jsxs("label", {
660
+ className: "block space-y-1",
661
+ children: [/* @__PURE__ */ jsx("span", {
662
+ className: "text-xs text-muted-foreground",
663
+ children: "Max Duration"
664
+ }), /* @__PURE__ */ jsx("input", {
665
+ type: "text",
666
+ placeholder: "e.g. 5s",
667
+ value: maxDuration,
668
+ onChange: (e) => setMaxDuration(e.target.value),
669
+ className: `${inputClass} placeholder:text-muted-foreground/50`
670
+ })]
671
+ })]
672
+ }),
673
+ /* @__PURE__ */ jsxs("label", {
674
+ className: "block space-y-1",
675
+ children: [/* @__PURE__ */ jsx("span", {
676
+ className: "text-xs text-muted-foreground",
677
+ children: "Limit"
678
+ }), /* @__PURE__ */ jsx("input", {
679
+ type: "number",
680
+ min: 1,
681
+ max: 1e3,
682
+ value: limit,
683
+ onChange: (e) => {
684
+ const n = Number(e.target.value);
685
+ setLimit(Number.isNaN(n) ? 20 : Math.max(1, Math.min(1e3, n)));
686
+ },
687
+ className: inputClass
688
+ })]
689
+ }),
690
+ /* @__PURE__ */ jsx("button", {
691
+ onClick: handleSubmit,
692
+ disabled: isLoading,
693
+ 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",
694
+ children: isLoading ? "Searching..." : "Find Traces"
695
+ })
696
+ ]
697
+ });
698
+ }
699
+ //#endregion
700
+ //#region src/components/observability/TraceSearch/ScatterPlot.tsx
701
+ /**
702
+ * ScatterPlot - Scatter chart showing trace duration vs timestamp.
703
+ */
704
+ function CustomTooltip$1({ active, payload }) {
705
+ if (!active || !payload?.[0]) return null;
706
+ const d = payload[0].payload;
707
+ return /* @__PURE__ */ jsxs("div", {
708
+ className: "bg-background border border-border rounded px-3 py-2 text-xs shadow-lg",
709
+ children: [
710
+ /* @__PURE__ */ jsxs("div", {
711
+ className: "font-medium text-foreground",
713
712
  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
- })]
713
+ d.serviceName,
714
+ ": ",
715
+ d.rootSpanName
716
+ ]
717
+ }),
718
+ /* @__PURE__ */ jsxs("div", {
719
+ className: "text-muted-foreground mt-1",
720
+ children: [
721
+ d.spanCount,
722
+ " span",
723
+ d.spanCount !== 1 ? "s" : "",
724
+ " ·",
725
+ " ",
726
+ formatDuration(d.y)
727
+ ]
728
+ }),
729
+ /* @__PURE__ */ jsx("div", {
730
+ className: "text-muted-foreground",
731
+ children: formatTimestamp$1(d.x)
732
+ })
733
+ ]
734
+ });
735
+ }
736
+ function ScatterPlot({ traces, onSelectTrace }) {
737
+ const data = useMemo(() => traces.map((t) => ({
738
+ x: t.timestampMs,
739
+ y: t.durationMs,
740
+ traceId: t.traceId,
741
+ serviceName: t.serviceName,
742
+ rootSpanName: t.rootSpanName,
743
+ spanCount: t.spanCount,
744
+ hasError: t.errorCount > 0
745
+ })), [traces]);
746
+ const handleClick = useCallback((entry) => {
747
+ const payload = entry?.payload;
748
+ if (payload?.traceId) onSelectTrace(payload.traceId);
749
+ }, [onSelectTrace]);
750
+ if (traces.length === 0) return null;
751
+ return /* @__PURE__ */ jsx("div", {
752
+ className: "border border-border rounded-lg p-4 bg-background",
753
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
754
+ width: "100%",
755
+ height: 200,
756
+ children: /* @__PURE__ */ jsxs(ScatterChart, {
757
+ margin: {
758
+ top: 8,
759
+ right: 8,
760
+ bottom: 4,
761
+ left: 0
762
+ },
763
+ children: [
764
+ /* @__PURE__ */ jsx(CartesianGrid, {
765
+ strokeDasharray: "3 3",
766
+ stroke: "hsl(var(--border))",
767
+ opacity: .4
733
768
  }),
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
- ]
769
+ /* @__PURE__ */ jsx(XAxis, {
770
+ dataKey: "x",
771
+ type: "number",
772
+ domain: ["dataMin", "dataMax"],
773
+ tickFormatter: (v) => {
774
+ const d = new Date(v);
775
+ return `${d.getHours().toString().padStart(2, "0")}:${d.getMinutes().toString().padStart(2, "0")}`;
776
+ },
777
+ tick: {
778
+ fontSize: 11,
779
+ fill: "hsl(var(--muted-foreground))"
780
+ },
781
+ stroke: "hsl(var(--border))",
782
+ name: "Time"
768
783
  }),
769
- /* @__PURE__ */ jsx("div", {
770
- className: "text-xs text-muted-foreground mt-1 text-right",
771
- children: formatTimestamp$1(t.timestampMs)
784
+ /* @__PURE__ */ jsx(YAxis, {
785
+ dataKey: "y",
786
+ type: "number",
787
+ tickFormatter: (v) => formatDuration(v),
788
+ tick: {
789
+ fontSize: 11,
790
+ fill: "hsl(var(--muted-foreground))"
791
+ },
792
+ stroke: "hsl(var(--border))",
793
+ name: "Duration",
794
+ width: 70
795
+ }),
796
+ /* @__PURE__ */ jsx(Tooltip, { content: /* @__PURE__ */ jsx(CustomTooltip$1, {}) }),
797
+ /* @__PURE__ */ jsx(Scatter, {
798
+ data,
799
+ onClick: handleClick,
800
+ cursor: "pointer",
801
+ children: data.map((point, i) => /* @__PURE__ */ jsx(Cell, {
802
+ fill: point.hasError ? "#ef4444" : getServiceColor(point.serviceName),
803
+ stroke: point.hasError ? "#ef4444" : "none",
804
+ strokeWidth: point.hasError ? 2 : 0
805
+ }, i))
772
806
  })
773
807
  ]
774
- }, t.traceId))
808
+ })
775
809
  })
776
- ] });
810
+ });
811
+ }
812
+ //#endregion
813
+ //#region src/components/observability/TraceSearch/SortDropdown.tsx
814
+ const SORT_OPTIONS = [
815
+ {
816
+ value: "recent",
817
+ label: "Most Recent"
818
+ },
819
+ {
820
+ value: "longest",
821
+ label: "Longest First"
822
+ },
823
+ {
824
+ value: "shortest",
825
+ label: "Shortest First"
826
+ },
827
+ {
828
+ value: "mostSpans",
829
+ label: "Most Spans"
830
+ },
831
+ {
832
+ value: "leastSpans",
833
+ label: "Least Spans"
834
+ }
835
+ ];
836
+ function SortDropdown({ value, onChange }) {
837
+ return /* @__PURE__ */ jsx("select", {
838
+ value,
839
+ onChange: (e) => onChange(e.target.value),
840
+ className: "bg-muted/50 border border-border rounded px-2 py-1.5 text-sm text-foreground",
841
+ children: SORT_OPTIONS.map((opt) => /* @__PURE__ */ jsx("option", {
842
+ value: opt.value,
843
+ children: opt.label
844
+ }, opt.value))
845
+ });
846
+ }
847
+ //#endregion
848
+ //#region src/components/observability/TraceSearch/DurationBar.tsx
849
+ /**
850
+ * DurationBar - Horizontal bar showing relative trace duration.
851
+ */
852
+ function DurationBar({ durationMs, maxDurationMs, color }) {
853
+ const rawPct = maxDurationMs > 0 ? durationMs / maxDurationMs * 100 : 0;
854
+ return /* @__PURE__ */ jsxs("div", {
855
+ className: "flex items-center gap-2",
856
+ children: [/* @__PURE__ */ jsx("div", {
857
+ className: "flex-1 h-2 bg-muted/30 rounded overflow-hidden",
858
+ children: /* @__PURE__ */ jsx("div", {
859
+ className: "h-full rounded",
860
+ style: {
861
+ width: `${durationMs <= 0 ? 0 : Math.min(Math.max(rawPct, 1), 100)}%`,
862
+ backgroundColor: color,
863
+ opacity: .7
864
+ }
865
+ })
866
+ }), /* @__PURE__ */ jsx("span", {
867
+ className: "text-xs text-foreground/80 shrink-0 w-16 text-right font-mono",
868
+ children: formatDuration(durationMs)
869
+ })]
870
+ });
871
+ }
872
+ //#endregion
873
+ //#region src/components/observability/TraceSearch/index.tsx
874
+ function sortTraces(traces, sort) {
875
+ const sorted = [...traces];
876
+ switch (sort) {
877
+ case "longest": return sorted.sort((a, b) => b.durationMs - a.durationMs);
878
+ case "shortest": return sorted.sort((a, b) => a.durationMs - b.durationMs);
879
+ case "mostSpans": return sorted.sort((a, b) => b.spanCount - a.spanCount);
880
+ case "leastSpans": return sorted.sort((a, b) => a.spanCount - b.spanCount);
881
+ default: return sorted.sort((a, b) => b.timestampMs - a.timestampMs);
882
+ }
883
+ }
884
+ function TraceSearch({ services = [], service, operations = [], traces, isLoading, error, onSelectTrace, onSearch, onCompare, sort: controlledSort, onSortChange }) {
885
+ const [internalSort, setInternalSort] = useState("recent");
886
+ const currentSort = controlledSort ?? internalSort;
887
+ const handleSortChange = (s) => {
888
+ if (onSortChange) onSortChange(s);
889
+ else setInternalSort(s);
890
+ };
891
+ const [selected, setSelected] = useState(/* @__PURE__ */ new Set());
892
+ const toggleSelected = (traceId) => {
893
+ setSelected((prev) => {
894
+ const next = new Set(prev);
895
+ if (next.has(traceId)) next.delete(traceId);
896
+ else {
897
+ if (next.size >= 2) return prev;
898
+ next.add(traceId);
899
+ }
900
+ return next;
901
+ });
902
+ };
903
+ const handleFormSubmit = (values) => {
904
+ onSearch?.({
905
+ service: values.service || void 0,
906
+ operation: values.operation || void 0,
907
+ tags: values.tags || void 0,
908
+ lookback: values.lookback || void 0,
909
+ minDuration: values.minDuration || void 0,
910
+ maxDuration: values.maxDuration || void 0,
911
+ limit: values.limit
912
+ });
913
+ };
914
+ const sortedTraces = useMemo(() => sortTraces(traces, currentSort), [traces, currentSort]);
915
+ const maxDurationMs = useMemo(() => Math.max(...traces.map((t) => t.durationMs), 0), [traces]);
916
+ const selectedArr = Array.from(selected);
917
+ return /* @__PURE__ */ jsxs("div", {
918
+ className: "flex gap-6 min-h-0",
919
+ children: [onSearch && /* @__PURE__ */ jsx("div", {
920
+ className: "w-72 shrink-0 border border-border rounded-lg p-4 self-start",
921
+ children: /* @__PURE__ */ jsx(SearchForm, {
922
+ services,
923
+ operations,
924
+ initialValues: { service },
925
+ onSubmit: handleFormSubmit,
926
+ isLoading
927
+ })
928
+ }), /* @__PURE__ */ jsxs("div", {
929
+ className: "flex-1 min-w-0 space-y-4",
930
+ children: [
931
+ traces.length > 0 && /* @__PURE__ */ jsx(ScatterPlot, {
932
+ traces,
933
+ onSelectTrace
934
+ }),
935
+ /* @__PURE__ */ jsxs("div", {
936
+ className: "flex items-center justify-between gap-2",
937
+ children: [/* @__PURE__ */ jsxs("span", {
938
+ className: "text-sm text-muted-foreground",
939
+ children: [
940
+ traces.length,
941
+ " Trace",
942
+ traces.length !== 1 ? "s" : ""
943
+ ]
944
+ }), /* @__PURE__ */ jsxs("div", {
945
+ className: "flex items-center gap-2",
946
+ children: [onCompare && selected.size === 2 && /* @__PURE__ */ jsx("button", {
947
+ onClick: () => onCompare(selectedArr),
948
+ className: "px-3 py-1.5 text-xs font-medium bg-foreground text-background rounded hover:bg-foreground/90 transition-colors",
949
+ children: "Compare"
950
+ }), /* @__PURE__ */ jsx(SortDropdown, {
951
+ value: currentSort,
952
+ onChange: handleSortChange
953
+ })]
954
+ })]
955
+ }),
956
+ isLoading && /* @__PURE__ */ jsxs("div", {
957
+ className: "flex items-center gap-2 text-muted-foreground py-8",
958
+ children: [/* @__PURE__ */ jsx("div", { className: "w-4 h-4 border-2 border-muted-foreground border-t-transparent rounded-full animate-spin" }), "Loading traces..."]
959
+ }),
960
+ error && /* @__PURE__ */ jsxs("div", {
961
+ className: "text-red-400 py-4",
962
+ children: ["Error loading traces: ", error.message]
963
+ }),
964
+ !isLoading && !error && traces.length === 0 && /* @__PURE__ */ jsx("div", {
965
+ className: "text-muted-foreground py-8",
966
+ children: "No traces found"
967
+ }),
968
+ sortedTraces.length > 0 && /* @__PURE__ */ jsx("div", {
969
+ className: "space-y-2",
970
+ children: sortedTraces.map((t) => /* @__PURE__ */ jsx("div", {
971
+ className: "border border-border rounded-lg px-4 py-3 hover:border-foreground/30 hover:bg-muted/30 cursor-pointer transition-colors",
972
+ children: /* @__PURE__ */ jsxs("div", {
973
+ className: "flex items-center gap-2",
974
+ children: [onCompare && /* @__PURE__ */ jsx("input", {
975
+ type: "checkbox",
976
+ checked: selected.has(t.traceId),
977
+ onChange: () => toggleSelected(t.traceId),
978
+ onClick: (e) => e.stopPropagation(),
979
+ className: "shrink-0",
980
+ disabled: !selected.has(t.traceId) && selected.size >= 2
981
+ }), /* @__PURE__ */ jsxs("div", {
982
+ className: "flex-1 min-w-0",
983
+ onClick: () => onSelectTrace(t.traceId),
984
+ children: [
985
+ /* @__PURE__ */ jsx("div", {
986
+ className: "flex items-baseline justify-between gap-2",
987
+ children: /* @__PURE__ */ jsxs("div", {
988
+ className: "flex items-baseline gap-1.5 min-w-0",
989
+ children: [/* @__PURE__ */ jsxs("span", {
990
+ className: "font-medium text-foreground truncate",
991
+ children: [
992
+ t.serviceName,
993
+ ": ",
994
+ t.rootSpanName
995
+ ]
996
+ }), /* @__PURE__ */ jsx("span", {
997
+ className: "text-xs font-mono text-muted-foreground shrink-0",
998
+ children: t.traceId.slice(0, 7)
999
+ })]
1000
+ })
1001
+ }),
1002
+ /* @__PURE__ */ jsx("div", {
1003
+ className: "mt-1.5",
1004
+ children: /* @__PURE__ */ jsx(DurationBar, {
1005
+ durationMs: t.durationMs,
1006
+ maxDurationMs,
1007
+ color: getServiceColor(t.serviceName)
1008
+ })
1009
+ }),
1010
+ /* @__PURE__ */ jsxs("div", {
1011
+ className: "flex items-center flex-wrap gap-1.5 mt-1.5",
1012
+ children: [
1013
+ /* @__PURE__ */ jsxs("span", {
1014
+ className: "text-xs px-1.5 py-0.5 rounded bg-muted text-muted-foreground",
1015
+ children: [
1016
+ t.spanCount,
1017
+ " Span",
1018
+ t.spanCount !== 1 ? "s" : ""
1019
+ ]
1020
+ }),
1021
+ t.errorCount > 0 && /* @__PURE__ */ jsxs("span", {
1022
+ className: "text-xs px-1.5 py-0.5 rounded bg-red-500/20 text-red-400",
1023
+ children: [
1024
+ t.errorCount,
1025
+ " Error",
1026
+ t.errorCount !== 1 ? "s" : ""
1027
+ ]
1028
+ }),
1029
+ t.services.map((svc) => /* @__PURE__ */ jsxs("span", {
1030
+ className: "inline-flex items-center gap-1 text-xs px-1.5 py-0.5 rounded",
1031
+ style: {
1032
+ backgroundColor: `${getServiceColor(svc.name)}20`,
1033
+ color: getServiceColor(svc.name)
1034
+ },
1035
+ children: [
1036
+ svc.hasError && /* @__PURE__ */ jsx("span", { className: "w-1.5 h-1.5 rounded-full bg-red-500 shrink-0" }),
1037
+ svc.name,
1038
+ " (",
1039
+ svc.count,
1040
+ ")"
1041
+ ]
1042
+ }, svc.name))
1043
+ ]
1044
+ }),
1045
+ /* @__PURE__ */ jsx("div", {
1046
+ className: "text-xs text-muted-foreground mt-1 text-right",
1047
+ children: formatTimestamp$1(t.timestampMs)
1048
+ })
1049
+ ]
1050
+ })]
1051
+ })
1052
+ }, t.traceId))
1053
+ })
1054
+ ]
1055
+ })]
1056
+ });
777
1057
  }
778
-
779
1058
  //#endregion
780
1059
  //#region src/components/observability/utils/flatten-tree.ts
781
1060
  function flattenTree(rootSpans, collapsedIds) {
@@ -790,6 +1069,17 @@ function flattenTree(rootSpans, collapsedIds) {
790
1069
  rootSpans.forEach((root) => traverse(root, 0));
791
1070
  return result;
792
1071
  }
1072
+ /** Flatten all spans (ignoring collapse state) with depth. */
1073
+ function flattenAllSpans(rootSpans) {
1074
+ return flattenTree(rootSpans, /* @__PURE__ */ new Set());
1075
+ }
1076
+ function spanMatchesSearch(span, query) {
1077
+ const q = query.toLowerCase();
1078
+ if (span.name.toLowerCase().includes(q)) return true;
1079
+ if (span.serviceName.toLowerCase().includes(q)) return true;
1080
+ for (const val of Object.values(span.attributes)) if (String(val).toLowerCase().includes(q)) return true;
1081
+ return false;
1082
+ }
793
1083
  function getAllSpanIds(rootSpans) {
794
1084
  const ids = [];
795
1085
  function traverse(span) {
@@ -799,15 +1089,26 @@ function getAllSpanIds(rootSpans) {
799
1089
  rootSpans.forEach((root) => traverse(root));
800
1090
  return ids;
801
1091
  }
802
-
803
1092
  //#endregion
804
1093
  //#region src/components/observability/TraceTimeline/TraceHeader.tsx
805
- function TraceHeader({ trace }) {
1094
+ function computeMaxDepth(spans) {
1095
+ let max = 0;
1096
+ function walk(nodes, depth) {
1097
+ for (const node of nodes) {
1098
+ if (depth > max) max = depth;
1099
+ walk(node.children, depth + 1);
1100
+ }
1101
+ }
1102
+ walk(spans, 1);
1103
+ return max;
1104
+ }
1105
+ function TraceHeader({ trace, services = [], onHeaderToggle, isCollapsed = false }) {
806
1106
  const [copied, setCopied] = useState(false);
807
1107
  const rootSpan = trace.rootSpans[0];
808
1108
  const rootServiceName = rootSpan?.serviceName ?? "unknown";
809
1109
  const rootSpanName = rootSpan?.name ?? "unknown";
810
1110
  const totalDuration = trace.maxTimeMs - trace.minTimeMs;
1111
+ const maxDepth = computeMaxDepth(trace.rootSpans);
811
1112
  const handleCopyTraceId = async () => {
812
1113
  try {
813
1114
  await navigator.clipboard.writeText(trace.traceId);
@@ -817,9 +1118,35 @@ function TraceHeader({ trace }) {
817
1118
  console.error("Failed to copy trace ID:", err);
818
1119
  }
819
1120
  };
820
- return /* @__PURE__ */ jsx("div", {
1121
+ return /* @__PURE__ */ jsxs("div", {
821
1122
  className: "bg-background border-b border-border px-4 py-3",
822
- children: /* @__PURE__ */ jsxs("div", {
1123
+ children: [/* @__PURE__ */ jsxs("div", {
1124
+ className: "flex items-center gap-2 mb-1",
1125
+ children: [onHeaderToggle && /* @__PURE__ */ jsx("button", {
1126
+ onClick: onHeaderToggle,
1127
+ className: "p-0.5 text-muted-foreground hover:text-foreground",
1128
+ "aria-label": isCollapsed ? "Expand header" : "Collapse header",
1129
+ children: /* @__PURE__ */ jsx("svg", {
1130
+ className: `w-4 h-4 transition-transform ${isCollapsed ? "-rotate-90" : ""}`,
1131
+ fill: "none",
1132
+ stroke: "currentColor",
1133
+ viewBox: "0 0 24 24",
1134
+ children: /* @__PURE__ */ jsx("path", {
1135
+ strokeLinecap: "round",
1136
+ strokeLinejoin: "round",
1137
+ strokeWidth: 2,
1138
+ d: "M19 9l-7 7-7-7"
1139
+ })
1140
+ })
1141
+ }), /* @__PURE__ */ jsxs("span", {
1142
+ className: "text-sm font-semibold text-foreground",
1143
+ children: [
1144
+ rootServiceName,
1145
+ ": ",
1146
+ rootSpanName
1147
+ ]
1148
+ })]
1149
+ }), !isCollapsed && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("div", {
823
1150
  className: "flex items-center gap-6 flex-wrap",
824
1151
  children: [
825
1152
  /* @__PURE__ */ jsxs("div", {
@@ -845,43 +1172,30 @@ function TraceHeader({ trace }) {
845
1172
  className: "flex items-center gap-2",
846
1173
  children: [/* @__PURE__ */ jsx("span", {
847
1174
  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
- ]
1175
+ children: "Duration:"
1176
+ }), /* @__PURE__ */ jsx("span", {
1177
+ className: "text-sm font-medium text-foreground",
1178
+ children: formatDuration(totalDuration)
865
1179
  })]
866
1180
  }),
867
1181
  /* @__PURE__ */ jsxs("div", {
868
1182
  className: "flex items-center gap-2",
869
1183
  children: [/* @__PURE__ */ jsx("span", {
870
1184
  className: "text-xs font-semibold text-muted-foreground",
871
- children: "Duration:"
1185
+ children: "Spans:"
872
1186
  }), /* @__PURE__ */ jsx("span", {
873
1187
  className: "text-sm font-medium text-foreground",
874
- children: formatDuration(totalDuration)
1188
+ children: trace.totalSpanCount
875
1189
  })]
876
1190
  }),
877
1191
  /* @__PURE__ */ jsxs("div", {
878
1192
  className: "flex items-center gap-2",
879
1193
  children: [/* @__PURE__ */ jsx("span", {
880
1194
  className: "text-xs font-semibold text-muted-foreground",
881
- children: "Spans:"
1195
+ children: "Depth:"
882
1196
  }), /* @__PURE__ */ jsx("span", {
883
1197
  className: "text-sm font-medium text-foreground",
884
- children: trace.totalSpanCount
1198
+ children: maxDepth
885
1199
  })]
886
1200
  }),
887
1201
  /* @__PURE__ */ jsxs("div", {
@@ -895,10 +1209,21 @@ function TraceHeader({ trace }) {
895
1209
  })]
896
1210
  })
897
1211
  ]
898
- })
1212
+ }), services.length > 0 && /* @__PURE__ */ jsx("div", {
1213
+ className: "flex items-center gap-3 mt-2 flex-wrap",
1214
+ children: services.map((svc) => /* @__PURE__ */ jsxs("div", {
1215
+ className: "flex items-center gap-1.5",
1216
+ children: [/* @__PURE__ */ jsx("span", {
1217
+ className: "w-2.5 h-2.5 rounded-full flex-shrink-0",
1218
+ style: { backgroundColor: getServiceColor(svc) }
1219
+ }), /* @__PURE__ */ jsx("span", {
1220
+ className: "text-xs text-muted-foreground",
1221
+ children: svc
1222
+ })]
1223
+ }, svc))
1224
+ })] })]
899
1225
  });
900
1226
  }
901
-
902
1227
  //#endregion
903
1228
  //#region src/components/observability/TraceTimeline/Tooltip.tsx
904
1229
  function Tooltip$1({ content, children }) {
@@ -928,7 +1253,6 @@ function Tooltip$1({ content, children }) {
928
1253
  children: content
929
1254
  }), document.body)] });
930
1255
  }
931
-
932
1256
  //#endregion
933
1257
  //#region src/components/observability/TraceTimeline/TimelineBar.tsx
934
1258
  function TimelineBar({ span, relativeStart, relativeDuration }) {
@@ -936,45 +1260,48 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
936
1260
  const barColor = getSpanBarColor(span.serviceName, isError);
937
1261
  const leftPercent = relativeStart * 100;
938
1262
  const widthPercent = Math.max(.2, relativeDuration * 100);
1263
+ const isWide = widthPercent > 8;
1264
+ const tooltipText = `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`;
1265
+ const durationLabel = formatDuration(span.durationMs);
939
1266
  return /* @__PURE__ */ jsx("div", {
940
1267
  className: "relative h-full",
941
1268
  children: /* @__PURE__ */ jsx(Tooltip$1, {
942
- content: `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`,
943
- children: /* @__PURE__ */ jsx("div", {
1269
+ content: tooltipText,
1270
+ children: /* @__PURE__ */ jsxs("div", {
944
1271
  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",
1272
+ children: [/* @__PURE__ */ jsx("div", {
1273
+ className: "absolute top-1/2 -translate-y-1/2 h-2 rounded-sm cursor-pointer hover:opacity-80 transition-opacity flex items-center",
947
1274
  style: {
948
1275
  left: `${leftPercent}%`,
949
1276
  width: `max(2px, ${widthPercent}%)`,
950
1277
  backgroundColor: barColor
951
- }
952
- })
1278
+ },
1279
+ children: isWide && /* @__PURE__ */ jsx("span", {
1280
+ className: "text-[10px] font-mono text-white px-1 truncate",
1281
+ children: durationLabel
1282
+ })
1283
+ }), !isWide && /* @__PURE__ */ jsx("span", {
1284
+ className: "absolute top-1/2 -translate-y-1/2 text-[10px] font-mono text-muted-foreground whitespace-nowrap",
1285
+ style: { left: `calc(${leftPercent + widthPercent}% + 4px)` },
1286
+ children: durationLabel
1287
+ })]
953
1288
  })
954
1289
  })
955
1290
  });
956
1291
  }
957
-
958
1292
  //#endregion
959
1293
  //#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 }) {
1294
+ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, isParentOfHovered = false, relativeStart, relativeDuration, onClick, onToggleCollapse, onMouseEnter, onMouseLeave, uiFind }) {
973
1295
  const hasChildren = span.children.length > 0;
974
1296
  const isError = span.status === "ERROR";
975
- const httpContext = getHttpContext(span);
1297
+ const serviceColor = getServiceColor(span.serviceName);
1298
+ const isDimmed = uiFind ? !spanMatchesSearch(span, uiFind) : false;
976
1299
  return /* @__PURE__ */ jsxs("div", {
977
1300
  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" : ""}`,
1301
+ style: {
1302
+ borderLeft: `3px solid ${serviceColor}`,
1303
+ opacity: isDimmed ? .4 : 1
1304
+ },
978
1305
  onClick,
979
1306
  onMouseEnter,
980
1307
  onMouseLeave,
@@ -1029,7 +1356,8 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
1029
1356
  })
1030
1357
  }),
1031
1358
  /* @__PURE__ */ jsx("span", {
1032
- className: "text-xs text-muted-foreground flex-shrink-0 mr-2",
1359
+ className: "text-xs flex-shrink-0 mr-2 font-medium",
1360
+ style: { color: serviceColor },
1033
1361
  children: span.serviceName
1034
1362
  }),
1035
1363
  /* @__PURE__ */ jsx("span", {
@@ -1044,10 +1372,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
1044
1372
  ")"
1045
1373
  ]
1046
1374
  }),
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
1375
  /* @__PURE__ */ jsx("span", {
1052
1376
  className: "text-xs text-muted-foreground flex-shrink-0 ml-2",
1053
1377
  children: formatDuration(span.durationMs)
@@ -1063,7 +1387,6 @@ const SpanRow = memo(function SpanRow({ span, level, isCollapsed, isSelected, is
1063
1387
  })]
1064
1388
  });
1065
1389
  });
1066
-
1067
1390
  //#endregion
1068
1391
  //#region src/components/observability/utils/attributes.ts
1069
1392
  function formatAttributeValue(value) {
@@ -1082,500 +1405,203 @@ function formatSeriesLabel(labels) {
1082
1405
  function isComplexValue(value) {
1083
1406
  return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
1084
1407
  }
1085
-
1086
1408
  //#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",
1409
+ //#region src/components/observability/TraceTimeline/SpanDetailInline.tsx
1410
+ function CollapsibleSection({ title, count, children }) {
1411
+ const [open, setOpen] = useState(false);
1412
+ if (count === 0) return null;
1413
+ return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("button", {
1414
+ className: "flex items-center gap-1 text-xs font-medium text-foreground hover:text-blue-600 dark:hover:text-blue-400 py-1",
1415
+ onClick: (e) => {
1416
+ e.stopPropagation();
1417
+ setOpen((p) => !p);
1418
+ },
1127
1419
  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
- })] })
1420
+ /* @__PURE__ */ jsx("span", {
1421
+ className: "w-3 text-center",
1422
+ children: open ? "" : ""
1423
+ }),
1424
+ title,
1425
+ /* @__PURE__ */ jsxs("span", {
1426
+ className: "text-muted-foreground",
1427
+ children: [
1428
+ "(",
1429
+ count,
1430
+ ")"
1431
+ ]
1432
+ })
1159
1433
  ]
1160
- });
1434
+ }), open && /* @__PURE__ */ jsx("div", {
1435
+ className: "ml-4 mt-1 space-y-1",
1436
+ children
1437
+ })] });
1161
1438
  }
1162
- function AttributeRow({ attrKey, value, highlighted }) {
1163
- const isComplex = isComplexValue(value);
1164
- const formattedValue = formatAttributeValue(value);
1439
+ function KeyValueRow({ k, v }) {
1440
+ const formatted = formatAttributeValue(v);
1165
1441
  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", {
1442
+ className: "flex gap-2 text-xs font-mono py-0.5",
1443
+ children: [
1444
+ /* @__PURE__ */ jsx("span", {
1445
+ className: "text-muted-foreground flex-shrink-0",
1446
+ children: k
1447
+ }),
1448
+ /* @__PURE__ */ jsx("span", {
1177
1449
  className: "text-foreground",
1178
- children: formattedValue
1450
+ children: "="
1451
+ }),
1452
+ /* @__PURE__ */ jsx("span", {
1453
+ className: "text-foreground break-all",
1454
+ children: formatted
1179
1455
  })
1180
- })]
1181
- });
1182
- }
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",
1212
- 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",
1217
- 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",
1221
- 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
- })]
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
- })
1245
- }),
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
- })]
1267
- }),
1268
- !hasAttributes && /* @__PURE__ */ jsx("div", {
1269
- className: "px-3 pb-3 text-xs text-muted-foreground italic",
1270
- children: "No attributes"
1271
- })
1272
- ]
1273
- }, index);
1274
- })
1275
- });
1276
- }
1277
-
1278
- //#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
- })
1456
+ ]
1436
1457
  });
1437
1458
  }
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);
1459
+ function SpanDetailInline({ span, traceStartMs }) {
1443
1460
  const [copiedId, setCopiedId] = useState(false);
1444
- const handleTabChange = useCallback((tab) => {
1445
- setActiveTab(tab);
1446
- }, []);
1447
- const handleCopySpanId = useCallback(async () => {
1461
+ const serviceColor = getServiceColor(span.serviceName);
1462
+ const relativeStartMs = span.startTimeUnixMs - traceStartMs;
1463
+ const handleCopy = useCallback(async () => {
1448
1464
  try {
1449
1465
  await navigator.clipboard.writeText(span.spanId);
1450
1466
  setCopiedId(true);
1451
1467
  setTimeout(() => setCopiedId(false), 2e3);
1452
- } catch (err) {
1453
- console.error("Failed to copy span ID:", err);
1454
- }
1468
+ } catch {}
1455
1469
  }, [span.spanId]);
1470
+ const spanAttrs = Object.entries(span.attributes).sort(([a], [b]) => a.localeCompare(b));
1471
+ const resourceAttrs = Object.entries(span.resourceAttributes).sort(([a], [b]) => a.localeCompare(b));
1456
1472
  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",
1473
+ className: "border-b border-border bg-muted/50 px-4 py-3",
1474
+ style: { borderLeft: `3px solid ${serviceColor}` },
1475
+ onClick: (e) => e.stopPropagation(),
1464
1476
  children: [
1465
1477
  /* @__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
- })
1478
+ className: "mb-2",
1479
+ children: [/* @__PURE__ */ jsx("div", {
1480
+ className: "text-sm font-medium text-foreground",
1481
+ children: span.name
1482
+ }), /* @__PURE__ */ jsxs("div", {
1483
+ className: "flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted-foreground mt-1",
1484
+ children: [
1485
+ /* @__PURE__ */ jsxs("span", { children: ["Service: ", /* @__PURE__ */ jsx("span", {
1486
+ className: "text-foreground",
1487
+ children: span.serviceName
1488
+ })] }),
1489
+ /* @__PURE__ */ jsxs("span", { children: [
1490
+ "Duration:",
1491
+ " ",
1492
+ /* @__PURE__ */ jsx("span", {
1493
+ className: "text-foreground",
1494
+ children: formatDuration(span.durationMs)
1489
1495
  })
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: [
1496
+ ] }),
1497
+ /* @__PURE__ */ jsxs("span", { children: [
1498
+ "Start:",
1499
+ " ",
1503
1500
  /* @__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
- })
1501
+ className: "text-foreground",
1502
+ children: formatDuration(relativeStartMs)
1533
1503
  })
1534
- ]
1535
- })
1536
- ]
1504
+ ] }),
1505
+ /* @__PURE__ */ jsxs("span", { children: ["Kind: ", /* @__PURE__ */ jsx("span", {
1506
+ className: "text-foreground",
1507
+ children: span.kind
1508
+ })] }),
1509
+ span.status !== "UNSET" && /* @__PURE__ */ jsxs("span", { children: [
1510
+ "Status:",
1511
+ " ",
1512
+ /* @__PURE__ */ jsx("span", {
1513
+ className: span.status === "ERROR" ? "text-red-500" : "text-foreground",
1514
+ children: span.status
1515
+ })
1516
+ ] })
1517
+ ]
1518
+ })]
1537
1519
  }),
1538
- /* @__PURE__ */ jsx("div", {
1539
- className: "flex border-b border-border",
1540
- role: "tablist",
1541
- "aria-label": "Span detail tabs",
1520
+ /* @__PURE__ */ jsxs("div", {
1521
+ className: "space-y-1",
1542
1522
  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",
1523
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1524
+ title: "Tags",
1525
+ count: spanAttrs.length,
1526
+ children: spanAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
1527
+ k,
1528
+ v
1529
+ }, k))
1530
+ }),
1531
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1532
+ title: "Process",
1533
+ count: resourceAttrs.length,
1534
+ children: resourceAttrs.map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
1535
+ k,
1536
+ v
1537
+ }, k))
1538
+ }),
1539
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1540
+ title: "Events",
1541
+ count: span.events.length,
1542
+ children: span.events.map((event, i) => /* @__PURE__ */ jsxs("div", {
1543
+ className: "text-xs border-l-2 border-border pl-2 py-1.5 space-y-0.5",
1544
+ children: [/* @__PURE__ */ jsxs("div", {
1545
+ className: "flex items-center gap-2",
1546
+ children: [/* @__PURE__ */ jsx("span", {
1547
+ className: "font-mono text-muted-foreground flex-shrink-0",
1548
+ children: formatRelativeTime$1(event.timeUnixMs, span.startTimeUnixMs)
1549
+ }), /* @__PURE__ */ jsx("span", {
1550
+ className: "font-medium text-foreground",
1551
+ children: event.name
1552
+ })]
1553
+ }), Object.entries(event.attributes).map(([k, v]) => /* @__PURE__ */ jsx(KeyValueRow, {
1554
+ k,
1555
+ v
1556
+ }, k))]
1557
+ }, i))
1558
+ }),
1559
+ /* @__PURE__ */ jsx(CollapsibleSection, {
1560
+ title: "Links",
1561
+ count: span.links.length,
1562
+ children: span.links.map((link, i) => /* @__PURE__ */ jsxs("div", {
1563
+ className: "text-xs font-mono py-0.5",
1555
1564
  children: [
1556
- "(",
1557
- count,
1558
- ")"
1565
+ /* @__PURE__ */ jsx("span", {
1566
+ className: "text-muted-foreground",
1567
+ children: "trace:"
1568
+ }),
1569
+ " ",
1570
+ link.traceId,
1571
+ " ",
1572
+ /* @__PURE__ */ jsx("span", {
1573
+ className: "text-muted-foreground",
1574
+ children: "span:"
1575
+ }),
1576
+ " ",
1577
+ link.spanId
1559
1578
  ]
1560
- })]
1561
- }, tab);
1562
- })
1579
+ }, i))
1580
+ })
1581
+ ]
1563
1582
  }),
1564
1583
  /* @__PURE__ */ jsxs("div", {
1565
- className: "flex-1 overflow-auto p-4",
1584
+ className: "flex items-center justify-end gap-2 mt-2 pt-2 border-t border-border",
1566
1585
  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
1586
+ /* @__PURE__ */ jsx("span", {
1587
+ className: "text-xs text-muted-foreground",
1588
+ children: "SpanID:"
1589
+ }),
1590
+ /* @__PURE__ */ jsx("code", {
1591
+ className: "text-xs font-mono text-foreground",
1592
+ children: span.spanId
1593
+ }),
1594
+ /* @__PURE__ */ jsx("button", {
1595
+ onClick: handleCopy,
1596
+ className: "text-xs text-muted-foreground hover:text-foreground",
1597
+ "aria-label": "Copy span ID",
1598
+ children: copiedId ? "✓" : "Copy"
1572
1599
  })
1573
1600
  ]
1574
1601
  })
1575
1602
  ]
1576
1603
  });
1577
1604
  }
1578
-
1579
1605
  //#endregion
1580
1606
  //#region src/components/observability/shared/LoadingSkeleton.tsx
1581
1607
  function LoadingSkeleton() {
@@ -1617,7 +1643,6 @@ function LoadingSkeleton() {
1617
1643
  })]
1618
1644
  });
1619
1645
  }
1620
-
1621
1646
  //#endregion
1622
1647
  //#region src/components/KeyboardShortcuts/context.ts
1623
1648
  const noop = () => {};
@@ -1637,7 +1662,6 @@ function useRegisterShortcuts(id, group) {
1637
1662
  unregister
1638
1663
  ]);
1639
1664
  }
1640
-
1641
1665
  //#endregion
1642
1666
  //#region src/components/KeyboardShortcuts/ShortcutsHelpDialog.tsx
1643
1667
  function ShortcutsHelpDialog({ open, onClose, groups }) {
@@ -1694,7 +1718,6 @@ function ShortcutsHelpDialog({ open, onClose, groups }) {
1694
1718
  })
1695
1719
  });
1696
1720
  }
1697
-
1698
1721
  //#endregion
1699
1722
  //#region src/components/KeyboardShortcuts/KeyboardShortcutsProvider.tsx
1700
1723
  const GENERAL_GROUP = {
@@ -1705,8 +1728,8 @@ const GENERAL_GROUP = {
1705
1728
  description: "Toggle shortcuts help"
1706
1729
  },
1707
1730
  {
1708
- keys: ["Shift", "S"],
1709
- description: "Services tab"
1731
+ keys: ["Shift", "T"],
1732
+ description: "Traces tab"
1710
1733
  },
1711
1734
  {
1712
1735
  keys: ["Shift", "L"],
@@ -1737,8 +1760,8 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
1737
1760
  }, []);
1738
1761
  useEffect(() => {
1739
1762
  function handleKeyDown(e) {
1740
- const target = e.target;
1741
- if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.tagName === "SELECT" || target.isContentEditable) return;
1763
+ if (!(e.target instanceof HTMLElement)) return;
1764
+ if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA" || e.target.tagName === "SELECT" || e.target.isContentEditable) return;
1742
1765
  if (e.shiftKey && e.key === "?") {
1743
1766
  e.preventDefault();
1744
1767
  setIsOpen((v) => !v);
@@ -1749,7 +1772,7 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
1749
1772
  setIsOpen(false);
1750
1773
  return;
1751
1774
  }
1752
- if (e.shiftKey && e.key === "S") {
1775
+ if (e.shiftKey && e.key === "T") {
1753
1776
  e.preventDefault();
1754
1777
  onNavigateServices();
1755
1778
  return;
@@ -1789,7 +1812,6 @@ function KeyboardShortcutsProvider({ children, onNavigateServices, onNavigateLog
1789
1812
  })]
1790
1813
  });
1791
1814
  }
1792
-
1793
1815
  //#endregion
1794
1816
  //#region src/components/observability/TraceTimeline/shortcuts.ts
1795
1817
  const TRACE_VIEWER_SHORTCUTS = {
@@ -1841,7 +1863,914 @@ const TRACE_VIEWER_SHORTCUTS = {
1841
1863
  }
1842
1864
  ]
1843
1865
  };
1844
-
1866
+ //#endregion
1867
+ //#region src/components/observability/TraceTimeline/TimeRuler.tsx
1868
+ const TICK_COUNT = 5;
1869
+ function TimeRuler({ totalDurationMs, leftColumnWidth, offsetMs = 0 }) {
1870
+ const ticks = Array.from({ length: TICK_COUNT + 1 }, (_, i) => {
1871
+ const fraction = i / TICK_COUNT;
1872
+ return {
1873
+ label: formatDuration(offsetMs + totalDurationMs * fraction),
1874
+ percent: fraction * 100
1875
+ };
1876
+ });
1877
+ return /* @__PURE__ */ jsxs("div", {
1878
+ className: "flex border-b border-border bg-background",
1879
+ children: [/* @__PURE__ */ jsx("div", {
1880
+ className: "flex-shrink-0",
1881
+ style: { width: leftColumnWidth }
1882
+ }), /* @__PURE__ */ jsx("div", {
1883
+ className: "flex-1 relative h-6 px-2",
1884
+ children: ticks.map((tick) => /* @__PURE__ */ jsxs("div", {
1885
+ className: "absolute top-0 h-full flex flex-col justify-end",
1886
+ style: { left: `${tick.percent}%` },
1887
+ children: [/* @__PURE__ */ jsx("div", { className: "h-2 border-l border-muted-foreground/40" }), /* @__PURE__ */ jsx("span", {
1888
+ className: "text-[10px] text-muted-foreground font-mono -translate-x-1/2 absolute bottom-0 whitespace-nowrap",
1889
+ style: {
1890
+ left: 0,
1891
+ transform: tick.percent === 100 ? "translateX(-100%)" : tick.percent === 0 ? "none" : "translateX(-50%)"
1892
+ },
1893
+ children: tick.label
1894
+ })]
1895
+ }, tick.percent))
1896
+ })]
1897
+ });
1898
+ }
1899
+ //#endregion
1900
+ //#region src/components/observability/TraceTimeline/SpanSearch.tsx
1901
+ function SpanSearch({ value, onChange, matchCount, currentMatch, onPrev, onNext }) {
1902
+ return /* @__PURE__ */ jsxs("div", {
1903
+ className: "flex items-center gap-1 px-2 py-1 border-b border-border bg-background",
1904
+ children: [/* @__PURE__ */ jsx("input", {
1905
+ type: "text",
1906
+ placeholder: "Find...",
1907
+ value,
1908
+ onChange: (e) => onChange(e.target.value),
1909
+ className: "bg-muted text-foreground text-sm px-2 py-0.5 rounded border border-border outline-none focus:border-blue-500 w-48"
1910
+ }), value && /* @__PURE__ */ jsxs(Fragment, { children: [
1911
+ /* @__PURE__ */ jsx("span", {
1912
+ className: "text-xs text-muted-foreground whitespace-nowrap",
1913
+ children: matchCount > 0 ? `${currentMatch + 1}/${matchCount}` : "0 matches"
1914
+ }),
1915
+ /* @__PURE__ */ jsx("button", {
1916
+ onClick: onPrev,
1917
+ disabled: matchCount === 0,
1918
+ className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
1919
+ "aria-label": "Previous match",
1920
+ children: /* @__PURE__ */ jsx("svg", {
1921
+ className: "w-3.5 h-3.5",
1922
+ fill: "none",
1923
+ stroke: "currentColor",
1924
+ viewBox: "0 0 24 24",
1925
+ children: /* @__PURE__ */ jsx("path", {
1926
+ strokeLinecap: "round",
1927
+ strokeLinejoin: "round",
1928
+ strokeWidth: 2,
1929
+ d: "M5 15l7-7 7 7"
1930
+ })
1931
+ })
1932
+ }),
1933
+ /* @__PURE__ */ jsx("button", {
1934
+ onClick: onNext,
1935
+ disabled: matchCount === 0,
1936
+ className: "p-0.5 text-muted-foreground hover:text-foreground disabled:opacity-30",
1937
+ "aria-label": "Next match",
1938
+ children: /* @__PURE__ */ jsx("svg", {
1939
+ className: "w-3.5 h-3.5",
1940
+ fill: "none",
1941
+ stroke: "currentColor",
1942
+ viewBox: "0 0 24 24",
1943
+ children: /* @__PURE__ */ jsx("path", {
1944
+ strokeLinecap: "round",
1945
+ strokeLinejoin: "round",
1946
+ strokeWidth: 2,
1947
+ d: "M19 9l-7 7-7-7"
1948
+ })
1949
+ })
1950
+ })
1951
+ ] })]
1952
+ });
1953
+ }
1954
+ //#endregion
1955
+ //#region src/components/observability/TraceTimeline/ViewTabs.tsx
1956
+ const VIEWS = [
1957
+ "timeline",
1958
+ "graph",
1959
+ "statistics",
1960
+ "flamegraph"
1961
+ ];
1962
+ const VIEW_LABELS = {
1963
+ timeline: "Timeline",
1964
+ graph: "Graph",
1965
+ statistics: "Statistics",
1966
+ flamegraph: "Flamegraph"
1967
+ };
1968
+ function ViewTabs({ activeView, onChange }) {
1969
+ return /* @__PURE__ */ jsx("div", {
1970
+ className: "flex border-b border-border bg-background",
1971
+ children: VIEWS.map((view) => /* @__PURE__ */ jsx("button", {
1972
+ onClick: () => onChange(view),
1973
+ 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"}`,
1974
+ children: VIEW_LABELS[view]
1975
+ }, view))
1976
+ });
1977
+ }
1978
+ //#endregion
1979
+ //#region src/components/observability/TraceTimeline/GraphView.tsx
1980
+ /**
1981
+ * GraphView - SVG-based DAG showing service dependencies within a trace.
1982
+ */
1983
+ function buildDAG(trace) {
1984
+ const nodeMap = /* @__PURE__ */ new Map();
1985
+ const edgeMap = /* @__PURE__ */ new Map();
1986
+ const childServices = /* @__PURE__ */ new Map();
1987
+ function walk(span, parentService) {
1988
+ const svc = span.serviceName;
1989
+ const existing = nodeMap.get(svc);
1990
+ if (existing) {
1991
+ existing.spanCount++;
1992
+ if (span.status === "ERROR") existing.errorCount++;
1993
+ } else nodeMap.set(svc, {
1994
+ spanCount: 1,
1995
+ errorCount: span.status === "ERROR" ? 1 : 0
1996
+ });
1997
+ if (parentService && parentService !== svc) {
1998
+ const key = `${parentService}→${svc}`;
1999
+ const edge = edgeMap.get(key);
2000
+ if (edge) {
2001
+ edge.callCount++;
2002
+ edge.totalDurationMs += span.durationMs;
2003
+ } else edgeMap.set(key, {
2004
+ callCount: 1,
2005
+ totalDurationMs: span.durationMs
2006
+ });
2007
+ if (!childServices.has(parentService)) childServices.set(parentService, /* @__PURE__ */ new Set());
2008
+ const parentChildren = childServices.get(parentService);
2009
+ if (parentChildren) parentChildren.add(svc);
2010
+ }
2011
+ for (const child of span.children) walk(child, svc);
2012
+ }
2013
+ for (const root of trace.rootSpans) walk(root);
2014
+ const edges = [];
2015
+ for (const [key, meta] of edgeMap) {
2016
+ const [from, to] = key.split("→");
2017
+ if (from && to) edges.push({
2018
+ from,
2019
+ to,
2020
+ ...meta
2021
+ });
2022
+ }
2023
+ return {
2024
+ nodeMap,
2025
+ edges,
2026
+ childServices
2027
+ };
2028
+ }
2029
+ const NODE_W = 160;
2030
+ const NODE_H = 60;
2031
+ const LAYER_GAP_Y = 100;
2032
+ const NODE_GAP_X = 40;
2033
+ function layoutNodes(nodeMap, edges) {
2034
+ const children = /* @__PURE__ */ new Map();
2035
+ const hasParent = /* @__PURE__ */ new Set();
2036
+ for (const e of edges) {
2037
+ if (!children.has(e.from)) children.set(e.from, /* @__PURE__ */ new Set());
2038
+ const fromChildren = children.get(e.from);
2039
+ if (fromChildren) fromChildren.add(e.to);
2040
+ hasParent.add(e.to);
2041
+ }
2042
+ const roots = [...nodeMap.keys()].filter((s) => !hasParent.has(s));
2043
+ if (roots.length === 0 && nodeMap.size > 0) {
2044
+ const firstKey = nodeMap.keys().next().value;
2045
+ if (firstKey !== void 0) roots.push(firstKey);
2046
+ }
2047
+ const layerOf = /* @__PURE__ */ new Map();
2048
+ const enqueueCount = /* @__PURE__ */ new Map();
2049
+ const maxEnqueue = nodeMap.size * 2;
2050
+ const queue = [];
2051
+ for (const r of roots) {
2052
+ layerOf.set(r, 0);
2053
+ queue.push(r);
2054
+ }
2055
+ while (queue.length > 0) {
2056
+ const cur = queue.shift();
2057
+ if (!cur) continue;
2058
+ const curLayer = layerOf.get(cur);
2059
+ if (curLayer === void 0) continue;
2060
+ const kids = children.get(cur);
2061
+ if (!kids) continue;
2062
+ for (const kid of kids) {
2063
+ const prev = layerOf.get(kid);
2064
+ const count = enqueueCount.get(kid) ?? 0;
2065
+ if (prev === void 0 && count < maxEnqueue) {
2066
+ layerOf.set(kid, curLayer + 1);
2067
+ enqueueCount.set(kid, count + 1);
2068
+ queue.push(kid);
2069
+ }
2070
+ }
2071
+ }
2072
+ for (const name of nodeMap.keys()) if (!layerOf.has(name)) layerOf.set(name, 0);
2073
+ const layers = /* @__PURE__ */ new Map();
2074
+ for (const [name, layer] of layerOf) {
2075
+ if (!layers.has(layer)) layers.set(layer, []);
2076
+ const layerNames = layers.get(layer);
2077
+ if (layerNames) layerNames.push(name);
2078
+ }
2079
+ const nodes = [];
2080
+ const totalWidth = Math.max(...Array.from(layers.values()).map((l) => l.length), 1) * (NODE_W + NODE_GAP_X) - NODE_GAP_X;
2081
+ for (const [layer, names] of layers) {
2082
+ const offsetX = (totalWidth - (names.length * (NODE_W + NODE_GAP_X) - NODE_GAP_X)) / 2;
2083
+ names.forEach((name, i) => {
2084
+ const meta = nodeMap.get(name);
2085
+ if (!meta) return;
2086
+ nodes.push({
2087
+ name,
2088
+ spanCount: meta.spanCount,
2089
+ errorCount: meta.errorCount,
2090
+ layer,
2091
+ x: offsetX + i * (NODE_W + NODE_GAP_X),
2092
+ y: layer * (NODE_H + LAYER_GAP_Y)
2093
+ });
2094
+ });
2095
+ }
2096
+ return nodes;
2097
+ }
2098
+ function GraphView({ trace }) {
2099
+ const { nodes, edges, svgWidth, svgHeight } = useMemo(() => {
2100
+ const { nodeMap, edges } = buildDAG(trace);
2101
+ const nodes = layoutNodes(nodeMap, edges);
2102
+ const maxX = Math.max(...nodes.map((n) => n.x + NODE_W), NODE_W);
2103
+ const maxY = Math.max(...nodes.map((n) => n.y + NODE_H), NODE_H);
2104
+ const padding = 40;
2105
+ return {
2106
+ nodes,
2107
+ edges,
2108
+ svgWidth: maxX + padding * 2,
2109
+ svgHeight: maxY + padding * 2
2110
+ };
2111
+ }, [trace]);
2112
+ const nodeByName = useMemo(() => {
2113
+ const m = /* @__PURE__ */ new Map();
2114
+ for (const n of nodes) m.set(n.name, n);
2115
+ return m;
2116
+ }, [nodes]);
2117
+ const padding = 40;
2118
+ return /* @__PURE__ */ jsx("div", {
2119
+ className: "flex-1 overflow-auto bg-background p-4 flex justify-center",
2120
+ children: /* @__PURE__ */ jsxs("svg", {
2121
+ viewBox: `0 0 ${svgWidth} ${svgHeight}`,
2122
+ width: svgWidth,
2123
+ height: svgHeight,
2124
+ role: "img",
2125
+ "aria-label": "Service dependency graph",
2126
+ children: [
2127
+ /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("marker", {
2128
+ id: "arrowhead",
2129
+ markerWidth: "10",
2130
+ markerHeight: "7",
2131
+ refX: "9",
2132
+ refY: "3.5",
2133
+ orient: "auto",
2134
+ children: /* @__PURE__ */ jsx("polygon", {
2135
+ points: "0 0, 10 3.5, 0 7",
2136
+ fill: "#94a3b8"
2137
+ })
2138
+ }) }),
2139
+ edges.map((edge) => {
2140
+ const from = nodeByName.get(edge.from);
2141
+ const to = nodeByName.get(edge.to);
2142
+ if (!from || !to) return null;
2143
+ const x1 = padding + from.x + NODE_W / 2;
2144
+ const y1 = padding + from.y + NODE_H;
2145
+ const x2 = padding + to.x + NODE_W / 2;
2146
+ const y2 = padding + to.y;
2147
+ const midY = (y1 + y2) / 2;
2148
+ return /* @__PURE__ */ jsxs("g", { children: [/* @__PURE__ */ jsx("path", {
2149
+ d: `M ${x1} ${y1} C ${x1} ${midY}, ${x2} ${midY}, ${x2} ${y2}`,
2150
+ fill: "none",
2151
+ stroke: "#475569",
2152
+ strokeWidth: 1.5,
2153
+ markerEnd: "url(#arrowhead)"
2154
+ }), edge.callCount > 1 && /* @__PURE__ */ jsxs("text", {
2155
+ x: (x1 + x2) / 2,
2156
+ y: midY - 6,
2157
+ textAnchor: "middle",
2158
+ fontSize: 11,
2159
+ fill: "#94a3b8",
2160
+ children: [edge.callCount, "x"]
2161
+ })] }, `${edge.from}→${edge.to}`);
2162
+ }),
2163
+ nodes.map((node) => {
2164
+ const color = getServiceColor(node.name);
2165
+ const hasError = node.errorCount > 0;
2166
+ const textColor = "#f8fafc";
2167
+ const nx = padding + node.x;
2168
+ const ny = padding + node.y;
2169
+ return /* @__PURE__ */ jsxs("g", { children: [
2170
+ /* @__PURE__ */ jsx("rect", {
2171
+ x: nx,
2172
+ y: ny,
2173
+ width: NODE_W,
2174
+ height: NODE_H,
2175
+ rx: 8,
2176
+ ry: 8,
2177
+ fill: color,
2178
+ stroke: hasError ? "#ef4444" : "none",
2179
+ strokeWidth: hasError ? 2 : 0
2180
+ }),
2181
+ /* @__PURE__ */ jsx("text", {
2182
+ x: nx + NODE_W / 2,
2183
+ y: ny + 24,
2184
+ textAnchor: "middle",
2185
+ fontSize: 13,
2186
+ fontWeight: 600,
2187
+ fill: textColor,
2188
+ children: node.name.length > 18 ? node.name.slice(0, 16) + "..." : node.name
2189
+ }),
2190
+ /* @__PURE__ */ jsxs("text", {
2191
+ x: nx + NODE_W / 2,
2192
+ y: ny + 44,
2193
+ textAnchor: "middle",
2194
+ fontSize: 11,
2195
+ fill: textColor,
2196
+ opacity: .85,
2197
+ children: [
2198
+ node.spanCount,
2199
+ " span",
2200
+ node.spanCount !== 1 ? "s" : "",
2201
+ node.errorCount > 0 && ` · ${node.errorCount} err`
2202
+ ]
2203
+ })
2204
+ ] }, node.name);
2205
+ })
2206
+ ]
2207
+ })
2208
+ });
2209
+ }
2210
+ //#endregion
2211
+ //#region src/components/observability/TraceTimeline/StatisticsView.tsx
2212
+ function computeSelfTime(span) {
2213
+ const childrenTotal = span.children.reduce((sum, child) => sum + child.durationMs, 0);
2214
+ return Math.max(0, span.durationMs - childrenTotal);
2215
+ }
2216
+ function computeStats(trace) {
2217
+ const allFlattened = flattenAllSpans(trace.rootSpans);
2218
+ const groups = /* @__PURE__ */ new Map();
2219
+ for (const { span } of allFlattened) {
2220
+ const key = `${span.serviceName}:${span.name}`;
2221
+ let group = groups.get(key);
2222
+ if (!group) {
2223
+ group = {
2224
+ spans: [],
2225
+ selfTimes: []
2226
+ };
2227
+ groups.set(key, group);
2228
+ }
2229
+ group.spans.push(span);
2230
+ group.selfTimes.push(computeSelfTime(span));
2231
+ }
2232
+ const stats = [];
2233
+ for (const [key, { spans, selfTimes }] of groups) {
2234
+ const durations = spans.map((s) => s.durationMs);
2235
+ const count = spans.length;
2236
+ const totalDuration = durations.reduce((a, b) => a + b, 0);
2237
+ const selfTimeTotal = selfTimes.reduce((a, b) => a + b, 0);
2238
+ const firstSpan = spans[0];
2239
+ if (!firstSpan) continue;
2240
+ stats.push({
2241
+ key,
2242
+ serviceName: firstSpan.serviceName,
2243
+ spanName: firstSpan.name,
2244
+ count,
2245
+ totalDuration,
2246
+ avgDuration: totalDuration / count,
2247
+ minDuration: Math.min(...durations),
2248
+ maxDuration: Math.max(...durations),
2249
+ selfTimeTotal,
2250
+ selfTimeAvg: selfTimeTotal / count,
2251
+ selfTimeMin: Math.min(...selfTimes),
2252
+ selfTimeMax: Math.max(...selfTimes)
2253
+ });
2254
+ }
2255
+ return stats;
2256
+ }
2257
+ function getSortValue(stat, field) {
2258
+ switch (field) {
2259
+ case "name": return stat.key.toLowerCase();
2260
+ case "count": return stat.count;
2261
+ case "total": return stat.totalDuration;
2262
+ case "avg": return stat.avgDuration;
2263
+ case "min": return stat.minDuration;
2264
+ case "max": return stat.maxDuration;
2265
+ case "selfTotal": return stat.selfTimeTotal;
2266
+ case "selfAvg": return stat.selfTimeAvg;
2267
+ case "selfMin": return stat.selfTimeMin;
2268
+ case "selfMax": return stat.selfTimeMax;
2269
+ }
2270
+ }
2271
+ const COLUMNS = [
2272
+ {
2273
+ label: "Name",
2274
+ field: "name"
2275
+ },
2276
+ {
2277
+ label: "Count",
2278
+ field: "count"
2279
+ },
2280
+ {
2281
+ label: "Total",
2282
+ field: "total"
2283
+ },
2284
+ {
2285
+ label: "Avg",
2286
+ field: "avg"
2287
+ },
2288
+ {
2289
+ label: "Min",
2290
+ field: "min"
2291
+ },
2292
+ {
2293
+ label: "Max",
2294
+ field: "max"
2295
+ },
2296
+ {
2297
+ label: "ST Total",
2298
+ field: "selfTotal"
2299
+ },
2300
+ {
2301
+ label: "ST Avg",
2302
+ field: "selfAvg"
2303
+ },
2304
+ {
2305
+ label: "ST Min",
2306
+ field: "selfMin"
2307
+ },
2308
+ {
2309
+ label: "ST Max",
2310
+ field: "selfMax"
2311
+ }
2312
+ ];
2313
+ function StatisticsView({ trace }) {
2314
+ const [sortField, setSortField] = useState("total");
2315
+ const [sortAsc, setSortAsc] = useState(false);
2316
+ const stats = useMemo(() => computeStats(trace), [trace]);
2317
+ const sorted = useMemo(() => {
2318
+ const copy = [...stats];
2319
+ copy.sort((a, b) => {
2320
+ const aVal = getSortValue(a, sortField);
2321
+ const bVal = getSortValue(b, sortField);
2322
+ let cmp;
2323
+ if (typeof aVal === "string" && typeof bVal === "string") cmp = aVal.localeCompare(bVal);
2324
+ else if (typeof aVal === "number" && typeof bVal === "number") cmp = aVal - bVal;
2325
+ else cmp = 0;
2326
+ return sortAsc ? cmp : -cmp;
2327
+ });
2328
+ return copy;
2329
+ }, [
2330
+ stats,
2331
+ sortField,
2332
+ sortAsc
2333
+ ]);
2334
+ const handleSort = (field) => {
2335
+ if (sortField === field) setSortAsc((p) => !p);
2336
+ else {
2337
+ setSortField(field);
2338
+ setSortAsc(false);
2339
+ }
2340
+ };
2341
+ return /* @__PURE__ */ jsx("div", {
2342
+ className: "flex-1 overflow-auto p-2",
2343
+ children: /* @__PURE__ */ jsxs("table", {
2344
+ className: "w-full text-sm border-collapse",
2345
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsx("tr", {
2346
+ className: "border-b border-border",
2347
+ children: COLUMNS.map((col) => /* @__PURE__ */ jsxs("th", {
2348
+ className: "px-3 py-2 text-left text-xs font-medium text-muted-foreground cursor-pointer select-none hover:text-foreground whitespace-nowrap",
2349
+ onClick: () => handleSort(col.field),
2350
+ children: [
2351
+ col.label,
2352
+ " ",
2353
+ sortField === col.field ? sortAsc ? "▲" : "▼" : ""
2354
+ ]
2355
+ }, col.field))
2356
+ }) }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((stat, i) => /* @__PURE__ */ jsxs("tr", {
2357
+ className: `border-b border-border/50 ${i % 2 === 0 ? "bg-background" : "bg-muted/30"}`,
2358
+ children: [
2359
+ /* @__PURE__ */ jsxs("td", {
2360
+ className: "px-3 py-1.5 text-foreground font-mono text-xs whitespace-nowrap",
2361
+ children: [
2362
+ /* @__PURE__ */ jsx("span", {
2363
+ className: "text-muted-foreground",
2364
+ children: stat.serviceName
2365
+ }),
2366
+ /* @__PURE__ */ jsx("span", {
2367
+ className: "text-muted-foreground/50",
2368
+ children: ":"
2369
+ }),
2370
+ " ",
2371
+ stat.spanName
2372
+ ]
2373
+ }),
2374
+ /* @__PURE__ */ jsx("td", {
2375
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2376
+ children: stat.count
2377
+ }),
2378
+ /* @__PURE__ */ jsx("td", {
2379
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2380
+ children: formatDuration(stat.totalDuration)
2381
+ }),
2382
+ /* @__PURE__ */ jsx("td", {
2383
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2384
+ children: formatDuration(stat.avgDuration)
2385
+ }),
2386
+ /* @__PURE__ */ jsx("td", {
2387
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2388
+ children: formatDuration(stat.minDuration)
2389
+ }),
2390
+ /* @__PURE__ */ jsx("td", {
2391
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2392
+ children: formatDuration(stat.maxDuration)
2393
+ }),
2394
+ /* @__PURE__ */ jsx("td", {
2395
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2396
+ children: formatDuration(stat.selfTimeTotal)
2397
+ }),
2398
+ /* @__PURE__ */ jsx("td", {
2399
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2400
+ children: formatDuration(stat.selfTimeAvg)
2401
+ }),
2402
+ /* @__PURE__ */ jsx("td", {
2403
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2404
+ children: formatDuration(stat.selfTimeMin)
2405
+ }),
2406
+ /* @__PURE__ */ jsx("td", {
2407
+ className: "px-3 py-1.5 text-foreground tabular-nums",
2408
+ children: formatDuration(stat.selfTimeMax)
2409
+ })
2410
+ ]
2411
+ }, stat.key)) })]
2412
+ })
2413
+ });
2414
+ }
2415
+ //#endregion
2416
+ //#region src/components/observability/TraceTimeline/FlamegraphView.tsx
2417
+ const ROW_HEIGHT = 24;
2418
+ const MIN_WIDTH = 1;
2419
+ const LABEL_MIN_WIDTH = 40;
2420
+ function findSpanById(rootSpans, spanId) {
2421
+ for (const root of rootSpans) {
2422
+ if (root.spanId === spanId) return root;
2423
+ const found = findSpanById(root.children, spanId);
2424
+ if (found) return found;
2425
+ }
2426
+ return null;
2427
+ }
2428
+ function getAncestorPath(rootSpans, targetId) {
2429
+ const path = [];
2430
+ function walk(span, ancestors) {
2431
+ if (span.spanId === targetId) {
2432
+ path.push(...ancestors, span);
2433
+ return true;
2434
+ }
2435
+ for (const child of span.children) if (walk(child, [...ancestors, span])) return true;
2436
+ return false;
2437
+ }
2438
+ for (const root of rootSpans) if (walk(root, [])) break;
2439
+ return path;
2440
+ }
2441
+ function FlamegraphView({ trace, onSpanClick, selectedSpanId }) {
2442
+ const [zoomSpanId, setZoomSpanId] = useState(null);
2443
+ const [tooltip, setTooltip] = useState(null);
2444
+ const zoomRoot = useMemo(() => {
2445
+ if (!zoomSpanId) return null;
2446
+ return findSpanById(trace.rootSpans, zoomSpanId);
2447
+ }, [trace.rootSpans, zoomSpanId]);
2448
+ const breadcrumbs = useMemo(() => {
2449
+ if (!zoomSpanId) return [];
2450
+ return getAncestorPath(trace.rootSpans, zoomSpanId);
2451
+ }, [trace.rootSpans, zoomSpanId]);
2452
+ const viewRoots = zoomRoot ? [zoomRoot] : trace.rootSpans;
2453
+ const viewMinTime = zoomRoot ? zoomRoot.startTimeUnixMs : trace.minTimeMs;
2454
+ const viewDuration = (zoomRoot ? zoomRoot.endTimeUnixMs : trace.maxTimeMs) - viewMinTime;
2455
+ const flatSpans = useMemo(() => flattenAllSpans(viewRoots).map((fs) => ({
2456
+ span: fs.span,
2457
+ depth: fs.level
2458
+ })), [viewRoots]);
2459
+ const maxDepth = useMemo(() => flatSpans.reduce((max, fs) => Math.max(max, fs.depth), 0) + 1, [flatSpans]);
2460
+ const svgWidth = 1200;
2461
+ const svgHeight = maxDepth * ROW_HEIGHT;
2462
+ const handleClick = useCallback((span) => {
2463
+ onSpanClick?.(span);
2464
+ setZoomSpanId(span.spanId);
2465
+ }, [onSpanClick]);
2466
+ const handleZoomOut = useCallback((spanId) => {
2467
+ setZoomSpanId(spanId);
2468
+ }, []);
2469
+ return /* @__PURE__ */ jsxs("div", {
2470
+ className: "flex-1 overflow-auto p-2",
2471
+ children: [
2472
+ breadcrumbs.length > 0 && /* @__PURE__ */ jsxs("div", {
2473
+ className: "flex items-center gap-1 text-xs text-muted-foreground mb-2 flex-wrap",
2474
+ children: [/* @__PURE__ */ jsx("button", {
2475
+ className: "hover:text-foreground underline",
2476
+ onClick: () => handleZoomOut(null),
2477
+ children: "root"
2478
+ }), breadcrumbs.map((bc, i) => /* @__PURE__ */ jsxs("span", {
2479
+ className: "flex items-center gap-1",
2480
+ children: [/* @__PURE__ */ jsx("span", {
2481
+ className: "text-muted-foreground/50",
2482
+ children: ">"
2483
+ }), i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxs("button", {
2484
+ className: "hover:text-foreground underline",
2485
+ onClick: () => handleZoomOut(bc.spanId),
2486
+ children: [
2487
+ bc.serviceName,
2488
+ ": ",
2489
+ bc.name
2490
+ ]
2491
+ }) : /* @__PURE__ */ jsxs("span", {
2492
+ className: "text-foreground",
2493
+ children: [
2494
+ bc.serviceName,
2495
+ ": ",
2496
+ bc.name
2497
+ ]
2498
+ })]
2499
+ }, bc.spanId))]
2500
+ }),
2501
+ /* @__PURE__ */ jsx("div", {
2502
+ className: "overflow-x-auto",
2503
+ children: /* @__PURE__ */ jsx("svg", {
2504
+ width: svgWidth,
2505
+ height: svgHeight,
2506
+ className: "block",
2507
+ onMouseLeave: () => setTooltip(null),
2508
+ children: flatSpans.map(({ span, depth }) => {
2509
+ const x = viewDuration > 0 ? (span.startTimeUnixMs - viewMinTime) / viewDuration * svgWidth : 0;
2510
+ const w = viewDuration > 0 ? Math.max(MIN_WIDTH, span.durationMs / viewDuration * svgWidth) : svgWidth;
2511
+ const y = depth * ROW_HEIGHT;
2512
+ const color = getServiceColor(span.serviceName);
2513
+ const isSelected = span.spanId === selectedSpanId;
2514
+ const showLabel = w >= LABEL_MIN_WIDTH;
2515
+ const label = `${span.serviceName}: ${span.name}`;
2516
+ return /* @__PURE__ */ jsxs("g", {
2517
+ className: "cursor-pointer",
2518
+ onClick: () => handleClick(span),
2519
+ onMouseEnter: (e) => setTooltip({
2520
+ span,
2521
+ x: e.clientX,
2522
+ y: e.clientY
2523
+ }),
2524
+ onMouseMove: (e) => setTooltip((prev) => prev ? {
2525
+ ...prev,
2526
+ x: e.clientX,
2527
+ y: e.clientY
2528
+ } : null),
2529
+ onMouseLeave: () => setTooltip(null),
2530
+ children: [/* @__PURE__ */ jsx("rect", {
2531
+ x,
2532
+ y,
2533
+ width: w,
2534
+ height: ROW_HEIGHT - 1,
2535
+ fill: color,
2536
+ opacity: .85,
2537
+ rx: 2,
2538
+ stroke: isSelected ? "#ffffff" : "transparent",
2539
+ strokeWidth: isSelected ? 2 : 0,
2540
+ className: "hover:opacity-100"
2541
+ }), showLabel && /* @__PURE__ */ jsx("text", {
2542
+ x: x + 4,
2543
+ y: y + ROW_HEIGHT / 2 + 1,
2544
+ dominantBaseline: "middle",
2545
+ fill: "#ffffff",
2546
+ fontSize: 11,
2547
+ fontFamily: "monospace",
2548
+ clipPath: `inset(0 0 0 0)`,
2549
+ children: /* @__PURE__ */ jsx("tspan", { children: label.length > w / 7 ? label.slice(0, Math.floor(w / 7) - 1) + "…" : label })
2550
+ })]
2551
+ }, span.spanId);
2552
+ })
2553
+ })
2554
+ }),
2555
+ tooltip && /* @__PURE__ */ jsxs("div", {
2556
+ className: "fixed z-50 pointer-events-none bg-popover border border-border rounded px-3 py-2 text-xs shadow-lg",
2557
+ style: {
2558
+ left: tooltip.x + 12,
2559
+ top: tooltip.y + 12
2560
+ },
2561
+ children: [
2562
+ /* @__PURE__ */ jsx("div", {
2563
+ className: "font-medium text-foreground",
2564
+ children: tooltip.span.name
2565
+ }),
2566
+ /* @__PURE__ */ jsx("div", {
2567
+ className: "text-muted-foreground",
2568
+ children: tooltip.span.serviceName
2569
+ }),
2570
+ /* @__PURE__ */ jsx("div", {
2571
+ className: "text-foreground mt-1",
2572
+ children: formatDuration(tooltip.span.durationMs)
2573
+ })
2574
+ ]
2575
+ })
2576
+ ]
2577
+ });
2578
+ }
2579
+ //#endregion
2580
+ //#region src/components/observability/TraceTimeline/Minimap.tsx
2581
+ /**
2582
+ * Minimap - Compressed overview of all spans with a draggable viewport.
2583
+ */
2584
+ const MINIMAP_HEIGHT = 40;
2585
+ const SPAN_HEIGHT = 2;
2586
+ const SPAN_GAP = 1;
2587
+ const MIN_VIEWPORT_WIDTH = .02;
2588
+ const HANDLE_WIDTH = 6;
2589
+ function Minimap({ trace, viewStart, viewEnd, onViewChange }) {
2590
+ const containerRef = useRef(null);
2591
+ const dragRef = useRef(null);
2592
+ const cleanupRef = useRef(null);
2593
+ useEffect(() => {
2594
+ return () => {
2595
+ cleanupRef.current?.();
2596
+ };
2597
+ }, []);
2598
+ const allSpans = useMemo(() => flattenAllSpans(trace.rootSpans), [trace.rootSpans]);
2599
+ const traceDuration = trace.maxTimeMs - trace.minTimeMs;
2600
+ const getFraction = useCallback((clientX) => {
2601
+ const el = containerRef.current;
2602
+ if (!el) return 0;
2603
+ const rect = el.getBoundingClientRect();
2604
+ if (!rect.width) return 0;
2605
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
2606
+ }, []);
2607
+ const clampView = useCallback((start, end) => {
2608
+ let s = Math.max(0, Math.min(1 - MIN_VIEWPORT_WIDTH, start));
2609
+ let e = Math.max(s + MIN_VIEWPORT_WIDTH, Math.min(1, end));
2610
+ if (e > 1) {
2611
+ e = 1;
2612
+ s = Math.max(0, e - Math.max(MIN_VIEWPORT_WIDTH, end - start));
2613
+ }
2614
+ return [s, e];
2615
+ }, []);
2616
+ const handleMouseDown = useCallback((e, mode) => {
2617
+ e.preventDefault();
2618
+ e.stopPropagation();
2619
+ cleanupRef.current?.();
2620
+ dragRef.current = {
2621
+ mode,
2622
+ startX: e.clientX,
2623
+ origViewStart: viewStart,
2624
+ origViewEnd: viewEnd
2625
+ };
2626
+ const handleMouseMove = (ev) => {
2627
+ const drag = dragRef.current;
2628
+ if (!drag || !containerRef.current) return;
2629
+ const rect = containerRef.current.getBoundingClientRect();
2630
+ if (!rect.width) return;
2631
+ const deltaFrac = (ev.clientX - drag.startX) / rect.width;
2632
+ let newStart;
2633
+ let newEnd;
2634
+ if (drag.mode === "pan") {
2635
+ const width = drag.origViewEnd - drag.origViewStart;
2636
+ newStart = drag.origViewStart + deltaFrac;
2637
+ newEnd = newStart + width;
2638
+ if (newStart < 0) {
2639
+ newStart = 0;
2640
+ newEnd = width;
2641
+ }
2642
+ if (newEnd > 1) {
2643
+ newEnd = 1;
2644
+ newStart = 1 - width;
2645
+ }
2646
+ } else if (drag.mode === "resize-left") {
2647
+ newStart = drag.origViewStart + deltaFrac;
2648
+ newEnd = drag.origViewEnd;
2649
+ } else {
2650
+ newStart = drag.origViewStart;
2651
+ newEnd = drag.origViewEnd + deltaFrac;
2652
+ }
2653
+ const [s, e] = clampView(newStart, newEnd);
2654
+ onViewChange(s, e);
2655
+ };
2656
+ const handleMouseUp = () => {
2657
+ dragRef.current = null;
2658
+ cleanupRef.current = null;
2659
+ window.removeEventListener("mousemove", handleMouseMove);
2660
+ window.removeEventListener("mouseup", handleMouseUp);
2661
+ };
2662
+ window.addEventListener("mousemove", handleMouseMove);
2663
+ window.addEventListener("mouseup", handleMouseUp);
2664
+ cleanupRef.current = handleMouseUp;
2665
+ }, [
2666
+ viewStart,
2667
+ viewEnd,
2668
+ onViewChange,
2669
+ clampView
2670
+ ]);
2671
+ const handleBackgroundClick = useCallback((e) => {
2672
+ if (dragRef.current) return;
2673
+ if (e.target !== e.currentTarget) return;
2674
+ const frac = getFraction(e.clientX);
2675
+ const half = (viewEnd - viewStart) / 2;
2676
+ const [s, eVal] = clampView(frac - half, frac + half);
2677
+ onViewChange(s, eVal);
2678
+ }, [
2679
+ viewStart,
2680
+ viewEnd,
2681
+ onViewChange,
2682
+ getFraction,
2683
+ clampView
2684
+ ]);
2685
+ const handleKeyDown = useCallback((e) => {
2686
+ const step = .05;
2687
+ const width = viewEnd - viewStart;
2688
+ let newStart;
2689
+ if (e.key === "ArrowLeft" || e.key === "ArrowUp") newStart = viewStart - step;
2690
+ else if (e.key === "ArrowRight" || e.key === "ArrowDown") newStart = viewStart + step;
2691
+ else return;
2692
+ e.preventDefault();
2693
+ const [s, eVal] = clampView(newStart, newStart + width);
2694
+ onViewChange(s, eVal);
2695
+ }, [
2696
+ viewStart,
2697
+ viewEnd,
2698
+ onViewChange,
2699
+ clampView
2700
+ ]);
2701
+ const viewStartPct = viewStart * 100;
2702
+ const viewEndPct = viewEnd * 100;
2703
+ const viewWidthPct = viewEndPct - viewStartPct;
2704
+ const totalRows = allSpans.length;
2705
+ const availableHeight = MINIMAP_HEIGHT - 4;
2706
+ const rowHeight = totalRows > 0 ? Math.min(SPAN_HEIGHT + SPAN_GAP, availableHeight / totalRows) : SPAN_HEIGHT;
2707
+ return /* @__PURE__ */ jsxs("div", {
2708
+ ref: containerRef,
2709
+ className: "relative w-full border-b border-border bg-muted/30 select-none",
2710
+ style: { height: MINIMAP_HEIGHT },
2711
+ onClick: handleBackgroundClick,
2712
+ onKeyDown: handleKeyDown,
2713
+ role: "slider",
2714
+ tabIndex: 0,
2715
+ "aria-label": "Trace minimap viewport",
2716
+ "aria-valuemin": 0,
2717
+ "aria-valuemax": 100,
2718
+ "aria-valuenow": Math.round(viewStartPct),
2719
+ children: [
2720
+ traceDuration > 0 && allSpans.map(({ span }, i) => {
2721
+ const left = (span.startTimeUnixMs - trace.minTimeMs) / traceDuration * 100;
2722
+ const width = Math.max(.2, span.durationMs / traceDuration * 100);
2723
+ const color = getSpanBarColor(span.serviceName, span.status === "ERROR");
2724
+ return /* @__PURE__ */ jsx("div", {
2725
+ className: "absolute pointer-events-none",
2726
+ style: {
2727
+ left: `${left}%`,
2728
+ width: `${width}%`,
2729
+ top: 2 + i * rowHeight,
2730
+ height: Math.max(1, rowHeight - SPAN_GAP),
2731
+ backgroundColor: color,
2732
+ opacity: .8,
2733
+ borderRadius: 1
2734
+ }
2735
+ }, span.spanId);
2736
+ }),
2737
+ viewStartPct > 0 && /* @__PURE__ */ jsx("div", {
2738
+ className: "absolute top-0 left-0 h-full bg-black/30 pointer-events-none",
2739
+ style: { width: `${viewStartPct}%` }
2740
+ }),
2741
+ viewEndPct < 100 && /* @__PURE__ */ jsx("div", {
2742
+ className: "absolute top-0 h-full bg-black/30 pointer-events-none",
2743
+ style: {
2744
+ left: `${viewEndPct}%`,
2745
+ right: 0
2746
+ }
2747
+ }),
2748
+ /* @__PURE__ */ jsxs("div", {
2749
+ className: "absolute top-0 h-full border border-blue-500/50 bg-blue-500/10 cursor-grab active:cursor-grabbing",
2750
+ style: {
2751
+ left: `${viewStartPct}%`,
2752
+ width: `${viewWidthPct}%`
2753
+ },
2754
+ onMouseDown: (e) => handleMouseDown(e, "pan"),
2755
+ children: [/* @__PURE__ */ jsx("div", {
2756
+ className: "absolute top-0 left-0 h-full cursor-ew-resize z-10",
2757
+ style: {
2758
+ width: HANDLE_WIDTH,
2759
+ marginLeft: -HANDLE_WIDTH / 2
2760
+ },
2761
+ onMouseDown: (e) => handleMouseDown(e, "resize-left")
2762
+ }), /* @__PURE__ */ jsx("div", {
2763
+ className: "absolute top-0 right-0 h-full cursor-ew-resize z-10",
2764
+ style: {
2765
+ width: HANDLE_WIDTH,
2766
+ marginRight: -HANDLE_WIDTH / 2
2767
+ },
2768
+ onMouseDown: (e) => handleMouseDown(e, "resize-right")
2769
+ })]
2770
+ })
2771
+ ]
2772
+ });
2773
+ }
1845
2774
  //#endregion
1846
2775
  //#region src/components/observability/TraceTimeline/index.tsx
1847
2776
  /**
@@ -1930,25 +2859,43 @@ function isSpanAncestorOf(potentialAncestor, descendantId, flattenedSpans) {
1930
2859
  }
1931
2860
  return false;
1932
2861
  }
1933
- function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpanId, isLoading, error }) {
2862
+ function collectServices(rootSpans) {
2863
+ const set = /* @__PURE__ */ new Set();
2864
+ function walk(span) {
2865
+ set.add(span.serviceName);
2866
+ span.children.forEach(walk);
2867
+ }
2868
+ rootSpans.forEach(walk);
2869
+ return Array.from(set).sort();
2870
+ }
2871
+ function TraceTimeline({ rows, onSpanClick, onSpanDeselect, selectedSpanId: externalSelectedSpanId, isLoading, error, view: externalView, onViewChange, uiFind: externalUiFind, onUiFindChange, viewStart: externalViewStart, viewEnd: externalViewEnd, onViewRangeChange }) {
1934
2872
  useRegisterShortcuts("trace-viewer", TRACE_VIEWER_SHORTCUTS);
1935
2873
  const [collapsedIds, setCollapsedIds] = useState(/* @__PURE__ */ new Set());
1936
2874
  const [internalSelectedSpanId, setInternalSelectedSpanId] = useState(null);
1937
2875
  const [hoveredSpanId, setHoveredSpanId] = useState(null);
2876
+ const [internalView, setInternalView] = useState("timeline");
2877
+ const [internalUiFind, setInternalUiFind] = useState("");
2878
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
2879
+ const [headerCollapsed, setHeaderCollapsed] = useState(false);
2880
+ const [internalViewStart, setInternalViewStart] = useState(0);
2881
+ const [internalViewEnd, setInternalViewEnd] = useState(1);
1938
2882
  const selectedSpanId = externalSelectedSpanId ?? internalSelectedSpanId;
2883
+ const viewStart = externalViewStart ?? internalViewStart;
2884
+ const viewEnd = externalViewEnd ?? internalViewEnd;
2885
+ const activeView = externalView ?? internalView;
2886
+ const uiFind = externalUiFind ?? internalUiFind;
1939
2887
  const scrollRef = useRef(null);
1940
2888
  const announcementRef = useRef(null);
1941
2889
  const parsedTrace = useMemo(() => buildTrace(rows), [rows]);
2890
+ const services = useMemo(() => parsedTrace ? collectServices(parsedTrace.rootSpans) : [], [parsedTrace]);
1942
2891
  const flattenedSpans = useMemo(() => {
1943
2892
  if (!parsedTrace) return [];
1944
2893
  return flattenTree(parsedTrace.rootSpans, collapsedIds);
1945
2894
  }, [parsedTrace, collapsedIds]);
1946
- const virtualizer = useVirtualizer({
1947
- count: flattenedSpans.length,
1948
- getScrollElement: () => scrollRef.current,
1949
- estimateSize: () => 32,
1950
- overscan: 5
1951
- });
2895
+ const matchingIndices = useMemo(() => {
2896
+ if (!uiFind) return [];
2897
+ return flattenedSpans.map((item, idx) => spanMatchesSearch(item.span, uiFind) ? idx : -1).filter((idx) => idx !== -1);
2898
+ }, [flattenedSpans, uiFind]);
1952
2899
  const handleToggleCollapse = (spanId) => {
1953
2900
  setCollapsedIds((prev) => {
1954
2901
  const next = new Set(prev);
@@ -1957,11 +2904,22 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
1957
2904
  return next;
1958
2905
  });
1959
2906
  };
2907
+ const handleDeselect = useCallback(() => {
2908
+ setInternalSelectedSpanId(null);
2909
+ onSpanDeselect?.();
2910
+ }, [onSpanDeselect]);
1960
2911
  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]);
2912
+ if (selectedSpanId === span.spanId) handleDeselect();
2913
+ else {
2914
+ setInternalSelectedSpanId(span.spanId);
2915
+ onSpanClick?.(span);
2916
+ if (announcementRef.current) announcementRef.current.textContent = `Selected span: ${span.name}, duration: ${formatDuration(span.durationMs)}`;
2917
+ }
2918
+ }, [
2919
+ onSpanClick,
2920
+ selectedSpanId,
2921
+ handleDeselect
2922
+ ]);
1965
2923
  const handleExpandAll = useCallback(() => {
1966
2924
  setCollapsedIds(/* @__PURE__ */ new Set());
1967
2925
  }, []);
@@ -2010,23 +2968,77 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2010
2968
  return next;
2011
2969
  });
2012
2970
  }, [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",
2971
+ const handleViewChange = useCallback((view) => {
2972
+ if (onViewChange) onViewChange(view);
2973
+ else setInternalView(view);
2974
+ }, [onViewChange]);
2975
+ const handleUiFindChange = useCallback((value) => {
2976
+ if (onUiFindChange) onUiFindChange(value);
2977
+ else setInternalUiFind(value);
2978
+ setCurrentMatchIndex(0);
2979
+ }, [onUiFindChange]);
2980
+ const handleViewRangeChange = useCallback((start, end) => {
2981
+ if (onViewRangeChange) onViewRangeChange(start, end);
2982
+ else {
2983
+ setInternalViewStart(start);
2984
+ setInternalViewEnd(end);
2985
+ }
2986
+ }, [onViewRangeChange]);
2987
+ const scrollToSpan = useCallback((spanId) => {
2988
+ (scrollRef.current?.querySelector(`[data-span-id="${spanId}"]`))?.scrollIntoView({
2989
+ block: "center",
2021
2990
  behavior: "smooth"
2022
2991
  });
2992
+ }, []);
2993
+ const handleSearchNext = useCallback(() => {
2994
+ if (matchingIndices.length === 0) return;
2995
+ const next = (currentMatchIndex + 1) % matchingIndices.length;
2996
+ setCurrentMatchIndex(next);
2997
+ const idx = matchingIndices[next];
2998
+ if (idx !== void 0) {
2999
+ const item = flattenedSpans[idx];
3000
+ if (item) {
3001
+ handleSpanClick(item.span);
3002
+ scrollToSpan(item.span.spanId);
3003
+ }
3004
+ }
2023
3005
  }, [
2024
- selectedSpanId,
3006
+ matchingIndices,
3007
+ currentMatchIndex,
2025
3008
  flattenedSpans,
2026
- virtualizer
3009
+ handleSpanClick,
3010
+ scrollToSpan
3011
+ ]);
3012
+ const handleSearchPrev = useCallback(() => {
3013
+ if (matchingIndices.length === 0) return;
3014
+ const prev = (currentMatchIndex - 1 + matchingIndices.length) % matchingIndices.length;
3015
+ setCurrentMatchIndex(prev);
3016
+ const idx = matchingIndices[prev];
3017
+ if (idx !== void 0) {
3018
+ const item = flattenedSpans[idx];
3019
+ if (item) {
3020
+ handleSpanClick(item.span);
3021
+ scrollToSpan(item.span.spanId);
3022
+ }
3023
+ }
3024
+ }, [
3025
+ matchingIndices,
3026
+ currentMatchIndex,
3027
+ flattenedSpans,
3028
+ handleSpanClick,
3029
+ scrollToSpan
2027
3030
  ]);
3031
+ useEffect(() => {
3032
+ if (!selectedSpanId) return;
3033
+ scrollToSpan(selectedSpanId);
3034
+ }, [selectedSpanId, scrollToSpan]);
2028
3035
  useEffect(() => {
2029
3036
  const handleKeyDown = (e) => {
3037
+ if (e.key === "Escape" && selectedSpanId) {
3038
+ e.preventDefault();
3039
+ handleDeselect();
3040
+ return;
3041
+ }
2030
3042
  if (!(scrollRef.current?.parentElement)?.contains(document.activeElement)) return;
2031
3043
  switch (e.key) {
2032
3044
  case "ArrowUp":
@@ -2049,10 +3061,7 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2049
3061
  e.preventDefault();
2050
3062
  handleCollapseExpand(false);
2051
3063
  break;
2052
- case "Escape":
2053
- e.preventDefault();
2054
- handleDeselect();
2055
- break;
3064
+ case "Escape": break;
2056
3065
  case "Enter":
2057
3066
  if (selectedSpanId) {
2058
3067
  e.preventDefault();
@@ -2120,10 +3129,9 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2120
3129
  })
2121
3130
  });
2122
3131
  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", {
3132
+ return /* @__PURE__ */ jsx("div", {
2125
3133
  className: "flex h-full bg-background",
2126
- children: [/* @__PURE__ */ jsxs("div", {
3134
+ children: /* @__PURE__ */ jsxs("div", {
2127
3135
  className: "flex flex-col flex-1 min-w-0",
2128
3136
  children: [
2129
3137
  /* @__PURE__ */ jsx("div", {
@@ -2133,39 +3141,58 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2133
3141
  "aria-live": "polite",
2134
3142
  "aria-atomic": "true"
2135
3143
  }),
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;
3144
+ /* @__PURE__ */ jsx(TraceHeader, {
3145
+ trace: parsedTrace,
3146
+ services,
3147
+ onHeaderToggle: () => setHeaderCollapsed((p) => !p),
3148
+ isCollapsed: headerCollapsed
3149
+ }),
3150
+ /* @__PURE__ */ jsx(ViewTabs, {
3151
+ activeView,
3152
+ onChange: handleViewChange
3153
+ }),
3154
+ /* @__PURE__ */ jsx(SpanSearch, {
3155
+ value: uiFind,
3156
+ onChange: handleUiFindChange,
3157
+ matchCount: matchingIndices.length,
3158
+ currentMatch: currentMatchIndex,
3159
+ onPrev: handleSearchPrev,
3160
+ onNext: handleSearchNext
3161
+ }),
3162
+ activeView === "graph" ? /* @__PURE__ */ jsx(GraphView, { trace: parsedTrace }) : activeView === "statistics" ? /* @__PURE__ */ jsx(StatisticsView, { trace: parsedTrace }) : activeView === "flamegraph" ? /* @__PURE__ */ jsx(FlamegraphView, {
3163
+ trace: parsedTrace,
3164
+ onSpanClick: handleSpanClick,
3165
+ selectedSpanId: selectedSpanId ?? void 0
3166
+ }) : /* @__PURE__ */ jsxs(Fragment, { children: [
3167
+ /* @__PURE__ */ jsx(Minimap, {
3168
+ trace: parsedTrace,
3169
+ viewStart,
3170
+ viewEnd,
3171
+ onViewChange: handleViewRangeChange
3172
+ }),
3173
+ /* @__PURE__ */ jsx(TimeRuler, {
3174
+ totalDurationMs: totalDurationMs * (viewEnd - viewStart),
3175
+ leftColumnWidth: "24rem",
3176
+ offsetMs: totalDurationMs * viewStart
3177
+ }),
3178
+ /* @__PURE__ */ jsx("div", {
3179
+ ref: scrollRef,
3180
+ className: "flex-1 overflow-auto outline-none",
3181
+ role: "tree",
3182
+ "aria-label": "Trace timeline",
3183
+ tabIndex: 0,
3184
+ children: flattenedSpans.map((item) => {
2152
3185
  const { span, level } = item;
2153
3186
  const isCollapsed = collapsedIds.has(span.spanId);
2154
3187
  const isSelected = span.spanId === selectedSpanId;
2155
3188
  const isHovered = span.spanId === hoveredSpanId;
2156
3189
  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, {
3190
+ const viewRange = viewEnd - viewStart;
3191
+ const relativeStart = (calculateRelativeTime(span.startTimeUnixMs, parsedTrace.minTimeMs, parsedTrace.maxTimeMs) - viewStart) / viewRange;
3192
+ const relativeDuration = calculateRelativeDuration(span.durationMs, totalDurationMs) / viewRange;
3193
+ return /* @__PURE__ */ jsxs("div", {
3194
+ "data-span-id": span.spanId,
3195
+ children: [/* @__PURE__ */ jsx(SpanRow, {
2169
3196
  span,
2170
3197
  level,
2171
3198
  isCollapsed,
@@ -2177,34 +3204,30 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2177
3204
  onClick: () => handleSpanClick(span),
2178
3205
  onToggleCollapse: () => handleToggleCollapse(span.spanId),
2179
3206
  onMouseEnter: () => setHoveredSpanId(span.spanId),
2180
- onMouseLeave: () => setHoveredSpanId(null)
2181
- })
3207
+ onMouseLeave: () => setHoveredSpanId(null),
3208
+ uiFind: uiFind || void 0
3209
+ }), isSelected && /* @__PURE__ */ jsx(SpanDetailInline, {
3210
+ span,
3211
+ traceStartMs: parsedTrace.minTimeMs
3212
+ })]
2182
3213
  }, span.spanId);
2183
3214
  })
2184
3215
  })
2185
- })
3216
+ ] })
2186
3217
  ]
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
- })]
3218
+ })
2195
3219
  });
2196
3220
  }
2197
-
2198
3221
  //#endregion
2199
3222
  //#region src/components/observability/TraceDetail/index.tsx
2200
- function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onBack }) {
3223
+ function TraceDetail({ traceId, rows, isLoading, error, selectedSpanId, onSpanClick, onSpanDeselect, onBack }) {
2201
3224
  return /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsxs("div", {
2202
3225
  className: "flex items-center gap-1.5 text-sm text-muted-foreground mb-4",
2203
3226
  children: [
2204
- /* @__PURE__ */ jsxs("button", {
3227
+ /* @__PURE__ */ jsx("button", {
2205
3228
  onClick: onBack,
2206
3229
  className: "hover:text-foreground transition-colors",
2207
- children: ["Services / ", service]
3230
+ children: "Traces"
2208
3231
  }),
2209
3232
  /* @__PURE__ */ jsx("span", { children: "/" }),
2210
3233
  /* @__PURE__ */ jsxs("span", {
@@ -2217,10 +3240,10 @@ function TraceDetail({ service, traceId, rows, isLoading, error, selectedSpanId,
2217
3240
  isLoading,
2218
3241
  error,
2219
3242
  selectedSpanId,
2220
- onSpanClick
3243
+ onSpanClick,
3244
+ onSpanDeselect
2221
3245
  })] });
2222
3246
  }
2223
-
2224
3247
  //#endregion
2225
3248
  //#region src/components/observability/LogTimeline/LogRow.tsx
2226
3249
  function formatTimestamp(timeMs) {
@@ -2346,7 +3369,6 @@ const LogRow = memo(function LogRow({ log, isSelected, onClick, searchText, rela
2346
3369
  ]
2347
3370
  });
2348
3371
  });
2349
-
2350
3372
  //#endregion
2351
3373
  //#region src/components/observability/LogTimeline/LogDetailPane/AttributesTab.tsx
2352
3374
  function AttributesTab({ log }) {
@@ -2379,7 +3401,6 @@ function AttributesTab({ log }) {
2379
3401
  })
2380
3402
  });
2381
3403
  }
2382
-
2383
3404
  //#endregion
2384
3405
  //#region src/components/observability/LogTimeline/LogDetailPane/JsonTreeView.tsx
2385
3406
  function JsonTreeView({ data, level = 0 }) {
@@ -2467,7 +3488,6 @@ function formatPrimitiveValue(value) {
2467
3488
  if (typeof value === "number") return String(value);
2468
3489
  return String(value);
2469
3490
  }
2470
-
2471
3491
  //#endregion
2472
3492
  //#region src/components/observability/LogTimeline/LogDetailPane/index.tsx
2473
3493
  function LogDetailPane({ log, onClose, onTraceLinkClick, initialTab = "message", wordWrap = true }) {
@@ -2679,7 +3699,6 @@ function getSeverityColor(severity) {
2679
3699
  bg: "bg-gray-50 dark:bg-gray-800/20"
2680
3700
  };
2681
3701
  }
2682
-
2683
3702
  //#endregion
2684
3703
  //#region src/components/observability/LogTimeline/shortcuts.ts
2685
3704
  const LOG_VIEWER_SHORTCUTS = {
@@ -2731,7 +3750,6 @@ const LOG_VIEWER_SHORTCUTS = {
2731
3750
  }
2732
3751
  ]
2733
3752
  };
2734
-
2735
3753
  //#endregion
2736
3754
  //#region src/components/observability/LogTimeline/index.tsx
2737
3755
  /**
@@ -2937,7 +3955,7 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
2937
3955
  useEffect(() => {
2938
3956
  const handleKeyDown = (e) => {
2939
3957
  const isFormField = e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement;
2940
- if (isFormField && e.key === "Escape") {
3958
+ if (isFormField && e.key === "Escape" && e.target instanceof HTMLElement) {
2941
3959
  e.target.blur();
2942
3960
  return;
2943
3961
  }
@@ -3178,7 +4196,6 @@ function LogTimeline({ rows, onLogClick, onTraceLinkClick, selectedLogId: extern
3178
4196
  })]
3179
4197
  });
3180
4198
  }
3181
-
3182
4199
  //#endregion
3183
4200
  //#region src/components/observability/LogTimeline/LogFilter.tsx
3184
4201
  /**
@@ -3279,7 +4296,7 @@ function MultiSelect({ options, selected, onChange, testId }) {
3279
4296
  useEffect(() => {
3280
4297
  if (!dropOpen) return;
3281
4298
  const handler = (e) => {
3282
- if (ref.current && !ref.current.contains(e.target)) setDropOpen(false);
4299
+ if (ref.current && e.target instanceof Node && !ref.current.contains(e.target)) setDropOpen(false);
3283
4300
  };
3284
4301
  document.addEventListener("mousedown", handler);
3285
4302
  return () => document.removeEventListener("mousedown", handler);
@@ -3730,7 +4747,6 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
3730
4747
  })]
3731
4748
  });
3732
4749
  }
3733
-
3734
4750
  //#endregion
3735
4751
  //#region src/components/observability/utils/lttb.ts
3736
4752
  function triangleArea(p1, p2, p3) {
@@ -3796,7 +4812,27 @@ function downsampleLTTB(data, targetPoints) {
3796
4812
  if (lastPoint) sampled.push(lastPoint);
3797
4813
  return sampled;
3798
4814
  }
3799
-
4815
+ //#endregion
4816
+ //#region src/components/observability/shared/TooltipEntryList.tsx
4817
+ function TooltipEntryList({ payload, displayLabelMap, formatValue }) {
4818
+ return payload.map((entry, i) => {
4819
+ const dataKey = entry.dataKey;
4820
+ const value = entry.value;
4821
+ if (typeof dataKey !== "string" || typeof value !== "number") return null;
4822
+ return /* @__PURE__ */ jsxs("p", {
4823
+ className: "text-sm",
4824
+ style: { color: entry.color },
4825
+ children: [
4826
+ /* @__PURE__ */ jsxs("span", {
4827
+ className: "font-medium",
4828
+ children: [displayLabelMap.get(dataKey) ?? dataKey, ":"]
4829
+ }),
4830
+ " ",
4831
+ formatValue(value)
4832
+ ]
4833
+ }, i);
4834
+ });
4835
+ }
3800
4836
  //#endregion
3801
4837
  //#region src/components/observability/utils/units.ts
3802
4838
  const BYTE_SCALES = [
@@ -3971,7 +5007,6 @@ function formatDisplayValue(value, scale) {
3971
5007
  function formatOtelValue(value, unit) {
3972
5008
  return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
3973
5009
  }
3974
-
3975
5010
  //#endregion
3976
5011
  //#region src/components/observability/MetricTimeSeries/index.tsx
3977
5012
  /**
@@ -4280,18 +5315,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
4280
5315
  children: [/* @__PURE__ */ jsx("p", {
4281
5316
  className: "text-gray-400 text-xs mb-2",
4282
5317
  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))]
5318
+ }), /* @__PURE__ */ jsx(TooltipEntryList, {
5319
+ payload,
5320
+ displayLabelMap,
5321
+ formatValue
5322
+ })]
4295
5323
  });
4296
5324
  }
4297
5325
  function MetricLoadingSkeleton({ height = 400 }) {
@@ -4338,7 +5366,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
4338
5366
  })
4339
5367
  });
4340
5368
  }
4341
-
4342
5369
  //#endregion
4343
5370
  //#region src/components/observability/MetricHistogram/index.tsx
4344
5371
  /**
@@ -4352,6 +5379,9 @@ const COLORS = [
4352
5379
  "#00C49F",
4353
5380
  "#0088FE"
4354
5381
  ];
5382
+ function isBucketData(v) {
5383
+ return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
5384
+ }
4355
5385
  const defaultFormatBucketLabel = (bound, index, bounds) => {
4356
5386
  if (index === 0) return `≤${bound}`;
4357
5387
  if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
@@ -4394,7 +5424,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
4394
5424
  };
4395
5425
  buckets.push(bucket);
4396
5426
  }
4397
- bucket[seriesName] = (bucket[seriesName] ?? 0) + count;
5427
+ const prev = bucket[seriesName];
5428
+ bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
4398
5429
  }
4399
5430
  }
4400
5431
  buckets.sort((a, b) => a.lowerBound - b.lowerBound);
@@ -4531,25 +5562,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
4531
5562
  }
4532
5563
  function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
4533
5564
  if (!active || !payload?.length) return null;
4534
- const bucket = payload[0]?.payload;
4535
- if (!bucket) return null;
5565
+ const raw = payload[0]?.payload;
5566
+ if (!isBucketData(raw)) return null;
4536
5567
  return /* @__PURE__ */ jsxs("div", {
4537
5568
  className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
4538
5569
  children: [/* @__PURE__ */ jsxs("p", {
4539
5570
  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))]
5571
+ children: ["Bucket: ", boundsScale ? `${formatDisplayValue(raw.lowerBound, boundsScale)} – ${raw.upperBound === Infinity ? "∞" : formatDisplayValue(raw.upperBound, boundsScale)}` : raw.bucket]
5572
+ }), /* @__PURE__ */ jsx(TooltipEntryList, {
5573
+ payload,
5574
+ displayLabelMap,
5575
+ formatValue
5576
+ })]
4553
5577
  });
4554
5578
  }
4555
5579
  function HistogramLoadingSkeleton({ height = 400 }) {
@@ -4589,7 +5613,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
4589
5613
  })
4590
5614
  });
4591
5615
  }
4592
-
4593
5616
  //#endregion
4594
5617
  //#region src/components/observability/MetricStat/index.tsx
4595
5618
  /**
@@ -4782,7 +5805,6 @@ function TrendIndicator({ direction, value }) {
4782
5805
  children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
4783
5806
  });
4784
5807
  }
4785
-
4786
5808
  //#endregion
4787
5809
  //#region src/components/observability/MetricTable/index.tsx
4788
5810
  /**
@@ -4922,7 +5944,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
4922
5944
  })]
4923
5945
  });
4924
5946
  }
4925
-
4926
5947
  //#endregion
4927
5948
  //#region src/lib/renderer.tsx
4928
5949
  /**
@@ -5027,7 +6048,6 @@ function Renderer({ tree, registry, fallback }) {
5027
6048
  fallback
5028
6049
  });
5029
6050
  }
5030
-
5031
6051
  //#endregion
5032
6052
  //#region src/lib/catalog.ts
5033
6053
  const dashboardCatalog = createCatalog({
@@ -5227,8 +6247,7 @@ const dashboardCatalog = createCatalog({
5227
6247
  }
5228
6248
  }
5229
6249
  });
5230
- const componentList = Object.keys(dashboardCatalog.components);
5231
-
6250
+ Object.keys(dashboardCatalog.components);
5232
6251
  //#endregion
5233
6252
  //#region src/components/dashboard/Badge/index.tsx
5234
6253
  function Badge({ element }) {
@@ -5252,7 +6271,6 @@ function Badge({ element }) {
5252
6271
  children: text
5253
6272
  });
5254
6273
  }
5255
-
5256
6274
  //#endregion
5257
6275
  //#region src/components/dashboard/Card/index.tsx
5258
6276
  function Card({ element, children }) {
@@ -5293,7 +6311,6 @@ function Card({ element, children }) {
5293
6311
  })]
5294
6312
  });
5295
6313
  }
5296
-
5297
6314
  //#endregion
5298
6315
  //#region src/components/dashboard/Divider/index.tsx
5299
6316
  function Divider({ element }) {
@@ -5331,7 +6348,6 @@ function Divider({ element }) {
5331
6348
  margin: "16px 0"
5332
6349
  } });
5333
6350
  }
5334
-
5335
6351
  //#endregion
5336
6352
  //#region src/components/dashboard/Empty/index.tsx
5337
6353
  function Empty({ element, onAction }) {
@@ -5374,7 +6390,6 @@ function Empty({ element, onAction }) {
5374
6390
  ]
5375
6391
  });
5376
6392
  }
5377
-
5378
6393
  //#endregion
5379
6394
  //#region src/components/dashboard/Grid/index.tsx
5380
6395
  function Grid({ element, children }) {
@@ -5392,7 +6407,6 @@ function Grid({ element, children }) {
5392
6407
  children
5393
6408
  });
5394
6409
  }
5395
-
5396
6410
  //#endregion
5397
6411
  //#region src/components/dashboard/Heading/index.tsx
5398
6412
  function Heading({ element }) {
@@ -5411,7 +6425,6 @@ function Heading({ element }) {
5411
6425
  children: text
5412
6426
  });
5413
6427
  }
5414
-
5415
6428
  //#endregion
5416
6429
  //#region src/components/dashboard/Stack/index.tsx
5417
6430
  function Stack({ element, children }) {
@@ -5435,7 +6448,6 @@ function Stack({ element, children }) {
5435
6448
  children
5436
6449
  });
5437
6450
  }
5438
-
5439
6451
  //#endregion
5440
6452
  //#region src/components/dashboard/Text/index.tsx
5441
6453
  function Text({ element }) {
@@ -5454,7 +6466,6 @@ function Text({ element }) {
5454
6466
  children: content
5455
6467
  });
5456
6468
  }
5457
-
5458
6469
  //#endregion
5459
6470
  //#region src/components/observability/renderers/OtelLogTimeline.tsx
5460
6471
  function OtelLogTimeline(props) {
@@ -5476,7 +6487,6 @@ function OtelLogTimeline(props) {
5476
6487
  })
5477
6488
  });
5478
6489
  }
5479
-
5480
6490
  //#endregion
5481
6491
  //#region src/components/observability/renderers/OtelMetricDiscovery.tsx
5482
6492
  const TYPE_ORDER = {
@@ -5554,7 +6564,6 @@ function OtelMetricDiscovery(props) {
5554
6564
  })
5555
6565
  });
5556
6566
  }
5557
-
5558
6567
  //#endregion
5559
6568
  //#region src/components/observability/renderers/OtelMetricHistogram.tsx
5560
6569
  function OtelMetricHistogram(props) {
@@ -5575,7 +6584,6 @@ function OtelMetricHistogram(props) {
5575
6584
  unit: props.element.props.unit ?? void 0
5576
6585
  });
5577
6586
  }
5578
-
5579
6587
  //#endregion
5580
6588
  //#region src/components/observability/renderers/OtelMetricStat.tsx
5581
6589
  function OtelMetricStat(props) {
@@ -5596,7 +6604,6 @@ function OtelMetricStat(props) {
5596
6604
  formatValue: formatOtelValue
5597
6605
  });
5598
6606
  }
5599
-
5600
6607
  //#endregion
5601
6608
  //#region src/components/observability/renderers/OtelMetricTable.tsx
5602
6609
  function OtelMetricTable(props) {
@@ -5615,7 +6622,6 @@ function OtelMetricTable(props) {
5615
6622
  maxRows: props.element.props.maxRows ?? 100
5616
6623
  });
5617
6624
  }
5618
-
5619
6625
  //#endregion
5620
6626
  //#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
5621
6627
  function OtelMetricTimeSeries(props) {
@@ -5637,7 +6643,6 @@ function OtelMetricTimeSeries(props) {
5637
6643
  unit: props.element.props.unit ?? void 0
5638
6644
  });
5639
6645
  }
5640
-
5641
6646
  //#endregion
5642
6647
  //#region src/components/observability/renderers/OtelTraceDetail.tsx
5643
6648
  function OtelTraceDetail(props) {
@@ -5649,19 +6654,15 @@ function OtelTraceDetail(props) {
5649
6654
  children: "No data source"
5650
6655
  });
5651
6656
  const rows = props.data?.data ?? [];
5652
- const firstRow = rows[0];
5653
- const service = firstRow?.ServiceName ?? "unknown";
5654
- const traceId = firstRow?.TraceId ?? "";
6657
+ const traceId = rows[0]?.TraceId ?? "";
5655
6658
  return /* @__PURE__ */ jsx(TraceDetail, {
5656
6659
  rows,
5657
6660
  isLoading: props.loading,
5658
6661
  error: props.error ?? void 0,
5659
- service,
5660
6662
  traceId,
5661
6663
  onBack: () => {}
5662
6664
  });
5663
6665
  }
5664
-
5665
6666
  //#endregion
5666
6667
  //#region src/components/observability/DynamicDashboard/index.tsx
5667
6668
  const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
@@ -5687,24 +6688,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
5687
6688
  children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
5688
6689
  });
5689
6690
  }
5690
-
6691
+ //#endregion
6692
+ //#region src/components/observability/TraceComparison/index.tsx
6693
+ function computeTraceStats(rows) {
6694
+ if (rows.length === 0) return {
6695
+ durationMs: 0,
6696
+ spanCount: 0
6697
+ };
6698
+ let minTs = Infinity;
6699
+ let maxEnd = -Infinity;
6700
+ for (const row of rows) {
6701
+ const startMs = parseInt(row.Timestamp, 10) / 1e6;
6702
+ const endMs = startMs + (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
6703
+ minTs = Math.min(minTs, startMs);
6704
+ maxEnd = Math.max(maxEnd, endMs);
6705
+ }
6706
+ return {
6707
+ durationMs: maxEnd - minTs,
6708
+ spanCount: rows.length
6709
+ };
6710
+ }
6711
+ function collectSignatures(rows) {
6712
+ const map = /* @__PURE__ */ new Map();
6713
+ for (const row of rows) {
6714
+ const key = `${row.ServiceName ?? "unknown"}::${row.SpanName ?? ""}`;
6715
+ const durMs = (row.Duration ? parseInt(row.Duration, 10) : 0) / 1e6;
6716
+ const existing = map.get(key);
6717
+ if (existing) {
6718
+ existing.count++;
6719
+ existing.totalDurationMs += durMs;
6720
+ } else map.set(key, {
6721
+ count: 1,
6722
+ totalDurationMs: durMs
6723
+ });
6724
+ }
6725
+ return map;
6726
+ }
6727
+ function computeDiff(rowsA, rowsB) {
6728
+ const sigA = collectSignatures(rowsA);
6729
+ const sigB = collectSignatures(rowsB);
6730
+ const allKeys = new Set([...sigA.keys(), ...sigB.keys()]);
6731
+ const result = [];
6732
+ for (const key of allKeys) {
6733
+ const [serviceName = "unknown", spanName = ""] = key.split("::");
6734
+ const a = sigA.get(key);
6735
+ const b = sigB.get(key);
6736
+ const countA = a?.count ?? 0;
6737
+ const countB = b?.count ?? 0;
6738
+ const avgA = a ? a.totalDurationMs / a.count : 0;
6739
+ const avgB = b ? b.totalDurationMs / b.count : 0;
6740
+ result.push({
6741
+ serviceName,
6742
+ spanName,
6743
+ countA,
6744
+ countB,
6745
+ avgDurationA: avgA,
6746
+ avgDurationB: avgB,
6747
+ deltaMs: avgB - avgA
6748
+ });
6749
+ }
6750
+ return result.sort((a, b) => {
6751
+ const aShared = a.countA > 0 && a.countB > 0;
6752
+ if (aShared !== (b.countA > 0 && b.countB > 0)) return aShared ? 1 : -1;
6753
+ return Math.abs(b.deltaMs) - Math.abs(a.deltaMs);
6754
+ });
6755
+ }
6756
+ function formatDelta(ms) {
6757
+ return `${ms > 0 ? "+" : ""}${formatDuration(ms)}`;
6758
+ }
6759
+ function TraceComparison({ traceIdA, traceIdB, onBack }) {
6760
+ const dsA = useMemo(() => ({
6761
+ method: "getTrace",
6762
+ params: { traceId: traceIdA }
6763
+ }), [traceIdA]);
6764
+ const dsB = useMemo(() => ({
6765
+ method: "getTrace",
6766
+ params: { traceId: traceIdB }
6767
+ }), [traceIdB]);
6768
+ const { data: rowsA, loading: loadingA, error: errorA } = useKopaiData(dsA);
6769
+ const { data: rowsB, loading: loadingB, error: errorB } = useKopaiData(dsB);
6770
+ const statsA = useMemo(() => computeTraceStats(rowsA ?? []), [rowsA]);
6771
+ const statsB = useMemo(() => computeTraceStats(rowsB ?? []), [rowsB]);
6772
+ const diff = useMemo(() => computeDiff(rowsA ?? [], rowsB ?? []), [rowsA, rowsB]);
6773
+ const durationDelta = statsB.durationMs - statsA.durationMs;
6774
+ const spanDelta = statsB.spanCount - statsA.spanCount;
6775
+ const isLoading = loadingA || loadingB;
6776
+ return /* @__PURE__ */ jsxs("div", {
6777
+ className: "flex flex-col gap-4",
6778
+ children: [
6779
+ /* @__PURE__ */ jsxs("div", {
6780
+ className: "flex items-center justify-between bg-background border border-border rounded-lg p-4",
6781
+ children: [/* @__PURE__ */ jsxs("div", {
6782
+ className: "flex items-center gap-4",
6783
+ children: [/* @__PURE__ */ jsx("button", {
6784
+ onClick: onBack,
6785
+ className: "text-sm text-muted-foreground hover:text-foreground transition-colors",
6786
+ children: "← Back"
6787
+ }), /* @__PURE__ */ jsxs("div", {
6788
+ className: "flex items-center gap-6 text-sm",
6789
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6790
+ className: "text-muted-foreground mr-1",
6791
+ children: "A:"
6792
+ }), /* @__PURE__ */ jsxs("span", {
6793
+ className: "font-mono text-xs text-foreground",
6794
+ children: [traceIdA.slice(0, 16), "..."]
6795
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6796
+ className: "text-muted-foreground mr-1",
6797
+ children: "B:"
6798
+ }), /* @__PURE__ */ jsxs("span", {
6799
+ className: "font-mono text-xs text-foreground",
6800
+ children: [traceIdB.slice(0, 16), "..."]
6801
+ })] })]
6802
+ })]
6803
+ }), !isLoading && /* @__PURE__ */ jsxs("div", {
6804
+ className: "flex items-center gap-6 text-sm",
6805
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6806
+ className: "text-muted-foreground mr-1",
6807
+ children: "Duration delta:"
6808
+ }), /* @__PURE__ */ jsx("span", {
6809
+ className: durationDelta > 0 ? "text-red-400" : durationDelta < 0 ? "text-green-400" : "text-foreground",
6810
+ children: formatDelta(durationDelta)
6811
+ })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("span", {
6812
+ className: "text-muted-foreground mr-1",
6813
+ children: "Span count delta:"
6814
+ }), /* @__PURE__ */ jsx("span", {
6815
+ className: spanDelta > 0 ? "text-red-400" : spanDelta < 0 ? "text-green-400" : "text-foreground",
6816
+ children: spanDelta > 0 ? `+${spanDelta}` : String(spanDelta)
6817
+ })] })]
6818
+ })]
6819
+ }),
6820
+ /* @__PURE__ */ jsxs("div", {
6821
+ className: "grid grid-cols-2 gap-4",
6822
+ style: { height: "50vh" },
6823
+ children: [/* @__PURE__ */ jsx("div", {
6824
+ className: "border border-border rounded-lg overflow-hidden",
6825
+ children: /* @__PURE__ */ jsx(TraceTimeline, {
6826
+ rows: rowsA ?? [],
6827
+ isLoading: loadingA,
6828
+ error: errorA ?? void 0
6829
+ })
6830
+ }), /* @__PURE__ */ jsx("div", {
6831
+ className: "border border-border rounded-lg overflow-hidden",
6832
+ children: /* @__PURE__ */ jsx(TraceTimeline, {
6833
+ rows: rowsB ?? [],
6834
+ isLoading: loadingB,
6835
+ error: errorB ?? void 0
6836
+ })
6837
+ })]
6838
+ }),
6839
+ !isLoading && diff.length > 0 && /* @__PURE__ */ jsxs("div", {
6840
+ className: "border border-border rounded-lg overflow-hidden",
6841
+ children: [/* @__PURE__ */ jsx("div", {
6842
+ className: "px-4 py-3 border-b border-border bg-background",
6843
+ children: /* @__PURE__ */ jsx("h3", {
6844
+ className: "text-sm font-medium text-foreground",
6845
+ children: "Structural Diff"
6846
+ })
6847
+ }), /* @__PURE__ */ jsx("div", {
6848
+ className: "overflow-x-auto",
6849
+ children: /* @__PURE__ */ jsxs("table", {
6850
+ className: "w-full text-sm",
6851
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
6852
+ className: "border-b border-border bg-muted/30",
6853
+ children: [
6854
+ /* @__PURE__ */ jsx("th", {
6855
+ className: "text-left px-4 py-2 text-muted-foreground font-medium",
6856
+ children: "Service"
6857
+ }),
6858
+ /* @__PURE__ */ jsx("th", {
6859
+ className: "text-left px-4 py-2 text-muted-foreground font-medium",
6860
+ children: "Span"
6861
+ }),
6862
+ /* @__PURE__ */ jsx("th", {
6863
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6864
+ children: "Count A"
6865
+ }),
6866
+ /* @__PURE__ */ jsx("th", {
6867
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6868
+ children: "Count B"
6869
+ }),
6870
+ /* @__PURE__ */ jsx("th", {
6871
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6872
+ children: "Avg Dur A"
6873
+ }),
6874
+ /* @__PURE__ */ jsx("th", {
6875
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6876
+ children: "Avg Dur B"
6877
+ }),
6878
+ /* @__PURE__ */ jsx("th", {
6879
+ className: "text-right px-4 py-2 text-muted-foreground font-medium",
6880
+ children: "Delta"
6881
+ })
6882
+ ]
6883
+ }) }), /* @__PURE__ */ jsx("tbody", { children: diff.map((row) => {
6884
+ const onlyA = row.countA > 0 && row.countB === 0;
6885
+ const onlyB = row.countA === 0 && row.countB > 0;
6886
+ return /* @__PURE__ */ jsxs("tr", {
6887
+ className: `border-b border-border/50 ${onlyA ? "bg-red-500/5" : onlyB ? "bg-green-500/5" : ""}`,
6888
+ children: [
6889
+ /* @__PURE__ */ jsx("td", {
6890
+ className: "px-4 py-1.5 text-foreground",
6891
+ children: row.serviceName
6892
+ }),
6893
+ /* @__PURE__ */ jsx("td", {
6894
+ className: "px-4 py-1.5 font-mono text-xs text-foreground",
6895
+ children: row.spanName
6896
+ }),
6897
+ /* @__PURE__ */ jsx("td", {
6898
+ className: "px-4 py-1.5 text-right text-foreground",
6899
+ children: row.countA || /* @__PURE__ */ jsx("span", {
6900
+ className: "text-muted-foreground",
6901
+ children: "-"
6902
+ })
6903
+ }),
6904
+ /* @__PURE__ */ jsx("td", {
6905
+ className: "px-4 py-1.5 text-right text-foreground",
6906
+ children: row.countB || /* @__PURE__ */ jsx("span", {
6907
+ className: "text-muted-foreground",
6908
+ children: "-"
6909
+ })
6910
+ }),
6911
+ /* @__PURE__ */ jsx("td", {
6912
+ className: "px-4 py-1.5 text-right text-foreground",
6913
+ children: row.countA > 0 ? formatDuration(row.avgDurationA) : /* @__PURE__ */ jsx("span", {
6914
+ className: "text-muted-foreground",
6915
+ children: "-"
6916
+ })
6917
+ }),
6918
+ /* @__PURE__ */ jsx("td", {
6919
+ className: "px-4 py-1.5 text-right text-foreground",
6920
+ children: row.countB > 0 ? formatDuration(row.avgDurationB) : /* @__PURE__ */ jsx("span", {
6921
+ className: "text-muted-foreground",
6922
+ children: "-"
6923
+ })
6924
+ }),
6925
+ /* @__PURE__ */ jsx("td", {
6926
+ className: "px-4 py-1.5 text-right",
6927
+ children: row.countA > 0 && row.countB > 0 ? /* @__PURE__ */ jsx("span", {
6928
+ className: row.deltaMs > 0 ? "text-red-400" : row.deltaMs < 0 ? "text-green-400" : "text-foreground",
6929
+ children: formatDelta(row.deltaMs)
6930
+ }) : /* @__PURE__ */ jsx("span", {
6931
+ className: onlyA ? "text-red-400" : "text-green-400",
6932
+ children: onlyA ? "removed" : "added"
6933
+ })
6934
+ })
6935
+ ]
6936
+ }, `${row.serviceName}::${row.spanName}`);
6937
+ }) })]
6938
+ })
6939
+ })]
6940
+ })
6941
+ ]
6942
+ });
6943
+ }
5691
6944
  //#endregion
5692
6945
  //#region src/components/observability/ServiceList/shortcuts.ts
5693
6946
  const SERVICES_SHORTCUTS = {
5694
- name: "Services",
6947
+ name: "Traces",
5695
6948
  shortcuts: [{
5696
6949
  keys: ["Backspace"],
5697
6950
  description: "Go back"
5698
6951
  }]
5699
6952
  };
5700
-
5701
6953
  //#endregion
5702
6954
  //#region src/pages/observability.tsx
5703
6955
  const TABS = [
5704
6956
  {
5705
6957
  key: "services",
5706
- label: "Services",
5707
- shortcutKey: "S"
6958
+ label: "Traces",
6959
+ shortcutKey: "T"
5708
6960
  },
5709
6961
  {
5710
6962
  key: "logs",
@@ -5724,20 +6976,53 @@ function readURLState() {
5724
6976
  const span = params.get("span");
5725
6977
  const dashboardId = params.get("dashboardId");
5726
6978
  const rawTab = params.get("tab");
6979
+ const tab = service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services";
6980
+ const rawLimit = params.get("limit");
6981
+ const limit = rawLimit ? parseInt(rawLimit, 10) : null;
5727
6982
  return {
5728
- tab: service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services",
6983
+ tab,
5729
6984
  service,
6985
+ operation: params.get("operation"),
6986
+ tags: params.get("tags"),
6987
+ lookback: params.get("lookback"),
6988
+ tsMin: params.get("tsMin"),
6989
+ tsMax: params.get("tsMax"),
6990
+ minDuration: params.get("minDuration"),
6991
+ maxDuration: params.get("maxDuration"),
6992
+ limit: limit !== null && !isNaN(limit) ? limit : null,
6993
+ sort: params.get("sort"),
5730
6994
  trace,
5731
6995
  span,
6996
+ view: params.get("view"),
6997
+ uiFind: params.get("uiFind"),
6998
+ compare: params.get("compare"),
6999
+ viewStart: params.get("viewStart"),
7000
+ viewEnd: params.get("viewEnd"),
5732
7001
  dashboardId
5733
7002
  };
5734
7003
  }
5735
7004
  function pushURLState(state, { replace = false } = {}) {
5736
7005
  const params = new URLSearchParams();
5737
7006
  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);
7007
+ if (state.tab === "services") {
7008
+ if (state.service) params.set("service", state.service);
7009
+ if (state.operation) params.set("operation", state.operation);
7010
+ if (state.tags) params.set("tags", state.tags);
7011
+ if (state.lookback) params.set("lookback", state.lookback);
7012
+ if (state.tsMin) params.set("tsMin", state.tsMin);
7013
+ if (state.tsMax) params.set("tsMax", state.tsMax);
7014
+ if (state.minDuration) params.set("minDuration", state.minDuration);
7015
+ if (state.maxDuration) params.set("maxDuration", state.maxDuration);
7016
+ if (state.limit != null && state.limit !== 20) params.set("limit", String(state.limit));
7017
+ if (state.sort) params.set("sort", state.sort);
7018
+ if (state.trace) params.set("trace", state.trace);
7019
+ if (state.span) params.set("span", state.span);
7020
+ if (state.view) params.set("view", state.view);
7021
+ if (state.uiFind) params.set("uiFind", state.uiFind);
7022
+ if (state.compare) params.set("compare", state.compare);
7023
+ if (state.viewStart) params.set("viewStart", state.viewStart);
7024
+ if (state.viewEnd) params.set("viewEnd", state.viewEnd);
7025
+ }
5741
7026
  const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
5742
7027
  if (dashboardId) params.set("dashboardId", dashboardId);
5743
7028
  const qs = params.toString();
@@ -5754,8 +7039,22 @@ let _cachedSearch = "";
5754
7039
  let _cachedState = {
5755
7040
  tab: "services",
5756
7041
  service: null,
7042
+ operation: null,
7043
+ tags: null,
7044
+ lookback: null,
7045
+ tsMin: null,
7046
+ tsMax: null,
7047
+ minDuration: null,
7048
+ maxDuration: null,
7049
+ limit: null,
7050
+ sort: null,
5757
7051
  trace: null,
5758
7052
  span: null,
7053
+ view: null,
7054
+ uiFind: null,
7055
+ compare: null,
7056
+ viewStart: null,
7057
+ viewEnd: null,
5759
7058
  dashboardId: null
5760
7059
  };
5761
7060
  function getURLSnapshot() {
@@ -5865,6 +7164,26 @@ function parseDuration(input) {
5865
7164
  s: 1e9
5866
7165
  }[unit]));
5867
7166
  }
7167
+ function parseLogfmt(str) {
7168
+ const result = {};
7169
+ const re = /(\w+)=(?:"([^"]*)"|([\S]*))/g;
7170
+ let m;
7171
+ while ((m = re.exec(str)) !== null) {
7172
+ const key = m[1];
7173
+ if (key) result[key] = m[2] ?? m[3] ?? "";
7174
+ }
7175
+ return result;
7176
+ }
7177
+ const LOOKBACK_MS = {
7178
+ "5m": 5 * 6e4,
7179
+ "15m": 15 * 6e4,
7180
+ "30m": 30 * 6e4,
7181
+ "1h": 60 * 6e4,
7182
+ "2h": 120 * 6e4,
7183
+ "6h": 360 * 6e4,
7184
+ "12h": 720 * 6e4,
7185
+ "24h": 1440 * 6e4
7186
+ };
5868
7187
  function LogsTab() {
5869
7188
  const [initState] = useState(() => readLogFilters());
5870
7189
  const [filters, setFilters] = useState(initState.filters);
@@ -5924,185 +7243,145 @@ function LogsTab() {
5924
7243
  })]
5925
7244
  });
5926
7245
  }
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) => {
7246
+ function TraceSearchView({ onSelectTrace, onCompare }) {
7247
+ const urlState = useURLState();
7248
+ const service = urlState.service;
7249
+ const ds = useMemo(() => {
5958
7250
  const params = {
5959
- serviceName: service,
5960
- limit: filters.limit,
7251
+ limit: urlState.limit ?? 20,
5961
7252
  sortOrder: "DESC"
5962
7253
  };
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);
7254
+ if (service) params.serviceName = service;
7255
+ if (urlState.operation) params.spanName = urlState.operation;
7256
+ if (urlState.lookback) {
7257
+ const ms = LOOKBACK_MS[urlState.lookback];
7258
+ if (ms) params.timestampMin = String((Date.now() - ms) * 1e6);
7259
+ }
7260
+ if (urlState.tsMin) params.timestampMin = urlState.tsMin;
7261
+ if (urlState.tsMax) params.timestampMax = urlState.tsMax;
7262
+ if (urlState.minDuration) {
7263
+ const parsed = parseDuration(urlState.minDuration);
5967
7264
  if (parsed) params.durationMin = parsed;
5968
7265
  }
5969
- if (filters.maxDuration) {
5970
- const parsed = parseDuration(filters.maxDuration);
7266
+ if (urlState.maxDuration) {
7267
+ const parsed = parseDuration(urlState.maxDuration);
5971
7268
  if (parsed) params.durationMax = parsed;
5972
7269
  }
5973
- setDs({
5974
- method: "searchTracesPage",
7270
+ if (urlState.tags) {
7271
+ const tagMap = parseLogfmt(urlState.tags);
7272
+ if (Object.keys(tagMap).length > 0) params.tags = tagMap;
7273
+ }
7274
+ return {
7275
+ method: "searchTraceSummariesPage",
5975
7276
  params
7277
+ };
7278
+ }, [
7279
+ service,
7280
+ urlState.operation,
7281
+ urlState.lookback,
7282
+ urlState.tsMin,
7283
+ urlState.tsMax,
7284
+ urlState.minDuration,
7285
+ urlState.maxDuration,
7286
+ urlState.limit,
7287
+ urlState.tags
7288
+ ]);
7289
+ const handleSearch = useCallback((filters) => {
7290
+ pushURLState({
7291
+ tab: "services",
7292
+ service: filters.service ?? service,
7293
+ operation: filters.operation ?? null,
7294
+ tags: filters.tags ?? null,
7295
+ lookback: filters.lookback ?? null,
7296
+ minDuration: filters.minDuration ?? null,
7297
+ maxDuration: filters.maxDuration ?? null,
7298
+ limit: filters.limit
5976
7299
  });
5977
7300
  }, [service]);
5978
7301
  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(() => {
7302
+ const { data: servicesData } = useKopaiData(useMemo(() => ({ method: "getServices" }), []));
7303
+ const _services = servicesData?.services ?? [];
7304
+ const { data: opsData } = useKopaiData(useMemo(() => service ? {
7305
+ method: "getOperations",
7306
+ params: { serviceName: service }
7307
+ } : void 0, [service]));
7308
+ const operations = opsData?.operations ?? [];
7309
+ const traces = useMemo(() => {
5999
7310
  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();
7311
+ return data.data.map((row) => ({
7312
+ traceId: row.traceId,
7313
+ rootSpanName: row.rootSpanName,
7314
+ serviceName: row.rootServiceName,
7315
+ durationMs: parseInt(row.durationNs, 10) / 1e6,
7316
+ statusCode: row.errorCount > 0 ? "ERROR" : "OK",
7317
+ timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
7318
+ spanCount: row.spanCount,
7319
+ services: row.services,
7320
+ errorCount: row.errorCount
7321
+ }));
6003
7322
  }, [data]);
6004
7323
  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]),
7324
+ services: _services,
7325
+ service: service ?? "",
7326
+ traces,
6051
7327
  operations,
6052
7328
  isLoading: loading,
6053
7329
  error: error ?? void 0,
6054
7330
  onSelectTrace,
6055
- onBack,
7331
+ onCompare,
6056
7332
  onSearch: handleSearch
6057
7333
  });
6058
7334
  }
6059
- function TraceDetailView({ service, traceId, selectedSpanId, onSelectSpan, onBack }) {
7335
+ function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
6060
7336
  const { data, loading, error } = useKopaiData(useMemo(() => ({
6061
7337
  method: "getTrace",
6062
7338
  params: { traceId }
6063
7339
  }), [traceId]));
6064
7340
  return /* @__PURE__ */ jsx(TraceDetail, {
6065
- service,
6066
7341
  traceId,
6067
7342
  rows: data ?? [],
6068
7343
  isLoading: loading,
6069
7344
  error: error ?? void 0,
6070
7345
  selectedSpanId: selectedSpanId ?? void 0,
6071
7346
  onSpanClick: (span) => onSelectSpan(span.spanId),
7347
+ onSpanDeselect: onDeselectSpan,
6072
7348
  onBack
6073
7349
  });
6074
7350
  }
6075
- function ServicesTab({ selectedService, selectedTraceId, selectedSpanId, onSelectService, onSelectTrace, onSelectSpan, onBackToServices, onBackToTraceList }) {
7351
+ function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
6076
7352
  useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
6077
- const backToServicesRef = useRef(onBackToServices);
6078
- backToServicesRef.current = onBackToServices;
6079
- const backToTraceListRef = useRef(onBackToTraceList);
6080
- backToTraceListRef.current = onBackToTraceList;
7353
+ const backRef = useRef(onBack);
7354
+ backRef.current = onBack;
6081
7355
  useEffect(() => {
6082
7356
  const handleKeyDown = (e) => {
6083
7357
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
6084
7358
  if (e.key === "Backspace") {
6085
7359
  e.preventDefault();
6086
- if (selectedTraceId && selectedService) backToTraceListRef.current();
6087
- else if (selectedService) backToServicesRef.current();
7360
+ backRef.current();
6088
7361
  }
6089
7362
  };
6090
7363
  window.addEventListener("keydown", handleKeyDown);
6091
7364
  return () => window.removeEventListener("keydown", handleKeyDown);
6092
- }, [selectedService, selectedTraceId]);
6093
- if (selectedTraceId && selectedService) return /* @__PURE__ */ jsx(TraceDetailView, {
6094
- service: selectedService,
7365
+ }, []);
7366
+ if (compareParam) {
7367
+ const [traceIdA, traceIdB] = compareParam.split(",");
7368
+ if (traceIdA && traceIdB) return /* @__PURE__ */ jsx(TraceComparison, {
7369
+ traceIdA,
7370
+ traceIdB,
7371
+ onBack
7372
+ });
7373
+ }
7374
+ if (selectedTraceId) return /* @__PURE__ */ jsx(TraceDetailView, {
6095
7375
  traceId: selectedTraceId,
6096
7376
  selectedSpanId,
6097
7377
  onSelectSpan,
6098
- onBack: onBackToTraceList
7378
+ onDeselectSpan,
7379
+ onBack
6099
7380
  });
6100
- if (selectedService) return /* @__PURE__ */ jsx(TraceSearchView, {
6101
- service: selectedService,
6102
- onBack: onBackToServices,
6103
- onSelectTrace
7381
+ return /* @__PURE__ */ jsx(TraceSearchView, {
7382
+ onSelectTrace,
7383
+ onCompare
6104
7384
  });
6105
- return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
6106
7385
  }
6107
7386
  const METRICS_TREE = {
6108
7387
  root: "root",
@@ -6209,40 +7488,56 @@ function getDefaultClient() {
6209
7488
  }
6210
7489
  function ObservabilityPage({ client }) {
6211
7490
  const activeClient = client ?? getDefaultClient();
6212
- const { tab: activeTab, service: selectedService, trace: selectedTraceId, span: selectedSpanId } = useURLState();
7491
+ const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
6213
7492
  const handleTabChange = useCallback((tab) => {
6214
7493
  pushURLState({ tab });
6215
7494
  }, []);
6216
- const handleSelectService = useCallback((service) => {
6217
- pushURLState({
6218
- tab: "services",
6219
- service
6220
- });
6221
- }, []);
6222
7495
  const handleSelectTrace = useCallback((traceId) => {
6223
7496
  pushURLState({
7497
+ ...readURLState(),
6224
7498
  tab: "services",
6225
- service: selectedService,
6226
7499
  trace: traceId
6227
7500
  });
6228
- }, [selectedService]);
7501
+ }, []);
6229
7502
  const handleSelectSpan = useCallback((spanId) => {
6230
7503
  pushURLState({
7504
+ ...readURLState(),
6231
7505
  tab: "services",
6232
- service: selectedService,
6233
- trace: selectedTraceId,
6234
7506
  span: spanId
6235
7507
  }, { replace: true });
6236
- }, [selectedService, selectedTraceId]);
6237
- const handleBackToServices = useCallback(() => {
6238
- pushURLState({ tab: "services" });
6239
7508
  }, []);
6240
- const handleBackToTraceList = useCallback(() => {
7509
+ const handleDeselectSpan = useCallback(() => {
7510
+ pushURLState({
7511
+ ...readURLState(),
7512
+ span: null
7513
+ }, { replace: true });
7514
+ }, []);
7515
+ const handleCompare = useCallback((traceIds) => {
6241
7516
  pushURLState({
7517
+ ...readURLState(),
6242
7518
  tab: "services",
6243
- service: selectedService
7519
+ trace: null,
7520
+ span: null,
7521
+ view: null,
7522
+ uiFind: null,
7523
+ viewStart: null,
7524
+ viewEnd: null,
7525
+ compare: traceIds.join(",")
6244
7526
  });
6245
- }, [selectedService]);
7527
+ }, []);
7528
+ const handleBack = useCallback(() => {
7529
+ pushURLState({
7530
+ ...readURLState(),
7531
+ tab: "services",
7532
+ trace: null,
7533
+ span: null,
7534
+ view: null,
7535
+ uiFind: null,
7536
+ viewStart: null,
7537
+ viewEnd: null,
7538
+ compare: null
7539
+ });
7540
+ }, []);
6246
7541
  return /* @__PURE__ */ jsx(KopaiSDKProvider, {
6247
7542
  client: activeClient,
6248
7543
  children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
@@ -6257,21 +7552,20 @@ function ObservabilityPage({ client }) {
6257
7552
  }),
6258
7553
  activeTab === "logs" && /* @__PURE__ */ jsx(LogsTab, {}),
6259
7554
  activeTab === "services" && /* @__PURE__ */ jsx(ServicesTab, {
6260
- selectedService,
6261
7555
  selectedTraceId,
6262
7556
  selectedSpanId,
6263
- onSelectService: handleSelectService,
7557
+ compareParam,
6264
7558
  onSelectTrace: handleSelectTrace,
6265
7559
  onSelectSpan: handleSelectSpan,
6266
- onBackToServices: handleBackToServices,
6267
- onBackToTraceList: handleBackToTraceList
7560
+ onDeselectSpan: handleDeselectSpan,
7561
+ onBack: handleBack,
7562
+ onCompare: handleCompare
6268
7563
  }),
6269
7564
  activeTab === "metrics" && /* @__PURE__ */ jsx(MetricsTab, {})
6270
7565
  ] })
6271
7566
  })
6272
7567
  });
6273
7568
  }
6274
-
6275
7569
  //#endregion
6276
7570
  //#region src/lib/generate-prompt-instructions.ts
6277
7571
  function formatPropType(prop) {
@@ -6401,7 +7695,7 @@ ${JSON.stringify(unifiedSchema)}
6401
7695
 
6402
7696
  ${JSON.stringify(exampleElements)}`;
6403
7697
  }
6404
-
6405
7698
  //#endregion
6406
7699
  export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
7700
+
6407
7701
  //# sourceMappingURL=index.mjs.map