The dashboard reader takes the list the server answers

GET /v1/o11y/dashboards answers the V2 list, {dashboards, total, tags}, a page
at a time, and api/v1/dashboards/getAll still handed its callers data as if it
were a bare array. Home sorts it on first render, so a signed-in person landed
on "Something went wrong" (TypeError: s.data.sort is not a function) as soon as
the gui config stopped failing ahead of it. The export panel, the assistant's
dashboard picker and the legacy list read the same value.

getAll now reads every page of the V2 list, newest first, and maps each item to
the Dashboard those screens render: title from the display name, falling back to
the name, tags as the V2 list page shows them, and no body, which the screens
that need one fetch by id.

Co-authored-by: Hanzo Dev <[email protected]>
This commit is contained in:
zooqueen
2026-09-24 16:01:37 -07:00
co-authored by Hanzo Dev
parent b891dcf681
commit 3316e13c8f
2 changed files with 159 additions and 6 deletions
@@ -0,0 +1,100 @@
import axios from 'api';
import getAll from '../getAll';
jest.mock('api', () => ({
get: jest.fn(),
}));
// The body GET /v1/o11y/dashboards answers: the V2 list, a page at a time.
const listed = (id: string, name: string, display?: string): object => ({
id,
createdAt: '2026-09-01T00:00:00Z',
updatedAt: '2026-09-02T00:00:00Z',
createdBy: '[email protected]',
updatedBy: '[email protected]',
orgId: 'org',
locked: true,
source: 'user',
schemaVersion: 'v6',
name,
image: 'data:image/svg+xml;base64,AA==',
tags: [
{ key: 'team', value: 'infra' },
{ key: 'prod', value: 'prod' },
],
spec: display ? { display: { name: display, description: 'about it' } } : {},
});
const page = (dashboards: object[], total: number): object => ({
status: 200,
data: { status: 'success', data: { dashboards, total, tags: [] } },
});
describe('getAll', () => {
beforeEach(() => jest.clearAllMocks());
it('reads the V2 list into the Dashboard the legacy screens render', async () => {
(axios.get as jest.Mock).mockResolvedValueOnce(
page([listed('a', 'fallback', 'Checkout'), listed('b', 'Payments')], 2),
);
const result = await getAll();
expect(axios.get).toHaveBeenCalledWith('/dashboards', {
params: { limit: 200, offset: 0, sort: 'updated_at', order: 'desc' },
});
expect(result.httpStatusCode).toBe(200);
expect(result.data).toStrictEqual([
{
id: 'a',
createdAt: '2026-09-01T00:00:00Z',
updatedAt: '2026-09-02T00:00:00Z',
createdBy: '[email protected]',
updatedBy: '[email protected]',
locked: true,
data: {
title: 'Checkout',
description: 'about it',
tags: ['team:infra', 'prod'],
image: 'data:image/svg+xml;base64,AA==',
variables: {},
},
},
expect.objectContaining({
id: 'b',
data: expect.objectContaining({
title: 'Payments',
description: undefined,
}),
}),
]);
// The screens sort and slice what they get; it has to be an array.
expect(Array.isArray(result.data)).toBe(true);
});
it('reads every page, not only the first', async () => {
const first = Array.from({ length: 200 }, (_, i) =>
listed(`p1-${i}`, `d${i}`),
);
(axios.get as jest.Mock)
.mockResolvedValueOnce(page(first, 201))
.mockResolvedValueOnce(page([listed('p2-0', 'last')], 201));
const result = await getAll();
expect(axios.get).toHaveBeenCalledTimes(2);
expect((axios.get as jest.Mock).mock.calls[1][1]).toStrictEqual({
params: { limit: 200, offset: 200, sort: 'updated_at', order: 'desc' },
});
expect(result.data).toHaveLength(201);
});
it('answers an empty org with an empty list', async () => {
(axios.get as jest.Mock).mockResolvedValueOnce(page([], 0));
const result = await getAll();
expect(result.data).toStrictEqual([]);
});
});
+59 -6
View File
@@ -1,16 +1,69 @@
import axios from 'api';
import { ErrorResponseHandlerV2 } from 'api/ErrorResponseHandlerV2';
import { AxiosError } from 'axios';
import { tagsToStrings } from 'pages/DashboardsListPageV2/utils';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Dashboard, PayloadProps } from 'types/api/dashboard/getAll';
import { Dashboard } from 'types/api/dashboard/getAll';
// GET /dashboards answers the V2 list — {dashboards, total, tags}, one page at
// a time — where this reader once received a bare array. Home, the export panel,
// the assistant's picker and the legacy list all read it through here and sort
// or map what they get, so the first array method they called threw and took the
// console down with it. This reads every page and hands them the Dashboard they
// render. A list item carries no widgets, layout or variables; the screens that
// need a dashboard's body fetch it by id.
const PAGE = 200; // dashboardtypes.MaxListLimit
interface ListedDashboard {
id: string;
createdAt: string;
updatedAt: string;
createdBy: string;
updatedBy: string;
locked: boolean;
name: string;
image?: string;
tags: { key: string; value: string }[] | null;
spec?: { display?: { name?: string; description?: string } };
}
interface ListPage {
status: string;
data: { dashboards: ListedDashboard[] | null; total: number };
}
const toDashboard = (d: ListedDashboard): Dashboard => ({
id: d.id,
createdAt: d.createdAt,
updatedAt: d.updatedAt,
createdBy: d.createdBy,
updatedBy: d.updatedBy,
locked: d.locked,
data: {
title: d.spec?.display?.name || d.name,
description: d.spec?.display?.description,
tags: tagsToStrings(d.tags),
image: d.image,
variables: {},
},
});
const getAll = async (): Promise<SuccessResponseV2<Dashboard[]>> => {
try {
const response = await axios.get<PayloadProps>('/dashboards');
return {
httpStatusCode: response.status,
data: response.data.data,
};
const dashboards: Dashboard[] = [];
let httpStatusCode = 200;
for (let offset = 0; ; offset += PAGE) {
const response = await axios.get<ListPage>('/dashboards', {
params: { limit: PAGE, offset, sort: 'updated_at', order: 'desc' },
});
httpStatusCode = response.status;
const listed = response.data.data.dashboards ?? [];
dashboards.push(...listed.map(toDashboard));
if (listed.length < PAGE || dashboards.length >= response.data.data.total) {
break;
}
}
return { httpStatusCode, data: dashboards };
} catch (error) {
ErrorResponseHandlerV2(error as AxiosError<ErrorV2Resp>);
}