@kopai/ui 0.4.0 → 0.6.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 (36) hide show
  1. package/README.md +23 -0
  2. package/dist/index.cjs +1598 -233
  3. package/dist/index.d.cts +566 -4
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +565 -3
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +1597 -209
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +13 -12
  10. package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +234 -0
  11. package/src/components/observability/DynamicDashboard/index.tsx +64 -0
  12. package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +10 -1
  13. package/src/components/observability/MetricHistogram/index.tsx +85 -19
  14. package/src/components/observability/MetricStat/MetricStat.stories.tsx +2 -1
  15. package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +23 -1
  16. package/src/components/observability/MetricTimeSeries/index.tsx +70 -27
  17. package/src/components/observability/__fixtures__/metrics.ts +97 -0
  18. package/src/components/observability/index.ts +3 -0
  19. package/src/components/observability/renderers/OtelLogTimeline.tsx +28 -0
  20. package/src/components/observability/renderers/OtelMetricHistogram.tsx +2 -0
  21. package/src/components/observability/renderers/OtelMetricStat.tsx +1 -13
  22. package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +2 -0
  23. package/src/components/observability/renderers/OtelTraceDetail.tsx +35 -0
  24. package/src/components/observability/renderers/index.ts +2 -0
  25. package/src/components/observability/utils/attributes.ts +7 -0
  26. package/src/components/observability/utils/units.test.ts +116 -0
  27. package/src/components/observability/utils/units.ts +132 -0
  28. package/src/index.ts +1 -0
  29. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +7 -1
  30. package/src/lib/generate-prompt-instructions.test.ts +1 -1
  31. package/src/lib/generate-prompt-instructions.ts +18 -6
  32. package/src/lib/observability-catalog.ts +7 -1
  33. package/src/lib/renderer.tsx +1 -1
  34. package/src/pages/observability.test.tsx +124 -0
  35. package/src/pages/observability.tsx +71 -36
  36. package/src/lib/dashboard-datasource.ts +0 -76
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import z$1, { z } from "zod";
6
6
  import { dataFilterSchemas } from "@kopai/core";
7
7
  import { useVirtualizer } from "@tanstack/react-virtual";
8
8
  import { createPortal } from "react-dom";
9
- import "recharts";
9
+ import { Area, AreaChart, Bar, BarChart, Brush, CartesianGrid, Cell, Legend, Line, LineChart, ReferenceLine, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
10
10
 
11
11
  //#region src/providers/kopai-provider.tsx
12
12
  const KopaiSDKContext = createContext(null);
@@ -376,13 +376,19 @@ const observabilityCatalog = createCatalog({
376
376
  MetricTimeSeries: {
377
377
  props: z.object({
378
378
  height: z.number().nullable(),
379
- showBrush: z.boolean().nullable()
379
+ showBrush: z.boolean().nullable(),
380
+ yAxisLabel: z.string().nullable(),
381
+ unit: z.string().nullable()
380
382
  }),
381
383
  hasChildren: false,
382
384
  description: "Time series line chart for Gauge/Sum metrics"
383
385
  },
384
386
  MetricHistogram: {
385
- props: z.object({ height: z.number().nullable() }),
387
+ props: z.object({
388
+ height: z.number().nullable(),
389
+ yAxisLabel: z.string().nullable(),
390
+ unit: z.string().nullable()
391
+ }),
386
392
  hasChildren: false,
387
393
  description: "Histogram bar chart for distribution metrics"
388
394
  },
@@ -407,111 +413,6 @@ const observabilityCatalog = createCatalog({
407
413
  }
408
414
  });
409
415
 
410
- //#endregion
411
- //#region src/lib/renderer.tsx
412
- /**
413
- * Creates a typed Renderer component bound to a catalog and component implementations.
414
- *
415
- * @param _catalog - The catalog created via createCatalog (used for type inference)
416
- * @param components - React component implementations matching catalog definitions
417
- * @returns A Renderer component that only needs `tree` and optional `fallback`
418
- *
419
- * @example
420
- * ```tsx
421
- * const DashboardRenderer = createRendererFromCatalog(catalog, {
422
- * Card: ({ element, children }) => <div className="card">{children}</div>,
423
- * Table: ({ element, data }) => <table>...</table>,
424
- * });
425
- *
426
- * <DashboardRenderer tree={uiTree} />
427
- * ```
428
- */
429
- function createRendererFromCatalog(_catalog, components) {
430
- return function CatalogRenderer({ tree, fallback }) {
431
- return /* @__PURE__ */ jsx(Renderer, {
432
- tree,
433
- registry: components,
434
- fallback
435
- });
436
- };
437
- }
438
- /**
439
- * Wrapper component for elements with dataSource
440
- */
441
- function DataSourceElement({ element, Component, children }) {
442
- const [paramsOverride, setParamsOverride] = useState({});
443
- const { data, loading, error, refetch } = useKopaiData(useMemo(() => {
444
- if (!element.dataSource) return void 0;
445
- return {
446
- ...element.dataSource,
447
- params: {
448
- ...element.dataSource.params,
449
- ...paramsOverride
450
- }
451
- };
452
- }, [element.dataSource, paramsOverride]));
453
- return /* @__PURE__ */ jsx(Component, {
454
- element,
455
- hasData: true,
456
- data,
457
- loading,
458
- error,
459
- refetch,
460
- updateParams: useCallback((params) => {
461
- setParamsOverride((prev) => ({
462
- ...prev,
463
- ...params
464
- }));
465
- }, []),
466
- children
467
- });
468
- }
469
- /**
470
- * Internal element renderer - recursively renders elements and children
471
- */
472
- function ElementRenderer({ element, tree, registry, fallback }) {
473
- const Component = registry[element.type] ?? fallback;
474
- if (!Component) {
475
- console.warn(`No renderer for component type: ${element.type}`);
476
- return null;
477
- }
478
- const children = element.children?.map((childKey) => {
479
- const childElement = tree.elements[childKey];
480
- if (!childElement) return null;
481
- return /* @__PURE__ */ jsx(ElementRenderer, {
482
- element: childElement,
483
- tree,
484
- registry,
485
- fallback
486
- }, childKey);
487
- });
488
- if (element.dataSource) return /* @__PURE__ */ jsx(DataSourceElement, {
489
- element,
490
- Component,
491
- children
492
- });
493
- return /* @__PURE__ */ jsx(Component, {
494
- element,
495
- hasData: false,
496
- children
497
- });
498
- }
499
- /**
500
- * Renders a UITree using a component registry.
501
- * Prefer using {@link createRendererFromCatalog} for type-safe rendering.
502
- */
503
- function Renderer({ tree, registry, fallback }) {
504
- if (!tree || !tree.root) return null;
505
- const rootElement = tree.elements[tree.root];
506
- if (!rootElement) return null;
507
- return /* @__PURE__ */ jsx(ElementRenderer, {
508
- element: rootElement,
509
- tree,
510
- registry,
511
- fallback
512
- });
513
- }
514
-
515
416
  //#endregion
516
417
  //#region src/components/observability/TabBar/index.tsx
517
418
  function renderLabel(label, shortcutKey) {
@@ -1000,7 +901,7 @@ function TraceHeader({ trace }) {
1000
901
 
1001
902
  //#endregion
1002
903
  //#region src/components/observability/TraceTimeline/Tooltip.tsx
1003
- function Tooltip({ content, children }) {
904
+ function Tooltip$1({ content, children }) {
1004
905
  const [isVisible, setIsVisible] = useState(false);
1005
906
  const [position, setPosition] = useState({
1006
907
  x: 0,
@@ -1037,7 +938,7 @@ function TimelineBar({ span, relativeStart, relativeDuration }) {
1037
938
  const widthPercent = Math.max(.2, relativeDuration * 100);
1038
939
  return /* @__PURE__ */ jsx("div", {
1039
940
  className: "relative h-full",
1040
- children: /* @__PURE__ */ jsx(Tooltip, {
941
+ children: /* @__PURE__ */ jsx(Tooltip$1, {
1041
942
  content: `${span.name}\n${formatDuration(span.durationMs)}\nStatus: ${isError ? "ERROR" : "OK"}`,
1042
943
  children: /* @__PURE__ */ jsx("div", {
1043
944
  className: "absolute inset-0",
@@ -1172,6 +1073,12 @@ function formatAttributeValue(value) {
1172
1073
  if (Array.isArray(value) || typeof value === "object") return JSON.stringify(value, null, 2);
1173
1074
  return String(value);
1174
1075
  }
1076
+ function formatSeriesLabel(labels) {
1077
+ const entries = Object.entries(labels);
1078
+ if (entries.length === 0) return "";
1079
+ if (entries.length === 1) return String(entries[0][1]);
1080
+ return entries.map(([k, v]) => `${k}=${v}`).join(", ");
1081
+ }
1175
1082
  function isComplexValue(value) {
1176
1083
  return typeof value === "object" && value !== null && (Array.isArray(value) || Object.keys(value).length > 0);
1177
1084
  }
@@ -3825,91 +3732,1295 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
3825
3732
  }
3826
3733
 
3827
3734
  //#endregion
3828
- //#region src/components/observability/ServiceList/shortcuts.ts
3829
- const SERVICES_SHORTCUTS = {
3830
- name: "Services",
3831
- shortcuts: [{
3832
- keys: ["Backspace"],
3833
- description: "Go back"
3834
- }]
3835
- };
3735
+ //#region src/components/observability/utils/lttb.ts
3736
+ function triangleArea(p1, p2, p3) {
3737
+ return Math.abs((p1.x - p3.x) * (p2.y - p1.y) - (p1.x - p2.x) * (p3.y - p1.y)) / 2;
3738
+ }
3739
+ function downsampleLTTB(data, targetPoints) {
3740
+ if (data.length <= 2 || targetPoints >= data.length) return data.slice();
3741
+ if (targetPoints <= 2) return [data[0], data[data.length - 1]];
3742
+ const sampled = [];
3743
+ const bucketSize = (data.length - 2) / (targetPoints - 2);
3744
+ const firstPoint = data[0];
3745
+ if (!firstPoint) return data;
3746
+ sampled.push(firstPoint);
3747
+ let prevSelectedIndex = 0;
3748
+ for (let i = 0; i < targetPoints - 2; i++) {
3749
+ const bucketStart = Math.floor((i + 0) * bucketSize) + 1;
3750
+ const bucketEnd = Math.min(Math.floor((i + 1) * bucketSize) + 1, data.length - 1);
3751
+ const nextBucketStart = Math.floor((i + 1) * bucketSize) + 1;
3752
+ const nextBucketEnd = Math.min(Math.floor((i + 2) * bucketSize) + 1, data.length - 1);
3753
+ let avgX = 0;
3754
+ let avgY = 0;
3755
+ let nextBucketCount = 0;
3756
+ for (let j = nextBucketStart; j < nextBucketEnd; j++) {
3757
+ const point = data[j];
3758
+ if (point) {
3759
+ avgX += point.x;
3760
+ avgY += point.y;
3761
+ nextBucketCount++;
3762
+ }
3763
+ }
3764
+ if (nextBucketCount > 0) {
3765
+ avgX /= nextBucketCount;
3766
+ avgY /= nextBucketCount;
3767
+ } else {
3768
+ const lastPoint = data[data.length - 1];
3769
+ if (lastPoint) {
3770
+ avgX = lastPoint.x;
3771
+ avgY = lastPoint.y;
3772
+ }
3773
+ }
3774
+ const avgPoint = {
3775
+ x: avgX,
3776
+ y: avgY
3777
+ };
3778
+ let maxArea = -1;
3779
+ let maxAreaIndex = bucketStart;
3780
+ const prevPoint = data[prevSelectedIndex];
3781
+ if (!prevPoint) continue;
3782
+ for (let j = bucketStart; j < bucketEnd; j++) {
3783
+ const currentPoint = data[j];
3784
+ if (!currentPoint) continue;
3785
+ const area = triangleArea(prevPoint, currentPoint, avgPoint);
3786
+ if (area > maxArea) {
3787
+ maxArea = area;
3788
+ maxAreaIndex = j;
3789
+ }
3790
+ }
3791
+ const selectedPoint = data[maxAreaIndex];
3792
+ if (selectedPoint) sampled.push(selectedPoint);
3793
+ prevSelectedIndex = maxAreaIndex;
3794
+ }
3795
+ const lastPoint = data[data.length - 1];
3796
+ if (lastPoint) sampled.push(lastPoint);
3797
+ return sampled;
3798
+ }
3836
3799
 
3837
3800
  //#endregion
3838
- //#region src/components/observability/renderers/OtelMetricDiscovery.tsx
3839
- const TYPE_ORDER = {
3840
- Gauge: 0,
3841
- Sum: 1,
3842
- Histogram: 2,
3843
- ExponentialHistogram: 3,
3844
- Summary: 4
3801
+ //#region src/components/observability/utils/units.ts
3802
+ const BYTE_SCALES = [
3803
+ {
3804
+ threshold: 0xe8d4a51000,
3805
+ divisor: 0xe8d4a51000,
3806
+ suffix: "TB"
3807
+ },
3808
+ {
3809
+ threshold: 1e9,
3810
+ divisor: 1e9,
3811
+ suffix: "GB"
3812
+ },
3813
+ {
3814
+ threshold: 1e6,
3815
+ divisor: 1e6,
3816
+ suffix: "MB"
3817
+ },
3818
+ {
3819
+ threshold: 1e3,
3820
+ divisor: 1e3,
3821
+ suffix: "KB"
3822
+ },
3823
+ {
3824
+ threshold: 0,
3825
+ divisor: 1,
3826
+ suffix: "B"
3827
+ }
3828
+ ];
3829
+ const SECOND_SCALES = [
3830
+ {
3831
+ threshold: 3600,
3832
+ divisor: 3600,
3833
+ suffix: "h"
3834
+ },
3835
+ {
3836
+ threshold: 60,
3837
+ divisor: 60,
3838
+ suffix: "min"
3839
+ },
3840
+ {
3841
+ threshold: 1,
3842
+ divisor: 1,
3843
+ suffix: "s"
3844
+ },
3845
+ {
3846
+ threshold: 0,
3847
+ divisor: .001,
3848
+ suffix: "ms"
3849
+ }
3850
+ ];
3851
+ const MS_SCALES = [{
3852
+ threshold: 1e3,
3853
+ divisor: 1e3,
3854
+ suffix: "s"
3855
+ }, {
3856
+ threshold: 0,
3857
+ divisor: 1,
3858
+ suffix: "ms"
3859
+ }];
3860
+ const US_SCALES = [
3861
+ {
3862
+ threshold: 1e6,
3863
+ divisor: 1e6,
3864
+ suffix: "s"
3865
+ },
3866
+ {
3867
+ threshold: 1e3,
3868
+ divisor: 1e3,
3869
+ suffix: "ms"
3870
+ },
3871
+ {
3872
+ threshold: 0,
3873
+ divisor: 1,
3874
+ suffix: "μs"
3875
+ }
3876
+ ];
3877
+ const GENERIC_SCALES = [
3878
+ {
3879
+ threshold: 1e9,
3880
+ divisor: 1e9,
3881
+ suffix: "B"
3882
+ },
3883
+ {
3884
+ threshold: 1e6,
3885
+ divisor: 1e6,
3886
+ suffix: "M"
3887
+ },
3888
+ {
3889
+ threshold: 1e3,
3890
+ divisor: 1e3,
3891
+ suffix: "K"
3892
+ },
3893
+ {
3894
+ threshold: 0,
3895
+ divisor: 1,
3896
+ suffix: ""
3897
+ }
3898
+ ];
3899
+ const BRACE_UNIT_PATTERN = /^\{(.+)\}$/;
3900
+ const UNIT_SCALE_MAP = {
3901
+ By: BYTE_SCALES,
3902
+ s: SECOND_SCALES,
3903
+ ms: MS_SCALES,
3904
+ us: US_SCALES
3845
3905
  };
3846
- function OtelMetricDiscovery(props) {
3847
- const data = props.hasData ? props.data : null;
3848
- const loading = props.hasData ? props.loading : false;
3849
- const error = props.hasData ? props.error : null;
3850
- const sorted = useMemo(() => {
3851
- if (!data?.metrics) return [];
3852
- return [...data.metrics].sort((a, b) => a.name.localeCompare(b.name) || (TYPE_ORDER[a.type] ?? 99) - (TYPE_ORDER[b.type] ?? 99));
3853
- }, [data]);
3854
- if (loading && !sorted.length) return /* @__PURE__ */ jsx("p", {
3855
- className: "text-muted-foreground py-4",
3856
- children: "Loading metrics…"
3906
+ function pickScale(scales, maxValue) {
3907
+ const abs = Math.abs(maxValue);
3908
+ for (const s of scales) if (abs >= s.threshold && s.threshold > 0) return s;
3909
+ return scales[scales.length - 1];
3910
+ }
3911
+ function resolveUnitScale(unit, maxValue) {
3912
+ if (!unit) {
3913
+ const s = pickScale(GENERIC_SCALES, maxValue);
3914
+ return {
3915
+ divisor: s.divisor,
3916
+ suffix: s.suffix,
3917
+ label: "",
3918
+ isPercent: false
3919
+ };
3920
+ }
3921
+ if (unit === "1") return {
3922
+ divisor: .01,
3923
+ suffix: "%",
3924
+ label: "Percent",
3925
+ isPercent: true
3926
+ };
3927
+ const scales = UNIT_SCALE_MAP[unit];
3928
+ if (scales) {
3929
+ const s = pickScale(scales, maxValue);
3930
+ return {
3931
+ divisor: s.divisor,
3932
+ suffix: s.suffix,
3933
+ label: s.suffix,
3934
+ isPercent: false
3935
+ };
3936
+ }
3937
+ const braceMatch = BRACE_UNIT_PATTERN.exec(unit);
3938
+ if (braceMatch) {
3939
+ const cleaned = braceMatch[1];
3940
+ const s = pickScale(GENERIC_SCALES, maxValue);
3941
+ const suffix = s.suffix ? `${s.suffix} ${cleaned}` : cleaned;
3942
+ return {
3943
+ divisor: s.divisor,
3944
+ suffix,
3945
+ label: cleaned,
3946
+ isPercent: false
3947
+ };
3948
+ }
3949
+ const s = pickScale(GENERIC_SCALES, maxValue);
3950
+ const suffix = s.suffix ? `${s.suffix} ${unit}` : unit;
3951
+ return {
3952
+ divisor: s.divisor,
3953
+ suffix,
3954
+ label: unit,
3955
+ isPercent: false
3956
+ };
3957
+ }
3958
+ function formatTickValue(value, scale) {
3959
+ const scaled = value / scale.divisor;
3960
+ if (scale.isPercent) return `${scaled.toFixed(1)}`;
3961
+ if (Number.isInteger(scaled) && Math.abs(scaled) < 1e4) return scaled.toString();
3962
+ return scaled.toFixed(1);
3963
+ }
3964
+ function formatDisplayValue(value, scale) {
3965
+ const tick = formatTickValue(value, scale);
3966
+ if (!scale.suffix) return tick;
3967
+ if (scale.isPercent) return `${tick}${scale.suffix}`;
3968
+ return `${tick} ${scale.suffix}`;
3969
+ }
3970
+ /** Convenience: resolve + format in one call (for MetricStat) */
3971
+ function formatOtelValue(value, unit) {
3972
+ return formatDisplayValue(value, resolveUnitScale(unit, Math.abs(value)));
3973
+ }
3974
+
3975
+ //#endregion
3976
+ //#region src/components/observability/MetricTimeSeries/index.tsx
3977
+ /**
3978
+ * MetricTimeSeries - Accepts OtelMetricsRow[] and renders line charts.
3979
+ */
3980
+ const COLORS$1 = [
3981
+ "#8884d8",
3982
+ "#82ca9d",
3983
+ "#ffc658",
3984
+ "#ff7300",
3985
+ "#00C49F",
3986
+ "#0088FE",
3987
+ "#FFBB28",
3988
+ "#FF8042",
3989
+ "#a4de6c",
3990
+ "#d0ed57"
3991
+ ];
3992
+ const defaultFormatTime = (timestamp) => {
3993
+ return new Date(timestamp).toLocaleTimeString("en-US", {
3994
+ hour: "2-digit",
3995
+ minute: "2-digit",
3996
+ second: "2-digit",
3997
+ hour12: false
3857
3998
  });
3858
- if (error) return /* @__PURE__ */ jsxs("p", {
3859
- className: "text-red-400 py-4",
3860
- children: ["Error: ", error.message]
3999
+ };
4000
+ function getStrokeDashArray(style) {
4001
+ if (style === "solid") return void 0;
4002
+ if (style === "dotted") return "2 2";
4003
+ return "5 5";
4004
+ }
4005
+ /** Build metrics from denormalized rows */
4006
+ function buildMetrics(rows) {
4007
+ const metricMap = /* @__PURE__ */ new Map();
4008
+ const metricMeta = /* @__PURE__ */ new Map();
4009
+ for (const row of rows) {
4010
+ const name = row.MetricName ?? "unknown";
4011
+ const type = row.MetricType;
4012
+ if (type === "Histogram" || type === "ExponentialHistogram" || type === "Summary") continue;
4013
+ if (!metricMap.has(name)) metricMap.set(name, /* @__PURE__ */ new Map());
4014
+ if (!metricMeta.has(name)) metricMeta.set(name, {
4015
+ description: row.MetricDescription ?? "",
4016
+ unit: row.MetricUnit ?? "",
4017
+ type,
4018
+ serviceName: row.ServiceName ?? "unknown"
4019
+ });
4020
+ const seriesKey = row.Attributes ? JSON.stringify(Object.fromEntries(Object.entries(row.Attributes).sort(([a], [b]) => a.localeCompare(b)))) : "__default__";
4021
+ const seriesMap = metricMap.get(name);
4022
+ if (!seriesMap.has(seriesKey)) {
4023
+ const labels = {};
4024
+ if (row.Attributes) for (const [k, v] of Object.entries(row.Attributes)) labels[k] = String(v);
4025
+ seriesMap.set(seriesKey, {
4026
+ key: seriesKey === "__default__" ? name : seriesKey,
4027
+ labels,
4028
+ dataPoints: []
4029
+ });
4030
+ }
4031
+ if (!("Value" in row)) continue;
4032
+ const value = row.Value;
4033
+ const timestamp = parseInt(row.TimeUnix, 10) / 1e6;
4034
+ seriesMap.get(seriesKey).dataPoints.push({
4035
+ timestamp,
4036
+ value
4037
+ });
4038
+ }
4039
+ const results = [];
4040
+ for (const [name, seriesMap] of metricMap) {
4041
+ const meta = metricMeta.get(name);
4042
+ const series = Array.from(seriesMap.values());
4043
+ for (const s of series) s.dataPoints.sort((a, b) => a.timestamp - b.timestamp);
4044
+ results.push({
4045
+ name,
4046
+ description: meta.description,
4047
+ unit: meta.unit,
4048
+ type: meta.type,
4049
+ series,
4050
+ serviceName: meta.serviceName
4051
+ });
4052
+ }
4053
+ return results;
4054
+ }
4055
+ function toRechartsData(metrics) {
4056
+ const timestampMap = /* @__PURE__ */ new Map();
4057
+ for (const metric of metrics) for (const series of metric.series) {
4058
+ const seriesName = series.key === "__default__" ? metric.name : series.key;
4059
+ for (const dp of series.dataPoints) {
4060
+ if (!timestampMap.has(dp.timestamp)) timestampMap.set(dp.timestamp, { timestamp: dp.timestamp });
4061
+ timestampMap.get(dp.timestamp)[seriesName] = dp.value;
4062
+ }
4063
+ }
4064
+ return Array.from(timestampMap.values()).sort((a, b) => a.timestamp - b.timestamp);
4065
+ }
4066
+ function getSeriesKeys(metrics) {
4067
+ const keys = /* @__PURE__ */ new Set();
4068
+ for (const m of metrics) for (const s of m.series) keys.add(s.key === "__default__" ? m.name : s.key);
4069
+ return Array.from(keys);
4070
+ }
4071
+ function buildDisplayLabelMap(metrics) {
4072
+ const map = /* @__PURE__ */ new Map();
4073
+ for (const m of metrics) for (const s of m.series) {
4074
+ const dataKey = s.key === "__default__" ? m.name : s.key;
4075
+ const label = formatSeriesLabel(s.labels);
4076
+ map.set(dataKey, label || m.name);
4077
+ }
4078
+ return map;
4079
+ }
4080
+ function downsampleRechartsData(data, seriesKeys, maxPoints) {
4081
+ if (data.length <= maxPoints) return data;
4082
+ const timestamps = /* @__PURE__ */ new Set();
4083
+ for (const key of seriesKeys) {
4084
+ const pts = [];
4085
+ for (const d of data) {
4086
+ const v = d[key];
4087
+ if (v !== void 0) pts.push({
4088
+ x: d.timestamp,
4089
+ y: v
4090
+ });
4091
+ }
4092
+ if (pts.length === 0) continue;
4093
+ const ds = downsampleLTTB(pts, Math.ceil(maxPoints / seriesKeys.length));
4094
+ for (const p of ds) timestamps.add(p.x);
4095
+ }
4096
+ return data.filter((d) => timestamps.has(d.timestamp));
4097
+ }
4098
+ function MetricTimeSeries({ rows, isLoading = false, error, maxDataPoints = 500, showBrush = true, height = 400, unit: unitProp, yAxisLabel, formatTime = defaultFormatTime, formatValue, onBrushChange, legendMaxLength = 30, thresholdLines }) {
4099
+ const [hiddenSeries, setHiddenSeries] = useState(/* @__PURE__ */ new Set());
4100
+ const parsedMetrics = useMemo(() => buildMetrics(rows), [rows]);
4101
+ const effectiveUnit = unitProp ?? parsedMetrics[0]?.unit ?? "";
4102
+ const chartData = useMemo(() => toRechartsData(parsedMetrics), [parsedMetrics]);
4103
+ const seriesKeys = useMemo(() => getSeriesKeys(parsedMetrics), [parsedMetrics]);
4104
+ const displayLabelMap = useMemo(() => buildDisplayLabelMap(parsedMetrics), [parsedMetrics]);
4105
+ const displayData = useMemo(() => downsampleRechartsData(chartData, seriesKeys, maxDataPoints), [
4106
+ chartData,
4107
+ seriesKeys,
4108
+ maxDataPoints
4109
+ ]);
4110
+ const { tickFormatter, displayFormatter, resolvedYAxisLabel } = useMemo(() => {
4111
+ let max = 0;
4112
+ for (const dp of displayData) for (const key of seriesKeys) {
4113
+ const v = dp[key];
4114
+ if (v !== void 0 && Math.abs(v) > max) max = Math.abs(v);
4115
+ }
4116
+ const scale = resolveUnitScale(effectiveUnit, max);
4117
+ return {
4118
+ tickFormatter: formatValue ?? ((v) => formatTickValue(v, scale)),
4119
+ displayFormatter: formatValue ?? ((v) => formatDisplayValue(v, scale)),
4120
+ resolvedYAxisLabel: yAxisLabel ?? (scale.label || void 0)
4121
+ };
4122
+ }, [
4123
+ displayData,
4124
+ seriesKeys,
4125
+ effectiveUnit,
4126
+ formatValue,
4127
+ yAxisLabel
4128
+ ]);
4129
+ const handleLegendClick = useCallback((dataKey) => {
4130
+ setHiddenSeries((prev) => {
4131
+ const next = new Set(prev);
4132
+ if (next.has(dataKey)) next.delete(dataKey);
4133
+ else next.add(dataKey);
4134
+ return next;
4135
+ });
4136
+ }, []);
4137
+ const handleBrushChange = useCallback((brushData) => {
4138
+ if (!onBrushChange || !displayData.length) return;
4139
+ const { startIndex, endIndex } = brushData;
4140
+ if (startIndex === void 0 || endIndex === void 0) return;
4141
+ const sp = displayData[startIndex], ep = displayData[endIndex];
4142
+ if (sp && ep) onBrushChange(sp.timestamp, ep.timestamp);
4143
+ }, [displayData, onBrushChange]);
4144
+ if (isLoading) return /* @__PURE__ */ jsx(MetricLoadingSkeleton, { height });
4145
+ if (error) return /* @__PURE__ */ jsx("div", {
4146
+ className: "flex items-center justify-center bg-background rounded-lg border border-red-800",
4147
+ style: { height },
4148
+ children: /* @__PURE__ */ jsxs("div", {
4149
+ className: "text-center p-4",
4150
+ children: [/* @__PURE__ */ jsx("p", {
4151
+ className: "text-red-400 font-medium",
4152
+ children: "Error loading metrics"
4153
+ }), /* @__PURE__ */ jsx("p", {
4154
+ className: "text-gray-500 text-sm mt-1",
4155
+ children: error.message
4156
+ })]
4157
+ })
3861
4158
  });
3862
- if (!sorted.length) return /* @__PURE__ */ jsx("p", {
3863
- className: "text-muted-foreground py-4",
3864
- children: "No metrics discovered."
4159
+ if (rows.length === 0 || displayData.length === 0) return /* @__PURE__ */ jsx("div", {
4160
+ className: "flex items-center justify-center bg-background rounded-lg border border-gray-800",
4161
+ style: { height },
4162
+ children: /* @__PURE__ */ jsx("p", {
4163
+ className: "text-gray-500",
4164
+ children: "No metric data available"
4165
+ })
3865
4166
  });
3866
4167
  return /* @__PURE__ */ jsx("div", {
3867
- className: "overflow-x-auto",
3868
- children: /* @__PURE__ */ jsxs("table", {
3869
- className: "w-full text-sm text-left text-foreground border-collapse",
3870
- children: [/* @__PURE__ */ jsx("thead", {
3871
- className: "text-xs uppercase text-muted-foreground border-b border-border",
3872
- children: /* @__PURE__ */ jsxs("tr", { children: [
3873
- /* @__PURE__ */ jsx("th", {
3874
- className: "px-3 py-2",
3875
- children: "Name"
4168
+ className: "bg-background rounded-lg p-4",
4169
+ style: { height },
4170
+ "data-testid": "metric-time-series",
4171
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
4172
+ width: "100%",
4173
+ height: "100%",
4174
+ children: /* @__PURE__ */ jsxs(LineChart, {
4175
+ data: displayData,
4176
+ margin: {
4177
+ top: 5,
4178
+ right: 30,
4179
+ left: 20,
4180
+ bottom: 5
4181
+ },
4182
+ children: [
4183
+ /* @__PURE__ */ jsx(CartesianGrid, {
4184
+ strokeDasharray: "3 3",
4185
+ stroke: "#374151"
3876
4186
  }),
3877
- /* @__PURE__ */ jsx("th", {
3878
- className: "px-3 py-2",
3879
- children: "Type"
4187
+ /* @__PURE__ */ jsx(XAxis, {
4188
+ dataKey: "timestamp",
4189
+ tickFormatter: formatTime,
4190
+ stroke: "#9CA3AF",
4191
+ tick: {
4192
+ fill: "#9CA3AF",
4193
+ fontSize: 12
4194
+ }
3880
4195
  }),
3881
- /* @__PURE__ */ jsx("th", {
3882
- className: "px-3 py-2",
3883
- children: "Unit"
4196
+ /* @__PURE__ */ jsx(YAxis, {
4197
+ tickFormatter,
4198
+ stroke: "#9CA3AF",
4199
+ tick: {
4200
+ fill: "#9CA3AF",
4201
+ fontSize: 12
4202
+ },
4203
+ label: resolvedYAxisLabel ? {
4204
+ value: resolvedYAxisLabel,
4205
+ angle: -90,
4206
+ position: "insideLeft",
4207
+ fill: "#9CA3AF"
4208
+ } : void 0
3884
4209
  }),
3885
- /* @__PURE__ */ jsx("th", {
3886
- className: "px-3 py-2",
3887
- children: "Description"
4210
+ /* @__PURE__ */ jsx(Tooltip, { content: (props) => /* @__PURE__ */ jsx(CustomTooltip, {
4211
+ ...props,
4212
+ formatTime,
4213
+ formatValue: displayFormatter,
4214
+ displayLabelMap
4215
+ }) }),
4216
+ /* @__PURE__ */ jsx(Legend, {
4217
+ onClick: (e) => {
4218
+ const dk = e?.dataKey;
4219
+ if (typeof dk === "string") handleLegendClick(dk);
4220
+ },
4221
+ formatter: (value) => {
4222
+ const label = displayLabelMap.get(value) ?? value;
4223
+ const truncated = label.length > legendMaxLength ? label.slice(0, legendMaxLength - 3) + "..." : label;
4224
+ const isHidden = hiddenSeries.has(value);
4225
+ return /* @__PURE__ */ jsx("span", {
4226
+ style: {
4227
+ color: isHidden ? "#6B7280" : "#E5E7EB",
4228
+ textDecoration: isHidden ? "line-through" : "none",
4229
+ cursor: "pointer"
4230
+ },
4231
+ title: truncated !== label ? label : void 0,
4232
+ children: truncated
4233
+ });
4234
+ }
4235
+ }),
4236
+ thresholdLines?.map((t, i) => /* @__PURE__ */ jsx(ReferenceLine, {
4237
+ y: t.value,
4238
+ stroke: t.color,
4239
+ strokeDasharray: getStrokeDashArray(t.style),
4240
+ strokeWidth: 1.5,
4241
+ label: t.label ? {
4242
+ value: t.label,
4243
+ position: "right",
4244
+ fill: t.color,
4245
+ fontSize: 11
4246
+ } : void 0
4247
+ }, `t-${i}`)),
4248
+ seriesKeys.map((key, i) => /* @__PURE__ */ jsx(Line, {
4249
+ type: "monotone",
4250
+ dataKey: key,
4251
+ stroke: COLORS$1[i % COLORS$1.length],
4252
+ strokeWidth: 2,
4253
+ dot: false,
4254
+ activeDot: { r: 4 },
4255
+ hide: hiddenSeries.has(key),
4256
+ connectNulls: true
4257
+ }, key)),
4258
+ showBrush && displayData.length > 10 && /* @__PURE__ */ jsx(Brush, {
4259
+ dataKey: "timestamp",
4260
+ height: 30,
4261
+ stroke: "#6B7280",
4262
+ fill: "#1F2937",
4263
+ tickFormatter: formatTime,
4264
+ onChange: handleBrushChange
3888
4265
  })
3889
- ] })
3890
- }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((m) => /* @__PURE__ */ jsxs("tr", {
3891
- className: "border-b border-border/50 hover:bg-muted/40",
4266
+ ]
4267
+ })
4268
+ })
4269
+ });
4270
+ }
4271
+ function CustomTooltip({ active, payload, label, formatTime, formatValue, displayLabelMap }) {
4272
+ if (!active || !payload || label == null) return null;
4273
+ return /* @__PURE__ */ jsxs("div", {
4274
+ className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
4275
+ children: [/* @__PURE__ */ jsx("p", {
4276
+ className: "text-gray-400 text-xs mb-2",
4277
+ 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))]
4290
+ });
4291
+ }
4292
+ function MetricLoadingSkeleton({ height = 400 }) {
4293
+ return /* @__PURE__ */ jsx("div", {
4294
+ className: "bg-background rounded-lg p-4 animate-pulse",
4295
+ style: { height },
4296
+ "data-testid": "metric-time-series-loading",
4297
+ children: /* @__PURE__ */ jsxs("div", {
4298
+ className: "h-full flex flex-col",
4299
+ children: [/* @__PURE__ */ jsxs("div", {
4300
+ className: "flex flex-1 gap-2",
4301
+ children: [/* @__PURE__ */ jsx("div", {
4302
+ className: "flex flex-col justify-between w-12",
4303
+ children: [
4304
+ 1,
4305
+ 2,
4306
+ 3,
4307
+ 4,
4308
+ 5
4309
+ ].map((i) => /* @__PURE__ */ jsx("div", { className: "h-3 w-8 bg-gray-700 rounded" }, i))
4310
+ }), /* @__PURE__ */ jsx("div", {
4311
+ className: "flex-1 relative",
4312
+ children: /* @__PURE__ */ jsx("div", {
4313
+ className: "absolute inset-0 flex flex-col justify-between",
4314
+ children: [
4315
+ 1,
4316
+ 2,
4317
+ 3,
4318
+ 4,
4319
+ 5
4320
+ ].map((i) => /* @__PURE__ */ jsx("div", { className: "h-px bg-gray-800" }, i))
4321
+ })
4322
+ })]
4323
+ }), /* @__PURE__ */ jsx("div", {
4324
+ className: "flex justify-between mt-2 px-14",
3892
4325
  children: [
3893
- /* @__PURE__ */ jsx("td", {
3894
- className: "px-3 py-2 font-mono whitespace-nowrap",
3895
- children: m.name
4326
+ 1,
4327
+ 2,
4328
+ 3,
4329
+ 4,
4330
+ 5
4331
+ ].map((i) => /* @__PURE__ */ jsx("div", { className: "h-3 w-12 bg-gray-700 rounded" }, i))
4332
+ })]
4333
+ })
4334
+ });
4335
+ }
4336
+
4337
+ //#endregion
4338
+ //#region src/components/observability/MetricHistogram/index.tsx
4339
+ /**
4340
+ * MetricHistogram - Accepts OtelMetricsRow[] and renders histogram bar charts.
4341
+ */
4342
+ const COLORS = [
4343
+ "#8884d8",
4344
+ "#82ca9d",
4345
+ "#ffc658",
4346
+ "#ff7300",
4347
+ "#00C49F",
4348
+ "#0088FE"
4349
+ ];
4350
+ const defaultFormatBucketLabel = (bound, index, bounds) => {
4351
+ if (index === 0) return `≤${bound}`;
4352
+ if (index === bounds.length) return `>${bounds[bounds.length - 1]}`;
4353
+ return `${bounds[index - 1]}-${bound}`;
4354
+ };
4355
+ const defaultFormatValue$2 = (value) => {
4356
+ if (value >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
4357
+ if (value >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
4358
+ return value.toFixed(0);
4359
+ };
4360
+ function buildHistogramData(rows, formatLabel = defaultFormatBucketLabel) {
4361
+ const buckets = [];
4362
+ const seriesKeysSet = /* @__PURE__ */ new Set();
4363
+ const displayLabelMap = /* @__PURE__ */ new Map();
4364
+ let unit = "";
4365
+ for (const row of rows) {
4366
+ if (row.MetricType !== "Histogram") continue;
4367
+ if (!unit && row.MetricUnit) unit = row.MetricUnit;
4368
+ const name = row.MetricName ?? "count";
4369
+ const key = row.Attributes ? JSON.stringify(row.Attributes) : "__default__";
4370
+ const seriesName = key === "__default__" ? name : key;
4371
+ seriesKeysSet.add(seriesName);
4372
+ if (!displayLabelMap.has(seriesName)) if (key === "__default__") displayLabelMap.set(seriesName, name);
4373
+ else {
4374
+ const labels = {};
4375
+ if (row.Attributes) for (const [k, v] of Object.entries(row.Attributes)) labels[k] = String(v);
4376
+ displayLabelMap.set(seriesName, formatSeriesLabel(labels) || name);
4377
+ }
4378
+ const bounds = row.ExplicitBounds ?? [];
4379
+ const counts = row.BucketCounts ?? [];
4380
+ for (let i = 0; i < counts.length; i++) {
4381
+ const count = counts[i] ?? 0;
4382
+ const bucketLabel = formatLabel(i < bounds.length ? bounds[i] : Infinity, i, bounds);
4383
+ let bucket = buckets.find((b) => b.bucket === bucketLabel);
4384
+ if (!bucket) {
4385
+ bucket = {
4386
+ bucket: bucketLabel,
4387
+ lowerBound: i === 0 ? 0 : bounds[i - 1] ?? 0,
4388
+ upperBound: bounds[i] ?? Infinity
4389
+ };
4390
+ buckets.push(bucket);
4391
+ }
4392
+ bucket[seriesName] = (bucket[seriesName] ?? 0) + count;
4393
+ }
4394
+ }
4395
+ buckets.sort((a, b) => a.lowerBound - b.lowerBound);
4396
+ return {
4397
+ buckets,
4398
+ seriesKeys: Array.from(seriesKeysSet),
4399
+ displayLabelMap,
4400
+ unit
4401
+ };
4402
+ }
4403
+ function MetricHistogram({ rows, isLoading = false, error, height = 400, unit: unitProp, yAxisLabel, showLegend = true, formatBucketLabel, formatValue = defaultFormatValue$2, labelStyle = "staggered" }) {
4404
+ const bucketLabelFormatter = formatBucketLabel ?? defaultFormatBucketLabel;
4405
+ const { buckets, seriesKeys, displayLabelMap, unit } = useMemo(() => {
4406
+ if (rows.length === 0) return {
4407
+ buckets: [],
4408
+ seriesKeys: [],
4409
+ displayLabelMap: /* @__PURE__ */ new Map(),
4410
+ unit: ""
4411
+ };
4412
+ return buildHistogramData(rows, bucketLabelFormatter);
4413
+ }, [rows, bucketLabelFormatter]);
4414
+ const effectiveUnit = unitProp ?? unit;
4415
+ const boundsScale = useMemo(() => {
4416
+ if (!effectiveUnit || buckets.length === 0) return null;
4417
+ for (let i = buckets.length - 1; i >= 0; i--) if (buckets[i].upperBound !== Infinity) return resolveUnitScale(effectiveUnit, buckets[i].upperBound);
4418
+ return null;
4419
+ }, [effectiveUnit, buckets]);
4420
+ if (isLoading) return /* @__PURE__ */ jsx(HistogramLoadingSkeleton, { height });
4421
+ if (error) return /* @__PURE__ */ jsx("div", {
4422
+ className: "flex items-center justify-center bg-background rounded-lg border border-red-800",
4423
+ style: { height },
4424
+ children: /* @__PURE__ */ jsxs("div", {
4425
+ className: "text-center p-4",
4426
+ children: [/* @__PURE__ */ jsx("p", {
4427
+ className: "text-red-400 font-medium",
4428
+ children: "Error loading histogram"
4429
+ }), /* @__PURE__ */ jsx("p", {
4430
+ className: "text-gray-500 text-sm mt-1",
4431
+ children: error.message
4432
+ })]
4433
+ })
4434
+ });
4435
+ if (rows.length === 0 || buckets.length === 0) return /* @__PURE__ */ jsx("div", {
4436
+ className: "flex items-center justify-center bg-background rounded-lg border border-gray-800",
4437
+ style: { height },
4438
+ children: /* @__PURE__ */ jsx("p", {
4439
+ className: "text-gray-500",
4440
+ children: "No histogram data available"
4441
+ })
4442
+ });
4443
+ return /* @__PURE__ */ jsx("div", {
4444
+ className: "bg-background rounded-lg p-4",
4445
+ style: { height },
4446
+ "data-testid": "metric-histogram",
4447
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
4448
+ width: "100%",
4449
+ height: "100%",
4450
+ children: /* @__PURE__ */ jsxs(BarChart, {
4451
+ data: buckets,
4452
+ margin: {
4453
+ top: 5,
4454
+ right: 30,
4455
+ left: 20,
4456
+ bottom: 5
4457
+ },
4458
+ children: [
4459
+ /* @__PURE__ */ jsx(CartesianGrid, {
4460
+ strokeDasharray: "3 3",
4461
+ stroke: "#374151"
3896
4462
  }),
3897
- /* @__PURE__ */ jsx("td", {
3898
- className: "px-3 py-2 text-muted-foreground",
3899
- children: m.type
4463
+ /* @__PURE__ */ jsx(XAxis, {
4464
+ dataKey: "bucket",
4465
+ stroke: "#9CA3AF",
4466
+ tick: labelStyle === "staggered" ? (props) => {
4467
+ if (!props.payload) return /* @__PURE__ */ jsx("g", {});
4468
+ const yOffset = props.payload.index % 2 === 0 ? 0 : 12;
4469
+ return /* @__PURE__ */ jsx("g", {
4470
+ transform: `translate(${props.x},${props.y})`,
4471
+ children: /* @__PURE__ */ jsx("text", {
4472
+ x: 0,
4473
+ y: yOffset,
4474
+ dy: 12,
4475
+ textAnchor: "middle",
4476
+ fill: "#9CA3AF",
4477
+ fontSize: 11,
4478
+ children: props.payload.value
4479
+ })
4480
+ });
4481
+ } : {
4482
+ fill: "#9CA3AF",
4483
+ fontSize: 11
4484
+ },
4485
+ angle: labelStyle === "rotated" ? -45 : 0,
4486
+ textAnchor: labelStyle === "rotated" ? "end" : "middle",
4487
+ height: labelStyle === "staggered" ? 50 : 60,
4488
+ interval: 0
3900
4489
  }),
3901
- /* @__PURE__ */ jsx("td", {
3902
- className: "px-3 py-2 text-muted-foreground",
3903
- children: m.unit || ""
4490
+ /* @__PURE__ */ jsx(YAxis, {
4491
+ tickFormatter: formatValue,
4492
+ stroke: "#9CA3AF",
4493
+ tick: {
4494
+ fill: "#9CA3AF",
4495
+ fontSize: 12
4496
+ },
4497
+ label: yAxisLabel ? {
4498
+ value: yAxisLabel,
4499
+ angle: -90,
4500
+ position: "insideLeft",
4501
+ fill: "#9CA3AF"
4502
+ } : void 0
3904
4503
  }),
3905
- /* @__PURE__ */ jsx("td", {
3906
- className: "px-3 py-2 text-muted-foreground",
3907
- children: m.description || "–"
3908
- })
4504
+ /* @__PURE__ */ jsx(Tooltip, { content: (props) => /* @__PURE__ */ jsx(HistogramTooltip, {
4505
+ ...props,
4506
+ formatValue,
4507
+ boundsScale,
4508
+ displayLabelMap
4509
+ }) }),
4510
+ showLegend && seriesKeys.length > 1 && /* @__PURE__ */ jsx(Legend, { formatter: (value) => displayLabelMap.get(value) ?? value }),
4511
+ seriesKeys.map((key, i) => /* @__PURE__ */ jsx(Bar, {
4512
+ dataKey: key,
4513
+ fill: COLORS[i % COLORS.length],
4514
+ radius: [
4515
+ 4,
4516
+ 4,
4517
+ 0,
4518
+ 0
4519
+ ],
4520
+ children: buckets.map((_, bi) => /* @__PURE__ */ jsx(Cell, { fill: COLORS[i % COLORS.length] }, `cell-${bi}`))
4521
+ }, key))
3909
4522
  ]
3910
- }, `${m.name}-${m.type}`)) })]
4523
+ })
4524
+ })
4525
+ });
4526
+ }
4527
+ function HistogramTooltip({ active, payload, formatValue, boundsScale, displayLabelMap }) {
4528
+ if (!active || !payload?.length) return null;
4529
+ const bucket = payload[0]?.payload;
4530
+ if (!bucket) return null;
4531
+ return /* @__PURE__ */ jsxs("div", {
4532
+ className: "bg-background border border-gray-700 rounded-lg p-3 shadow-lg",
4533
+ children: [/* @__PURE__ */ jsxs("p", {
4534
+ 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))]
4548
+ });
4549
+ }
4550
+ function HistogramLoadingSkeleton({ height = 400 }) {
4551
+ return /* @__PURE__ */ jsx("div", {
4552
+ className: "bg-background rounded-lg p-4 animate-pulse",
4553
+ style: { height },
4554
+ "data-testid": "metric-histogram-loading",
4555
+ children: /* @__PURE__ */ jsx("div", {
4556
+ className: "h-full flex flex-col",
4557
+ children: /* @__PURE__ */ jsxs("div", {
4558
+ className: "flex flex-1 gap-2",
4559
+ children: [/* @__PURE__ */ jsx("div", {
4560
+ className: "flex flex-col justify-between w-12",
4561
+ children: [
4562
+ 1,
4563
+ 2,
4564
+ 3,
4565
+ 4
4566
+ ].map((i) => /* @__PURE__ */ jsx("div", { className: "h-3 w-8 bg-gray-700 rounded" }, i))
4567
+ }), /* @__PURE__ */ jsx("div", {
4568
+ className: "flex-1 flex items-end justify-around gap-2 pb-8",
4569
+ children: [
4570
+ 30,
4571
+ 50,
4572
+ 80,
4573
+ 65,
4574
+ 45,
4575
+ 25,
4576
+ 15,
4577
+ 8
4578
+ ].map((h, i) => /* @__PURE__ */ jsx("div", {
4579
+ className: "w-8 bg-gray-700 rounded-t",
4580
+ style: { height: `${h}%` }
4581
+ }, i))
4582
+ })]
4583
+ })
4584
+ })
4585
+ });
4586
+ }
4587
+
4588
+ //#endregion
4589
+ //#region src/components/observability/MetricStat/index.tsx
4590
+ /**
4591
+ * MetricStat - Accepts OtelMetricsRow[] and renders stat cards with optional sparklines.
4592
+ */
4593
+ const THRESHOLD_COLORS = {
4594
+ green: {
4595
+ bg: "bg-green-900/20",
4596
+ border: "border-green-700",
4597
+ text: "text-green-400",
4598
+ stroke: "#4ade80",
4599
+ fill: "#22c55e"
4600
+ },
4601
+ yellow: {
4602
+ bg: "bg-yellow-900/20",
4603
+ border: "border-yellow-700",
4604
+ text: "text-yellow-400",
4605
+ stroke: "#facc15",
4606
+ fill: "#eab308"
4607
+ },
4608
+ red: {
4609
+ bg: "bg-red-900/20",
4610
+ border: "border-red-700",
4611
+ text: "text-red-400",
4612
+ stroke: "#f87171",
4613
+ fill: "#ef4444"
4614
+ },
4615
+ gray: {
4616
+ bg: "bg-background",
4617
+ border: "border-gray-800",
4618
+ text: "text-gray-400",
4619
+ stroke: "#9ca3af",
4620
+ fill: "#6b7280"
4621
+ }
4622
+ };
4623
+ function getColorConfig(color) {
4624
+ return THRESHOLD_COLORS[color] ?? {
4625
+ bg: "bg-background",
4626
+ border: "border-gray-800",
4627
+ text: "text-gray-400",
4628
+ stroke: color,
4629
+ fill: color
4630
+ };
4631
+ }
4632
+ function getThresholdColor(value, thresholds) {
4633
+ const sorted = [...thresholds].sort((a, b) => a.value - b.value);
4634
+ for (const t of sorted) if (value < t.value) return t.color;
4635
+ return sorted[sorted.length - 1]?.color ?? "gray";
4636
+ }
4637
+ const defaultFormatValue$1 = (value, unit) => {
4638
+ let formatted;
4639
+ if (Math.abs(value) >= 1e6) formatted = `${(value / 1e6).toFixed(1)}M`;
4640
+ else if (Math.abs(value) >= 1e3) formatted = `${(value / 1e3).toFixed(1)}K`;
4641
+ else if (Number.isInteger(value)) formatted = value.toString();
4642
+ else formatted = value.toFixed(2);
4643
+ return unit ? `${formatted} ${unit}` : formatted;
4644
+ };
4645
+ function buildStatData(rows) {
4646
+ let latestTimestamp = 0;
4647
+ let latestValue = null;
4648
+ let unit = "";
4649
+ let metricName = "Metric";
4650
+ const dataPoints = [];
4651
+ for (const row of rows) {
4652
+ if (row.MetricType === "Histogram" || row.MetricType === "ExponentialHistogram" || row.MetricType === "Summary") continue;
4653
+ const timestamp = parseInt(row.TimeUnix, 10) / 1e6;
4654
+ const value = "Value" in row ? row.Value : 0;
4655
+ if (!unit && row.MetricUnit) unit = row.MetricUnit;
4656
+ if (!metricName || metricName === "Metric") metricName = row.MetricName ?? "Metric";
4657
+ dataPoints.push({
4658
+ timestamp,
4659
+ value
4660
+ });
4661
+ if (timestamp > latestTimestamp) {
4662
+ latestTimestamp = timestamp;
4663
+ latestValue = value;
4664
+ }
4665
+ }
4666
+ dataPoints.sort((a, b) => a.timestamp - b.timestamp);
4667
+ return {
4668
+ latestValue,
4669
+ unit,
4670
+ timestamp: latestTimestamp,
4671
+ dataPoints,
4672
+ metricName
4673
+ };
4674
+ }
4675
+ function MetricStat({ rows, isLoading = false, error, label, formatValue = defaultFormatValue$1, showTimestamp = false, trend, trendValue, className = "", showSparkline = false, sparklinePoints = 20, sparklineHeight = 40, thresholds, colorBackground, colorValue = false }) {
4676
+ const { latestValue, unit, timestamp, dataPoints, metricName } = useMemo(() => buildStatData(rows), [rows]);
4677
+ const sparklineData = useMemo(() => {
4678
+ if (!showSparkline || dataPoints.length === 0) return [];
4679
+ return dataPoints.slice(-sparklinePoints).map((dp) => ({ value: dp.value }));
4680
+ }, [
4681
+ dataPoints,
4682
+ showSparkline,
4683
+ sparklinePoints
4684
+ ]);
4685
+ const colorConfig = getColorConfig(useMemo(() => {
4686
+ if (!thresholds || latestValue === null) return "gray";
4687
+ return getThresholdColor(latestValue, thresholds);
4688
+ }, [thresholds, latestValue]));
4689
+ const shouldColorBackground = colorBackground ?? thresholds !== void 0;
4690
+ const displayLabel = label ?? metricName;
4691
+ const bgClass = shouldColorBackground ? colorConfig.bg : "bg-background";
4692
+ const borderClass = shouldColorBackground ? `border ${colorConfig.border}` : "";
4693
+ const valueClass = colorValue ? colorConfig.text : "text-white";
4694
+ if (isLoading) return /* @__PURE__ */ jsxs("div", {
4695
+ className: `bg-background rounded-lg p-4 animate-pulse ${className}`,
4696
+ "data-testid": "metric-stat-loading",
4697
+ children: [/* @__PURE__ */ jsx("div", { className: "h-4 w-24 bg-gray-700 rounded mb-2" }), /* @__PURE__ */ jsx("div", { className: "h-10 w-32 bg-gray-700 rounded" })]
4698
+ });
4699
+ if (error) return /* @__PURE__ */ jsx("div", {
4700
+ className: `bg-background rounded-lg p-4 border border-red-800 ${className}`,
4701
+ "data-testid": "metric-stat-error",
4702
+ children: /* @__PURE__ */ jsx("p", {
4703
+ className: "text-red-400 text-sm",
4704
+ children: error.message
3911
4705
  })
3912
4706
  });
4707
+ if (latestValue === null) return /* @__PURE__ */ jsxs("div", {
4708
+ className: `bg-background rounded-lg p-4 border border-gray-800 ${className}`,
4709
+ "data-testid": "metric-stat-empty",
4710
+ children: [/* @__PURE__ */ jsx("p", {
4711
+ className: "text-gray-500 text-sm",
4712
+ children: displayLabel
4713
+ }), /* @__PURE__ */ jsx("p", {
4714
+ className: "text-gray-600 text-2xl font-semibold",
4715
+ children: "--"
4716
+ })]
4717
+ });
4718
+ return /* @__PURE__ */ jsxs("div", {
4719
+ className: `${bgClass} ${borderClass} rounded-lg p-4 ${className}`,
4720
+ "data-testid": "metric-stat",
4721
+ children: [
4722
+ /* @__PURE__ */ jsxs("div", {
4723
+ className: "flex items-center justify-between mb-1",
4724
+ children: [/* @__PURE__ */ jsx("p", {
4725
+ className: "text-gray-400 text-sm font-medium",
4726
+ children: displayLabel
4727
+ }), trend && /* @__PURE__ */ jsx(TrendIndicator, {
4728
+ direction: trend,
4729
+ value: trendValue
4730
+ })]
4731
+ }),
4732
+ /* @__PURE__ */ jsx("p", {
4733
+ className: `${valueClass} text-3xl font-bold`,
4734
+ children: formatValue(latestValue, unit)
4735
+ }),
4736
+ showTimestamp && /* @__PURE__ */ jsx("p", {
4737
+ className: "text-gray-500 text-xs mt-1",
4738
+ children: new Date(timestamp).toLocaleTimeString()
4739
+ }),
4740
+ showSparkline && sparklineData.length > 0 && /* @__PURE__ */ jsx("div", {
4741
+ className: "mt-2",
4742
+ style: { height: sparklineHeight },
4743
+ children: /* @__PURE__ */ jsx(ResponsiveContainer, {
4744
+ width: "100%",
4745
+ height: "100%",
4746
+ children: /* @__PURE__ */ jsxs(AreaChart, {
4747
+ data: sparklineData,
4748
+ margin: {
4749
+ top: 0,
4750
+ right: 0,
4751
+ left: 0,
4752
+ bottom: 0
4753
+ },
4754
+ children: [/* @__PURE__ */ jsx(YAxis, {
4755
+ domain: ["dataMin", "dataMax"],
4756
+ hide: true
4757
+ }), /* @__PURE__ */ jsx(Area, {
4758
+ type: "monotone",
4759
+ dataKey: "value",
4760
+ stroke: colorConfig.stroke,
4761
+ fill: colorConfig.fill,
4762
+ fillOpacity: .3,
4763
+ strokeWidth: 1.5,
4764
+ isAnimationActive: false
4765
+ })]
4766
+ })
4767
+ })
4768
+ })
4769
+ ]
4770
+ });
4771
+ }
4772
+ function TrendIndicator({ direction, value }) {
4773
+ const colorClass = direction === "up" ? "text-green-400" : direction === "down" ? "text-red-400" : "text-gray-400";
4774
+ const arrow = direction === "up" ? "↑" : direction === "down" ? "↓" : "→";
4775
+ return /* @__PURE__ */ jsxs("span", {
4776
+ className: `text-sm font-medium ${colorClass}`,
4777
+ children: [arrow, value !== void 0 && ` ${Math.abs(value).toFixed(1)}%`]
4778
+ });
4779
+ }
4780
+
4781
+ //#endregion
4782
+ //#region src/components/observability/MetricTable/index.tsx
4783
+ /**
4784
+ * MetricTable - Accepts OtelMetricsRow[] and renders tabular metric data.
4785
+ */
4786
+ const defaultFormatValue = (value) => {
4787
+ if (Number.isInteger(value)) return value.toLocaleString();
4788
+ return value.toFixed(2);
4789
+ };
4790
+ const defaultFormatTimestamp = (timestamp) => {
4791
+ return new Date(timestamp).toLocaleString();
4792
+ };
4793
+ function formatLabels(labels) {
4794
+ const entries = Object.entries(labels);
4795
+ if (entries.length === 0) return "-";
4796
+ return entries.map(([k, v]) => `${k}=${v}`).join(", ");
4797
+ }
4798
+ function buildTableRows(rows, maxRows) {
4799
+ const result = [];
4800
+ for (const row of rows) {
4801
+ if (row.MetricType === "Histogram" || row.MetricType === "ExponentialHistogram" || row.MetricType === "Summary") continue;
4802
+ const timestamp = Number(BigInt(row.TimeUnix) / 1000000n);
4803
+ const value = "Value" in row ? row.Value : 0;
4804
+ const labels = {};
4805
+ if (row.Attributes) for (const [k, v] of Object.entries(row.Attributes)) labels[k] = String(v);
4806
+ const key = row.Attributes ? JSON.stringify(row.Attributes) : "__default__";
4807
+ result.push({
4808
+ id: `${row.MetricName}-${key}-${timestamp}`,
4809
+ timestamp,
4810
+ metricName: row.MetricName ?? "unknown",
4811
+ labels,
4812
+ value,
4813
+ unit: row.MetricUnit ?? ""
4814
+ });
4815
+ }
4816
+ return result.sort((a, b) => b.timestamp - a.timestamp).slice(0, maxRows);
4817
+ }
4818
+ function MetricTable({ rows, isLoading = false, error, maxRows = 100, formatValue = defaultFormatValue, formatTimestamp = defaultFormatTimestamp, columns = [
4819
+ "timestamp",
4820
+ "metric",
4821
+ "labels",
4822
+ "value"
4823
+ ], className = "" }) {
4824
+ const tableRows = useMemo(() => buildTableRows(rows, maxRows), [rows, maxRows]);
4825
+ if (isLoading) return /* @__PURE__ */ jsx("div", {
4826
+ className: `bg-background rounded-lg p-4 ${className}`,
4827
+ children: /* @__PURE__ */ jsxs("div", {
4828
+ className: "animate-pulse",
4829
+ "data-testid": "metric-table-loading",
4830
+ children: [/* @__PURE__ */ jsx("div", { className: "h-10 bg-gray-800 rounded mb-2" }), [
4831
+ 1,
4832
+ 2,
4833
+ 3,
4834
+ 4,
4835
+ 5
4836
+ ].map((i) => /* @__PURE__ */ jsx("div", { className: "h-12 bg-gray-800/50 rounded mb-1" }, i))]
4837
+ })
4838
+ });
4839
+ if (error) return /* @__PURE__ */ jsx("div", {
4840
+ className: `bg-background rounded-lg p-4 border border-red-800 ${className}`,
4841
+ "data-testid": "metric-table-error",
4842
+ children: /* @__PURE__ */ jsxs("p", {
4843
+ className: "text-red-400",
4844
+ children: ["Error loading metrics: ", error.message]
4845
+ })
4846
+ });
4847
+ if (tableRows.length === 0) return /* @__PURE__ */ jsx("div", {
4848
+ className: `bg-background rounded-lg p-4 border border-gray-800 ${className}`,
4849
+ "data-testid": "metric-table-empty",
4850
+ children: /* @__PURE__ */ jsx("p", {
4851
+ className: "text-gray-500 text-center py-4",
4852
+ children: "No metric data available"
4853
+ })
4854
+ });
4855
+ return /* @__PURE__ */ jsxs("div", {
4856
+ className: `bg-background rounded-lg overflow-hidden ${className}`,
4857
+ "data-testid": "metric-table",
4858
+ children: [/* @__PURE__ */ jsx("div", {
4859
+ className: "overflow-x-auto",
4860
+ children: /* @__PURE__ */ jsxs("table", {
4861
+ className: "w-full text-sm",
4862
+ children: [/* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs("tr", {
4863
+ className: "bg-gray-800 text-gray-300 text-left",
4864
+ children: [
4865
+ columns.includes("timestamp") && /* @__PURE__ */ jsx("th", {
4866
+ className: "px-4 py-3 font-medium",
4867
+ children: "Timestamp"
4868
+ }),
4869
+ columns.includes("metric") && /* @__PURE__ */ jsx("th", {
4870
+ className: "px-4 py-3 font-medium",
4871
+ children: "Metric"
4872
+ }),
4873
+ columns.includes("labels") && /* @__PURE__ */ jsx("th", {
4874
+ className: "px-4 py-3 font-medium",
4875
+ children: "Labels"
4876
+ }),
4877
+ columns.includes("value") && /* @__PURE__ */ jsx("th", {
4878
+ className: "px-4 py-3 font-medium text-right",
4879
+ children: "Value"
4880
+ })
4881
+ ]
4882
+ }) }), /* @__PURE__ */ jsx("tbody", {
4883
+ className: "divide-y divide-gray-800",
4884
+ children: tableRows.map((row) => /* @__PURE__ */ jsxs("tr", {
4885
+ className: "hover:bg-gray-800/50 transition-colors",
4886
+ children: [
4887
+ columns.includes("timestamp") && /* @__PURE__ */ jsx("td", {
4888
+ className: "px-4 py-3 text-gray-400 whitespace-nowrap",
4889
+ children: formatTimestamp(row.timestamp)
4890
+ }),
4891
+ columns.includes("metric") && /* @__PURE__ */ jsx("td", {
4892
+ className: "px-4 py-3 text-gray-200 font-mono",
4893
+ children: row.metricName
4894
+ }),
4895
+ columns.includes("labels") && /* @__PURE__ */ jsx("td", {
4896
+ className: "px-4 py-3 text-gray-400 font-mono text-xs",
4897
+ children: formatLabels(row.labels)
4898
+ }),
4899
+ columns.includes("value") && /* @__PURE__ */ jsxs("td", {
4900
+ className: "px-4 py-3 text-white font-medium text-right whitespace-nowrap",
4901
+ children: [formatValue(row.value), row.unit && /* @__PURE__ */ jsx("span", {
4902
+ className: "text-gray-500 ml-1",
4903
+ children: row.unit
4904
+ })]
4905
+ })
4906
+ ]
4907
+ }, row.id))
4908
+ })]
4909
+ })
4910
+ }), tableRows.length === maxRows && /* @__PURE__ */ jsxs("div", {
4911
+ className: "px-4 py-2 bg-gray-800 text-gray-500 text-xs text-center",
4912
+ children: [
4913
+ "Showing first ",
4914
+ maxRows,
4915
+ " rows"
4916
+ ]
4917
+ })]
4918
+ });
4919
+ }
4920
+
4921
+ //#endregion
4922
+ //#region src/lib/renderer.tsx
4923
+ /**
4924
+ * Creates a typed Renderer component bound to a catalog and component implementations.
4925
+ *
4926
+ * @param _catalog - The catalog created via createCatalog (used for type inference)
4927
+ * @param components - React component implementations matching catalog definitions
4928
+ * @returns A Renderer component that only needs `tree` and optional `fallback`
4929
+ *
4930
+ * @example
4931
+ * ```tsx
4932
+ * const DashboardRenderer = createRendererFromCatalog(catalog, {
4933
+ * Card: ({ element, children }) => <div className="card">{children}</div>,
4934
+ * Table: ({ element, data }) => <table>...</table>,
4935
+ * });
4936
+ *
4937
+ * <DashboardRenderer tree={uiTree} />
4938
+ * ```
4939
+ */
4940
+ function createRendererFromCatalog(_catalog, components) {
4941
+ return function CatalogRenderer({ tree, fallback }) {
4942
+ return /* @__PURE__ */ jsx(Renderer, {
4943
+ tree,
4944
+ registry: components,
4945
+ fallback
4946
+ });
4947
+ };
4948
+ }
4949
+ /**
4950
+ * Wrapper component for elements with dataSource
4951
+ */
4952
+ function DataSourceElement({ element, Component, children }) {
4953
+ const [paramsOverride, setParamsOverride] = useState({});
4954
+ const { data, loading, error, refetch } = useKopaiData(useMemo(() => {
4955
+ if (!element.dataSource) return void 0;
4956
+ return {
4957
+ ...element.dataSource,
4958
+ params: {
4959
+ ...element.dataSource.params,
4960
+ ...paramsOverride
4961
+ }
4962
+ };
4963
+ }, [element.dataSource, paramsOverride]));
4964
+ return /* @__PURE__ */ jsx(Component, {
4965
+ element,
4966
+ hasData: true,
4967
+ data,
4968
+ loading,
4969
+ error,
4970
+ refetch,
4971
+ updateParams: useCallback((params) => {
4972
+ setParamsOverride((prev) => ({
4973
+ ...prev,
4974
+ ...params
4975
+ }));
4976
+ }, []),
4977
+ children
4978
+ });
4979
+ }
4980
+ /**
4981
+ * Internal element renderer - recursively renders elements and children
4982
+ */
4983
+ function ElementRenderer({ element, tree, registry, fallback }) {
4984
+ const Component = registry[element.type] ?? fallback;
4985
+ if (!Component) {
4986
+ console.warn(`No renderer for component type: ${element.type}`);
4987
+ return null;
4988
+ }
4989
+ const children = element.children?.map((childKey) => {
4990
+ const childElement = tree.elements[childKey];
4991
+ if (!childElement) return null;
4992
+ return /* @__PURE__ */ jsx(ElementRenderer, {
4993
+ element: childElement,
4994
+ tree,
4995
+ registry,
4996
+ fallback
4997
+ }, childKey);
4998
+ });
4999
+ if (element.dataSource) return /* @__PURE__ */ jsx(DataSourceElement, {
5000
+ element,
5001
+ Component,
5002
+ children
5003
+ });
5004
+ return /* @__PURE__ */ jsx(Component, {
5005
+ element,
5006
+ hasData: false,
5007
+ children
5008
+ });
5009
+ }
5010
+ /**
5011
+ * Renders a UITree using a component registry.
5012
+ * Prefer using {@link createRendererFromCatalog} for type-safe rendering.
5013
+ */
5014
+ function Renderer({ tree, registry, fallback }) {
5015
+ if (!tree || !tree.root) return null;
5016
+ const rootElement = tree.elements[tree.root];
5017
+ if (!rootElement) return null;
5018
+ return /* @__PURE__ */ jsx(ElementRenderer, {
5019
+ element: rootElement,
5020
+ tree,
5021
+ registry,
5022
+ fallback
5023
+ });
3913
5024
  }
3914
5025
 
3915
5026
  //#endregion
@@ -4339,6 +5450,245 @@ function Text({ element }) {
4339
5450
  });
4340
5451
  }
4341
5452
 
5453
+ //#endregion
5454
+ //#region src/components/observability/renderers/OtelLogTimeline.tsx
5455
+ function OtelLogTimeline(props) {
5456
+ if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5457
+ style: {
5458
+ padding: 24,
5459
+ color: "var(--muted)"
5460
+ },
5461
+ children: "No data source"
5462
+ });
5463
+ const response = props.data;
5464
+ return /* @__PURE__ */ jsx(LogTimeline, {
5465
+ rows: response?.data ?? [],
5466
+ isLoading: props.loading,
5467
+ error: props.error ?? void 0
5468
+ });
5469
+ }
5470
+
5471
+ //#endregion
5472
+ //#region src/components/observability/renderers/OtelMetricDiscovery.tsx
5473
+ const TYPE_ORDER = {
5474
+ Gauge: 0,
5475
+ Sum: 1,
5476
+ Histogram: 2,
5477
+ ExponentialHistogram: 3,
5478
+ Summary: 4
5479
+ };
5480
+ function OtelMetricDiscovery(props) {
5481
+ const data = props.hasData ? props.data : null;
5482
+ const loading = props.hasData ? props.loading : false;
5483
+ const error = props.hasData ? props.error : null;
5484
+ const sorted = useMemo(() => {
5485
+ if (!data?.metrics) return [];
5486
+ return [...data.metrics].sort((a, b) => a.name.localeCompare(b.name) || (TYPE_ORDER[a.type] ?? 99) - (TYPE_ORDER[b.type] ?? 99));
5487
+ }, [data]);
5488
+ if (loading && !sorted.length) return /* @__PURE__ */ jsx("p", {
5489
+ className: "text-muted-foreground py-4",
5490
+ children: "Loading metrics…"
5491
+ });
5492
+ if (error) return /* @__PURE__ */ jsxs("p", {
5493
+ className: "text-red-400 py-4",
5494
+ children: ["Error: ", error.message]
5495
+ });
5496
+ if (!sorted.length) return /* @__PURE__ */ jsx("p", {
5497
+ className: "text-muted-foreground py-4",
5498
+ children: "No metrics discovered."
5499
+ });
5500
+ return /* @__PURE__ */ jsx("div", {
5501
+ className: "overflow-x-auto",
5502
+ children: /* @__PURE__ */ jsxs("table", {
5503
+ className: "w-full text-sm text-left text-foreground border-collapse",
5504
+ children: [/* @__PURE__ */ jsx("thead", {
5505
+ className: "text-xs uppercase text-muted-foreground border-b border-border",
5506
+ children: /* @__PURE__ */ jsxs("tr", { children: [
5507
+ /* @__PURE__ */ jsx("th", {
5508
+ className: "px-3 py-2",
5509
+ children: "Name"
5510
+ }),
5511
+ /* @__PURE__ */ jsx("th", {
5512
+ className: "px-3 py-2",
5513
+ children: "Type"
5514
+ }),
5515
+ /* @__PURE__ */ jsx("th", {
5516
+ className: "px-3 py-2",
5517
+ children: "Unit"
5518
+ }),
5519
+ /* @__PURE__ */ jsx("th", {
5520
+ className: "px-3 py-2",
5521
+ children: "Description"
5522
+ })
5523
+ ] })
5524
+ }), /* @__PURE__ */ jsx("tbody", { children: sorted.map((m) => /* @__PURE__ */ jsxs("tr", {
5525
+ className: "border-b border-border/50 hover:bg-muted/40",
5526
+ children: [
5527
+ /* @__PURE__ */ jsx("td", {
5528
+ className: "px-3 py-2 font-mono whitespace-nowrap",
5529
+ children: m.name
5530
+ }),
5531
+ /* @__PURE__ */ jsx("td", {
5532
+ className: "px-3 py-2 text-muted-foreground",
5533
+ children: m.type
5534
+ }),
5535
+ /* @__PURE__ */ jsx("td", {
5536
+ className: "px-3 py-2 text-muted-foreground",
5537
+ children: m.unit || "–"
5538
+ }),
5539
+ /* @__PURE__ */ jsx("td", {
5540
+ className: "px-3 py-2 text-muted-foreground",
5541
+ children: m.description || "–"
5542
+ })
5543
+ ]
5544
+ }, `${m.name}-${m.type}`)) })]
5545
+ })
5546
+ });
5547
+ }
5548
+
5549
+ //#endregion
5550
+ //#region src/components/observability/renderers/OtelMetricHistogram.tsx
5551
+ function OtelMetricHistogram(props) {
5552
+ if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5553
+ style: {
5554
+ padding: 24,
5555
+ color: "var(--muted)"
5556
+ },
5557
+ children: "No data source"
5558
+ });
5559
+ const response = props.data;
5560
+ return /* @__PURE__ */ jsx(MetricHistogram, {
5561
+ rows: response?.data ?? [],
5562
+ isLoading: props.loading,
5563
+ error: props.error ?? void 0,
5564
+ height: props.element.props.height ?? 400,
5565
+ yAxisLabel: props.element.props.yAxisLabel ?? void 0,
5566
+ unit: props.element.props.unit ?? void 0
5567
+ });
5568
+ }
5569
+
5570
+ //#endregion
5571
+ //#region src/components/observability/renderers/OtelMetricStat.tsx
5572
+ function OtelMetricStat(props) {
5573
+ if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5574
+ style: {
5575
+ padding: 24,
5576
+ color: "var(--muted)"
5577
+ },
5578
+ children: "No data source"
5579
+ });
5580
+ const response = props.data;
5581
+ return /* @__PURE__ */ jsx(MetricStat, {
5582
+ rows: response?.data ?? [],
5583
+ isLoading: props.loading,
5584
+ error: props.error ?? void 0,
5585
+ label: props.element.props.label ?? void 0,
5586
+ showSparkline: props.element.props.showSparkline ?? false,
5587
+ formatValue: formatOtelValue
5588
+ });
5589
+ }
5590
+
5591
+ //#endregion
5592
+ //#region src/components/observability/renderers/OtelMetricTable.tsx
5593
+ function OtelMetricTable(props) {
5594
+ if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5595
+ style: {
5596
+ padding: 24,
5597
+ color: "var(--muted)"
5598
+ },
5599
+ children: "No data source"
5600
+ });
5601
+ const response = props.data;
5602
+ return /* @__PURE__ */ jsx(MetricTable, {
5603
+ rows: response?.data ?? [],
5604
+ isLoading: props.loading,
5605
+ error: props.error ?? void 0,
5606
+ maxRows: props.element.props.maxRows ?? 100
5607
+ });
5608
+ }
5609
+
5610
+ //#endregion
5611
+ //#region src/components/observability/renderers/OtelMetricTimeSeries.tsx
5612
+ function OtelMetricTimeSeries(props) {
5613
+ if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5614
+ style: {
5615
+ padding: 24,
5616
+ color: "var(--muted)"
5617
+ },
5618
+ children: "No data source"
5619
+ });
5620
+ const response = props.data;
5621
+ return /* @__PURE__ */ jsx(MetricTimeSeries, {
5622
+ rows: response?.data ?? [],
5623
+ isLoading: props.loading,
5624
+ error: props.error ?? void 0,
5625
+ height: props.element.props.height ?? 400,
5626
+ showBrush: props.element.props.showBrush ?? true,
5627
+ yAxisLabel: props.element.props.yAxisLabel ?? void 0,
5628
+ unit: props.element.props.unit ?? void 0
5629
+ });
5630
+ }
5631
+
5632
+ //#endregion
5633
+ //#region src/components/observability/renderers/OtelTraceDetail.tsx
5634
+ function OtelTraceDetail(props) {
5635
+ if (!props.hasData) return /* @__PURE__ */ jsx("div", {
5636
+ style: {
5637
+ padding: 24,
5638
+ color: "var(--muted)"
5639
+ },
5640
+ children: "No data source"
5641
+ });
5642
+ const rows = props.data?.data ?? [];
5643
+ const firstRow = rows[0];
5644
+ const service = firstRow?.ServiceName ?? "unknown";
5645
+ const traceId = firstRow?.TraceId ?? "";
5646
+ return /* @__PURE__ */ jsx(TraceDetail, {
5647
+ rows,
5648
+ isLoading: props.loading,
5649
+ error: props.error ?? void 0,
5650
+ service,
5651
+ traceId,
5652
+ onBack: () => {}
5653
+ });
5654
+ }
5655
+
5656
+ //#endregion
5657
+ //#region src/components/observability/DynamicDashboard/index.tsx
5658
+ const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
5659
+ Card,
5660
+ Grid,
5661
+ Stack,
5662
+ Heading,
5663
+ Text,
5664
+ Badge,
5665
+ Divider,
5666
+ Empty,
5667
+ LogTimeline: OtelLogTimeline,
5668
+ TraceDetail: OtelTraceDetail,
5669
+ MetricTimeSeries: OtelMetricTimeSeries,
5670
+ MetricHistogram: OtelMetricHistogram,
5671
+ MetricStat: OtelMetricStat,
5672
+ MetricTable: OtelMetricTable,
5673
+ MetricDiscovery: OtelMetricDiscovery
5674
+ });
5675
+ function DynamicDashboard({ kopaiClient, uiTree }) {
5676
+ return /* @__PURE__ */ jsx(KopaiSDKProvider, {
5677
+ client: kopaiClient,
5678
+ children: /* @__PURE__ */ jsx(MetricsRenderer, { tree: uiTree })
5679
+ });
5680
+ }
5681
+
5682
+ //#endregion
5683
+ //#region src/components/observability/ServiceList/shortcuts.ts
5684
+ const SERVICES_SHORTCUTS = {
5685
+ name: "Services",
5686
+ shortcuts: [{
5687
+ keys: ["Backspace"],
5688
+ description: "Go back"
5689
+ }]
5690
+ };
5691
+
4342
5692
  //#endregion
4343
5693
  //#region src/pages/observability.tsx
4344
5694
  const TABS = [
@@ -4363,12 +5713,14 @@ function readURLState() {
4363
5713
  const service = params.get("service");
4364
5714
  const trace = params.get("trace");
4365
5715
  const span = params.get("span");
5716
+ const dashboardId = params.get("dashboardId");
4366
5717
  const rawTab = params.get("tab");
4367
5718
  return {
4368
5719
  tab: service ? "services" : rawTab === "logs" || rawTab === "metrics" ? rawTab : "services",
4369
5720
  service,
4370
5721
  trace,
4371
- span
5722
+ span,
5723
+ dashboardId
4372
5724
  };
4373
5725
  }
4374
5726
  function pushURLState(state, { replace = false } = {}) {
@@ -4377,6 +5729,8 @@ function pushURLState(state, { replace = false } = {}) {
4377
5729
  if (state.service) params.set("service", state.service);
4378
5730
  if (state.trace) params.set("trace", state.trace);
4379
5731
  if (state.span) params.set("span", state.span);
5732
+ const dashboardId = state.dashboardId !== void 0 ? state.dashboardId : new URLSearchParams(window.location.search).get("dashboardId");
5733
+ if (dashboardId) params.set("dashboardId", dashboardId);
4380
5734
  const qs = params.toString();
4381
5735
  const url = `${window.location.pathname}${qs ? `?${qs}` : ""}`;
4382
5736
  if (replace) history.replaceState(null, "", url);
@@ -4392,7 +5746,8 @@ let _cachedState = {
4392
5746
  tab: "services",
4393
5747
  service: null,
4394
5748
  trace: null,
4395
- span: null
5749
+ span: null,
5750
+ dashboardId: null
4396
5751
  };
4397
5752
  function getURLSnapshot() {
4398
5753
  const search = window.location.search;
@@ -4740,23 +6095,7 @@ function ServicesTab({ selectedService, selectedTraceId, selectedSpanId, onSelec
4740
6095
  });
4741
6096
  return /* @__PURE__ */ jsx(ServiceListView, { onSelect: onSelectService });
4742
6097
  }
4743
- const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
4744
- Card,
4745
- Grid,
4746
- Stack,
4747
- Heading,
4748
- Text,
4749
- Badge,
4750
- Divider,
4751
- Empty,
4752
- LogTimeline: () => null,
4753
- TraceDetail: () => null,
4754
- MetricTimeSeries: () => null,
4755
- MetricHistogram: () => null,
4756
- MetricStat: () => null,
4757
- MetricTable: () => null,
4758
- MetricDiscovery: OtelMetricDiscovery
4759
- });
6098
+ const DASHBOARDS_API_BASE = "/dashboards";
4760
6099
  const METRICS_TREE = {
4761
6100
  root: "root",
4762
6101
  elements: {
@@ -4817,11 +6156,53 @@ const METRICS_TREE = {
4817
6156
  }
4818
6157
  }
4819
6158
  };
6159
+ function useDashboardTree(dashboardId) {
6160
+ const { data, isFetching, error } = useQuery({
6161
+ queryKey: ["dashboard-tree", dashboardId],
6162
+ 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);
6167
+ if (!parsed.success) {
6168
+ const issue = parsed.error.issues[0];
6169
+ const path = issue?.path.length ? issue.path.join(".") + ": " : "";
6170
+ throw new Error(`Dashboard has an invalid layout: ${path}${issue?.message}`);
6171
+ }
6172
+ return parsed.data;
6173
+ },
6174
+ enabled: !!dashboardId
6175
+ });
6176
+ return {
6177
+ loading: isFetching,
6178
+ error: error?.message ?? null,
6179
+ tree: data ?? null
6180
+ };
6181
+ }
4820
6182
  function MetricsTab() {
4821
- return /* @__PURE__ */ jsx(MetricsRenderer, { tree: METRICS_TREE });
6183
+ const kopaiClient = useKopaiSDK();
6184
+ const { dashboardId } = useURLState();
6185
+ const { loading, error, tree } = useDashboardTree(dashboardId);
6186
+ if (loading) return /* @__PURE__ */ jsx("p", {
6187
+ className: "text-muted-foreground text-sm",
6188
+ children: "Loading dashboard..."
6189
+ });
6190
+ if (error) return /* @__PURE__ */ jsxs("p", {
6191
+ className: "text-muted-foreground text-sm",
6192
+ children: ["Error: ", error]
6193
+ });
6194
+ return /* @__PURE__ */ jsx(DynamicDashboard, {
6195
+ kopaiClient,
6196
+ uiTree: tree ?? METRICS_TREE
6197
+ });
6198
+ }
6199
+ let _defaultClient;
6200
+ function getDefaultClient() {
6201
+ _defaultClient ??= new KopaiClient({ baseUrl: "" });
6202
+ return _defaultClient;
4822
6203
  }
4823
- const client = new KopaiClient({ baseUrl: "/signals" });
4824
- function ObservabilityPage() {
6204
+ function ObservabilityPage({ client }) {
6205
+ const activeClient = client ?? getDefaultClient();
4825
6206
  const { tab: activeTab, service: selectedService, trace: selectedTraceId, span: selectedSpanId } = useURLState();
4826
6207
  const handleTabChange = useCallback((tab) => {
4827
6208
  pushURLState({ tab });
@@ -4857,7 +6238,7 @@ function ObservabilityPage() {
4857
6238
  });
4858
6239
  }, [selectedService]);
4859
6240
  return /* @__PURE__ */ jsx(KopaiSDKProvider, {
4860
- client,
6241
+ client: activeClient,
4861
6242
  children: /* @__PURE__ */ jsx(KeyboardShortcutsProvider, {
4862
6243
  onNavigateServices: () => pushURLState({ tab: "services" }),
4863
6244
  onNavigateLogs: () => pushURLState({ tab: "logs" }),
@@ -4923,16 +6304,17 @@ function buildExampleElements(names, components) {
4923
6304
  for (const name of otherNames) {
4924
6305
  const key = `${String(name).toLowerCase()}-1`;
4925
6306
  childKeys.push(key);
4926
- elements[key] = {
6307
+ const element = {
4927
6308
  key,
4928
6309
  type: String(name),
4929
6310
  props: {},
4930
- parentKey: containerKey,
4931
- dataSource: {
4932
- method: "searchTracesPage",
4933
- params: { limit: 10 }
4934
- }
6311
+ parentKey: containerKey
6312
+ };
6313
+ if (!components[name]?.hasChildren) element.dataSource = {
6314
+ method: "searchTracesPage",
6315
+ params: { limit: 10 }
4935
6316
  };
6317
+ elements[key] = element;
4936
6318
  }
4937
6319
  return {
4938
6320
  root: containerKey,
@@ -4976,7 +6358,7 @@ function buildUnifiedSchema(treeSchema) {
4976
6358
  * const prompt = `Build a dashboard UI.\n\n${instructions}`;
4977
6359
  * ```
4978
6360
  */
4979
- function generatePromptInstructions(catalog) {
6361
+ function generatePromptInstructions(catalog, uiTreeVersion) {
4980
6362
  const componentNames = Object.keys(catalog.components);
4981
6363
  const componentSections = componentNames.map((name) => {
4982
6364
  const def = catalog.components[name];
@@ -4991,7 +6373,13 @@ ${roleLine}`;
4991
6373
  }).join("\n\n---\n\n");
4992
6374
  const unifiedSchema = buildUnifiedSchema(catalog.uiTreeSchema);
4993
6375
  const exampleElements = buildExampleElements(componentNames, catalog.components);
4994
- return `## Available Components
6376
+ return `## UI Tree Version
6377
+
6378
+ Use uiTreeVersion: "${uiTreeVersion}" when creating dashboards.
6379
+
6380
+ ---
6381
+
6382
+ ## Available Components
4995
6383
 
4996
6384
  ${componentSections}
4997
6385
 
@@ -5009,5 +6397,5 @@ ${JSON.stringify(exampleElements)}`;
5009
6397
  }
5010
6398
 
5011
6399
  //#endregion
5012
- export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, useKopaiSDK };
6400
+ export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
5013
6401
  //# sourceMappingURL=index.mjs.map