@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.
- package/README.md +23 -0
- package/dist/index.cjs +1591 -231
- package/dist/index.d.cts +559 -2
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +559 -2
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1590 -207
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -11
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +239 -0
- package/src/components/observability/DynamicDashboard/index.tsx +64 -0
- package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +10 -1
- package/src/components/observability/MetricHistogram/index.tsx +85 -19
- package/src/components/observability/MetricStat/MetricStat.stories.tsx +2 -1
- package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +23 -1
- package/src/components/observability/MetricTimeSeries/index.tsx +70 -27
- package/src/components/observability/__fixtures__/metrics.ts +97 -0
- package/src/components/observability/index.ts +3 -0
- package/src/components/observability/renderers/OtelLogTimeline.tsx +28 -0
- package/src/components/observability/renderers/OtelMetricHistogram.tsx +2 -0
- package/src/components/observability/renderers/OtelMetricStat.tsx +1 -13
- package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +2 -0
- package/src/components/observability/renderers/OtelTraceDetail.tsx +35 -0
- package/src/components/observability/renderers/index.ts +2 -0
- package/src/components/observability/utils/attributes.ts +7 -0
- package/src/components/observability/utils/units.test.ts +116 -0
- package/src/components/observability/utils/units.ts +132 -0
- package/src/index.ts +1 -0
- package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +7 -1
- package/src/lib/generate-prompt-instructions.test.ts +1 -1
- package/src/lib/generate-prompt-instructions.ts +18 -6
- package/src/lib/observability-catalog.ts +7 -1
- package/src/lib/renderer.tsx +1 -1
- package/src/pages/observability.test.tsx +129 -0
- package/src/pages/observability.tsx +60 -34
- 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({
|
|
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,93 +3732,1297 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
|
|
|
3825
3732
|
}
|
|
3826
3733
|
|
|
3827
3734
|
//#endregion
|
|
3828
|
-
//#region src/components/observability/
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
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/
|
|
3839
|
-
const
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
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
|
|
3847
|
-
const
|
|
3848
|
-
const
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
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
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
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 (
|
|
3863
|
-
className: "
|
|
3864
|
-
|
|
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: "
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
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(
|
|
3878
|
-
|
|
3879
|
-
|
|
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(
|
|
3882
|
-
|
|
3883
|
-
|
|
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(
|
|
3886
|
-
|
|
3887
|
-
|
|
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
|
-
})
|
|
3891
|
-
|
|
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
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
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(
|
|
3898
|
-
|
|
3899
|
-
|
|
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(
|
|
3902
|
-
|
|
3903
|
-
|
|
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(
|
|
3906
|
-
|
|
3907
|
-
|
|
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
|
-
}
|
|
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
|
+
})
|
|
3911
4584
|
})
|
|
3912
4585
|
});
|
|
3913
4586
|
}
|
|
3914
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
|
|
4705
|
+
})
|
|
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
|
+
});
|
|
5024
|
+
}
|
|
5025
|
+
|
|
3915
5026
|
//#endregion
|
|
3916
5027
|
//#region src/lib/catalog.ts
|
|
3917
5028
|
const dashboardCatalog = createCatalog({
|
|
@@ -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
|
|
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,12 +6156,49 @@ 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
|
-
|
|
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
|
+
});
|
|
4822
6198
|
}
|
|
4823
6199
|
let _defaultClient;
|
|
4824
6200
|
function getDefaultClient() {
|
|
4825
|
-
_defaultClient ??= new KopaiClient({ baseUrl: "
|
|
6201
|
+
_defaultClient ??= new KopaiClient({ baseUrl: "" });
|
|
4826
6202
|
return _defaultClient;
|
|
4827
6203
|
}
|
|
4828
6204
|
function ObservabilityPage({ client }) {
|
|
@@ -4928,16 +6304,17 @@ function buildExampleElements(names, components) {
|
|
|
4928
6304
|
for (const name of otherNames) {
|
|
4929
6305
|
const key = `${String(name).toLowerCase()}-1`;
|
|
4930
6306
|
childKeys.push(key);
|
|
4931
|
-
|
|
6307
|
+
const element = {
|
|
4932
6308
|
key,
|
|
4933
6309
|
type: String(name),
|
|
4934
6310
|
props: {},
|
|
4935
|
-
parentKey: containerKey
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
}
|
|
6311
|
+
parentKey: containerKey
|
|
6312
|
+
};
|
|
6313
|
+
if (!components[name]?.hasChildren) element.dataSource = {
|
|
6314
|
+
method: "searchTracesPage",
|
|
6315
|
+
params: { limit: 10 }
|
|
4940
6316
|
};
|
|
6317
|
+
elements[key] = element;
|
|
4941
6318
|
}
|
|
4942
6319
|
return {
|
|
4943
6320
|
root: containerKey,
|
|
@@ -4981,7 +6358,7 @@ function buildUnifiedSchema(treeSchema) {
|
|
|
4981
6358
|
* const prompt = `Build a dashboard UI.\n\n${instructions}`;
|
|
4982
6359
|
* ```
|
|
4983
6360
|
*/
|
|
4984
|
-
function generatePromptInstructions(catalog) {
|
|
6361
|
+
function generatePromptInstructions(catalog, uiTreeVersion) {
|
|
4985
6362
|
const componentNames = Object.keys(catalog.components);
|
|
4986
6363
|
const componentSections = componentNames.map((name) => {
|
|
4987
6364
|
const def = catalog.components[name];
|
|
@@ -4996,7 +6373,13 @@ ${roleLine}`;
|
|
|
4996
6373
|
}).join("\n\n---\n\n");
|
|
4997
6374
|
const unifiedSchema = buildUnifiedSchema(catalog.uiTreeSchema);
|
|
4998
6375
|
const exampleElements = buildExampleElements(componentNames, catalog.components);
|
|
4999
|
-
return `##
|
|
6376
|
+
return `## UI Tree Version
|
|
6377
|
+
|
|
6378
|
+
Use uiTreeVersion: "${uiTreeVersion}" when creating dashboards.
|
|
6379
|
+
|
|
6380
|
+
---
|
|
6381
|
+
|
|
6382
|
+
## Available Components
|
|
5000
6383
|
|
|
5001
6384
|
${componentSections}
|
|
5002
6385
|
|
|
@@ -5014,5 +6397,5 @@ ${JSON.stringify(exampleElements)}`;
|
|
|
5014
6397
|
}
|
|
5015
6398
|
|
|
5016
6399
|
//#endregion
|
|
5017
|
-
export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, useKopaiSDK };
|
|
6400
|
+
export { KopaiSDKProvider, ObservabilityPage, Renderer, createCatalog, createRendererFromCatalog, generatePromptInstructions, observabilityCatalog, useKopaiSDK };
|
|
5018
6401
|
//# sourceMappingURL=index.mjs.map
|