diff --git a/packages/grafana-test-utils/src/fixtures/folders.ts b/packages/grafana-test-utils/src/fixtures/folders.ts new file mode 100644 index 00000000000..7fc9bed9e20 --- /dev/null +++ b/packages/grafana-test-utils/src/fixtures/folders.ts @@ -0,0 +1,130 @@ +import { Chance } from 'chance'; + +import { DashboardsTreeItem, DashboardViewItem, UIDashboardViewItem } from '../types/browse-dashboards'; + +function wellFormedEmptyFolder( + seed = 1, + partial?: Partial> +): DashboardsTreeItem { + const random = Chance(seed); + + return { + item: { + kind: 'ui', + uiKind: 'empty-folder', + uid: random.guid(), + }, + level: 0, + isOpen: false, + ...partial, + }; +} + +function wellFormedDashboard( + seed = 1, + partial?: Partial>, + itemPartial?: Partial +): DashboardsTreeItem { + const random = Chance(seed); + + return { + item: { + kind: 'dashboard', + title: random.sentence({ words: 3 }), + uid: random.guid(), + tags: [random.word()], + ...itemPartial, + }, + level: 0, + isOpen: false, + ...partial, + }; +} + +function wellFormedFolder( + seed = 1, + partial?: Partial>, + itemPartial?: Partial +): DashboardsTreeItem { + const random = Chance(seed); + const uid = random.guid(); + + return { + item: { + kind: 'folder', + title: random.sentence({ words: 3 }), + uid, + url: `/dashboards/f/${uid}`, + ...itemPartial, + }, + level: 0, + isOpen: false, + ...partial, + }; +} + +export function treeViewersCanEdit() { + const [, { folderA, folderC }] = wellFormedTree(); + + return [ + [folderA, folderC], + { + folderA, + folderC, + }, + ] as const; +} + +export function wellFormedTree() { + let seed = 1; + + const folderA = wellFormedFolder(seed++); + const folderA_folderA = wellFormedFolder(seed++, { level: 1 }, { parentUID: folderA.item.uid }); + const folderA_folderB = wellFormedFolder(seed++, { level: 1 }, { parentUID: folderA.item.uid }); + const folderA_folderB_dashbdA = wellFormedDashboard(seed++, { level: 2 }, { parentUID: folderA_folderB.item.uid }); + const folderA_folderB_dashbdB = wellFormedDashboard(seed++, { level: 2 }, { parentUID: folderA_folderB.item.uid }); + const folderA_folderC = wellFormedFolder(seed++, { level: 1 }, { parentUID: folderA.item.uid }); + const folderA_folderC_dashbdA = wellFormedDashboard(seed++, { level: 2 }, { parentUID: folderA_folderC.item.uid }); + const folderA_folderC_dashbdB = wellFormedDashboard(seed++, { level: 2 }, { parentUID: folderA_folderC.item.uid }); + const folderA_dashbdD = wellFormedDashboard(seed++, { level: 1 }, { parentUID: folderA.item.uid }); + const folderB = wellFormedFolder(seed++); + const folderB_empty = wellFormedEmptyFolder(seed++); + const folderC = wellFormedFolder(seed++); + const dashbdD = wellFormedDashboard(seed++); + const dashbdE = wellFormedDashboard(seed++); + + return [ + [ + folderA, + folderA_folderA, + folderA_folderB, + folderA_folderB_dashbdA, + folderA_folderB_dashbdB, + folderA_folderC, + folderA_folderC_dashbdA, + folderA_folderC_dashbdB, + folderA_dashbdD, + folderB, + folderB_empty, + folderC, + dashbdD, + dashbdE, + ], + { + folderA, + folderA_folderA, + folderA_folderB, + folderA_folderB_dashbdA, + folderA_folderB_dashbdB, + folderA_folderC, + folderA_folderC_dashbdA, + folderA_folderC_dashbdB, + folderA_dashbdD, + folderB, + folderB_empty, + folderC, + dashbdD, + dashbdE, + }, + ] as const; +} diff --git a/packages/grafana-test-utils/src/handlers/all-handlers.ts b/packages/grafana-test-utils/src/handlers/all-handlers.ts index edf4c592ef2..3e626ba021b 100644 --- a/packages/grafana-test-utils/src/handlers/all-handlers.ts +++ b/packages/grafana-test-utils/src/handlers/all-handlers.ts @@ -1,7 +1,9 @@ import { HttpHandler } from 'msw'; +import folderHandlers from './api/folders/handlers'; import teamsHandlers from './api/teams/handlers'; +import appPlatformFolderHandlers from './apis/dashboard.grafana.app/v0alpha1/handlers'; -const allHandlers: HttpHandler[] = [...teamsHandlers]; +const allHandlers: HttpHandler[] = [...teamsHandlers, ...folderHandlers, ...appPlatformFolderHandlers]; export default allHandlers; diff --git a/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts b/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts new file mode 100644 index 00000000000..103b816ce3a --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/api/folders/handlers.ts @@ -0,0 +1,52 @@ +import { HttpResponse, http } from 'msw'; + +import { treeViewersCanEdit, wellFormedTree } from '../../../fixtures/folders'; + +const [mockTree] = wellFormedTree(); +const [mockTreeThatViewersCanEdit] = treeViewersCanEdit(); +const collator = new Intl.Collator(); + +const listFoldersHandler = () => + http.get('/api/folders', ({ request }) => { + const url = new URL(request.url); + const parentUid = url.searchParams.get('parentUid') ?? undefined; + const permission = url.searchParams.get('permission'); + + const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10); + const page = parseInt(url.searchParams.get('page') ?? '1', 10); + + const tree = permission === 'Edit' ? mockTreeThatViewersCanEdit : mockTree; + + // reconstruct a folder API response from the flat tree fixture + const folders = tree + .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid) + .map((folder) => { + return { + uid: folder.item.uid, + title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen", + }; + }) + .sort((a, b) => collator.compare(a.title, b.title)) // API always sorts by title + .slice(limit * (page - 1), limit * page); + + return HttpResponse.json(folders); + }); + +const getFolderHandler = () => + http.get('/api/folders/:uid', ({ params }) => { + const { uid } = params; + + const folder = mockTree.find((v) => v.item.uid === uid); + if (!folder) { + return HttpResponse.json({ message: 'folder not found', status: 'not-found' }, { status: 404 }); + } + + return HttpResponse.json({ + title: folder?.item.title, + uid: folder?.item.uid, + }); + }); + +const handlers = [listFoldersHandler(), getFolderHandler()]; + +export default handlers; diff --git a/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v0alpha1/handlers.ts b/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v0alpha1/handlers.ts new file mode 100644 index 00000000000..43813d6f4f8 --- /dev/null +++ b/packages/grafana-test-utils/src/handlers/apis/dashboard.grafana.app/v0alpha1/handlers.ts @@ -0,0 +1,52 @@ +import { Chance } from 'chance'; +import { HttpResponse, http } from 'msw'; + +import { wellFormedTree } from '../../../../fixtures/folders'; + +const [mockTree] = wellFormedTree(); + +type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>; + +const getSearchHandler = () => + http.get('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search', ({ request }) => { + const folderFilter = new URL(request.url).searchParams.get('folder') || null; + const typeFilter = new URL(request.url).searchParams.get('type') || null; + const response = mockTree + .filter((filterItem) => { + const filters: FilterArray = []; + if (folderFilter && folderFilter !== 'general') { + filters.push(({ item }) => item.kind === 'folder' && item.parentUID === folderFilter); + } + + if (folderFilter === 'general') { + filters.push(({ item }) => item.kind === 'folder' && item.parentUID === undefined); + } + + if (typeFilter) { + filters.push(({ item }) => item.kind === typeFilter); + } + + return filters.every((filterPredicate) => filterPredicate(filterItem)); + }) + + .map(({ item }) => { + const random = Chance(item.uid); + return { + resource: 'folders', + name: item.uid, + title: item.title, + field: { + // Generate mock deprecated IDs only in the mock handlers - not generating in + // mock data as it would require updating/tracking in the types as well + 'grafana.app/deprecatedInternalID': random.integer({ min: 1, max: 1000 }), + }, + }; + }); + + return HttpResponse.json({ + totalHits: response.length, + hits: response, + }); + }); + +export default [getSearchHandler()]; diff --git a/packages/grafana-test-utils/src/types/browse-dashboards.ts b/packages/grafana-test-utils/src/types/browse-dashboards.ts new file mode 100644 index 00000000000..5757b4a2ec0 --- /dev/null +++ b/packages/grafana-test-utils/src/types/browse-dashboards.ts @@ -0,0 +1,58 @@ +// FIXME: This file is a duplication of types within the core code +// Where should these live long term? +// @grafana/schema? +// New package @grafana/core? @grafana/types? + +enum ManagerKind { + Repo = 'repo', + Terraform = 'terraform', + Kubectl = 'kubectl', + Plugin = 'plugin', +} + +type DashboardViewItemKind = 'folder' | 'dashboard' | 'panel'; + +type DashboardViewItemWithUIItems = DashboardViewItem | UIDashboardViewItem; + +export interface DashboardsTreeItem { + item: T; + level: number; + isOpen: boolean; + parentUID?: string; +} + +export interface UIDashboardViewItem { + kind: 'ui'; + uiKind: 'empty-folder' | 'pagination-placeholder' | 'divider'; + uid: string; + // Optional title to make mock data easier to work with + title?: string; +} + +/** + * Type used in the folder view components + */ +export interface DashboardViewItem { + kind: DashboardViewItemKind; + uid: string; + title: string; + url?: string; + tags?: string[]; + + icon?: string; + + parentUID?: string; + /** @deprecated Not used in new Browse UI */ + parentTitle?: string; + /** @deprecated Not used in new Browse UI */ + parentKind?: string; + + // Used only for psuedo-folders, such as Starred or Recent + /** @deprecated Not used in new Browse UI */ + itemsUIDs?: string[]; + + // For enterprise sort options + sortMeta?: number | string; // value sorted by + sortMetaName?: string; // name of the value being sorted e.g. 'Views' + managedBy?: ManagerKind; +} diff --git a/packages/grafana-test-utils/src/unstable.ts b/packages/grafana-test-utils/src/unstable.ts index 5b60f83479c..8758e294464 100644 --- a/packages/grafana-test-utils/src/unstable.ts +++ b/packages/grafana-test-utils/src/unstable.ts @@ -1 +1,4 @@ +import { wellFormedTree } from './fixtures/folders'; + +export const getFolderFixtures = wellFormedTree; export { MOCK_TEAMS } from './fixtures/teams'; diff --git a/public/app/api/clients/dashboard/v0alpha1/baseAPI.ts b/public/app/api/clients/dashboard/v0alpha1/baseAPI.ts new file mode 100644 index 00000000000..dca53563fa0 --- /dev/null +++ b/public/app/api/clients/dashboard/v0alpha1/baseAPI.ts @@ -0,0 +1,14 @@ +import { createApi } from '@reduxjs/toolkit/query/react'; + +import { createBaseQuery } from 'app/api/createBaseQuery'; +import { getAPIBaseURL } from 'app/api/utils'; + +export const BASE_URL = getAPIBaseURL('dashboard.grafana.app', 'v0alpha1'); + +export const api = createApi({ + reducerPath: 'dashboardAPIv0alpha1', + baseQuery: createBaseQuery({ + baseURL: BASE_URL, + }), + endpoints: () => ({}), +}); diff --git a/public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts b/public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts new file mode 100644 index 00000000000..8730157dedf --- /dev/null +++ b/public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts @@ -0,0 +1,53 @@ +import { api } from './baseAPI'; +export const addTagTypes = ['Search'] as const; +const injectedRtkApi = api + .enhanceEndpoints({ + addTagTypes, + }) + .injectEndpoints({ + endpoints: (build) => ({ + getSearch: build.query({ + query: (queryArg) => ({ + url: `/search`, + params: { + query: queryArg.query, + folder: queryArg.folder, + sort: queryArg.sort, + }, + }), + providesTags: ['Search'], + }), + }), + overrideExisting: false, + }); +export { injectedRtkApi as generatedAPI }; +export type GetSearchApiResponse = /** status 200 undefined */ { + /** APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** Facet results */ + facets?: { + [key: string]: any; + }; + /** The dashboard body (unstructured for now) */ + hits: any[]; + /** Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + /** Max score */ + maxScore?: number; + /** Where the query started from */ + offset?: number; + /** Cost of running the query */ + queryCost?: number; + /** How are the results sorted */ + sortBy?: any; + /** The number of matching results */ + totalHits: number; +}; +export type GetSearchApiArg = { + /** user query string */ + query?: string; + /** search/list within a folder (not recursive) */ + folder?: string; + /** sortable field */ + sort?: string; +}; diff --git a/public/app/api/clients/dashboard/v0alpha1/index.ts b/public/app/api/clients/dashboard/v0alpha1/index.ts new file mode 100644 index 00000000000..f61b46bc1f9 --- /dev/null +++ b/public/app/api/clients/dashboard/v0alpha1/index.ts @@ -0,0 +1,27 @@ +import { generatedAPI, GetSearchApiArg } from './endpoints.gen'; + +type OverrideGetSearchRequestOptions = GetSearchApiArg & { + type: string; +}; + +export const dashboardAPIv0alpha1 = generatedAPI.enhanceEndpoints({ + addTagTypes: ['Folder', 'Dashboard'], + endpoints: { + getSearch: (endpointDefinition) => { + const originalQuery = endpointDefinition.query; + endpointDefinition.providesTags = ['Search', 'Folder', 'Dashboard']; + if (originalQuery) { + // TODO: Remove once API spec is updated with `type` + endpointDefinition.query = (requestOptions: OverrideGetSearchRequestOptions) => ({ + ...originalQuery(requestOptions), + params: { + ...requestOptions, + type: requestOptions.type, + }, + }); + } + }, + }, +}); + +export const { useGetSearchQuery } = dashboardAPIv0alpha1; diff --git a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx index 226583c54e1..38674034a0d 100644 --- a/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx +++ b/public/app/core/components/NestedFolderPicker/NestedFolderPicker.test.tsx @@ -1,89 +1,31 @@ -import { fireEvent, render as rtlRender, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { HttpResponse, http } from 'msw'; -import { SetupServer, setupServer } from 'msw/node'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { fireEvent, render, screen } from 'test/test-utils'; -import { config } from '@grafana/runtime'; +import { config, setBackendSrv } from '@grafana/runtime'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; -import { - treeViewersCanEdit, - wellFormedTree, -} from '../../../features/browse-dashboards/fixtures/dashboardsTreeItem.fixture'; - import { NestedFolderPicker } from './NestedFolderPicker'; -const [mockTree, { folderA, folderB, folderC, folderA_folderA, folderA_folderB }] = wellFormedTree(); -const [mockTreeThatViewersCanEdit /* shares folders with wellFormedTree */] = treeViewersCanEdit(); +const [_, { folderA, folderB, folderC, folderA_folderA, folderA_folderB, folderA_folderC }] = getFolderFixtures(); -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => backendSrv, -})); - -function render(...[ui, options]: Parameters) { - rtlRender({ui}, options); -} +setupMockServer(); +setBackendSrv(backendSrv); describe('NestedFolderPicker', () => { const mockOnChange = jest.fn(); const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView; - let server: SetupServer; beforeAll(() => { window.HTMLElement.prototype.scrollIntoView = function () {}; - - server = setupServer( - http.get('/api/folders/:uid', () => { - return HttpResponse.json({ - title: folderA.item.title, - uid: folderA.item.uid, - }); - }), - - http.get('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/settings', () => { - return HttpResponse.json({ - items: [], - }); - }), - - http.get('/api/folders', ({ request }) => { - const url = new URL(request.url); - const parentUid = url.searchParams.get('parentUid') ?? undefined; - const permission = url.searchParams.get('permission'); - - const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10); - const page = parseInt(url.searchParams.get('page') ?? '1', 10); - - const tree = permission === 'Edit' ? mockTreeThatViewersCanEdit : mockTree; - - // reconstruct a folder API response from the flat tree fixture - const folders = tree - .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid) - .map((folder) => { - return { - uid: folder.item.uid, - title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen", - }; - }) - .slice(limit * (page - 1), limit * page); - - return HttpResponse.json(folders); - }) - ); - - server.listen(); }); afterAll(() => { - server.close(); window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView; }); afterEach(() => { jest.resetAllMocks(); - server.resetHandlers(); }); it('renders a button with the correct label when no folder is selected', async () => { @@ -92,18 +34,18 @@ describe('NestedFolderPicker', () => { }); it('renders a button with the correct label when a folder is selected', async () => { - render(); + render(); expect( await screen.findByRole('button', { name: `Select folder: ${folderA.item.title} currently selected` }) ).toBeInTheDocument(); }); it('clicking the button opens the folder picker', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); // Select folder button is no longer visible @@ -118,73 +60,73 @@ describe('NestedFolderPicker', () => { }); it('can select a folder from the picker', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); - await userEvent.click(screen.getByLabelText(folderA.item.title)); + await user.click(screen.getByLabelText(folderA.item.title)); expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title); }); it('can clear a selection if clearable is specified', async () => { - render(); + const { user } = render(); - await userEvent.click(await screen.findByRole('button', { name: 'Clear selection' })); + await user.click(await screen.findByRole('button', { name: 'Clear selection' })); expect(mockOnChange).toHaveBeenCalledWith(undefined, undefined); }); it('can select a folder from the picker with the keyboard', async () => { - render(); + const { user } = render(); const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); - await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}'); - expect(mockOnChange).toHaveBeenCalledWith(folderA.item.uid, folderA.item.title); + await user.keyboard('{ArrowDown}{ArrowDown}{Enter}'); + expect(mockOnChange).toHaveBeenCalledWith(folderC.item.uid, folderC.item.title); }); it('shows the root folder by default', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); - await userEvent.click(screen.getByLabelText('Dashboards')); + await user.click(screen.getByLabelText('Dashboards')); expect(mockOnChange).toHaveBeenCalledWith('', 'Dashboards'); }); it('hides the root folder if the prop says so', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); expect(screen.queryByLabelText('Dashboards')).not.toBeInTheDocument(); }); it('hides folders specififed by UID', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); expect(screen.queryByLabelText(folderC.item.title)).not.toBeInTheDocument(); }); it('by default only shows items the user can edit', async () => { - render(); + const { user } = render(); const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); expect(screen.queryByLabelText(folderB.item.title)).not.toBeInTheDocument(); // folderB is not editable @@ -192,10 +134,10 @@ describe('NestedFolderPicker', () => { }); it('shows items the user can view, with the prop', async () => { - render(); + const { user } = render(); const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); expect(screen.getByLabelText(folderB.item.title)).toBeInTheDocument(); @@ -214,11 +156,11 @@ describe('NestedFolderPicker', () => { }); it('can expand and collapse a folder to show its children', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); // Expand Folder A @@ -240,34 +182,35 @@ describe('NestedFolderPicker', () => { fireEvent.mouseDown(screen.getByRole('button', { name: `Expand folder ${folderA.item.title}` })); // Select the first child - await userEvent.click(screen.getByLabelText(folderA_folderA.item.title)); + await user.click(screen.getByLabelText(folderA_folderA.item.title)); expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title); }); it('can expand and collapse a folder to show its children with the keyboard', async () => { - render(); + const { user } = render(); const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); // Expand Folder A - await userEvent.keyboard('{ArrowDown}{ArrowDown}{ArrowRight}'); + await user.keyboard('{ArrowDown}{ArrowDown}{ArrowDown}{ArrowDown}{ArrowRight}'); // Folder A's children are visible expect(await screen.findByLabelText(folderA_folderA.item.title)).toBeInTheDocument(); expect(await screen.findByLabelText(folderA_folderB.item.title)).toBeInTheDocument(); + expect(await screen.findByLabelText(folderA_folderC.item.title)).toBeInTheDocument(); // Collapse Folder A - await userEvent.keyboard('{ArrowLeft}'); + await user.keyboard('{ArrowLeft}'); expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument(); expect(screen.queryByLabelText(folderA_folderB.item.title)).not.toBeInTheDocument(); // Expand Folder A again - await userEvent.keyboard('{ArrowRight}'); + await user.keyboard('{ArrowRight}'); // Select the first child - await userEvent.keyboard('{ArrowDown}{Enter}'); - expect(mockOnChange).toHaveBeenCalledWith(folderA_folderA.item.uid, folderA_folderA.item.title); + await user.keyboard('{ArrowDown}{Enter}'); + expect(mockOnChange).toHaveBeenCalledWith(folderA_folderC.item.uid, folderA_folderC.item.title); }); }); @@ -283,11 +226,11 @@ describe('NestedFolderPicker', () => { }); it('does not show an expand button', async () => { - render(); + const { user } = render(); // Open the picker and wait for children to load const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); await screen.findByLabelText(folderA.item.title); // There should be no expand button @@ -296,13 +239,13 @@ describe('NestedFolderPicker', () => { }); it('does not expand a folder with the keyboard', async () => { - render(); + const { user } = render(); const button = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(button); + await user.click(button); // try to expand Folder A - await userEvent.keyboard('{ArrowDown}{ArrowDown}{ArrowRight}'); + await user.keyboard('{ArrowDown}{ArrowDown}{ArrowRight}'); // Folder A's children are not visible expect(screen.queryByLabelText(folderA_folderA.item.title)).not.toBeInTheDocument(); diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQuery.test.tsx b/public/app/core/components/NestedFolderPicker/useFoldersQuery.test.tsx index 18d83068b5e..cf0f0f67af1 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQuery.test.tsx +++ b/public/app/core/components/NestedFolderPicker/useFoldersQuery.test.tsx @@ -1,82 +1,26 @@ -import { act, renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { act, getWrapper, renderHook, waitFor } from 'test/test-utils'; import { GrafanaConfig } from '@grafana/data'; import * as runtime from '@grafana/runtime'; -import { DashboardsTreeItem } from 'app/features/browse-dashboards/types'; +import { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; +import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardViewItem } from '../../../features/search/types'; import { useFoldersQuery } from './useFoldersQuery'; import { getRootFolderItem } from './utils'; -const PAGE_SIZE = 10; +const [_, { folderA, folderB, folderC }] = getFolderFixtures(); -const legacyResponse = { - status: 'fulfilled', - originalArgs: { parentUid: undefined, page: 1, limit: PAGE_SIZE, permission: 'Edit' }, - data: [{ title: 'Legacy Folder', uid: 'legacy1', managedBy: undefined }], +runtime.setBackendSrv(backendSrv); +setupMockServer(); + +const wrapper = ({ children }: { children: ReactNode }) => { + const ProviderWrapper = getWrapper({ renderWithRouter: true }); + return {children}; }; -// Mock the legacy API client -jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => { - const PAGE_SIZE = 10; - return { - PAGE_SIZE, - browseDashboardsAPI: { - endpoints: { - listFolders: { - select: jest.fn(() => () => legacyResponse), - initiate: jest.fn(() => ({ - arg: { parentUid: undefined, page: 1, limit: PAGE_SIZE, permission: 'Edit' }, - unsubscribe: jest.fn(), - })), - }, - }, - }, - }; -}); - -const appPlatfromResponse = { - status: 'fulfilled', - originalArgs: { name: 'general' }, - data: { - items: [ - { - metadata: { name: 'app1', annotations: {} }, - spec: { title: 'AppPlatform Folder' }, - }, - ], - }, -}; - -// Mock the appPlatform API client -jest.mock('app/api/clients/folder/v1beta1', () => ({ - folderAPIv1beta1: { - endpoints: { - getFolderChildren: { - select: jest.fn(() => () => appPlatfromResponse), - initiate: jest.fn((arg: unknown) => ({ - arg, - unsubscribe: jest.fn(), - })), - }, - }, - }, -})); - -// Mock getPaginationPlaceholders to return empty array for simplicity -jest.mock('app/features/browse-dashboards/state/utils', () => ({ - getPaginationPlaceholders: jest.fn((): DashboardsTreeItem[] => []), -})); - -// Mock useDispatch and useSelector to just pass through -jest.mock('app/types/store', () => { - const mod = jest.requireActual('app/types/store'); - return { - ...mod, - useDispatch: () => (val: unknown) => val, - useSelector: (selector: Function) => selector(), - }; -}); describe('useFoldersQuery', () => { let configBackup: GrafanaConfig; @@ -89,28 +33,40 @@ describe('useFoldersQuery', () => { runtime.config.featureToggles = configBackup.featureToggles; }); - it('returns data using legacy api', () => { - runtime.config.featureToggles.foldersAppPlatformAPI = false; - const items = testFn(); - expect((items[1].item as DashboardViewItem).title).toBe('Legacy Folder'); - }); + describe.each([ + // foldersAppPlatformAPI enabled + true, + // foldersAppPlatformAPI disabled + false, + ])('foldersAppPlatformAPI feature toggle set to %s', (featureToggleState) => { + it('returns data using legacy api', async () => { + runtime.config.featureToggles.foldersAppPlatformAPI = featureToggleState; + const [_dashboardsContainer, ...items] = await testFn(); - it('returns appPlatform hook result when foldersAppPlatformAPI is on', () => { - runtime.config.featureToggles.foldersAppPlatformAPI = true; - const items = testFn(); - expect((items[1].item as DashboardViewItem).title).toBe('AppPlatform Folder'); + const sortedItemTitles = items.map((item) => (item.item as DashboardViewItem).title).sort(); + const expectedTitles = [folderA.item.title, folderB.item.title, folderC.item.title].sort(); + + expect(sortedItemTitles).toEqual(expectedTitles); + }); }); }); -function testFn() { - const { result } = renderHook(() => useFoldersQuery(true, {})); +async function testFn() { + const { result } = renderHook(() => useFoldersQuery(true, {}), { wrapper }); - expect(result.current.items).toEqual([getRootFolderItem()]); + expect(result.current.items[0]).toEqual(getRootFolderItem()); expect(result.current.isLoading).toBe(false); + act(() => { result.current.requestNextPage(undefined); }); - expect(result.current.items.length).toBe(2); + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + const withoutPaginationPlaceholders = result.current.items.filter((item) => item.item.kind !== 'ui'); + return expect(withoutPaginationPlaceholders.length).toBeGreaterThan(1); + }); + return result.current.items; } diff --git a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts index 4828417c10c..b877c69aba0 100644 --- a/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts +++ b/public/app/core/components/NestedFolderPicker/useFoldersQueryAppPlatform.ts @@ -2,7 +2,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { QueryStatus } from '@reduxjs/toolkit/query'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { folderAPIv1beta1 } from 'app/api/clients/folder/v1beta1'; +import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1'; import { DashboardViewItemWithUIItems, DashboardsTreeItem } from 'app/features/browse-dashboards/types'; import { useDispatch, useSelector } from 'app/types/store'; @@ -12,7 +12,7 @@ import { getPaginationPlaceholders } from '../../../features/browse-dashboards/s import { getRootFolderItem } from './utils'; -type GetFolderChildrenQuery = ReturnType>; +type GetFolderChildrenQuery = ReturnType>; type GetFolderChildrenRequest = { unsubscribe: () => void; }; @@ -32,9 +32,9 @@ export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Rec const requestsRef = useRef([]); // Keep a list of selectors for dynamic state selection - const [selectors, setSelectors] = useState< - Array> - >([]); + const [selectors, setSelectors] = useState>>( + [] + ); // This is an aggregated dynamic selector of all the selectors for all the request issued while loading the folder // tree and returns the whole tree that was loaded so far. @@ -50,7 +50,7 @@ export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Rec isLoading = true; } - const parentName = response.originalArgs?.name; + const parentName = response.originalArgs?.folder; if (parentName) { responseByParent[parentName] = response; } @@ -77,13 +77,13 @@ export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Rec return; } - const args = { name: finalParentUid }; + const args = { folder: finalParentUid, type: 'folder' }; // Make a request - const subscription = dispatch(folderAPIv1beta1.endpoints.getFolderChildren.initiate(args)); + const subscription = dispatch(dashboardAPIv0alpha1.endpoints.getSearch.initiate(args)); // Add selector for the response to the list so we can then have an aggregated selector for all the folders - const selector = folderAPIv1beta1.endpoints.getFolderChildren.select(args); + const selector = dashboardAPIv0alpha1.endpoints.getSearch.select(args); setSelectors((selectors) => selectors.concat(selector)); // the subscriptions are saved in a ref so they can be unsubscribed on unmount @@ -113,18 +113,18 @@ export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Rec response: GetFolderChildrenQuery | undefined, level: number ): Array> { - let folders = response?.data?.items ? [...response.data.items] : []; - folders.sort((a, b) => collator.compare(a.spec.title, b.spec.title)); + let folders = response?.data?.hits ? [...response.data.hits] : []; + folders.sort((a, b) => collator.compare(a.title, b.title)); const list = folders.flatMap((item) => { - const name = item.metadata.name!; + const name = item.name; const folderIsOpen = openFolders[name]; const flatItem: DashboardsTreeItem = { isOpen: Boolean(folderIsOpen), level: level, item: { kind: 'folder' as const, - title: item.spec.title, + title: item.title, // We use resource name as UID because well, not sure what metadata.uid would be used for now as you cannot // query by it. uid: name, diff --git a/public/app/core/reducers/root.ts b/public/app/core/reducers/root.ts index 67078fdbd27..bc296ee14b6 100644 --- a/public/app/core/reducers/root.ts +++ b/public/app/core/reducers/root.ts @@ -2,6 +2,7 @@ import { ReducersMapObject } from '@reduxjs/toolkit'; import { AnyAction, combineReducers } from 'redux'; import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable'; +import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1'; import sharedReducers from 'app/core/reducers'; import ldapReducers from 'app/features/admin/state/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; @@ -71,6 +72,7 @@ const rootReducers = { [provisioningAPIv0alpha1.reducerPath]: provisioningAPIv0alpha1.reducer, [folderAPIv1beta1.reducerPath]: folderAPIv1beta1.reducer, [advisorAPIv0alpha1.reducerPath]: advisorAPIv0alpha1.reducer, + [dashboardAPIv0alpha1.reducerPath]: dashboardAPIv0alpha1.reducer, // PLOP_INJECT_REDUCER // Used by the API client generator }; diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index 4a2c9f4a82b..a9a0dda6274 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -1,7 +1,6 @@ import { render as rtlRender, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { HttpResponse, http } from 'msw'; -import { setupServer, SetupServer } from 'msw/node'; import { ComponentProps } from 'react'; import * as React from 'react'; import { useParams } from 'react-router-dom-v5-compat'; @@ -9,13 +8,16 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; +import server, { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { contextSrv } from 'app/core/core'; import { backendSrv } from 'app/core/services/backend_srv'; import BrowseDashboardsPage from './BrowseDashboardsPage'; -import { wellFormedTree } from './fixtures/dashboardsTreeItem.fixture'; import * as permissions from './permissions'; -const [mockTree, { dashbdD, folderA, folderA_folderA }] = wellFormedTree(); + +setupMockServer(); +const [mockTree, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), @@ -111,7 +113,6 @@ jest.mock('app/features/browse-dashboards/api/services', () => { }); describe('browse-dashboards BrowseDashboardsPage', () => { - let server: SetupServer; const mockPermissions = { canCreateDashboards: true, canEditDashboards: true, @@ -123,33 +124,14 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canDeleteDashboards: true, }; - beforeAll(() => { - server = setupServer( - http.get('/api/folders/:uid', () => { - return HttpResponse.json({ - title: folderA.item.title, - uid: folderA.item.uid, - }); - }), - http.get('/api/search', () => { - return HttpResponse.json({}); - }), + beforeEach(() => { + server.use( http.get('/api/search/sorting', () => { return HttpResponse.json({ sortOptions: [], }); - }), - http.get('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/settings', () => { - return HttpResponse.json({ - items: [], - }); }) ); - server.listen(); - }); - - afterAll(() => { - server.close(); }); beforeEach(() => { @@ -170,7 +152,6 @@ describe('browse-dashboards BrowseDashboardsPage', () => { canDeleteDashboards: true, }); jest.restoreAllMocks(); - server.resetHandlers(); }); describe('at the root level', () => { diff --git a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx index c60a8a8de56..9b96316add6 100644 --- a/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseFolderLibraryPanelsPage.test.tsx @@ -1,9 +1,9 @@ -import { render as rtlRender, screen } from '@testing-library/react'; import { http, HttpResponse } from 'msw'; -import { SetupServer, setupServer } from 'msw/node'; import { useParams } from 'react-router-dom-v5-compat'; -import { TestProvider } from 'test/helpers/TestProvider'; +import { render, screen } from 'test/test-utils'; +import server, { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { contextSrv } from 'app/core/core'; import { backendSrv } from 'app/core/services/backend_srv'; @@ -11,10 +11,7 @@ import BrowseFolderLibraryPanelsPage from './BrowseFolderLibraryPanelsPage'; import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture'; import * as permissions from './permissions'; -function render(...[ui, options]: Parameters) { - rtlRender({ui}, options); -} - +setupMockServer(); jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), getBackendSrv: () => backendSrv, @@ -28,15 +25,15 @@ jest.mock('react-router-dom-v5-compat', () => ({ useParams: jest.fn(), })); -const mockFolderName = 'myFolder'; -const mockFolderUid = '12345'; +const [_, { folderA }] = getFolderFixtures(); +const mockFolderName = folderA.item.title; +const mockFolderUid = folderA.item.uid; const mockLibraryElementsResponse = getLibraryElementsResponse(1, { folderUid: mockFolderUid, }); describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { (useParams as jest.Mock).mockReturnValue({ uid: mockFolderUid }); - let server: SetupServer; const mockPermissions = { canCreateDashboards: true, canEditDashboards: true, @@ -48,14 +45,8 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { canDeleteDashboards: true, }; - beforeAll(() => { - server = setupServer( - http.get('/api/folders/:uid', () => { - return HttpResponse.json({ - title: mockFolderName, - uid: mockFolderUid, - }); - }), + beforeEach(() => { + server.use( http.get('/api/library-elements', () => { return HttpResponse.json({ result: mockLibraryElementsResponse, @@ -65,11 +56,6 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { return HttpResponse.json({}); }) ); - server.listen(); - }); - - afterAll(() => { - server.close(); }); beforeEach(() => { @@ -79,7 +65,6 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => { afterEach(() => { jest.restoreAllMocks(); - server.resetHandlers(); }); it('displays the folder title', async () => { diff --git a/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.test.tsx b/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.test.tsx index fb1a160d8b1..c941cd76150 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/MoveModal.test.tsx @@ -1,38 +1,26 @@ -import userEvent from '@testing-library/user-event'; import { HttpResponse, http } from 'msw'; -import { SetupServer, setupServer } from 'msw/node'; import { render, screen } from 'test/test-utils'; +import { setBackendSrv } from '@grafana/runtime'; +import server, { setupMockServer } from '@grafana/test-utils/server'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { backendSrv } from 'app/core/services/backend_srv'; -import { treeViewersCanEdit, wellFormedTree } from '../../fixtures/dashboardsTreeItem.fixture'; - import { MoveModal, Props } from './MoveModal'; -const [mockTree, { folderA }] = wellFormedTree(); -const [mockTreeThatViewersCanEdit /* shares folders with wellFormedTree */] = treeViewersCanEdit(); +const [_, { folderA }] = getFolderFixtures(); -jest.mock('@grafana/runtime', () => ({ - ...jest.requireActual('@grafana/runtime'), - getBackendSrv: () => backendSrv, -})); +setBackendSrv(backendSrv); +setupMockServer(); describe('browse-dashboards MoveModal', () => { const mockOnDismiss = jest.fn(); const mockOnConfirm = jest.fn(); let props: Props; - let server: SetupServer; window.HTMLElement.prototype.scrollIntoView = () => {}; - beforeAll(() => { - server = setupServer( - http.get('/api/folders/:uid', () => { - return HttpResponse.json({ - title: folderA.item.title, - uid: folderA.item.uid, - }); - }), - + beforeEach(() => { + server.use( http.get('/api/folders/:uid/counts', () => { return HttpResponse.json({ folder: 1, @@ -40,43 +28,9 @@ describe('browse-dashboards MoveModal', () => { librarypanel: 3, alertrule: 4, }); - }), - - http.get('/apis/provisioning.grafana.app/v0alpha1/namespaces/default/settings', () => { - return HttpResponse.json({ - items: [], - }); - }), - - http.get('/api/folders', ({ request }) => { - const url = new URL(request.url); - const parentUid = url.searchParams.get('parentUid') ?? undefined; - const permission = url.searchParams.get('permission'); - - const limit = parseInt(url.searchParams.get('limit') ?? '1000', 10); - const page = parseInt(url.searchParams.get('page') ?? '1', 10); - - const tree = permission === 'Edit' ? mockTreeThatViewersCanEdit : mockTree; - - // reconstruct a folder API response from the flat tree fixture - const folders = tree - .filter((v) => v.item.kind === 'folder' && v.item.parentUID === parentUid) - .map((folder) => { - return { - uid: folder.item.uid, - title: folder.item.kind === 'folder' ? folder.item.title : "invalid - this shouldn't happen", - }; - }) - .slice(limit * (page - 1), limit * page); - - return HttpResponse.json(folders); }) ); - server.listen(); - }); - - beforeEach(() => { props = { isOpen: true, onConfirm: mockOnConfirm, @@ -90,10 +44,6 @@ describe('browse-dashboards MoveModal', () => { }; }); - afterAll(() => { - server.close(); - }); - it('renders a dialog with the correct title', async () => { render(); @@ -130,36 +80,36 @@ describe('browse-dashboards MoveModal', () => { }); it('enables the `Move` button once a folder is selected', async () => { - render(); + const { user } = render(); expect(await screen.findByRole('button', { name: 'Move' })).toBeDisabled(); // Open the picker and wait for children to load const folderPicker = await screen.findByRole('button', { name: 'Select folder' }); - await userEvent.click(folderPicker); + await user.click(folderPicker); await screen.findByLabelText(folderA.item.title); // Select the folder - await userEvent.click(screen.getByLabelText(folderA.item.title)); + await user.click(screen.getByLabelText(folderA.item.title)); const moveButton = await screen.findByRole('button', { name: 'Move' }); expect(moveButton).toBeEnabled(); - await userEvent.click(moveButton); + await user.click(moveButton); expect(mockOnConfirm).toHaveBeenCalledWith(folderA.item.uid); }); it('calls onDismiss when clicking the `Cancel` button', async () => { - render(); + const { user } = render(); - await userEvent.click(await screen.findByRole('button', { name: 'Cancel' })); + await user.click(await screen.findByRole('button', { name: 'Cancel' })); expect(mockOnDismiss).toHaveBeenCalled(); }); it('calls onDismiss when clicking the X', async () => { - render(); + const { user } = render(); - await userEvent.click(await screen.findByRole('button', { name: 'Close' })); + await user.click(await screen.findByRole('button', { name: 'Close' })); expect(mockOnDismiss).toHaveBeenCalled(); }); }); diff --git a/public/app/features/browse-dashboards/components/BrowseView.test.tsx b/public/app/features/browse-dashboards/components/BrowseView.test.tsx index 1a19d2b5b61..929f5bbf53c 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.test.tsx @@ -3,14 +3,13 @@ import userEvent from '@testing-library/user-event'; import { TestProvider } from 'test/helpers/TestProvider'; import { selectors } from '@grafana/e2e-selectors'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { DashboardViewItem } from 'app/features/search/types'; -import { wellFormedTree } from '../fixtures/dashboardsTreeItem.fixture'; - import { BrowseView } from './BrowseView'; const [mockTree, { folderA, folderA_folderA, folderA_folderB, folderA_folderB_dashbdB, dashbdD, folderB_empty }] = - wellFormedTree(); + getFolderFixtures(); function render(...[ui, options]: Parameters) { rtlRender({ui}, options); diff --git a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts index 4ac6515c726..7b5d17a2825 100644 --- a/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts +++ b/public/app/features/browse-dashboards/fixtures/dashboardsTreeItem.fixture.ts @@ -1,5 +1,6 @@ import { Chance } from 'chance'; +import { getFolderFixtures } from '@grafana/test-utils/unstable'; import { DashboardViewItem } from 'app/features/search/types'; import { DashboardsTreeItem, UIDashboardViewItem } from '../types'; @@ -74,7 +75,7 @@ export function sharedWithMeFolder(seed = 1): DashboardsTreeItem) { provisioningAPIv0alpha1.middleware, folderAPIv1beta1.middleware, advisorAPIv0alpha1.middleware, + dashboardAPIv0alpha1.middleware, // PLOP_INJECT_MIDDLEWARE // Used by the API client generator ...extraMiddleware diff --git a/scripts/generate-rtk-apis.ts b/scripts/generate-rtk-apis.ts index 792594a91e1..2082e222156 100644 --- a/scripts/generate-rtk-apis.ts +++ b/scripts/generate-rtk-apis.ts @@ -79,6 +79,17 @@ const config: ConfigFile = { filterEndpoints: ['listPlaylist', 'getPlaylist', 'createPlaylist', 'deletePlaylist', 'replacePlaylist'], tag: true, }, + '../public/app/api/clients/dashboard/v0alpha1/endpoints.gen.ts': { + apiFile: '../public/app/api/clients/dashboard/v0alpha1/baseAPI.ts', + schemaFile: '../data/openapi/dashboard.grafana.app-v0alpha1.json', + filterEndpoints: [ + // Do not use any other endpoints from this version + // If other endpoints are required, they must be used from a newer version of the dashboard API + 'getSearch', + ], + tag: true, + }, + // PLOP_INJECT_API_CLIENT - Used by the API client generator }, };