@kopai/ui 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +23 -0
  2. package/dist/index.cjs +1598 -233
  3. package/dist/index.d.cts +566 -4
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +565 -3
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +1597 -209
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +13 -12
  10. package/src/components/observability/DynamicDashboard/DynamicDashboard.test.tsx +234 -0
  11. package/src/components/observability/DynamicDashboard/index.tsx +64 -0
  12. package/src/components/observability/MetricHistogram/MetricHistogram.stories.tsx +10 -1
  13. package/src/components/observability/MetricHistogram/index.tsx +85 -19
  14. package/src/components/observability/MetricStat/MetricStat.stories.tsx +2 -1
  15. package/src/components/observability/MetricTimeSeries/MetricTimeSeries.stories.tsx +23 -1
  16. package/src/components/observability/MetricTimeSeries/index.tsx +70 -27
  17. package/src/components/observability/__fixtures__/metrics.ts +97 -0
  18. package/src/components/observability/index.ts +3 -0
  19. package/src/components/observability/renderers/OtelLogTimeline.tsx +28 -0
  20. package/src/components/observability/renderers/OtelMetricHistogram.tsx +2 -0
  21. package/src/components/observability/renderers/OtelMetricStat.tsx +1 -13
  22. package/src/components/observability/renderers/OtelMetricTimeSeries.tsx +2 -0
  23. package/src/components/observability/renderers/OtelTraceDetail.tsx +35 -0
  24. package/src/components/observability/renderers/index.ts +2 -0
  25. package/src/components/observability/utils/attributes.ts +7 -0
  26. package/src/components/observability/utils/units.test.ts +116 -0
  27. package/src/components/observability/utils/units.ts +132 -0
  28. package/src/index.ts +1 -0
  29. package/src/lib/__snapshots__/generate-prompt-instructions.test.ts.snap +7 -1
  30. package/src/lib/generate-prompt-instructions.test.ts +1 -1
  31. package/src/lib/generate-prompt-instructions.ts +18 -6
  32. package/src/lib/observability-catalog.ts +7 -1
  33. package/src/lib/renderer.tsx +1 -1
  34. package/src/pages/observability.test.tsx +124 -0
  35. package/src/pages/observability.tsx +71 -36
  36. package/src/lib/dashboard-datasource.ts +0 -76
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kopai/ui",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Vladimir Adamic",
6
6
  "repository": {
@@ -19,7 +19,8 @@
19
19
  "import": "./dist/index.mjs",
20
20
  "require": "./dist/index.cjs"
21
21
  },
22
- "./globals.css": "./src/styles/globals.css"
22
+ "./globals.css": "./src/styles/globals.css",
23
+ "./package.json": "./package.json"
23
24
  },
24
25
  "main": "./dist/index.cjs",
25
26
  "module": "./dist/index.mjs",
@@ -30,30 +31,30 @@
30
31
  ],
31
32
  "dependencies": {
32
33
  "@tanstack/react-query": "^5",
33
- "@tanstack/react-virtual": "^3.11.2",
34
+ "@tanstack/react-virtual": "^3.13.19",
34
35
  "recharts": "^3.7.0",
35
- "@kopai/core": "0.6.0",
36
- "@kopai/sdk": "0.3.1"
36
+ "@kopai/core": "0.7.0",
37
+ "@kopai/sdk": "0.4.0"
37
38
  },
38
39
  "peerDependencies": {
39
40
  "react": "^19.2.4",
40
41
  "react-dom": "^19.2.4"
41
42
  },
42
43
  "devDependencies": {
43
- "@storybook/addon-docs": "^10.2.8",
44
- "@storybook/react": "^10.2.8",
45
- "@storybook/react-vite": "^10.2.8",
44
+ "@storybook/addon-docs": "^10.2.13",
45
+ "@storybook/react": "^10.2.13",
46
+ "@storybook/react-vite": "^10.2.13",
46
47
  "@testing-library/react": "^16.3.0",
47
48
  "@types/react": "^19.2.14",
48
49
  "@types/react-dom": "^19.1.0",
49
50
  "@vitejs/plugin-react": "^5.1.4",
50
- "@tailwindcss/postcss": "^4.1.18",
51
- "jsdom": "^28.0.0",
51
+ "@tailwindcss/postcss": "^4.2.1",
52
+ "jsdom": "^28.1.0",
52
53
  "postcss": "^8.5.3",
53
54
  "react": "^19.2.4",
54
55
  "react-dom": "^19.2.4",
55
- "storybook": "^10.2.8",
56
- "tailwindcss": "^4.1.18",
56
+ "storybook": "^10.2.13",
57
+ "tailwindcss": "^4.2.1",
57
58
  "tsdown": "^0.20.3",
58
59
  "vite": "^7.3.1",
59
60
  "@kopai/tsconfig": "0.2.0"
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from "vitest";
5
+ import { createElement } from "react";
6
+ import { render, waitFor } from "@testing-library/react";
7
+ import { DynamicDashboard, type UITree } from "./index.js";
8
+ import { queryClient } from "../../../providers/kopai-provider.js";
9
+ import type { KopaiClient } from "@kopai/sdk";
10
+
11
+ type MockClient = {
12
+ [K in keyof KopaiClient]: ReturnType<typeof vi.fn>;
13
+ };
14
+
15
+ function createMockClient(): MockClient {
16
+ return {
17
+ searchTracesPage: vi.fn().mockResolvedValue({ data: [] }),
18
+ searchLogsPage: vi.fn().mockResolvedValue({ data: [] }),
19
+ searchMetricsPage: vi.fn().mockResolvedValue({ data: [] }),
20
+ getTrace: vi.fn().mockResolvedValue({ data: [] }),
21
+ discoverMetrics: vi.fn().mockResolvedValue({ data: [] }),
22
+ searchTraces: vi.fn().mockResolvedValue({ data: [] }),
23
+ searchLogs: vi.fn().mockResolvedValue({ data: [] }),
24
+ searchMetrics: vi.fn().mockResolvedValue({ data: [] }),
25
+ createDashboard: vi.fn().mockResolvedValue({}),
26
+ };
27
+ }
28
+
29
+ /**
30
+ * UITree containing every component from the observability catalog.
31
+ *
32
+ * Structure:
33
+ * Stack (root)
34
+ * ├── Heading
35
+ * ├── Text
36
+ * ├── Badge
37
+ * ├── Divider
38
+ * ├── Empty
39
+ * ├── Card
40
+ * │ └── Grid
41
+ * │ ├── MetricTimeSeries (dataSource: searchMetricsPage)
42
+ * │ ├── MetricHistogram (dataSource: searchMetricsPage)
43
+ * │ ├── MetricStat (dataSource: searchMetricsPage)
44
+ * │ └── MetricTable (dataSource: searchMetricsPage)
45
+ * ├── LogTimeline (dataSource: searchLogsPage)
46
+ * ├── TraceDetail (dataSource: searchTracesPage)
47
+ * └── MetricDiscovery (dataSource: discoverMetrics)
48
+ */
49
+ const ALL_ELEMENTS_TREE = {
50
+ root: "root",
51
+ elements: {
52
+ root: {
53
+ key: "root",
54
+ type: "Stack" as const,
55
+ children: [
56
+ "heading",
57
+ "text",
58
+ "badge",
59
+ "divider",
60
+ "empty",
61
+ "card",
62
+ "log-timeline",
63
+ "trace-detail",
64
+ "metric-discovery",
65
+ ],
66
+ parentKey: "",
67
+ props: {
68
+ direction: "vertical" as const,
69
+ gap: "md" as const,
70
+ align: null,
71
+ },
72
+ },
73
+ heading: {
74
+ key: "heading",
75
+ type: "Heading" as const,
76
+ children: [],
77
+ parentKey: "root",
78
+ props: { text: "Dashboard", level: "h1" as const },
79
+ },
80
+ text: {
81
+ key: "text",
82
+ type: "Text" as const,
83
+ children: [],
84
+ parentKey: "root",
85
+ props: { content: "Overview", variant: null, color: null },
86
+ },
87
+ badge: {
88
+ key: "badge",
89
+ type: "Badge" as const,
90
+ children: [],
91
+ parentKey: "root",
92
+ props: { text: "Active", variant: "success" as const },
93
+ },
94
+ divider: {
95
+ key: "divider",
96
+ type: "Divider" as const,
97
+ children: [],
98
+ parentKey: "root",
99
+ props: { label: null },
100
+ },
101
+ empty: {
102
+ key: "empty",
103
+ type: "Empty" as const,
104
+ children: [],
105
+ parentKey: "root",
106
+ props: {
107
+ title: "No data",
108
+ description: null,
109
+ action: null,
110
+ actionLabel: null,
111
+ },
112
+ },
113
+ card: {
114
+ key: "card",
115
+ type: "Card" as const,
116
+ children: ["grid"],
117
+ parentKey: "root",
118
+ props: { title: "Metrics", description: null, padding: "md" as const },
119
+ },
120
+ grid: {
121
+ key: "grid",
122
+ type: "Grid" as const,
123
+ children: [
124
+ "metric-time-series",
125
+ "metric-histogram",
126
+ "metric-stat",
127
+ "metric-table",
128
+ ],
129
+ parentKey: "card",
130
+ props: { columns: 2, gap: "md" as const },
131
+ },
132
+ "metric-time-series": {
133
+ key: "metric-time-series",
134
+ type: "MetricTimeSeries" as const,
135
+ children: [],
136
+ parentKey: "grid",
137
+ props: { height: 300, showBrush: null },
138
+ dataSource: {
139
+ method: "searchMetricsPage" as const,
140
+ params: { metricType: "Gauge" as const },
141
+ },
142
+ },
143
+ "metric-histogram": {
144
+ key: "metric-histogram",
145
+ type: "MetricHistogram" as const,
146
+ children: [],
147
+ parentKey: "grid",
148
+ props: { height: 300 },
149
+ dataSource: {
150
+ method: "searchMetricsPage" as const,
151
+ params: { metricType: "Gauge" as const },
152
+ },
153
+ },
154
+ "metric-stat": {
155
+ key: "metric-stat",
156
+ type: "MetricStat" as const,
157
+ children: [],
158
+ parentKey: "grid",
159
+ props: { label: "Requests", showSparkline: true },
160
+ dataSource: {
161
+ method: "searchMetricsPage" as const,
162
+ params: { metricType: "Gauge" as const },
163
+ },
164
+ },
165
+ "metric-table": {
166
+ key: "metric-table",
167
+ type: "MetricTable" as const,
168
+ children: [],
169
+ parentKey: "grid",
170
+ props: { maxRows: 50 },
171
+ dataSource: {
172
+ method: "searchMetricsPage" as const,
173
+ params: { metricType: "Gauge" as const },
174
+ },
175
+ },
176
+ "log-timeline": {
177
+ key: "log-timeline",
178
+ type: "LogTimeline" as const,
179
+ children: [],
180
+ parentKey: "root",
181
+ props: { height: 400 },
182
+ dataSource: { method: "searchLogsPage" as const, params: {} },
183
+ },
184
+ "trace-detail": {
185
+ key: "trace-detail",
186
+ type: "TraceDetail" as const,
187
+ children: [],
188
+ parentKey: "root",
189
+ props: { height: 400 },
190
+ dataSource: { method: "searchTracesPage" as const, params: {} },
191
+ },
192
+ "metric-discovery": {
193
+ key: "metric-discovery",
194
+ type: "MetricDiscovery" as const,
195
+ children: [],
196
+ parentKey: "root",
197
+ props: {},
198
+ dataSource: { method: "discoverMetrics" as const, params: {} },
199
+ },
200
+ },
201
+ } satisfies UITree;
202
+
203
+ describe("DynamicDashboard", () => {
204
+ let mockClient: MockClient;
205
+
206
+ beforeEach(() => {
207
+ mockClient = createMockClient();
208
+ queryClient.clear();
209
+ vi.clearAllMocks();
210
+ });
211
+
212
+ it("renders a UITree containing all catalog components", async () => {
213
+ const { container } = render(
214
+ createElement(DynamicDashboard, {
215
+ kopaiClient: mockClient as unknown as KopaiClient,
216
+ uiTree: ALL_ELEMENTS_TREE,
217
+ })
218
+ );
219
+
220
+ // Wait for async data fetches to settle
221
+ await waitFor(() => {
222
+ expect(mockClient.searchMetricsPage).toHaveBeenCalled();
223
+ expect(mockClient.searchLogsPage).toHaveBeenCalled();
224
+ expect(mockClient.searchTracesPage).toHaveBeenCalled();
225
+ expect(mockClient.discoverMetrics).toHaveBeenCalled();
226
+ });
227
+
228
+ // Verify static content rendered
229
+ expect(container.textContent).toContain("Dashboard");
230
+ expect(container.textContent).toContain("Overview");
231
+ expect(container.textContent).toContain("Active");
232
+ expect(container.textContent).toContain("No data");
233
+ });
234
+ });
@@ -0,0 +1,64 @@
1
+ import {
2
+ createRendererFromCatalog,
3
+ type UITree,
4
+ } from "../../../lib/renderer.js";
5
+ import {
6
+ KopaiSDKProvider,
7
+ type KopaiClient,
8
+ } from "../../../providers/kopai-provider.js";
9
+ import { observabilityCatalog } from "../../../lib/observability-catalog.js";
10
+ import {
11
+ Heading,
12
+ Text,
13
+ Card,
14
+ Stack,
15
+ Grid,
16
+ Badge,
17
+ Divider,
18
+ Empty,
19
+ } from "../../dashboard/index.js";
20
+ import {
21
+ OtelLogTimeline,
22
+ OtelMetricDiscovery,
23
+ OtelMetricHistogram,
24
+ OtelMetricStat,
25
+ OtelMetricTable,
26
+ OtelMetricTimeSeries,
27
+ OtelTraceDetail,
28
+ } from "../renderers/index.js";
29
+
30
+ const MetricsRenderer = createRendererFromCatalog(observabilityCatalog, {
31
+ Card,
32
+ Grid,
33
+ Stack,
34
+ Heading,
35
+ Text,
36
+ Badge,
37
+ Divider,
38
+ Empty,
39
+ LogTimeline: OtelLogTimeline,
40
+ TraceDetail: OtelTraceDetail,
41
+ MetricTimeSeries: OtelMetricTimeSeries,
42
+ MetricHistogram: OtelMetricHistogram,
43
+ MetricStat: OtelMetricStat,
44
+ MetricTable: OtelMetricTable,
45
+ MetricDiscovery: OtelMetricDiscovery,
46
+ });
47
+
48
+ export { type UITree };
49
+
50
+ export interface DynamicDashboardProps {
51
+ kopaiClient: KopaiClient;
52
+ uiTree: UITree;
53
+ }
54
+
55
+ export function DynamicDashboard({
56
+ kopaiClient,
57
+ uiTree,
58
+ }: DynamicDashboardProps) {
59
+ return (
60
+ <KopaiSDKProvider client={kopaiClient}>
61
+ <MetricsRenderer tree={uiTree} />
62
+ </KopaiSDKProvider>
63
+ );
64
+ }
@@ -1,6 +1,9 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { MetricHistogram } from "./index.js";
3
- import { mockHistogramRows } from "../__fixtures__/metrics.js";
3
+ import {
4
+ mockHistogramRows,
5
+ mockNoAttributeHistogramRows,
6
+ } from "../__fixtures__/metrics.js";
4
7
 
5
8
  const meta: Meta<typeof MetricHistogram> = {
6
9
  title: "Observability/MetricHistogram",
@@ -18,3 +21,9 @@ export const Error: Story = {
18
21
  },
19
22
  };
20
23
  export const Empty: Story = { args: { rows: [] } };
24
+ export const NoAttributes: Story = {
25
+ args: { rows: mockNoAttributeHistogramRows },
26
+ };
27
+ export const WithUnit: Story = {
28
+ args: { rows: mockHistogramRows, unit: "ms" },
29
+ };
@@ -15,6 +15,12 @@ import {
15
15
  Cell,
16
16
  } from "recharts";
17
17
  import type { denormalizedSignals } from "@kopai/core";
18
+ import { formatSeriesLabel } from "../utils/attributes.js";
19
+ import {
20
+ resolveUnitScale,
21
+ formatDisplayValue,
22
+ type ResolvedScale,
23
+ } from "../utils/units.js";
18
24
 
19
25
  type OtelMetricsRow = denormalizedSignals.OtelMetricsRow;
20
26
 
@@ -32,6 +38,7 @@ export interface MetricHistogramProps {
32
38
  isLoading?: boolean;
33
39
  error?: Error;
34
40
  height?: number;
41
+ unit?: string;
35
42
  yAxisLabel?: string;
36
43
  showLegend?: boolean;
37
44
  formatBucketLabel?: (
@@ -69,17 +76,38 @@ const defaultFormatValue = (value: number): string => {
69
76
  function buildHistogramData(
70
77
  rows: OtelMetricsRow[],
71
78
  formatLabel = defaultFormatBucketLabel
72
- ): { buckets: BucketData[]; seriesKeys: string[] } {
79
+ ): {
80
+ buckets: BucketData[];
81
+ seriesKeys: string[];
82
+ displayLabelMap: Map<string, string>;
83
+ unit: string;
84
+ } {
73
85
  const buckets: BucketData[] = [];
74
86
  const seriesKeysSet = new Set<string>();
87
+ const displayLabelMap = new Map<string, string>();
88
+ let unit = "";
75
89
 
76
90
  for (const row of rows) {
77
91
  if (row.MetricType !== "Histogram") continue;
92
+ if (!unit && row.MetricUnit) unit = row.MetricUnit;
78
93
  const name = row.MetricName ?? "count";
79
94
  const key = row.Attributes ? JSON.stringify(row.Attributes) : "__default__";
80
95
  const seriesName = key === "__default__" ? name : key;
81
96
  seriesKeysSet.add(seriesName);
82
97
 
98
+ if (!displayLabelMap.has(seriesName)) {
99
+ if (key === "__default__") {
100
+ displayLabelMap.set(seriesName, name);
101
+ } else {
102
+ const labels: Record<string, string> = {};
103
+ if (row.Attributes) {
104
+ for (const [k, v] of Object.entries(row.Attributes))
105
+ labels[k] = String(v);
106
+ }
107
+ displayLabelMap.set(seriesName, formatSeriesLabel(labels) || name);
108
+ }
109
+ }
110
+
83
111
  const bounds = row.ExplicitBounds ?? [];
84
112
  const counts = row.BucketCounts ?? [];
85
113
 
@@ -102,7 +130,12 @@ function buildHistogramData(
102
130
  }
103
131
 
104
132
  buckets.sort((a, b) => a.lowerBound - b.lowerBound);
105
- return { buckets, seriesKeys: Array.from(seriesKeysSet) };
133
+ return {
134
+ buckets,
135
+ seriesKeys: Array.from(seriesKeysSet),
136
+ displayLabelMap,
137
+ unit,
138
+ };
106
139
  }
107
140
 
108
141
  export function MetricHistogram({
@@ -110,6 +143,7 @@ export function MetricHistogram({
110
143
  isLoading = false,
111
144
  error,
112
145
  height = 400,
146
+ unit: unitProp,
113
147
  yAxisLabel,
114
148
  showLegend = true,
115
149
  formatBucketLabel,
@@ -117,17 +151,30 @@ export function MetricHistogram({
117
151
  labelStyle = "staggered",
118
152
  }: MetricHistogramProps) {
119
153
  const bucketLabelFormatter = formatBucketLabel ?? defaultFormatBucketLabel;
120
- const unit = useMemo(() => {
121
- for (const r of rows)
122
- if (r.MetricType === "Histogram" && r.MetricUnit) return r.MetricUnit;
123
- return "";
124
- }, [rows]);
125
154
 
126
- const { buckets, seriesKeys } = useMemo(() => {
127
- if (rows.length === 0) return { buckets: [], seriesKeys: [] };
155
+ const { buckets, seriesKeys, displayLabelMap, unit } = useMemo(() => {
156
+ if (rows.length === 0)
157
+ return {
158
+ buckets: [],
159
+ seriesKeys: [],
160
+ displayLabelMap: new Map(),
161
+ unit: "",
162
+ };
128
163
  return buildHistogramData(rows, bucketLabelFormatter);
129
164
  }, [rows, bucketLabelFormatter]);
130
165
 
166
+ const effectiveUnit = unitProp ?? unit;
167
+
168
+ const boundsScale = useMemo(() => {
169
+ if (!effectiveUnit || buckets.length === 0) return null;
170
+ // Buckets are sorted by lowerBound — walk backwards to find last finite upperBound
171
+ for (let i = buckets.length - 1; i >= 0; i--) {
172
+ if (buckets[i]!.upperBound !== Infinity)
173
+ return resolveUnitScale(effectiveUnit, buckets[i]!.upperBound);
174
+ }
175
+ return null;
176
+ }, [effectiveUnit, buckets]);
177
+
131
178
  if (isLoading) return <HistogramLoadingSkeleton height={height} />;
132
179
 
133
180
  if (error) {
@@ -217,9 +264,20 @@ export function MetricHistogram({
217
264
  }
218
265
  />
219
266
  <Tooltip
220
- content={<HistogramTooltip formatValue={formatValue} unit={unit} />}
267
+ content={(props) => (
268
+ <HistogramTooltip
269
+ {...props}
270
+ formatValue={formatValue}
271
+ boundsScale={boundsScale}
272
+ displayLabelMap={displayLabelMap}
273
+ />
274
+ )}
221
275
  />
222
- {showLegend && seriesKeys.length > 1 && <Legend />}
276
+ {showLegend && seriesKeys.length > 1 && (
277
+ <Legend
278
+ formatter={(value: string) => displayLabelMap.get(value) ?? value}
279
+ />
280
+ )}
223
281
  {seriesKeys.map((key, i) => (
224
282
  <Bar
225
283
  key={key}
@@ -242,31 +300,39 @@ function HistogramTooltip({
242
300
  active,
243
301
  payload,
244
302
  formatValue,
245
- unit,
303
+ boundsScale,
304
+ displayLabelMap,
246
305
  }: {
247
306
  active?: boolean;
248
- payload?: Array<{
307
+ payload?: readonly {
249
308
  dataKey: string;
250
309
  value: number;
251
310
  color: string;
252
311
  payload: BucketData;
253
- }>;
312
+ }[];
254
313
  formatValue: (val: number) => string;
255
- unit?: string;
314
+ boundsScale: ResolvedScale | null;
315
+ displayLabelMap: Map<string, string>;
256
316
  }) {
257
317
  if (!active || !payload?.length) return null;
258
318
  const bucket = payload[0]?.payload;
259
319
  if (!bucket) return null;
320
+
321
+ const boundsLabel = boundsScale
322
+ ? `${formatDisplayValue(bucket.lowerBound, boundsScale)} – ${bucket.upperBound === Infinity ? "∞" : formatDisplayValue(bucket.upperBound, boundsScale)}`
323
+ : bucket.bucket;
324
+
260
325
  return (
261
326
  <div className="bg-background border border-gray-700 rounded-lg p-3 shadow-lg">
262
327
  <p className="text-gray-300 text-sm font-medium mb-2">
263
- Bucket: {bucket.bucket}
264
- {unit ? ` ${unit}` : ""}
328
+ Bucket: {boundsLabel}
265
329
  </p>
266
330
  {payload.map((entry, i) => (
267
331
  <p key={i} className="text-sm" style={{ color: entry.color }}>
268
- <span className="font-medium">{entry.dataKey}:</span>{" "}
269
- {formatValue(entry.value)} requests
332
+ <span className="font-medium">
333
+ {displayLabelMap.get(entry.dataKey) ?? entry.dataKey}:
334
+ </span>{" "}
335
+ {formatValue(entry.value)}
270
336
  </p>
271
337
  ))}
272
338
  </div>
@@ -1,6 +1,6 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { MetricStat } from "./index.js";
3
- import { mockStatRows } from "../__fixtures__/metrics.js";
3
+ import { mockStatRows, mockNoAttributeRows } from "../__fixtures__/metrics.js";
4
4
 
5
5
  const meta: Meta<typeof MetricStat> = {
6
6
  title: "Observability/MetricStat",
@@ -28,3 +28,4 @@ export const Error: Story = {
28
28
  args: { rows: [], error: new globalThis.Error("Failed to fetch stat") },
29
29
  };
30
30
  export const Empty: Story = { args: { rows: [] } };
31
+ export const NoAttributes: Story = { args: { rows: mockNoAttributeRows } };
@@ -1,6 +1,13 @@
1
1
  import type { Meta, StoryObj } from "@storybook/react";
2
2
  import { MetricTimeSeries } from "./index.js";
3
- import { mockGaugeRows, mockSumRows } from "../__fixtures__/metrics.js";
3
+ import {
4
+ mockGaugeRows,
5
+ mockSumRows,
6
+ mockNoAttributeRows,
7
+ mockMultiAttributeRows,
8
+ mockStatRows,
9
+ mockLargeByteRows,
10
+ } from "../__fixtures__/metrics.js";
4
11
 
5
12
  const meta: Meta<typeof MetricTimeSeries> = {
6
13
  title: "Observability/MetricTimeSeries",
@@ -26,3 +33,18 @@ export const Error: Story = {
26
33
  args: { rows: [], error: new globalThis.Error("Failed to fetch metrics") },
27
34
  };
28
35
  export const Empty: Story = { args: { rows: [] } };
36
+ export const NoAttributes: Story = {
37
+ args: { rows: mockNoAttributeRows },
38
+ };
39
+ export const MultiAttributes: Story = {
40
+ args: { rows: mockMultiAttributeRows },
41
+ };
42
+ export const BytesUnit: Story = {
43
+ args: { rows: mockNoAttributeRows, unit: "By" },
44
+ };
45
+ export const PercentUnit: Story = {
46
+ args: { rows: mockStatRows, unit: "1" },
47
+ };
48
+ export const GigabyteScale: Story = {
49
+ args: { rows: mockLargeByteRows, unit: "By" },
50
+ };