@kopai/ui 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -0
- package/dist/index.cjs +1598 -233
- package/dist/index.d.cts +566 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +565 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1597 -209
- package/dist/index.mjs.map +1 -1
- package/package.json +13 -12
- package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +234 -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 +124 -0
- package/src/pages/observability.tsx +71 -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({
|
|
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,116 +3762,1296 @@ function LogFilter({ value, onChange, rows = [], selectedServices = [], onSelect
|
|
|
3855
3762
|
}
|
|
3856
3763
|
|
|
3857
3764
|
//#endregion
|
|
3858
|
-
//#region src/components/observability/
|
|
3859
|
-
|
|
3860
|
-
*
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
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/
|
|
3871
|
-
|
|
3872
|
-
|
|
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/
|
|
4006
|
+
//#region src/components/observability/MetricTimeSeries/index.tsx
|
|
3877
4007
|
/**
|
|
3878
|
-
*
|
|
4008
|
+
* MetricTimeSeries - Accepts OtelMetricsRow[] and renders line charts.
|
|
3879
4009
|
*/
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
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
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
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)(
|
|
3948
|
-
|
|
3949
|
-
|
|
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)(
|
|
3952
|
-
|
|
3953
|
-
|
|
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)(
|
|
3956
|
-
|
|
3957
|
-
|
|
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)(
|
|
3960
|
-
|
|
3961
|
-
|
|
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
|
-
}
|
|
4297
|
+
})
|
|
3965
4298
|
})
|
|
3966
4299
|
});
|
|
3967
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"
|
|
4883
|
+
})
|
|
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
|
+
});
|
|
5054
|
+
}
|
|
3968
5055
|
|
|
3969
5056
|
//#endregion
|
|
3970
5057
|
//#region src/lib/catalog.ts
|
|
@@ -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
|
|
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,11 +6186,53 @@ 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
|
-
|
|
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
|
-
|
|
4878
|
-
function
|
|
6229
|
+
let _defaultClient;
|
|
6230
|
+
function getDefaultClient() {
|
|
6231
|
+
_defaultClient ??= new _kopai_sdk.KopaiClient({ baseUrl: "" });
|
|
6232
|
+
return _defaultClient;
|
|
6233
|
+
}
|
|
6234
|
+
function ObservabilityPage({ client }) {
|
|
6235
|
+
const activeClient = client ?? getDefaultClient();
|
|
4879
6236
|
const { tab: activeTab, service: selectedService, trace: selectedTraceId, span: selectedSpanId } = useURLState();
|
|
4880
6237
|
const handleTabChange = (0, react.useCallback)((tab) => {
|
|
4881
6238
|
pushURLState({ tab });
|
|
@@ -4911,7 +6268,7 @@ function ObservabilityPage() {
|
|
|
4911
6268
|
});
|
|
4912
6269
|
}, [selectedService]);
|
|
4913
6270
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KopaiSDKProvider, {
|
|
4914
|
-
client,
|
|
6271
|
+
client: activeClient,
|
|
4915
6272
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyboardShortcutsProvider, {
|
|
4916
6273
|
onNavigateServices: () => pushURLState({ tab: "services" }),
|
|
4917
6274
|
onNavigateLogs: () => pushURLState({ tab: "logs" }),
|
|
@@ -4977,16 +6334,17 @@ function buildExampleElements(names, components) {
|
|
|
4977
6334
|
for (const name of otherNames) {
|
|
4978
6335
|
const key = `${String(name).toLowerCase()}-1`;
|
|
4979
6336
|
childKeys.push(key);
|
|
4980
|
-
|
|
6337
|
+
const element = {
|
|
4981
6338
|
key,
|
|
4982
6339
|
type: String(name),
|
|
4983
6340
|
props: {},
|
|
4984
|
-
parentKey: containerKey
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
}
|
|
6341
|
+
parentKey: containerKey
|
|
6342
|
+
};
|
|
6343
|
+
if (!components[name]?.hasChildren) element.dataSource = {
|
|
6344
|
+
method: "searchTracesPage",
|
|
6345
|
+
params: { limit: 10 }
|
|
4989
6346
|
};
|
|
6347
|
+
elements[key] = element;
|
|
4990
6348
|
}
|
|
4991
6349
|
return {
|
|
4992
6350
|
root: containerKey,
|
|
@@ -5030,7 +6388,7 @@ function buildUnifiedSchema(treeSchema) {
|
|
|
5030
6388
|
* const prompt = `Build a dashboard UI.\n\n${instructions}`;
|
|
5031
6389
|
* ```
|
|
5032
6390
|
*/
|
|
5033
|
-
function generatePromptInstructions(catalog) {
|
|
6391
|
+
function generatePromptInstructions(catalog, uiTreeVersion) {
|
|
5034
6392
|
const componentNames = Object.keys(catalog.components);
|
|
5035
6393
|
const componentSections = componentNames.map((name) => {
|
|
5036
6394
|
const def = catalog.components[name];
|
|
@@ -5045,7 +6403,13 @@ ${roleLine}`;
|
|
|
5045
6403
|
}).join("\n\n---\n\n");
|
|
5046
6404
|
const unifiedSchema = buildUnifiedSchema(catalog.uiTreeSchema);
|
|
5047
6405
|
const exampleElements = buildExampleElements(componentNames, catalog.components);
|
|
5048
|
-
return `##
|
|
6406
|
+
return `## UI Tree Version
|
|
6407
|
+
|
|
6408
|
+
Use uiTreeVersion: "${uiTreeVersion}" when creating dashboards.
|
|
6409
|
+
|
|
6410
|
+
---
|
|
6411
|
+
|
|
6412
|
+
## Available Components
|
|
5049
6413
|
|
|
5050
6414
|
${componentSections}
|
|
5051
6415
|
|
|
@@ -5069,4 +6433,5 @@ exports.Renderer = Renderer;
|
|
|
5069
6433
|
exports.createCatalog = createCatalog;
|
|
5070
6434
|
exports.createRendererFromCatalog = createRendererFromCatalog;
|
|
5071
6435
|
exports.generatePromptInstructions = generatePromptInstructions;
|
|
6436
|
+
exports.observabilityCatalog = observabilityCatalog;
|
|
5072
6437
|
exports.useKopaiSDK = useKopaiSDK;
|