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:
@@ -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;
|
||||
+52
@@ -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,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user