@kopai/ui 0.9.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kopai/ui",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "license": "Apache-2.0",
5
5
  "author": "Vladimir Adamic",
6
6
  "repository": {
@@ -31,32 +31,32 @@
31
31
  ],
32
32
  "dependencies": {
33
33
  "@tanstack/react-query": "^5",
34
- "@tanstack/react-virtual": "^3.13.22",
35
- "recharts": "^3.8.0",
36
- "@kopai/core": "0.8.0",
37
- "@kopai/sdk": "0.6.0"
34
+ "@tanstack/react-virtual": "^3.13.23",
35
+ "recharts": "^3.8.1",
36
+ "@kopai/core": "0.9.0",
37
+ "@kopai/sdk": "0.7.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "react": "^19.2.4",
41
41
  "react-dom": "^19.2.4"
42
42
  },
43
43
  "devDependencies": {
44
- "@storybook/addon-docs": "^10.2.19",
45
- "@storybook/react": "^10.2.19",
46
- "@storybook/react-vite": "^10.2.19",
44
+ "@storybook/addon-docs": "^10.3.4",
45
+ "@storybook/react": "^10.3.4",
46
+ "@storybook/react-vite": "^10.3.4",
47
47
  "@testing-library/react": "^16.3.0",
48
48
  "@types/react": "^19.2.14",
49
49
  "@types/react-dom": "^19.1.0",
50
50
  "@vitejs/plugin-react": "^6.0.1",
51
- "@tailwindcss/postcss": "^4.2.1",
52
- "jsdom": "^28.1.0",
51
+ "@tailwindcss/postcss": "^4.2.2",
52
+ "jsdom": "^29.0.1",
53
53
  "postcss": "^8.5.8",
54
54
  "react": "^19.2.4",
55
55
  "react-dom": "^19.2.4",
56
- "storybook": "^10.2.19",
57
- "tailwindcss": "^4.2.1",
58
- "tsdown": "^0.21.2",
59
- "vite": "^8.0.0",
56
+ "storybook": "^10.3.4",
57
+ "tailwindcss": "^4.2.2",
58
+ "tsdown": "^0.21.7",
59
+ "vite": "^8.0.3",
60
60
  "@kopai/tsconfig": "0.2.0"
61
61
  },
62
62
  "scripts": {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import { describe, it, expect, vi, beforeEach } from "vitest";
5
5
  import { createElement } from "react";
6
- import { render, waitFor } from "@testing-library/react";
6
+ import { render, waitFor, fireEvent } from "@testing-library/react";
7
7
  import { DynamicDashboard, type UITree } from "./index.js";
8
8
  import { queryClient } from "../../../providers/kopai-provider.js";
9
9
  import type { KopaiClient } from "@kopai/sdk";
@@ -17,6 +17,9 @@ function createMockClient(): MockClient {
17
17
  searchTracesPage: vi.fn().mockResolvedValue({ data: [] }),
18
18
  searchLogsPage: vi.fn().mockResolvedValue({ data: [] }),
19
19
  searchMetricsPage: vi.fn().mockResolvedValue({ data: [] }),
20
+ searchAggregatedMetrics: vi
21
+ .fn()
22
+ .mockResolvedValue({ data: [], nextCursor: null }),
20
23
  getTrace: vi.fn().mockResolvedValue({ data: [] }),
21
24
  discoverMetrics: vi.fn().mockResolvedValue({ data: [] }),
22
25
  searchTraces: vi.fn().mockResolvedValue({ data: [] }),
@@ -219,6 +222,177 @@ describe("DynamicDashboard", () => {
219
222
  vi.clearAllMocks();
220
223
  });
221
224
 
225
+ it("renders TraceDetail with searchTraceSummariesPage without crashing", async () => {
226
+ const summaryTree = {
227
+ root: "root",
228
+ elements: {
229
+ root: {
230
+ key: "root",
231
+ type: "Stack" as const,
232
+ children: ["trace-detail"],
233
+ parentKey: "",
234
+ props: {
235
+ direction: "vertical" as const,
236
+ gap: "md" as const,
237
+ align: null,
238
+ },
239
+ },
240
+ "trace-detail": {
241
+ key: "trace-detail",
242
+ type: "TraceDetail" as const,
243
+ children: [],
244
+ parentKey: "root",
245
+ props: { height: 400 },
246
+ dataSource: {
247
+ method: "searchTraceSummariesPage" as const,
248
+ params: {
249
+ serviceName: "test-service",
250
+ limit: 20,
251
+ sortOrder: "DESC" as const,
252
+ },
253
+ },
254
+ },
255
+ },
256
+ } satisfies UITree;
257
+
258
+ mockClient.searchTraceSummariesPage.mockResolvedValue({
259
+ data: [
260
+ {
261
+ traceId: "0af7651916cd43dd8448eb211c80319c",
262
+ rootServiceName: "api-gateway",
263
+ rootSpanName: "GET /api/users",
264
+ startTimeNs: "1700000000000000000",
265
+ durationNs: "320000000",
266
+ spanCount: 8,
267
+ errorCount: 0,
268
+ services: [{ name: "api-gateway", count: 3, hasError: false }],
269
+ },
270
+ ],
271
+ nextCursor: null,
272
+ });
273
+
274
+ const { container } = render(
275
+ createElement(DynamicDashboard, {
276
+ kopaiClient: mockClient as unknown as KopaiClient,
277
+ uiTree: summaryTree,
278
+ })
279
+ );
280
+
281
+ await waitFor(() => {
282
+ expect(mockClient.searchTraceSummariesPage).toHaveBeenCalled();
283
+ });
284
+
285
+ // Should render trace summary list without crashing
286
+ await waitFor(() => {
287
+ expect(container.textContent).toContain("GET /api/users");
288
+ });
289
+ });
290
+
291
+ it("drills down from trace summary to trace detail on click", async () => {
292
+ const TRACE_ID = "0af7651916cd43dd8448eb211c80319c";
293
+
294
+ const summaryTree = {
295
+ root: "root",
296
+ elements: {
297
+ root: {
298
+ key: "root",
299
+ type: "Stack" as const,
300
+ children: ["trace-detail"],
301
+ parentKey: "",
302
+ props: {
303
+ direction: "vertical" as const,
304
+ gap: "md" as const,
305
+ align: null,
306
+ },
307
+ },
308
+ "trace-detail": {
309
+ key: "trace-detail",
310
+ type: "TraceDetail" as const,
311
+ children: [],
312
+ parentKey: "root",
313
+ props: { height: 400 },
314
+ dataSource: {
315
+ method: "searchTraceSummariesPage" as const,
316
+ params: {
317
+ serviceName: "test-service",
318
+ limit: 20,
319
+ sortOrder: "DESC" as const,
320
+ },
321
+ },
322
+ },
323
+ },
324
+ } satisfies UITree;
325
+
326
+ mockClient.searchTraceSummariesPage.mockResolvedValue({
327
+ data: [
328
+ {
329
+ traceId: TRACE_ID,
330
+ rootServiceName: "api-gateway",
331
+ rootSpanName: "GET /api/users",
332
+ startTimeNs: "1700000000000000000",
333
+ durationNs: "320000000",
334
+ spanCount: 8,
335
+ errorCount: 0,
336
+ services: [{ name: "api-gateway", count: 3, hasError: false }],
337
+ },
338
+ ],
339
+ nextCursor: null,
340
+ });
341
+
342
+ // Mock getTrace to return span data for drill-down
343
+ mockClient.getTrace.mockResolvedValue([
344
+ {
345
+ SpanId: "b7ad6b7169203331",
346
+ TraceId: TRACE_ID,
347
+ Timestamp: "1700000000000000000",
348
+ Duration: "320000000",
349
+ ParentSpanId: "",
350
+ ServiceName: "api-gateway",
351
+ SpanName: "GET /api/users",
352
+ SpanKind: "SERVER",
353
+ StatusCode: "OK",
354
+ StatusMessage: "",
355
+ ScopeName: "",
356
+ ScopeVersion: "",
357
+ SpanAttributes: {},
358
+ ResourceAttributes: { "service.name": "api-gateway" },
359
+ "Events.Name": [],
360
+ "Events.Timestamp": [],
361
+ "Events.Attributes": [],
362
+ },
363
+ ]);
364
+
365
+ const { container, getByText } = render(
366
+ createElement(DynamicDashboard, {
367
+ kopaiClient: mockClient as unknown as KopaiClient,
368
+ uiTree: summaryTree,
369
+ })
370
+ );
371
+
372
+ // Wait for summaries to render
373
+ await waitFor(() => {
374
+ expect(container.textContent).toContain("GET /api/users");
375
+ });
376
+
377
+ // Click the trace row to drill down
378
+ const traceRow = getByText("api-gateway: GET /api/users");
379
+ fireEvent.click(traceRow);
380
+
381
+ // Should fetch the full trace
382
+ await waitFor(() => {
383
+ expect(mockClient.getTrace).toHaveBeenCalledWith(
384
+ TRACE_ID,
385
+ expect.objectContaining({ signal: expect.any(AbortSignal) })
386
+ );
387
+ });
388
+
389
+ // Should render the trace detail view with "Traces" breadcrumb
390
+ await waitFor(() => {
391
+ expect(container.textContent).toContain("Traces");
392
+ expect(container.textContent).toContain(TRACE_ID.slice(0, 16));
393
+ });
394
+ });
395
+
222
396
  it("renders a UITree containing all catalog components", async () => {
223
397
  const { container } = render(
224
398
  createElement(DynamicDashboard, {
@@ -16,6 +16,10 @@ export interface ThresholdConfig {
16
16
 
17
17
  export interface MetricStatProps {
18
18
  rows: OtelMetricsRow[];
19
+ /** Pre-computed value (e.g. from aggregated queries). Bypasses row extraction when set. */
20
+ value?: number;
21
+ /** Unit string for formatting when using pre-computed value. */
22
+ unit?: string;
19
23
  isLoading?: boolean;
20
24
  error?: Error;
21
25
  label?: string;
@@ -140,6 +144,8 @@ function buildStatData(rows: OtelMetricsRow[]): {
140
144
 
141
145
  export function MetricStat({
142
146
  rows,
147
+ value: directValue,
148
+ unit: directUnit,
143
149
  isLoading = false,
144
150
  error,
145
151
  label,
@@ -155,10 +161,12 @@ export function MetricStat({
155
161
  colorBackground,
156
162
  colorValue = false,
157
163
  }: MetricStatProps) {
158
- const { latestValue, unit, timestamp, dataPoints, metricName } = useMemo(
159
- () => buildStatData(rows),
160
- [rows]
161
- );
164
+ const statData = useMemo(() => buildStatData(rows), [rows]);
165
+
166
+ // Pre-computed value (aggregated queries) bypasses row extraction
167
+ const latestValue = directValue ?? statData.latestValue;
168
+ const unit = directUnit ?? statData.unit;
169
+ const { timestamp, dataPoints, metricName } = statData;
162
170
 
163
171
  const sparklineData = useMemo(() => {
164
172
  if (!showSparkline || dataPoints.length === 0) return [];
@@ -5,11 +5,23 @@ import { formatOtelValue } from "../utils/units.js";
5
5
  import type { denormalizedSignals } from "@kopai/core";
6
6
 
7
7
  type OtelMetricsRow = denormalizedSignals.OtelMetricsRow;
8
+ type AggregatedMetricRow = denormalizedSignals.AggregatedMetricRow;
8
9
 
9
10
  type Props = RendererComponentProps<
10
11
  typeof observabilityCatalog.components.MetricStat
11
12
  >;
12
13
 
14
+ const EMPTY_ROWS: never[] = [];
15
+ const GROUPED_AGGREGATE_ERROR = new Error(
16
+ "MetricStat cannot display grouped aggregates. Remove groupBy or use MetricTable."
17
+ );
18
+
19
+ function isAggregatedRequest(props: Props & { hasData: true }): boolean {
20
+ const ds = props.element.dataSource;
21
+ if (!ds || ds.method !== "searchMetricsPage" || !ds.params) return false;
22
+ return !!ds.params.aggregate;
23
+ }
24
+
13
25
  export function OtelMetricStat(props: Props) {
14
26
  if (!props.hasData) {
15
27
  return (
@@ -17,6 +29,34 @@ export function OtelMetricStat(props: Props) {
17
29
  );
18
30
  }
19
31
 
32
+ if (isAggregatedRequest(props)) {
33
+ const response = props.data as { data: AggregatedMetricRow[] } | null;
34
+ const rows = response?.data ?? [];
35
+
36
+ if (rows.length > 1) {
37
+ return (
38
+ <MetricStat
39
+ rows={EMPTY_ROWS}
40
+ error={GROUPED_AGGREGATE_ERROR}
41
+ label={props.element.props.label ?? undefined}
42
+ formatValue={formatOtelValue}
43
+ />
44
+ );
45
+ }
46
+
47
+ return (
48
+ <MetricStat
49
+ rows={EMPTY_ROWS}
50
+ value={rows[0]?.value}
51
+ isLoading={props.loading}
52
+ error={props.error ?? undefined}
53
+ label={props.element.props.label ?? undefined}
54
+ showSparkline={false}
55
+ formatValue={formatOtelValue}
56
+ />
57
+ );
58
+ }
59
+
20
60
  const response = props.data as { data?: OtelMetricsRow[] } | null;
21
61
 
22
62
  return (
@@ -1,14 +1,90 @@
1
+ import { useState, useMemo, useCallback } from "react";
2
+ import { useQuery } from "@tanstack/react-query";
1
3
  import { observabilityCatalog } from "../../../lib/observability-catalog.js";
2
4
  import type { RendererComponentProps } from "../../../lib/renderer.js";
3
5
  import { TraceDetail } from "../index.js";
4
- import type { denormalizedSignals } from "@kopai/core";
6
+ import { TraceSearch } from "../TraceSearch/index.js";
7
+ import type { TraceSummary } from "../TraceSearch/index.js";
8
+ import { useKopaiSDK } from "../../../providers/kopai-provider.js";
9
+ import type { denormalizedSignals, dataFilterSchemas } from "@kopai/core";
5
10
 
6
11
  type OtelTracesRow = denormalizedSignals.OtelTracesRow;
12
+ type TraceSummaryRow = dataFilterSchemas.TraceSummaryRow;
7
13
 
8
14
  type Props = RendererComponentProps<
9
15
  typeof observabilityCatalog.components.TraceDetail
10
16
  >;
11
17
 
18
+ function isTraceSummariesSource(props: Props & { hasData: true }): boolean {
19
+ return props.element.dataSource?.method === "searchTraceSummariesPage";
20
+ }
21
+
22
+ function TraceSummariesView({
23
+ data,
24
+ loading,
25
+ error,
26
+ }: {
27
+ data: unknown;
28
+ loading: boolean;
29
+ error: Error | null;
30
+ }) {
31
+ const [selectedTraceId, setSelectedTraceId] = useState<string | null>(null);
32
+ const client = useKopaiSDK();
33
+
34
+ const response = data as { data?: TraceSummaryRow[] } | null;
35
+
36
+ const traces = useMemo<TraceSummary[]>(() => {
37
+ const rows = response?.data;
38
+ if (!Array.isArray(rows)) return [];
39
+ return rows.map((row) => ({
40
+ traceId: row.traceId,
41
+ rootSpanName: row.rootSpanName,
42
+ serviceName: row.rootServiceName,
43
+ durationMs: parseInt(row.durationNs, 10) / 1e6,
44
+ statusCode: row.errorCount > 0 ? "ERROR" : "OK",
45
+ timestampMs: parseInt(row.startTimeNs, 10) / 1e6,
46
+ spanCount: row.spanCount,
47
+ services: row.services,
48
+ errorCount: row.errorCount,
49
+ }));
50
+ }, [response]);
51
+
52
+ const {
53
+ data: traceRows,
54
+ isFetching: traceLoading,
55
+ error: traceError,
56
+ } = useQuery<OtelTracesRow[], Error>({
57
+ queryKey: ["kopai", "getTrace", selectedTraceId],
58
+ queryFn: ({ signal }) => client.getTrace(selectedTraceId!, { signal }),
59
+ enabled: !!selectedTraceId,
60
+ });
61
+
62
+ const handleBack = useCallback(() => setSelectedTraceId(null), []);
63
+
64
+ if (selectedTraceId) {
65
+ return (
66
+ <TraceDetail
67
+ traceId={selectedTraceId}
68
+ rows={traceRows ?? []}
69
+ isLoading={traceLoading}
70
+ error={traceError ?? undefined}
71
+ onBack={handleBack}
72
+ />
73
+ );
74
+ }
75
+
76
+ return (
77
+ <TraceSearch
78
+ services={[]}
79
+ service=""
80
+ traces={traces}
81
+ isLoading={loading}
82
+ error={error ?? undefined}
83
+ onSelectTrace={setSelectedTraceId}
84
+ />
85
+ );
86
+ }
87
+
12
88
  export function OtelTraceDetail(props: Props) {
13
89
  if (!props.hasData) {
14
90
  return (
@@ -16,8 +92,18 @@ export function OtelTraceDetail(props: Props) {
16
92
  );
17
93
  }
18
94
 
95
+ if (isTraceSummariesSource(props)) {
96
+ return (
97
+ <TraceSummariesView
98
+ data={props.data}
99
+ loading={props.loading}
100
+ error={props.error}
101
+ />
102
+ );
103
+ }
104
+
19
105
  const response = props.data as { data?: OtelTracesRow[] } | null;
20
- const rows = response?.data ?? [];
106
+ const rows = Array.isArray(response?.data) ? response.data : [];
21
107
  const traceId = rows[0]?.TraceId ?? "";
22
108
 
23
109
  return (
@@ -16,6 +16,7 @@ const createMockClient = () => ({
16
16
  searchTracesPage: vi.fn(),
17
17
  searchLogsPage: vi.fn(),
18
18
  searchMetricsPage: vi.fn(),
19
+ searchAggregatedMetrics: vi.fn(),
19
20
  getTrace: vi.fn(),
20
21
  discoverMetrics: vi.fn(),
21
22
  getDashboard: vi.fn(),
@@ -151,6 +152,36 @@ describe("useKopaiData", () => {
151
152
  expect(result.current.data).toEqual(mockData);
152
153
  expect(mockClient.searchMetricsPage).toHaveBeenCalled();
153
154
  });
155
+
156
+ it("routes to searchAggregatedMetrics when aggregate is set", async () => {
157
+ const mockData = {
158
+ data: [{ groups: { signal: "/v1/traces" }, value: 1024 }],
159
+ nextCursor: null,
160
+ };
161
+ mockClient.searchAggregatedMetrics.mockResolvedValue(mockData);
162
+
163
+ const dataSource: DataSource = {
164
+ method: "searchMetricsPage",
165
+ params: {
166
+ metricType: "Sum",
167
+ metricName: "kopai.ingestion.bytes",
168
+ aggregate: "sum",
169
+ groupBy: ["signal"],
170
+ },
171
+ };
172
+
173
+ const { result } = renderHook(() => useKopaiData(dataSource), {
174
+ wrapper: wrapper(mockClient),
175
+ });
176
+
177
+ await waitFor(() => {
178
+ expect(result.current.loading).toBe(false);
179
+ });
180
+
181
+ expect(result.current.data).toEqual(mockData);
182
+ expect(mockClient.searchAggregatedMetrics).toHaveBeenCalled();
183
+ expect(mockClient.searchMetricsPage).not.toHaveBeenCalled();
184
+ });
154
185
  });
155
186
 
156
187
  describe("getTrace", () => {
@@ -25,11 +25,18 @@ function fetchForDataSource(
25
25
  dataSource.params as Parameters<typeof client.searchLogsPage>[0],
26
26
  { signal }
27
27
  );
28
- case "searchMetricsPage":
29
- return client.searchMetricsPage(
30
- dataSource.params as Parameters<typeof client.searchMetricsPage>[0],
31
- { signal }
32
- );
28
+ case "searchMetricsPage": {
29
+ const params = dataSource.params as Parameters<
30
+ typeof client.searchMetricsPage
31
+ >[0];
32
+ if (params.aggregate) {
33
+ return client.searchAggregatedMetrics(
34
+ { ...params, aggregate: params.aggregate },
35
+ { signal }
36
+ );
37
+ }
38
+ return client.searchMetricsPage(params, { signal });
39
+ }
33
40
  case "getTrace":
34
41
  return client.getTrace(dataSource.params.traceId, { signal });
35
42
  case "discoverMetrics":
@@ -19,6 +19,7 @@ const createMockClient = () => ({
19
19
  searchTracesPage: vi.fn(),
20
20
  searchLogsPage: vi.fn(),
21
21
  searchMetricsPage: vi.fn(),
22
+ searchAggregatedMetrics: vi.fn(),
22
23
  getTrace: vi.fn(),
23
24
  discoverMetrics: vi.fn(),
24
25
  getDashboard: vi.fn(),
@@ -29,7 +29,7 @@ Accepts dataSource: yes
29
29
 
30
30
  ## Output Schema
31
31
 
32
- {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"root":{"type":"string","description":"root uiElement key in the elements array"},"elements":{"type":"object","propertyNames":{"type":"string","description":"equal to the element key"},"additionalProperties":{"oneOf":[{"type":"object","properties":{"key":{"type":"string"},"type":{"type":"string","const":"Card"},"children":{"type":"array","items":{"type":"string"}},"parentKey":{"type":"string"},"dataSource":{"$ref":"#/$defs/DataSource"},"props":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"],"additionalProperties":false}},"required":["key","type","children","parentKey","props"],"additionalProperties":false},{"type":"object","properties":{"key":{"type":"string"},"type":{"type":"string","const":"Button"},"children":{"type":"array","items":{"type":"string"}},"parentKey":{"type":"string"},"dataSource":{"$ref":"#/$defs/DataSource"},"props":{"type":"object","properties":{"label":{"type":"string"}},"required":["label"],"additionalProperties":false}},"required":["key","type","children","parentKey","props"],"additionalProperties":false}]}}},"required":["root","elements"],"additionalProperties":false,"$defs":{"DataSource":{"$schema":"https://json-schema.org/draft/2020-12/schema","oneOf":[{"type":"object","properties":{"method":{"type":"string","const":"searchTracesPage"},"params":{"type":"object","properties":{"traceId":{"description":"Unique identifier for a trace. All spans from the same trace share the same trace_id. The ID is a 16-byte array.","type":"string"},"spanId":{"description":"Unique identifier for a span within a trace. The ID is an 8-byte array.","type":"string"},"parentSpanId":{"description":"The span_id of this span's parent span. Empty if this is a root span.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"spanName":{"description":"Description of the span's operation. E.g., qualified method name or file name with line number.","type":"string"},"spanKind":{"description":"Type of span (INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER). Used to identify relationships between spans.","type":"string"},"statusCode":{"description":"Status code (UNSET, OK, ERROR).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"timestampMin":{"description":"Minimum start time of the span. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"timestampMax":{"description":"Maximum start time of the span. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"durationMin":{"description":"Minimum duration of the span in nanoseconds (end_time - start_time). Expressed as string in JSON.","type":"string"},"durationMax":{"description":"Maximum duration of the span in nanoseconds (end_time - start_time). Expressed as string in JSON.","type":"string"},"spanAttributes":{"description":"Key/value pairs describing the span.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"eventsAttributes":{"description":"Attribute key/value pairs on the event.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"linksAttributes":{"description":"Attribute key/value pairs on the link.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchLogsPage"},"params":{"type":"object","properties":{"traceId":{"description":"Unique identifier for a trace. All logs from the same trace share the same trace_id. The ID is a 16-byte array.","type":"string"},"spanId":{"description":"Unique identifier for a span within a trace. The ID is an 8-byte array.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"severityText":{"description":"Severity text (also known as log level). Original string representation as known at the source.","type":"string"},"severityNumberMin":{"description":"Minimum severity number (inclusive). Normalized to values described in Log Data Model.","type":"number"},"severityNumberMax":{"description":"Maximum severity number (inclusive). Normalized to values described in Log Data Model.","type":"number"},"bodyContains":{"description":"Filter logs where body contains this substring.","type":"string"},"timestampMin":{"description":"Minimum time when the event occurred. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"timestampMax":{"description":"Maximum time when the event occurred. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"logAttributes":{"description":"Additional attributes that describe the specific event occurrence.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"scopeAttributes":{"description":"Attributes of the instrumentation scope.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchMetricsPage"},"params":{"type":"object","properties":{"metricType":{"type":"string","enum":["Gauge","Sum","Histogram","ExponentialHistogram","Summary"],"description":"Metric type to query."},"metricName":{"description":"The name of the metric.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"timeUnixMin":{"description":"Minimum time when the data point was recorded. UNIX Epoch time in nanoseconds. Expressed as string in JSON.","type":"string"},"timeUnixMax":{"description":"Maximum time when the data point was recorded. UNIX Epoch time in nanoseconds. Expressed as string in JSON.","type":"string"},"attributes":{"description":"Key/value pairs that uniquely identify the timeseries.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"scopeAttributes":{"description":"Attributes of the instrumentation scope.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"required":["metricType"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getTrace"},"params":{"type":"object","properties":{"traceId":{"type":"string"}},"required":["traceId"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"discoverMetrics"},"params":{"type":"object","properties":{},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getServices"},"params":{"type":"object","properties":{},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getOperations"},"params":{"type":"object","properties":{"serviceName":{"type":"string"}},"required":["serviceName"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchTraceSummariesPage"},"params":{"type":"object","properties":{"serviceName":{"type":"string"},"spanName":{"type":"string"},"timestampMin":{"type":"string"},"timestampMax":{"type":"string"},"durationMin":{"type":"string"},"durationMax":{"type":"string"},"spanAttributes":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"default":20,"type":"integer","minimum":1,"maximum":1000},"cursor":{"type":"string"},"sortOrder":{"default":"DESC","type":"string","enum":["ASC","DESC"]}},"required":["limit","sortOrder"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false}]}}}
32
+ {"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"root":{"type":"string","description":"root uiElement key in the elements array"},"elements":{"type":"object","propertyNames":{"type":"string","description":"equal to the element key"},"additionalProperties":{"oneOf":[{"type":"object","properties":{"key":{"type":"string"},"type":{"type":"string","const":"Card"},"children":{"type":"array","items":{"type":"string"}},"parentKey":{"type":"string"},"dataSource":{"$ref":"#/$defs/DataSource"},"props":{"type":"object","properties":{"title":{"type":"string"}},"required":["title"],"additionalProperties":false}},"required":["key","type","children","parentKey","props"],"additionalProperties":false},{"type":"object","properties":{"key":{"type":"string"},"type":{"type":"string","const":"Button"},"children":{"type":"array","items":{"type":"string"}},"parentKey":{"type":"string"},"dataSource":{"$ref":"#/$defs/DataSource"},"props":{"type":"object","properties":{"label":{"type":"string"}},"required":["label"],"additionalProperties":false}},"required":["key","type","children","parentKey","props"],"additionalProperties":false}]}}},"required":["root","elements"],"additionalProperties":false,"$defs":{"DataSource":{"$schema":"https://json-schema.org/draft/2020-12/schema","oneOf":[{"type":"object","properties":{"method":{"type":"string","const":"searchTracesPage"},"params":{"type":"object","properties":{"traceId":{"description":"Unique identifier for a trace. All spans from the same trace share the same trace_id. The ID is a 16-byte array.","type":"string"},"spanId":{"description":"Unique identifier for a span within a trace. The ID is an 8-byte array.","type":"string"},"parentSpanId":{"description":"The span_id of this span's parent span. Empty if this is a root span.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"spanName":{"description":"Description of the span's operation. E.g., qualified method name or file name with line number.","type":"string"},"spanKind":{"description":"Type of span (INTERNAL, SERVER, CLIENT, PRODUCER, CONSUMER). Used to identify relationships between spans.","type":"string"},"statusCode":{"description":"Status code (UNSET, OK, ERROR).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"timestampMin":{"description":"Minimum start time of the span. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"timestampMax":{"description":"Maximum start time of the span. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"durationMin":{"description":"Minimum duration of the span in nanoseconds (end_time - start_time). Expressed as string in JSON.","type":"string"},"durationMax":{"description":"Maximum duration of the span in nanoseconds (end_time - start_time). Expressed as string in JSON.","type":"string"},"spanAttributes":{"description":"Key/value pairs describing the span.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"eventsAttributes":{"description":"Attribute key/value pairs on the event.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"linksAttributes":{"description":"Attribute key/value pairs on the link.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchLogsPage"},"params":{"type":"object","properties":{"traceId":{"description":"Unique identifier for a trace. All logs from the same trace share the same trace_id. The ID is a 16-byte array.","type":"string"},"spanId":{"description":"Unique identifier for a span within a trace. The ID is an 8-byte array.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"severityText":{"description":"Severity text (also known as log level). Original string representation as known at the source.","type":"string"},"severityNumberMin":{"description":"Minimum severity number (inclusive). Normalized to values described in Log Data Model.","type":"number"},"severityNumberMax":{"description":"Maximum severity number (inclusive). Normalized to values described in Log Data Model.","type":"number"},"bodyContains":{"description":"Filter logs where body contains this substring.","type":"string"},"timestampMin":{"description":"Minimum time when the event occurred. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"timestampMax":{"description":"Maximum time when the event occurred. UNIX Epoch time in nanoseconds since 00:00:00 UTC on 1 January 1970. Expressed as string in JSON.","type":"string"},"logAttributes":{"description":"Additional attributes that describe the specific event occurrence.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"scopeAttributes":{"description":"Attributes of the instrumentation scope.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchMetricsPage"},"params":{"type":"object","properties":{"metricType":{"type":"string","enum":["Gauge","Sum","Histogram","ExponentialHistogram","Summary"],"description":"Metric type to query."},"metricName":{"description":"The name of the metric.","type":"string"},"serviceName":{"description":"Service name from resource attributes (service.name).","type":"string"},"scopeName":{"description":"Name denoting the instrumentation scope.","type":"string"},"timeUnixMin":{"description":"Minimum time when the data point was recorded. UNIX Epoch time in nanoseconds. Expressed as string in JSON.","type":"string"},"timeUnixMax":{"description":"Maximum time when the data point was recorded. UNIX Epoch time in nanoseconds. Expressed as string in JSON.","type":"string"},"attributes":{"description":"Key/value pairs that uniquely identify the timeseries.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"description":"Attributes that describe the resource.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"scopeAttributes":{"description":"Attributes of the instrumentation scope.","type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"aggregate":{"description":"Aggregation function to apply to metric values. When set, returns aggregated results instead of raw data points.","type":"string","enum":["sum","avg","min","max","count"]},"groupBy":{"description":"Attribute keys to group by when aggregating (e.g. ['tenant.id', 'signal']).","type":"array","items":{"type":"string"}},"limit":{"description":"Max items to return. Default determined by datasource.","type":"integer","exclusiveMinimum":0,"maximum":1000},"cursor":{"description":"Opaque cursor from previous response for next page.","type":"string"},"sortOrder":{"description":"Sort by timestamp. ASC = oldest first, DESC = newest first.","type":"string","enum":["ASC","DESC"]}},"required":["metricType"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getTrace"},"params":{"type":"object","properties":{"traceId":{"type":"string"}},"required":["traceId"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"discoverMetrics"},"params":{"type":"object","properties":{},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getServices"},"params":{"type":"object","properties":{},"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"getOperations"},"params":{"type":"object","properties":{"serviceName":{"type":"string"}},"required":["serviceName"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false},{"type":"object","properties":{"method":{"type":"string","const":"searchTraceSummariesPage"},"params":{"type":"object","properties":{"serviceName":{"type":"string"},"spanName":{"type":"string"},"timestampMin":{"type":"string"},"timestampMax":{"type":"string"},"durationMin":{"type":"string"},"durationMax":{"type":"string"},"spanAttributes":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"resourceAttributes":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"}},"limit":{"default":20,"type":"integer","minimum":1,"maximum":1000},"cursor":{"type":"string"},"sortOrder":{"default":"DESC","type":"string","enum":["ASC","DESC"]}},"required":["limit","sortOrder"],"additionalProperties":false},"refetchIntervalMs":{"type":"number"}},"required":["method","params"],"additionalProperties":false}]}}}
33
33
 
34
34
  ---
35
35
 
@@ -52,6 +52,7 @@ type MockClient = {
52
52
  searchTracesPage: ReturnType<typeof vi.fn>;
53
53
  searchLogsPage: ReturnType<typeof vi.fn>;
54
54
  searchMetricsPage: ReturnType<typeof vi.fn>;
55
+ searchAggregatedMetrics: ReturnType<typeof vi.fn>;
55
56
  getTrace: ReturnType<typeof vi.fn>;
56
57
  discoverMetrics: ReturnType<typeof vi.fn>;
57
58
  searchTraces: ReturnType<typeof vi.fn>;
@@ -281,6 +282,7 @@ describe("Renderer with dataSource", () => {
281
282
  searchTracesPage: vi.fn(),
282
283
  searchLogsPage: vi.fn(),
283
284
  searchMetricsPage: vi.fn(),
285
+ searchAggregatedMetrics: vi.fn(),
284
286
  getTrace: vi.fn(),
285
287
  discoverMetrics: vi.fn(),
286
288
  searchTraces: vi.fn(),
@@ -17,6 +17,9 @@ function createMockClient(): MockClient {
17
17
  searchTracesPage: vi.fn().mockResolvedValue({ data: [] }),
18
18
  searchLogsPage: vi.fn().mockResolvedValue({ data: [] }),
19
19
  searchMetricsPage: vi.fn().mockResolvedValue({ data: [] }),
20
+ searchAggregatedMetrics: vi
21
+ .fn()
22
+ .mockResolvedValue({ data: [], nextCursor: null }),
20
23
  getTrace: vi.fn().mockResolvedValue({ data: [] }),
21
24
  discoverMetrics: vi.fn().mockResolvedValue({ data: [] }),
22
25
  searchTraces: vi.fn().mockResolvedValue({ data: [] }),