mirror of https://github.com/grafana/grafana.git
Folders: Update folder using app platform APIs (#110449)
This commit is contained in:
parent
6c517f82ed
commit
95080d9d56
|
|
@ -11,6 +11,7 @@ const collator = new Intl.Collator();
|
|||
const mockAccessControl = {
|
||||
'dashboards.permissions:write': true,
|
||||
'dashboards:create': true,
|
||||
'folders:write': true,
|
||||
};
|
||||
const additionalProperties = {
|
||||
canAdmin: true,
|
||||
|
|
@ -108,6 +109,19 @@ const createFolderHandler = () =>
|
|||
});
|
||||
});
|
||||
|
||||
const handlers = [listFoldersHandler(), getFolderHandler(), createFolderHandler()];
|
||||
const saveFolderHandler = () =>
|
||||
http.put<{ uid: string }, { title: string; version: number }>('/api/folders/:uid', async ({ params, request }) => {
|
||||
const { uid } = params;
|
||||
const body = await request.json();
|
||||
const folder = mockTree.find((v) => v.item.uid === uid);
|
||||
|
||||
if (!folder) {
|
||||
return HttpResponse.json({ message: 'folder not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
return HttpResponse.json({ ...folder.item, title: body.title });
|
||||
});
|
||||
|
||||
const handlers = [listFoldersHandler(), getFolderHandler(), createFolderHandler(), saveFolderHandler()];
|
||||
|
||||
export default handlers;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
/** Expected constant response from `/api/search/sorting` */
|
||||
export const SORT_OPTIONS = {
|
||||
sortOptions: [
|
||||
{
|
||||
description: 'Sort results in an alphabetically ascending order',
|
||||
displayName: 'Alphabetically (A–Z)',
|
||||
meta: '',
|
||||
name: 'alpha-asc',
|
||||
},
|
||||
{
|
||||
description: 'Sort results in an alphabetically descending order',
|
||||
displayName: 'Alphabetically (Z–A)',
|
||||
meta: '',
|
||||
name: 'alpha-desc',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -3,6 +3,8 @@ import { HttpResponse, http } from 'msw';
|
|||
|
||||
import { wellFormedTree } from '../../../fixtures/folders';
|
||||
|
||||
import { SORT_OPTIONS } from './constants';
|
||||
|
||||
const [mockTree] = wellFormedTree();
|
||||
|
||||
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
|
||||
|
|
@ -70,4 +72,6 @@ const getLegacySearchHandler = () =>
|
|||
return HttpResponse.json(response);
|
||||
});
|
||||
|
||||
export default [getLegacySearchHandler()];
|
||||
const getSearchSortingHandler = () => http.get('/api/search/sorting', () => HttpResponse.json(SORT_OPTIONS));
|
||||
|
||||
export default [getLegacySearchHandler(), getSearchSortingHandler()];
|
||||
|
|
|
|||
|
|
@ -7,32 +7,42 @@ const [mockTree] = wellFormedTree();
|
|||
|
||||
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
|
||||
|
||||
const typeMap: Record<string, string> = {
|
||||
folder: 'folders',
|
||||
dashboard: 'dashboards',
|
||||
};
|
||||
|
||||
const getSearchHandler = () =>
|
||||
http.get('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/search', ({ request }) => {
|
||||
http.get('/apis/dashboard.grafana.app/v0alpha1/namespaces/:namespace/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);
|
||||
}
|
||||
|
||||
if (folderFilter && folderFilter !== 'general') {
|
||||
filters.push(
|
||||
({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === folderFilter
|
||||
);
|
||||
}
|
||||
|
||||
if (folderFilter === 'general') {
|
||||
filters.push(
|
||||
({ item }) => (item.kind === 'folder' || item.kind === 'dashboard') && item.parentUID === undefined
|
||||
);
|
||||
}
|
||||
|
||||
return filters.every((filterPredicate) => filterPredicate(filterItem));
|
||||
})
|
||||
|
||||
.map(({ item }) => {
|
||||
const random = Chance(item.uid);
|
||||
return {
|
||||
resource: 'folders',
|
||||
resource: typeMap[item.kind],
|
||||
name: item.uid,
|
||||
title: item.title,
|
||||
field: {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,32 @@ const baseResponse = {
|
|||
apiVersion: 'folder.grafana.app/v1beta1',
|
||||
};
|
||||
|
||||
const folderToAppPlatform = (folder: (typeof mockTree)[number]['item'], id?: number, namespace?: string) => {
|
||||
return {
|
||||
...baseResponse,
|
||||
|
||||
metadata: {
|
||||
name: folder.uid,
|
||||
namespace: namespace ?? 'default',
|
||||
uid: folder.uid,
|
||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
||||
annotations: {
|
||||
// TODO: Generalise annotations in fixture data
|
||||
'grafana.app/createdBy': 'user:1',
|
||||
'grafana.app/updatedBy': 'user:2',
|
||||
'grafana.app/managedBy': 'user',
|
||||
'grafana.app/updatedTimestamp': '2024-01-01T00:00:00Z',
|
||||
'grafana.app/folder': folder.kind === 'folder' ? folder.parentUID : undefined,
|
||||
},
|
||||
labels: {
|
||||
'grafana.app/deprecatedInternalID': id ?? '123',
|
||||
},
|
||||
},
|
||||
spec: { title: folder.title, description: '' },
|
||||
status: {},
|
||||
};
|
||||
};
|
||||
|
||||
const folderNotFoundError = getErrorResponse('folder not found', 404);
|
||||
|
||||
const getFolderHandler = () =>
|
||||
|
|
@ -26,28 +52,9 @@ const getFolderHandler = () =>
|
|||
return HttpResponse.json(folderNotFoundError, { status: 404 });
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
...baseResponse,
|
||||
metadata: {
|
||||
name: response.item.uid,
|
||||
namespace,
|
||||
uid: response.item.uid,
|
||||
creationTimestamp: '2023-01-01T00:00:00Z',
|
||||
annotations: {
|
||||
// TODO: Generalise annotations in fixture data
|
||||
'grafana.app/createdBy': 'user:1',
|
||||
'grafana.app/updatedBy': 'user:2',
|
||||
'grafana.app/managedBy': 'user',
|
||||
'grafana.app/updatedTimestamp': '2024-01-01T00:00:00Z',
|
||||
'grafana.app/folder': response.item.kind === 'folder' ? response.item.parentUID : undefined,
|
||||
},
|
||||
labels: {
|
||||
'grafana.app/deprecatedInternalID': '123',
|
||||
},
|
||||
},
|
||||
spec: { title: response.item.title, description: '' },
|
||||
status: {},
|
||||
});
|
||||
const appPlatformFolder = folderToAppPlatform(response.item, undefined, namespace);
|
||||
|
||||
return HttpResponse.json(appPlatformFolder);
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -121,35 +128,40 @@ const createFolderHandler = () =>
|
|||
|
||||
const parentUid = body?.metadata?.annotations?.['grafana.app/folder'];
|
||||
const random = Chance(title);
|
||||
const name = random.string({ length: 10 });
|
||||
const uid = random.string({ length: 45 });
|
||||
const id = random.integer({ min: 1, max: 1000 });
|
||||
|
||||
return HttpResponse.json({
|
||||
...baseResponse,
|
||||
metadata: {
|
||||
name,
|
||||
namespace,
|
||||
uid,
|
||||
resourceVersion: '1756207979831',
|
||||
generation: 1,
|
||||
creationTimestamp: '2025-08-26T11:32:59Z',
|
||||
labels: {
|
||||
'grafana.app/deprecatedInternalID': id,
|
||||
},
|
||||
annotations: {
|
||||
'grafana.app/createdBy': 'user:1',
|
||||
'grafana.app/folder': parentUid,
|
||||
'grafana.app/updatedBy': 'user:1',
|
||||
'grafana.app/updatedTimestamp': '2025-08-26T11:32:59Z',
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
title,
|
||||
description: '',
|
||||
},
|
||||
status: {},
|
||||
});
|
||||
const appPlatformFolder = folderToAppPlatform(
|
||||
{ uid, title, parentUID: parentUid, kind: 'folder' },
|
||||
id,
|
||||
namespace
|
||||
);
|
||||
return HttpResponse.json(appPlatformFolder);
|
||||
}
|
||||
);
|
||||
export default [getFolderHandler(), getFolderParentsHandler(), createFolderHandler()];
|
||||
|
||||
const replaceFolderHandler = () =>
|
||||
http.put<{ folderUid: string; namespace: string }, PartialFolderPayload>(
|
||||
'/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid',
|
||||
async ({ params, request }) => {
|
||||
const body = await request.json();
|
||||
const { folderUid } = params;
|
||||
const response = mockTree.find(({ item }) => {
|
||||
return item.uid === folderUid;
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
return HttpResponse.json(folderNotFoundError, { status: 404 });
|
||||
}
|
||||
|
||||
const modifiedFolder = {
|
||||
...response.item,
|
||||
title: body.spec.title,
|
||||
};
|
||||
|
||||
const appPlatformFolder = folderToAppPlatform(modifiedFolder);
|
||||
|
||||
return HttpResponse.json(appPlatformFolder);
|
||||
}
|
||||
);
|
||||
export default [getFolderHandler(), getFolderParentsHandler(), createFolderHandler(), replaceFolderHandler()];
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ import {
|
|||
useDeleteMultipleFoldersMutationFacade,
|
||||
useMoveMultipleFoldersMutationFacade,
|
||||
} from './hooks';
|
||||
import { setupCreateFolder } from './test-utils';
|
||||
import { setupCreateFolder, setupUpdateFolder } from './test-utils';
|
||||
|
||||
import { useDeleteFolderMutation, useUpdateFolderMutation } from './index';
|
||||
|
||||
|
|
@ -68,7 +68,7 @@ const renderFolderHook = async () => {
|
|||
wrapper: getWrapper({}),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(result.current.isLoading).toBe(false);
|
||||
expect(result.current.data).toBeDefined();
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
|
@ -246,20 +246,20 @@ describe('useMoveMultipleFoldersMutationFacade', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('useCreateFolder', () => {
|
||||
describe.each([
|
||||
// app platform
|
||||
true,
|
||||
// legacy
|
||||
false,
|
||||
])('folderAppPlatformAPI toggle set to: %s', (toggle) => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.foldersAppPlatformAPI = toggle;
|
||||
});
|
||||
afterEach(() => {
|
||||
config.featureToggles = originalToggles;
|
||||
});
|
||||
describe.each([
|
||||
// app platform
|
||||
true,
|
||||
// legacy
|
||||
false,
|
||||
])('folderAppPlatformAPI toggle set to: %s', (toggle) => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.foldersAppPlatformAPI = toggle;
|
||||
});
|
||||
afterEach(() => {
|
||||
config.featureToggles = originalToggles;
|
||||
});
|
||||
|
||||
describe('useCreateFolder', () => {
|
||||
it('creates a folder', async () => {
|
||||
const { user } = setupCreateFolder();
|
||||
|
||||
|
|
@ -268,4 +268,15 @@ describe('useCreateFolder', () => {
|
|||
expect(await screen.findByText('Folder created')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('useUpdateFolder', () => {
|
||||
it('updates a folder', async () => {
|
||||
const { user } = await setupUpdateFolder(folderA_folderA.item.uid);
|
||||
|
||||
await user.type(screen.getByLabelText('Folder Title'), 'Updated Folder');
|
||||
await user.click(screen.getByText('Update Folder'));
|
||||
|
||||
expect(await screen.findByText('Folder updated')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
useDeleteFoldersMutation as useDeleteFoldersMutationLegacy,
|
||||
useNewFolderMutation as useLegacyNewFolderMutation,
|
||||
useMoveFoldersMutation as useMoveFoldersMutationLegacy,
|
||||
useSaveFolderMutation as useLegacySaveFolderMutation,
|
||||
MoveFoldersArgs,
|
||||
DeleteFoldersArgs,
|
||||
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
|
|
@ -44,6 +45,8 @@ import {
|
|||
useUpdateFolderMutation,
|
||||
Folder,
|
||||
CreateFolderApiArg,
|
||||
useReplaceFolderMutation,
|
||||
ReplaceFolderApiArg,
|
||||
} from './index';
|
||||
|
||||
/** Trigger necessary actions to ensure legacy folder stores are updated */
|
||||
|
|
@ -320,6 +323,38 @@ export function useCreateFolder() {
|
|||
return [createFolderAppPlatform, result] as const;
|
||||
}
|
||||
|
||||
export function useUpdateFolder() {
|
||||
const [updateFolder, result] = useReplaceFolderMutation();
|
||||
const legacyHook = useLegacySaveFolderMutation();
|
||||
|
||||
if (!config.featureToggles.foldersAppPlatformAPI) {
|
||||
return legacyHook;
|
||||
}
|
||||
|
||||
const updateFolderAppPlatform = async (folder: Pick<FolderDTO, 'uid' | 'title' | 'version' | 'parentUid'>) => {
|
||||
const payload: ReplaceFolderApiArg = {
|
||||
name: folder.uid,
|
||||
folder: {
|
||||
spec: { title: folder.title },
|
||||
metadata: {
|
||||
name: folder.uid,
|
||||
},
|
||||
status: {},
|
||||
},
|
||||
};
|
||||
|
||||
const result = await updateFolder(payload);
|
||||
dispatchRefetchChildren(folder.parentUid);
|
||||
|
||||
return {
|
||||
...result,
|
||||
data: result.data ? appPlatformFolderToLegacyFolder(result.data) : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
return [updateFolderAppPlatform, result] as const;
|
||||
}
|
||||
|
||||
function combinedState(
|
||||
result: ReturnType<typeof useGetFolderQuery>,
|
||||
resultParents: ReturnType<typeof useGetFolderParentsQuery>,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,8 @@ export const {
|
|||
useDeleteFolderMutation,
|
||||
useCreateFolderMutation,
|
||||
useUpdateFolderMutation,
|
||||
useReplaceFolderMutation,
|
||||
} = folderAPIv1beta1;
|
||||
|
||||
// eslint-disable-next-line no-barrel-files/no-barrel-files
|
||||
export { type Folder, type FolderList, type CreateFolderApiArg } from './endpoints.gen';
|
||||
export { type Folder, type FolderList, type CreateFolderApiArg, type ReplaceFolderApiArg } from './endpoints.gen';
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { render } from 'test/test-utils';
|
||||
import { useState } from 'react';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
|
||||
|
||||
import { useCreateFolder } from './hooks';
|
||||
import { useCreateFolder, useUpdateFolder } from './hooks';
|
||||
|
||||
const [_, { folderA }] = getFolderFixtures();
|
||||
|
||||
|
|
@ -19,5 +20,27 @@ const TestCreationComponent = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const TestUpdateComponent = ({ folderUID }: { folderUID: string }) => {
|
||||
const [updateFolder, result] = useUpdateFolder();
|
||||
const [title, setTitle] = useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<AppNotificationList />
|
||||
<label htmlFor="title">Folder Title</label>
|
||||
<input id="title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} />
|
||||
<button onClick={() => updateFolder({ title, uid: folderUID })}>Update Folder</button>
|
||||
<div>{result.isSuccess ? 'Folder updated' : 'Error updating folder'}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
/** Renders test component with a button that will create a new folder */
|
||||
export const setupCreateFolder = () => render(<TestCreationComponent />);
|
||||
|
||||
/** Renders test component with a button that allows updating a folder */
|
||||
export const setupUpdateFolder = async (folderUID: string) => {
|
||||
const view = render(<TestUpdateComponent folderUID={folderUID} />);
|
||||
await screen.findByText('Update Folder');
|
||||
return view;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,14 +1,11 @@
|
|||
import { render as rtlRender, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { HttpResponse, http } from 'msw';
|
||||
import { ComponentProps } from 'react';
|
||||
import * as React from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
import { render as testRender, screen, waitFor } from 'test/test-utils';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import server, { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import { 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';
|
||||
|
|
@ -16,17 +13,10 @@ import { backendSrv } from 'app/core/services/backend_srv';
|
|||
import BrowseDashboardsPage from './BrowseDashboardsPage';
|
||||
import * as permissions from './permissions';
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
const [_, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures();
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
unifiedAlertingEnabled: true,
|
||||
},
|
||||
}));
|
||||
const [_, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures();
|
||||
|
||||
jest.mock('react-virtualized-auto-sizer', () => {
|
||||
return {
|
||||
|
|
@ -51,42 +41,10 @@ jest.mock('react-router-dom-v5-compat', () => ({
|
|||
useParams: jest.fn().mockReturnValue({}),
|
||||
}));
|
||||
|
||||
function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
||||
const { rerender } = rtlRender(
|
||||
<TestProvider
|
||||
storeState={{
|
||||
navIndex: {
|
||||
'dashboards/browse': {
|
||||
text: 'Dashboards',
|
||||
id: 'dashboards/browse',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{ui}
|
||||
</TestProvider>,
|
||||
options
|
||||
);
|
||||
|
||||
const wrappedRerender = (ui: React.ReactElement) => {
|
||||
rerender(
|
||||
<TestProvider
|
||||
storeState={{
|
||||
navIndex: {
|
||||
'dashboards/browse': {
|
||||
text: 'Dashboards',
|
||||
id: 'dashboards/browse',
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{ui}
|
||||
</TestProvider>
|
||||
);
|
||||
};
|
||||
return {
|
||||
rerender: wrappedRerender,
|
||||
};
|
||||
function render(ui: Parameters<typeof testRender>[0]) {
|
||||
return testRender(ui, {
|
||||
preloadedState: { navIndex: { 'dashboards/browse': { text: 'Dashboards', id: 'dashboards/browse' } } },
|
||||
});
|
||||
}
|
||||
|
||||
describe('browse-dashboards BrowseDashboardsPage', () => {
|
||||
|
|
@ -102,16 +60,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
server.use(
|
||||
http.get('/api/search/sorting', () => {
|
||||
return HttpResponse.json({
|
||||
sortOptions: [],
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
config.unifiedAlertingEnabled = true;
|
||||
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
});
|
||||
|
|
@ -188,10 +137,10 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
|
|||
});
|
||||
|
||||
it('selecting an item hides the filters and shows the actions instead', async () => {
|
||||
render(<BrowseDashboardsPage queryParams={{}} />);
|
||||
const { user } = render(<BrowseDashboardsPage queryParams={{}} />);
|
||||
|
||||
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid));
|
||||
await userEvent.click(checkbox);
|
||||
await user.click(checkbox);
|
||||
|
||||
// Check the filters are now hidden
|
||||
expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument();
|
||||
|
|
@ -203,10 +152,10 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
|
|||
});
|
||||
|
||||
it('navigating into a child item resets the selected state', async () => {
|
||||
const { rerender } = render(<BrowseDashboardsPage queryParams={{}} />);
|
||||
const { rerender, user } = render(<BrowseDashboardsPage queryParams={{}} />);
|
||||
|
||||
const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid));
|
||||
await userEvent.click(checkbox);
|
||||
await user.click(checkbox);
|
||||
|
||||
// Check the actions are now visible
|
||||
expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
|
||||
|
|
@ -304,12 +253,12 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
|
|||
});
|
||||
|
||||
it('selecting an item hides the filters and shows the actions instead', async () => {
|
||||
render(<BrowseDashboardsPage queryParams={{}} />);
|
||||
const { user } = render(<BrowseDashboardsPage queryParams={{}} />);
|
||||
|
||||
const checkbox = await screen.findByTestId(
|
||||
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
|
||||
);
|
||||
await userEvent.click(checkbox);
|
||||
await user.click(checkbox);
|
||||
|
||||
// Check the filters are now hidden
|
||||
expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument();
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||
import { Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
|
||||
import { useGetFolderQueryFacade } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { getConfig } from 'app/core/config';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
|
@ -21,7 +21,6 @@ import { useGetResourceRepositoryView } from '../provisioning/hooks/useGetResour
|
|||
import { useSearchStateManager } from '../search/state/SearchStateManager';
|
||||
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
|
||||
|
||||
import { useSaveFolderMutation } from './api/browseDashboardsAPI';
|
||||
import { BrowseActions } from './components/BrowseActions/BrowseActions';
|
||||
import { BrowseFilters } from './components/BrowseFilters';
|
||||
import { BrowseView } from './components/BrowseView';
|
||||
|
|
@ -73,7 +72,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
|||
}, [isSearching, searchState.result, stateManager]);
|
||||
|
||||
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
|
||||
const [saveFolder] = useSaveFolderMutation();
|
||||
const [saveFolder] = useUpdateFolder();
|
||||
const navModel = useMemo(() => {
|
||||
if (!folderDTO) {
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { screen } from '@testing-library/react';
|
||||
import { render } from 'test/test-utils';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
|||
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Alert } from '@grafana/ui';
|
||||
import { useGetFolderQueryFacade } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { buildNavModel, getAlertingTabID } from 'app/features/folders/state/navModel';
|
||||
|
||||
|
|
@ -13,7 +13,6 @@ import { GRAFANA_RULER_CONFIG } from '../alerting/unified/api/featureDiscoveryAp
|
|||
import { stringifyErrorLike } from '../alerting/unified/utils/misc';
|
||||
import { rulerRuleType } from '../alerting/unified/utils/rules';
|
||||
|
||||
import { useSaveFolderMutation } from './api/browseDashboardsAPI';
|
||||
import { FolderActionsButton } from './components/FolderActionsButton';
|
||||
|
||||
const { useRulerNamespaceQuery } = alertRuleApi;
|
||||
|
|
@ -31,7 +30,7 @@ export function BrowseFolderAlertingPage() {
|
|||
namespace: folderUID,
|
||||
});
|
||||
|
||||
const [saveFolder] = useSaveFolderMutation();
|
||||
const [saveFolder] = useUpdateFolder();
|
||||
|
||||
const navModel = useMemo(() => {
|
||||
if (!folderDTO) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { http, HttpResponse } from 'msw';
|
|||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
import { render, screen } from 'test/test-utils';
|
||||
|
||||
import { config, setBackendSrv } from '@grafana/runtime';
|
||||
import server, { setupMockServer } from '@grafana/test-utils/server';
|
||||
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
|
@ -11,15 +12,9 @@ import BrowseFolderLibraryPanelsPage from './BrowseFolderLibraryPanelsPage';
|
|||
import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture';
|
||||
import * as permissions from './permissions';
|
||||
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getBackendSrv: () => backendSrv,
|
||||
config: {
|
||||
...jest.requireActual('@grafana/runtime').config,
|
||||
unifiedAlertingEnabled: true,
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useParams: jest.fn(),
|
||||
|
|
@ -46,19 +41,15 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
config.unifiedAlertingEnabled = true;
|
||||
server.use(
|
||||
http.get('/api/library-elements', () => {
|
||||
return HttpResponse.json({
|
||||
result: mockLibraryElementsResponse,
|
||||
});
|
||||
}),
|
||||
http.get('/api/search/sorting', () => {
|
||||
return HttpResponse.json({});
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
|
||||
|
|
@ -10,15 +11,13 @@ import { LibraryPanelsSearch } from '../library-panels/components/LibraryPanelsS
|
|||
import { OpenLibraryPanelModal } from '../library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal';
|
||||
import { LibraryElementDTO } from '../library-panels/types';
|
||||
|
||||
import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboardsAPI';
|
||||
|
||||
export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||
|
||||
export function BrowseFolderLibraryPanelsPage() {
|
||||
const { uid: folderUID = '' } = useParams();
|
||||
const { data: folderDTO } = useGetFolderQuery(folderUID);
|
||||
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
|
||||
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
|
||||
const [saveFolder] = useSaveFolderMutation();
|
||||
const [saveFolder] = useUpdateFolder();
|
||||
|
||||
const navModel = useMemo(() => {
|
||||
if (!folderDTO) {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ export const browseDashboardsAPI = createApi({
|
|||
}),
|
||||
|
||||
// save an existing folder (e.g. rename)
|
||||
saveFolder: builder.mutation<FolderDTO, FolderDTO>({
|
||||
saveFolder: builder.mutation<FolderDTO, Pick<FolderDTO, 'uid' | 'title' | 'version' | 'parentUid'>>({
|
||||
// because the getFolder calls contain the parents, renaming a parent/grandparent/etc needs to invalidate all child folders
|
||||
// we could do something smart and recursively invalidate these child folders but it doesn't seem worth it
|
||||
// instead let's just invalidate all the getFolder calls
|
||||
|
|
|
|||
Loading…
Reference in New Issue