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