Folders: Use app platform search endpoint and update tests (#108814)

* Update test utils package with mock folder + search endpoints

* Add dashboards v0alpha1 api client

* Update hook to use app platform search API

* Update tests and fixtures
This commit is contained in:
Tom Ratcliffe
2025-08-01 13:34:33 +01:00
committed by GitHub
parent 1130f69ef7
commit 4f30a3b62b
20 changed files with 539 additions and 373 deletions
@@ -0,0 +1,130 @@
import { Chance } from 'chance';
import { DashboardsTreeItem, DashboardViewItem, UIDashboardViewItem } from '../types/browse-dashboards';
function wellFormedEmptyFolder(
seed = 1,
partial?: Partial<DashboardsTreeItem<UIDashboardViewItem>>
): DashboardsTreeItem<UIDashboardViewItem> {
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<DashboardsTreeItem<DashboardViewItem>>,
itemPartial?: Partial<DashboardViewItem>
): DashboardsTreeItem<DashboardViewItem> {
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<DashboardsTreeItem<DashboardViewItem>>,
itemPartial?: Partial<DashboardViewItem>
): DashboardsTreeItem<DashboardViewItem> {
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;
}
@@ -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;
@@ -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;
@@ -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()];
@@ -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<T extends DashboardViewItemWithUIItems = DashboardViewItemWithUIItems> {
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;
}
@@ -1 +1,4 @@
import { wellFormedTree } from './fixtures/folders';
export const getFolderFixtures = wellFormedTree;
export { MOCK_TEAMS } from './fixtures/teams';
@@ -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: () => ({}),
});
@@ -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<GetSearchApiResponse, GetSearchApiArg>({
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;
};
@@ -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;
@@ -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<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, 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(<NestedFolderPicker onChange={mockOnChange} value="folderA" />);
render(<NestedFolderPicker onChange={mockOnChange} value={folderA.item.uid} />);
expect(
await screen.findByRole('button', { name: `Select folder: ${folderA.item.title} currently selected` })
).toBeInTheDocument();
});
it('clicking the button opens the folder picker', async () => {
render(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
// 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(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
// 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(<NestedFolderPicker clearable value={folderA.item.uid} onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker clearable value={folderA.item.uid} onChange={mockOnChange} />);
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(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
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(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
// 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(<NestedFolderPicker showRootFolder={false} onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker showRootFolder={false} onChange={mockOnChange} />);
// 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(<NestedFolderPicker excludeUIDs={[folderC.item.uid]} onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker excludeUIDs={[folderC.item.uid]} onChange={mockOnChange} />);
// 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(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
// 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(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker permission="view" onChange={mockOnChange} />);
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(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
// 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(<NestedFolderPicker onChange={mockOnChange} />);
const { user } = render(<NestedFolderPicker onChange={mockOnChange} />);
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();
@@ -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 <ProviderWrapper>{children}</ProviderWrapper>;
};
// 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;
}
@@ -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<ReturnType<typeof folderAPIv1beta1.endpoints.getFolderChildren.select>>;
type GetFolderChildrenQuery = ReturnType<ReturnType<typeof dashboardAPIv0alpha1.endpoints.getSearch.select>>;
type GetFolderChildrenRequest = {
unsubscribe: () => void;
};
@@ -32,9 +32,9 @@ export function useFoldersQueryAppPlatform(isBrowsing: boolean, openFolders: Rec
const requestsRef = useRef<GetFolderChildrenRequest[]>([]);
// Keep a list of selectors for dynamic state selection
const [selectors, setSelectors] = useState<
Array<ReturnType<typeof folderAPIv1beta1.endpoints.getFolderChildren.select>>
>([]);
const [selectors, setSelectors] = useState<Array<ReturnType<typeof dashboardAPIv0alpha1.endpoints.getSearch.select>>>(
[]
);
// 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<DashboardsTreeItem<DashboardViewItemWithUIItems>> {
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<DashboardViewItemWithUIItems> = {
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,
+2
View File
@@ -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
};
@@ -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', () => {
@@ -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<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, 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 () => {
@@ -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(<MoveModal {...props} />);
@@ -130,36 +80,36 @@ describe('browse-dashboards MoveModal', () => {
});
it('enables the `Move` button once a folder is selected', async () => {
render(<MoveModal {...props} />);
const { user } = render(<MoveModal {...props} />);
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(<MoveModal {...props} />);
const { user } = render(<MoveModal {...props} />);
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(<MoveModal {...props} />);
const { user } = render(<MoveModal {...props} />);
await userEvent.click(await screen.findByRole('button', { name: 'Close' }));
await user.click(await screen.findByRole('button', { name: 'Close' }));
expect(mockOnDismiss).toHaveBeenCalled();
});
});
@@ -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<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
@@ -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<DashboardViewIt
}
export function treeViewersCanEdit() {
const [, { folderA, folderC }] = wellFormedTree();
const [, { folderA, folderC }] = getFolderFixtures();
return [
[folderA, folderC],
@@ -84,58 +85,3 @@ export function treeViewersCanEdit() {
},
] as const;
}
export function wellFormedTree() {
let seed = 1;
// prettier-ignore so its easier to see the tree structure
/* prettier-ignore */ const folderA = wellFormedFolder(seed++);
/* prettier-ignore */ const folderA_folderA = wellFormedFolder(seed++, { level: 1}, { parentUID: folderA.item.uid });
/* prettier-ignore */ const folderA_folderB = wellFormedFolder(seed++, { level: 1}, { parentUID: folderA.item.uid });
/* prettier-ignore */ const folderA_folderB_dashbdA = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderB.item.uid });
/* prettier-ignore */ const folderA_folderB_dashbdB = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderB.item.uid });
/* prettier-ignore */ const folderA_folderC = wellFormedFolder(seed++, { level: 1},{ parentUID: folderA.item.uid });
/* prettier-ignore */ const folderA_folderC_dashbdA = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderC.item.uid });
/* prettier-ignore */ const folderA_folderC_dashbdB = wellFormedDashboard(seed++, { level: 2}, { parentUID: folderA_folderC.item.uid });
/* prettier-ignore */ const folderA_dashbdD = wellFormedDashboard(seed++, { level: 1}, { parentUID: folderA.item.uid });
/* prettier-ignore */ const folderB = wellFormedFolder(seed++);
/* prettier-ignore */ const folderB_empty = wellFormedEmptyFolder(seed++);
/* prettier-ignore */ const folderC = wellFormedFolder(seed++);
/* prettier-ignore */ const dashbdD = wellFormedDashboard(seed++);
/* prettier-ignore */ 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;
}
+2
View File
@@ -3,6 +3,7 @@ import { setupListeners } from '@reduxjs/toolkit/query';
import { Middleware } from 'redux';
import { alertingAPI as alertingPackageAPI } from '@grafana/alerting/unstable';
import { dashboardAPIv0alpha1 } from 'app/api/clients/dashboard/v0alpha1';
import { browseDashboardsAPI } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
import { publicDashboardApi } from 'app/features/dashboard/api/publicDashboardApi';
import { cloudMigrationAPI } from 'app/features/migrate-to-cloud/api';
@@ -53,6 +54,7 @@ export function configureStore(initialState?: Partial<StoreState>) {
provisioningAPIv0alpha1.middleware,
folderAPIv1beta1.middleware,
advisorAPIv0alpha1.middleware,
dashboardAPIv0alpha1.middleware,
// PLOP_INJECT_MIDDLEWARE
// Used by the API client generator
...extraMiddleware
+11
View File
@@ -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
},
};