@kopai/ui 0.7.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 (50) hide show
  1. package/dist/index.cjs +2451 -1157
  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 +2399 -1099
  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 +25 -14
  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/OtelLogTimeline.tsx +9 -5
  39. package/src/components/observability/renderers/OtelTraceDetail.tsx +1 -4
  40. package/src/components/observability/shared/TooltipEntryList.tsx +25 -0
  41. package/src/components/observability/utils/flatten-tree.ts +15 -0
  42. package/src/components/observability/utils/time.ts +9 -0
  43. package/src/hooks/use-kopai-data.test.ts +4 -0
  44. package/src/hooks/use-kopai-data.ts +11 -0
  45. package/src/hooks/use-live-logs.test.ts +4 -0
  46. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +1 -1
  47. package/src/lib/component-catalog.ts +15 -0
  48. package/src/pages/observability.test.tsx +16 -12
  49. package/src/pages/observability.tsx +323 -245
  50. package/src/providers/kopai-provider.tsx +4 -0
package/dist/index.mjs CHANGED
@@ -2,12 +2,11 @@ import { createContext, memo, useCallback, useContext, useEffect, useMemo, useRe
2
2
  import { QueryClient, QueryClientProvider, useQuery } from "@tanstack/react-query";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
4
  import { KopaiClient } from "@kopai/sdk";
5
- import z$1, { z } from "zod";
5
+ import { z } from "zod";
6
6
  import { dataFilterSchemas } from "@kopai/core";
7
- import { useVirtualizer } from "@tanstack/react-virtual";
7
+ import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Scatter, ScatterChart, Tooltip, XAxis, YAxis } from "recharts";
8
8
  import { createPortal } from "react-dom";
9
- import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
10
-
9
+ import { useVirtualizer } from "@tanstack/react-virtual";
11
10
  //#region src/providers/kopai-provider.tsx
12
11
  const KopaiSDKContext = createContext(null);
13
12
  const queryClient = new QueryClient({ defaultOptions: { queries: {
@@ -29,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
  }, []);
@@ -2007,26 +2965,80 @@ function TraceTimeline({ rows, onSpanClick, selectedSpanId: externalSelectedSpan
2007
2965
  else setCollapsedIds((prev) => {
2008
2966
  const next = new Set(prev);
2009
2967
  next.delete(selectedItem.span.spanId);
2010
- return next;
2011
- });
2012
- }, [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",
2968
+ return next;
2969
+ });
2970
+ }, [selectedSpanId, flattenedSpans]);
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
  /**
@@ -4009,7 +5044,14 @@ function buildMetrics(rows) {
4009
5044
  for (const row of rows) {
4010
5045
  const name = row.MetricName ?? "unknown";
4011
5046
  const type = row.MetricType;
4012
- if (type === "Histogram" || type === "ExponentialHistogram" || type === "Summary") continue;
5047
+ let value;
5048
+ if (type === "Gauge" || type === "Sum") value = "Value" in row ? row.Value : void 0;
5049
+ else if (type === "Histogram" || type === "ExponentialHistogram" || type === "Summary") {
5050
+ const sum = "Sum" in row ? row.Sum : void 0;
5051
+ const count = "Count" in row ? row.Count : void 0;
5052
+ if (sum != null && count != null && count > 0) value = sum / count;
5053
+ }
5054
+ if (value === void 0) continue;
4013
5055
  if (!metricMap.has(name)) metricMap.set(name, /* @__PURE__ */ new Map());
4014
5056
  if (!metricMeta.has(name)) metricMeta.set(name, {
4015
5057
  description: row.MetricDescription ?? "",
@@ -4028,8 +5070,6 @@ function buildMetrics(rows) {
4028
5070
  dataPoints: []
4029
5071
  });
4030
5072
  }
4031
- if (!("Value" in row)) continue;
4032
- const value = row.Value;
4033
5073
  const timestamp = parseInt(row.TimeUnix, 10) / 1e6;
4034
5074
  seriesMap.get(seriesKey).dataPoints.push({
4035
5075
  timestamp,
@@ -4275,18 +5315,11 @@ function CustomTooltip({ active, payload, label, formatTime, formatValue, displa
4275
5315
  children: [/* @__PURE__ */ jsx("p", {
4276
5316
  className: "text-gray-400 text-xs mb-2",
4277
5317
  children: formatTime(typeof label === "number" ? label : Number(label))
4278
- }), payload.map((entry, i) => /* @__PURE__ */ jsxs("p", {
4279
- className: "text-sm",
4280
- style: { color: entry.color },
4281
- children: [
4282
- /* @__PURE__ */ jsxs("span", {
4283
- className: "font-medium",
4284
- children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
4285
- }),
4286
- " ",
4287
- formatValue(entry.value)
4288
- ]
4289
- }, i))]
5318
+ }), /* @__PURE__ */ jsx(TooltipEntryList, {
5319
+ payload,
5320
+ displayLabelMap,
5321
+ formatValue
5322
+ })]
4290
5323
  });
4291
5324
  }
4292
5325
  function MetricLoadingSkeleton({ height = 400 }) {
@@ -4333,7 +5366,6 @@ function MetricLoadingSkeleton({ height = 400 }) {
4333
5366
  })
4334
5367
  });
4335
5368
  }
4336
-
4337
5369
  //#endregion
4338
5370
  //#region src/components/observability/MetricHistogram/index.tsx
4339
5371
  /**
@@ -4347,6 +5379,9 @@ const COLORS = [
4347
5379
  "#00C49F",
4348
5380
  "#0088FE"
4349
5381
  ];
5382
+ function isBucketData(v) {
5383
+ return typeof v === "object" && v !== null && "bucket" in v && "lowerBound" in v;
5384
+ }
4350
5385
  const defaultFormatBucketLabel = (bound, index, bounds) => {
4351
5386
  if (index === 0) return `≤${bound}`;
4352
5387
  if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
@@ -4389,7 +5424,8 @@ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
4389
5424
  };
4390
5425
  buckets.push(bucket);
4391
5426
  }
4392
- bucket[seriesName] = (bucket[seriesName] ?? 0) + count;
5427
+ const prev = bucket[seriesName];
5428
+ bucket[seriesName] = (typeof prev === "number" ? prev : 0) + count;
4393
5429
  }
4394
5430
  }
4395
5431
  buckets.sort((a, b) => a.lowerBound - b.lowerBound);
@@ -4526,25 +5562,18 @@ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: u
4526
5562
  }
4527
5563
  function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
4528
5564
  if (!active || !payload?.length) return null;
4529
- const bucket = payload[0]?.payload;
4530
- if (!bucket) return null;
5565
+ const raw = payload[0]?.payload;
5566
+ if (!isBucketData(raw)) return null;
4531
5567
  return /* @__PURE__ */ jsxs("div", {
4532
5568
  className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
4533
5569
  children: [/* @__PURE__ */ jsxs("p", {
4534
5570
  className: "text-gray-300 text-sm font-medium mb-2",
4535
- children: ["Bucket: ", boundsScale ? `${formatDisplayValue(bucket.lowerBound, boundsScale)} – ${bucket.upperBound === Infinity ? "∞" : formatDisplayValue(bucket.upperBound, boundsScale)}` : bucket.bucket]
4536
- }), payload.map((entry, i) => /* @__PURE__ */ jsxs("p", {
4537
- className: "text-sm",
4538
- style: { color: entry.color },
4539
- children: [
4540
- /* @__PURE__ */ jsxs("span", {
4541
- className: "font-medium",
4542
- children: [displayLabelMap.get(entry.dataKey) ?? entry.dataKey, ":"]
4543
- }),
4544
- " ",
4545
- formatValue(entry.value)
4546
- ]
4547
- }, 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
+ })]
4548
5577
  });
4549
5578
  }
4550
5579
  function HistogramLoadingSkeleton({ height = 400 }) {
@@ -4584,7 +5613,6 @@ function HistogramLoadingSkeleton({ height = 400 }) {
4584
5613
  })
4585
5614
  });
4586
5615
  }
4587
-
4588
5616
  //#endregion
4589
5617
  //#region src/components/observability/MetricStat/index.tsx
4590
5618
  /**
@@ -4777,7 +5805,6 @@ function TrendIndicator({ direction, value }) {
4777
5805
  children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
4778
5806
  });
4779
5807
  }
4780
-
4781
5808
  //#endregion
4782
5809
  //#region src/components/observability/MetricTable/index.tsx
4783
5810
  /**
@@ -4917,7 +5944,6 @@ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValu
4917
5944
  })]
4918
5945
  });
4919
5946
  }
4920
-
4921
5947
  //#endregion
4922
5948
  //#region src/lib/renderer.tsx
4923
5949
  /**
@@ -5022,7 +6048,6 @@ function Renderer({ tree, registry, fallback }) {
5022
6048
  fallback
5023
6049
  });
5024
6050
  }
5025
-
5026
6051
  //#endregion
5027
6052
  //#region src/lib/catalog.ts
5028
6053
  const dashboardCatalog = createCatalog({
@@ -5222,8 +6247,7 @@ const dashboardCatalog = createCatalog({
5222
6247
  }
5223
6248
  }
5224
6249
  });
5225
- const componentList = Object.keys(dashboardCatalog.components);
5226
-
6250
+ Object.keys(dashboardCatalog.components);
5227
6251
  //#endregion
5228
6252
  //#region src/components/dashboard/Badge/index.tsx
5229
6253
  function Badge({ element }) {
@@ -5247,7 +6271,6 @@ function Badge({ element }) {
5247
6271
  children: text
5248
6272
  });
5249
6273
  }
5250
-
5251
6274
  //#endregion
5252
6275
  //#region src/components/dashboard/Card/index.tsx
5253
6276
  function Card({ element, children }) {
@@ -5288,7 +6311,6 @@ function Card({ element, children }) {
5288
6311
  })]
5289
6312
  });
5290
6313
  }
5291
-
5292
6314
  //#endregion
5293
6315
  //#region src/components/dashboard/Divider/index.tsx
5294
6316
  function Divider({ element }) {
@@ -5326,7 +6348,6 @@ function Divider({ element }) {
5326
6348
  margin: "16px 0"
5327
6349
  } });
5328
6350
  }
5329
-
5330
6351
  //#endregion
5331
6352
  //#region src/components/dashboard/Empty/index.tsx
5332
6353
  function Empty({ element, onAction }) {
@@ -5369,7 +6390,6 @@ function Empty({ element, onAction }) {
5369
6390
  ]
5370
6391
  });
5371
6392
  }
5372
-
5373
6393
  //#endregion
5374
6394
  //#region src/components/dashboard/Grid/index.tsx
5375
6395
  function Grid({ element, children }) {
@@ -5387,7 +6407,6 @@ function Grid({ element, children }) {
5387
6407
  children
5388
6408
  });
5389
6409
  }
5390
-
5391
6410
  //#endregion
5392
6411
  //#region src/components/dashboard/Heading/index.tsx
5393
6412
  function Heading({ element }) {
@@ -5406,7 +6425,6 @@ function Heading({ element }) {
5406
6425
  children: text
5407
6426
  });
5408
6427
  }
5409
-
5410
6428
  //#endregion
5411
6429
  //#region src/components/dashboard/Stack/index.tsx
5412
6430
  function Stack({ element, children }) {
@@ -5430,7 +6448,6 @@ function Stack({ element, children }) {
5430
6448
  children
5431
6449
  });
5432
6450
  }
5433
-
5434
6451
  //#endregion
5435
6452
  //#region src/components/dashboard/Text/index.tsx
5436
6453
  function Text({ element }) {
@@ -5449,7 +6466,6 @@ function Text({ element }) {
5449
6466
  children: content
5450
6467
  });
5451
6468
  }
5452
-
5453
6469
  //#endregion
5454
6470
  //#region src/components/observability/renderers/OtelLogTimeline.tsx
5455
6471
  function OtelLogTimeline(props) {
@@ -5461,13 +6477,16 @@ function OtelLogTimeline(props) {
5461
6477
  children: "No data source"
5462
6478
  });
5463
6479
  const response = props.data;
5464
- return /* @__PURE__ */ jsx(LogTimeline, {
5465
- rows: response?.data ?? [],
5466
- isLoading: props.loading,
5467
- error: props.error ?? void 0
6480
+ return /* @__PURE__ */ jsx("div", {
6481
+ style: { height: props.element.props.height ?? 600 },
6482
+ className: "flex flex-col min-h-0",
6483
+ children: /* @__PURE__ */ jsx(LogTimeline, {
6484
+ rows: response?.data ?? [],
6485
+ isLoading: props.loading,
6486
+ error: props.error ?? void 0
6487
+ })
5468
6488
  });
5469
6489
  }
5470
-
5471
6490
  //#endregion
5472
6491
  //#region src/components/observability/renderers/OtelMetricDiscovery.tsx
5473
6492
  const TYPE_ORDER = {
@@ -5545,7 +6564,6 @@ function OtelMetricDiscovery(props) {
5545
6564
  })
5546
6565
  });
5547
6566
  }
5548
-
5549
6567
  //#endregion
5550
6568
  //#region src/components/observability/renderers/OtelMetricHistogram.tsx
5551
6569
  function OtelMetricHistogram(props) {
@@ -5566,7 +6584,6 @@ function OtelMetricHistogram(props) {
5566
6584
  unit: props.element.props.unit ?? void 0
5567
6585
  });
5568
6586
  }
5569
-
5570
6587
  //#endregion
5571
6588
  //#region src/components/observability/renderers/OtelMetricStat.tsx
5572
6589
  function OtelMetricStat(props) {
@@ -5587,7 +6604,6 @@ function OtelMetricStat(props) {
5587
6604
  formatValue: formatOtelValue
5588
6605
  });
5589
6606
  }
5590
-
5591
6607
  //#endregion
5592
6608
  //#region src/components/observability/renderers/OtelMetricTable.tsx
5593
6609
  function OtelMetricTable(props) {
@@ -5606,7 +6622,6 @@ function OtelMetricTable(props) {
5606
6622
  maxRows: props.element.props.maxRows ?? 100
5607
6623
  });
5608
6624
  }
5609
-
5610
6625
  //#endregion
5611
6626
  //#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
5612
6627
  function OtelMetricTimeSeries(props) {
@@ -5628,7 +6643,6 @@ function OtelMetricTimeSeries(props) {
5628
6643
  unit: props.element.props.unit ?? void 0
5629
6644
  });
5630
6645
  }
5631
-
5632
6646
  //#endregion
5633
6647
  //#region src/components/observability/renderers/OtelTraceDetail.tsx
5634
6648
  function OtelTraceDetail(props) {
@@ -5640,19 +6654,15 @@ function OtelTraceDetail(props) {
5640
6654
  children: "No data source"
5641
6655
  });
5642
6656
  const rows = props.data?.data ?? [];
5643
- const firstRow = rows[0];
5644
- const service = firstRow?.ServiceName ?? "unknown";
5645
- const traceId = firstRow?.TraceId ?? "";
6657
+ const traceId = rows[0]?.TraceId ?? "";
5646
6658
  return /* @__PURE__ */ jsx(TraceDetail, {
5647
6659
  rows,
5648
6660
  isLoading: props.loading,
5649
6661
  error: props.error ?? void 0,
5650
- service,
5651
6662
  traceId,
5652
6663
  onBack: () => {}
5653
6664
  });
5654
6665
  }
5655
-
5656
6666
  //#endregion
5657
6667
  //#region src/components/observability/DynamicDashboard/index.tsx
5658
6668
  const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
@@ -5678,24 +6688,275 @@ function DynamicDashboard({ kopaiClient, uiTree }) {
5678
6688
  children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
5679
6689
  });
5680
6690
  }
5681
-
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
+ }
5682
6944
  //#endregion
5683
6945
  //#region src/components/observability/ServiceList/shortcuts.ts
5684
6946
  const SERVICES_SHORTCUTS = {
5685
- name: "Services",
6947
+ name: "Traces",
5686
6948
  shortcuts: [{
5687
6949
  keys: ["Backspace"],
5688
6950
  description: "Go back"
5689
6951
  }]
5690
6952
  };
5691
-
5692
6953
  //#endregion
5693
6954
  //#region src/pages/observability.tsx
5694
6955
  const TABS = [
5695
6956
  {
5696
6957
  key: "services",
5697
- label: "Services",
5698
- shortcutKey: "S"
6958
+ label: "Traces",
6959
+ shortcutKey: "T"
5699
6960
  },
5700
6961
  {
5701
6962
  key: "logs",
@@ -5715,20 +6976,53 @@ function readURLState() {
5715
6976
  const span = params.get("span");
5716
6977
  const dashboardId = params.get("dashboardId");
5717
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;
5718
6982
  return {
5719
- tab: service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services",
6983
+ tab,
5720
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"),
5721
6994
  trace,
5722
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"),
5723
7001
  dashboardId
5724
7002
  };
5725
7003
  }
5726
7004
  function pushURLState(state, { replace = false } = {}) {
5727
7005
  const params = new URLSearchParams();
5728
7006
  if (state.tab !== "services") params.set("tab", state.tab);
5729
- if (state.service) params.set("service", state.service);
5730
- if (state.trace) params.set("trace", state.trace);
5731
- 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
+ }
5732
7026
  const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
5733
7027
  if (dashboardId) params.set("dashboardId", dashboardId);
5734
7028
  const qs = params.toString();
@@ -5745,8 +7039,22 @@ let _cachedSearch = "";
5745
7039
  let _cachedState = {
5746
7040
  tab: "services",
5747
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,
5748
7051
  trace: null,
5749
7052
  span: null,
7053
+ view: null,
7054
+ uiFind: null,
7055
+ compare: null,
7056
+ viewStart: null,
7057
+ viewEnd: null,
5750
7058
  dashboardId: null
5751
7059
  };
5752
7060
  function getURLSnapshot() {
@@ -5856,6 +7164,26 @@ function parseDuration(input) {
5856
7164
  s: 1e9
5857
7165
  }[unit]));
5858
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
+ };
5859
7187
  function LogsTab() {
5860
7188
  const [initState] = useState(() => readLogFilters());
5861
7189
  const [filters, setFilters] = useState(initState.filters);
@@ -5915,187 +7243,146 @@ function LogsTab() {
5915
7243
  })]
5916
7244
  });
5917
7245
  }
5918
- const SERVICES_DS = {
5919
- method: "searchTracesPage",
5920
- params: {
5921
- limit: 1e3,
5922
- sortOrder: "DESC"
5923
- }
5924
- };
5925
- function ServiceListView({ onSelect }) {
5926
- const { data, loading, error } = useKopaiData(SERVICES_DS);
5927
- return /* @__PURE__ */ jsx(ServiceList, {
5928
- services: useMemo(() => {
5929
- if (!data?.data) return [];
5930
- const names = /* @__PURE__ */ new Set();
5931
- for (const row of data.data) names.add(row.ServiceName ?? "unknown");
5932
- return Array.from(names).sort().map((name) => ({ name }));
5933
- }, [data]),
5934
- isLoading: loading,
5935
- error: error ?? void 0,
5936
- onSelect
5937
- });
5938
- }
5939
- function TraceSearchView({ service, onBack, onSelectTrace }) {
5940
- const [ds, setDs] = useState(() => ({
5941
- method: "searchTracesPage",
5942
- params: {
5943
- serviceName: service,
5944
- limit: 20,
5945
- sortOrder: "DESC"
5946
- }
5947
- }));
5948
- const handleSearch = useCallback((filters) => {
7246
+ function TraceSearchView({ onSelectTrace, onCompare }) {
7247
+ const urlState = useURLState();
7248
+ const service = urlState.service;
7249
+ const ds = useMemo(() => {
5949
7250
  const params = {
5950
- serviceName: service,
5951
- limit: filters.limit,
7251
+ limit: urlState.limit ?? 20,
5952
7252
  sortOrder: "DESC"
5953
7253
  };
5954
- if (filters.operation) params.spanName = filters.operation;
5955
- if (filters.lookbackMs) params.timestampMin = String((Date.now() - filters.lookbackMs) * 1e6);
5956
- if (filters.minDuration) {
5957
- 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);
5958
7264
  if (parsed) params.durationMin = parsed;
5959
7265
  }
5960
- if (filters.maxDuration) {
5961
- const parsed = parseDuration(filters.maxDuration);
7266
+ if (urlState.maxDuration) {
7267
+ const parsed = parseDuration(urlState.maxDuration);
5962
7268
  if (parsed) params.durationMax = parsed;
5963
7269
  }
5964
- setDs({
5965
- 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",
5966
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
5967
7299
  });
5968
7300
  }, [service]);
5969
7301
  const { data, loading, error } = useKopaiData(ds);
5970
- const client = useKopaiSDK();
5971
- const [fullTraces, setFullTraces] = useState(() => /* @__PURE__ */ new Map());
5972
- useEffect(() => {
5973
- if (!data?.data?.length) {
5974
- setFullTraces(/* @__PURE__ */ new Map());
5975
- return;
5976
- }
5977
- const traceIds = [...new Set(data.data.map((r) => r.TraceId))];
5978
- const ac = new AbortController();
5979
- Promise.allSettled(traceIds.map((tid) => client.getTrace(tid, { signal: ac.signal }).then((spans) => [tid, spans]))).then((results) => {
5980
- if (!ac.signal.aborted) {
5981
- const entries = results.filter((r) => r.status === "fulfilled").map((r) => r.value);
5982
- setFullTraces(new Map(entries));
5983
- }
5984
- }).catch((err) => {
5985
- if (!ac.signal.aborted) console.error("Failed to fetch full traces", err);
5986
- });
5987
- return () => ac.abort();
5988
- }, [data, client]);
5989
- 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(() => {
5990
7310
  if (!data?.data) return [];
5991
- const set = /* @__PURE__ */ new Set();
5992
- for (const row of data.data) if (row.SpanName) set.add(row.SpanName);
5993
- 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
+ }));
5994
7322
  }, [data]);
5995
7323
  return /* @__PURE__ */ jsx(TraceSearch, {
5996
- service,
5997
- traces: useMemo(() => {
5998
- if (!data?.data) return [];
5999
- const grouped = /* @__PURE__ */ new Map();
6000
- for (const row of data.data) {
6001
- const tid = row.TraceId;
6002
- if (!grouped.has(tid)) grouped.set(tid, []);
6003
- grouped.get(tid).push(row);
6004
- }
6005
- return Array.from(grouped.entries()).map(([traceId, searchSpans]) => {
6006
- const spans = fullTraces.get(traceId) ?? searchSpans;
6007
- const root = spans.find((s) => !s.ParentSpanId) ?? spans[0];
6008
- const durationNs = root.Duration ? parseInt(root.Duration, 10) : 0;
6009
- const svcMap = /* @__PURE__ */ new Map();
6010
- let errorCount = 0;
6011
- for (const s of spans) {
6012
- const svcName = s.ServiceName ?? "unknown";
6013
- const entry = svcMap.get(svcName) ?? {
6014
- count: 0,
6015
- hasError: false
6016
- };
6017
- entry.count++;
6018
- if (s.StatusCode === "ERROR") {
6019
- entry.hasError = true;
6020
- errorCount++;
6021
- }
6022
- svcMap.set(svcName, entry);
6023
- }
6024
- const services = Array.from(svcMap.entries()).map(([name, v]) => ({
6025
- name,
6026
- count: v.count,
6027
- hasError: v.hasError
6028
- })).sort((a, b) => b.count - a.count);
6029
- return {
6030
- traceId,
6031
- rootSpanName: root.SpanName ?? "unknown",
6032
- serviceName: root.ServiceName ?? "unknown",
6033
- durationMs: durationNs / 1e6,
6034
- statusCode: root.StatusCode ?? "UNSET",
6035
- timestampMs: parseInt(root.Timestamp, 10) / 1e6,
6036
- spanCount: spans.length,
6037
- services,
6038
- errorCount
6039
- };
6040
- });
6041
- }, [data, fullTraces]),
7324
+ services: _services,
7325
+ service: service ?? "",
7326
+ traces,
6042
7327
  operations,
6043
7328
  isLoading: loading,
6044
7329
  error: error ?? void 0,
6045
7330
  onSelectTrace,
6046
- onBack,
7331
+ onCompare,
6047
7332
  onSearch: handleSearch
6048
7333
  });
6049
7334
  }
6050
- function TraceDetailView({ service, traceId, selectedSpanId, onSelectSpan, onBack }) {
7335
+ function TraceDetailView({ traceId, selectedSpanId, onSelectSpan, onDeselectSpan, onBack }) {
6051
7336
  const { data, loading, error } = useKopaiData(useMemo(() => ({
6052
7337
  method: "getTrace",
6053
7338
  params: { traceId }
6054
7339
  }), [traceId]));
6055
7340
  return /* @__PURE__ */ jsx(TraceDetail, {
6056
- service,
6057
7341
  traceId,
6058
7342
  rows: data ?? [],
6059
7343
  isLoading: loading,
6060
7344
  error: error ?? void 0,
6061
7345
  selectedSpanId: selectedSpanId ?? void 0,
6062
7346
  onSpanClick: (span) => onSelectSpan(span.spanId),
7347
+ onSpanDeselect: onDeselectSpan,
6063
7348
  onBack
6064
7349
  });
6065
7350
  }
6066
- function ServicesTab({ selectedService, selectedTraceId, selectedSpanId, onSelectService, onSelectTrace, onSelectSpan, onBackToServices, onBackToTraceList }) {
7351
+ function ServicesTab({ selectedTraceId, selectedSpanId, compareParam, onSelectTrace, onSelectSpan, onDeselectSpan, onBack, onCompare }) {
6067
7352
  useRegisterShortcuts("services-tab", SERVICES_SHORTCUTS);
6068
- const backToServicesRef = useRef(onBackToServices);
6069
- backToServicesRef.current = onBackToServices;
6070
- const backToTraceListRef = useRef(onBackToTraceList);
6071
- backToTraceListRef.current = onBackToTraceList;
7353
+ const backRef = useRef(onBack);
7354
+ backRef.current = onBack;
6072
7355
  useEffect(() => {
6073
7356
  const handleKeyDown = (e) => {
6074
7357
  if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLSelectElement) return;
6075
7358
  if (e.key === "Backspace") {
6076
7359
  e.preventDefault();
6077
- if (selectedTraceId && selectedService) backToTraceListRef.current();
6078
- else if (selectedService) backToServicesRef.current();
7360
+ backRef.current();
6079
7361
  }
6080
7362
  };
6081
7363
  window.addEventListener("keydown", handleKeyDown);
6082
7364
  return () => window.removeEventListener("keydown", handleKeyDown);
6083
- }, [selectedService, selectedTraceId]);
6084
- if (selectedTraceId && selectedService) return /* @__PURE__ */ jsx(TraceDetailView, {
6085
- 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, {
6086
7375
  traceId: selectedTraceId,
6087
7376
  selectedSpanId,
6088
7377
  onSelectSpan,
6089
- onBack: onBackToTraceList
7378
+ onDeselectSpan,
7379
+ onBack
6090
7380
  });
6091
- if (selectedService) return /* @__PURE__ */ jsx(TraceSearchView, {
6092
- service: selectedService,
6093
- onBack: onBackToServices,
6094
- onSelectTrace
7381
+ return /* @__PURE__ */ jsx(TraceSearchView, {
7382
+ onSelectTrace,
7383
+ onCompare
6095
7384
  });
6096
- return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
6097
7385
  }
6098
- const DASHBOARDS_API_BASE = "/dashboards";
6099
7386
  const METRICS_TREE = {
6100
7387
  root: "root",
6101
7388
  elements: {
@@ -6156,14 +7443,12 @@ const METRICS_TREE = {
6156
7443
  }
6157
7444
  }
6158
7445
  };
6159
- function useDashboardTree(dashboardId) {
7446
+ function useDashboardTree(client, dashboardId) {
6160
7447
  const { data, isFetching, error } = useQuery({
6161
7448
  queryKey: ["dashboard-tree", dashboardId],
6162
7449
  queryFn: async ({ signal }) => {
6163
- const res = await fetch(`${DASHBOARDS_API_BASE}/${dashboardId}`, { signal });
6164
- if (!res.ok) throw new Error(`Failed to load dashboard: ${res.status}`);
6165
- const json = await res.json();
6166
- const parsed = observabilityCatalog.uiTreeSchema.safeParse(json.uiTree);
7450
+ const dashboard = await client.getDashboard(dashboardId, { signal });
7451
+ const parsed = observabilityCatalog.uiTreeSchema.safeParse(dashboard.uiTree);
6167
7452
  if (!parsed.success) {
6168
7453
  const issue = parsed.error.issues[0];
6169
7454
  const path = issue?.path.length ? issue.path.join(".") + ": " : "";
@@ -6182,7 +7467,7 @@ function useDashboardTree(dashboardId) {
6182
7467
  function MetricsTab() {
6183
7468
  const kopaiClient = useKopaiSDK();
6184
7469
  const { dashboardId } = useURLState();
6185
- const { loading, error, tree } = useDashboardTree(dashboardId);
7470
+ const { loading, error, tree } = useDashboardTree(kopaiClient, dashboardId);
6186
7471
  if (loading) return /* @__PURE__ */ jsx("p", {
6187
7472
  className: "text-muted-foreground text-sm",
6188
7473
  children: "Loading dashboard..."
@@ -6203,40 +7488,56 @@ function getDefaultClient() {
6203
7488
  }
6204
7489
  function ObservabilityPage({ client }) {
6205
7490
  const activeClient = client ?? getDefaultClient();
6206
- const { tab: activeTab, service: selectedService, trace: selectedTraceId, span: selectedSpanId } = useURLState();
7491
+ const { tab: activeTab, trace: selectedTraceId, span: selectedSpanId, compare: compareParam } = useURLState();
6207
7492
  const handleTabChange = useCallback((tab) => {
6208
7493
  pushURLState({ tab });
6209
7494
  }, []);
6210
- const handleSelectService = useCallback((service) => {
6211
- pushURLState({
6212
- tab: "services",
6213
- service
6214
- });
6215
- }, []);
6216
7495
  const handleSelectTrace = useCallback((traceId) => {
6217
7496
  pushURLState({
7497
+ ...readURLState(),
6218
7498
  tab: "services",
6219
- service: selectedService,
6220
7499
  trace: traceId
6221
7500
  });
6222
- }, [selectedService]);
7501
+ }, []);
6223
7502
  const handleSelectSpan = useCallback((spanId) => {
6224
7503
  pushURLState({
7504
+ ...readURLState(),
6225
7505
  tab: "services",
6226
- service: selectedService,
6227
- trace: selectedTraceId,
6228
7506
  span: spanId
6229
7507
  }, { replace: true });
6230
- }, [selectedService, selectedTraceId]);
6231
- const handleBackToServices = useCallback(() => {
6232
- pushURLState({ tab: "services" });
6233
7508
  }, []);
6234
- const handleBackToTraceList = useCallback(() => {
7509
+ const handleDeselectSpan = useCallback(() => {
7510
+ pushURLState({
7511
+ ...readURLState(),
7512
+ span: null
7513
+ }, { replace: true });
7514
+ }, []);
7515
+ const handleCompare = useCallback((traceIds) => {
6235
7516
  pushURLState({
7517
+ ...readURLState(),
6236
7518
  tab: "services",
6237
- service: selectedService
7519
+ trace: null,
7520
+ span: null,
7521
+ view: null,
7522
+ uiFind: null,
7523
+ viewStart: null,
7524
+ viewEnd: null,
7525
+ compare: traceIds.join(",")
6238
7526
  });
6239
- }, [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
+ }, []);
6240
7541
  return /* @__PURE__ */ jsx(KopaiSDKProvider, {
6241
7542
  client: activeClient,
6242
7543
  children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
@@ -6251,21 +7552,20 @@ function ObservabilityPage({ client }) {
6251
7552
  }),
6252
7553
  activeTab === "logs" && /* @__PURE__ */ jsx(LogsTab, {}),
6253
7554
  activeTab === "services" && /* @__PURE__ */ jsx(ServicesTab, {
6254
- selectedService,
6255
7555
  selectedTraceId,
6256
7556
  selectedSpanId,
6257
- onSelectService: handleSelectService,
7557
+ compareParam,
6258
7558
  onSelectTrace: handleSelectTrace,
6259
7559
  onSelectSpan: handleSelectSpan,
6260
- onBackToServices: handleBackToServices,
6261
- onBackToTraceList: handleBackToTraceList
7560
+ onDeselectSpan: handleDeselectSpan,
7561
+ onBack: handleBack,
7562
+ onCompare: handleCompare
6262
7563
  }),
6263
7564
  activeTab === "metrics" && /* @__PURE__ */ jsx(MetricsTab, {})
6264
7565
  ] })
6265
7566
  })
6266
7567
  });
6267
7568
  }
6268
-
6269
7569
  //#endregion
6270
7570
  //#region src/lib/generate-prompt-instructions.ts
6271
7571
  function formatPropType(prop) {
@@ -6395,7 +7695,7 @@ ${JSON.stringify(unifiedSchema)}
6395
7695
 
6396
7696
  ${JSON.stringify(exampleElements)}`;
6397
7697
  }
6398
-
6399
7698
  //#endregion
6400
7699
  export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
7700
+
6401
7701
  //# sourceMappingURL=index.mjs.map