Folders: Update folder using app platform APIs (#110449)
This commit is contained in:
@@ -11,6 +11,7 @@ const collator = new Intl.Collator();
|
|||||||
const mockAccessControl = {
|
const mockAccessControl = {
|
||||||
'dashboards.permissions:write': true,
|
'dashboards.permissions:write': true,
|
||||||
'dashboards:create': true,
|
'dashboards:create': true,
|
||||||
|
'folders:write': true,
|
||||||
};
|
};
|
||||||
const additionalProperties = {
|
const additionalProperties = {
|
||||||
canAdmin: true,
|
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;
|
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 { wellFormedTree } from '../../../fixtures/folders';
|
||||||
|
|
||||||
|
import { SORT_OPTIONS } from './constants';
|
||||||
|
|
||||||
const [mockTree] = wellFormedTree();
|
const [mockTree] = wellFormedTree();
|
||||||
|
|
||||||
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
|
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
|
||||||
@@ -70,4 +72,6 @@ const getLegacySearchHandler = () =>
|
|||||||
return HttpResponse.json(response);
|
return HttpResponse.json(response);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default [getLegacySearchHandler()];
|
const getSearchSortingHandler = () => http.get('/api/search/sorting', () => HttpResponse.json(SORT_OPTIONS));
|
||||||
|
|
||||||
|
export default [getLegacySearchHandler(), getSearchSortingHandler()];
|
||||||
|
|||||||
+19
-9
@@ -7,32 +7,42 @@ const [mockTree] = wellFormedTree();
|
|||||||
|
|
||||||
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
|
type FilterArray = Array<(v: (typeof mockTree)[number]) => boolean>;
|
||||||
|
|
||||||
|
const typeMap: Record<string, string> = {
|
||||||
|
folder: 'folders',
|
||||||
|
dashboard: 'dashboards',
|
||||||
|
};
|
||||||
|
|
||||||
const getSearchHandler = () =>
|
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 folderFilter = new URL(request.url).searchParams.get('folder') || null;
|
||||||
const typeFilter = new URL(request.url).searchParams.get('type') || null;
|
const typeFilter = new URL(request.url).searchParams.get('type') || null;
|
||||||
const response = mockTree
|
const response = mockTree
|
||||||
.filter((filterItem) => {
|
.filter((filterItem) => {
|
||||||
const filters: FilterArray = [];
|
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) {
|
if (typeFilter) {
|
||||||
filters.push(({ item }) => item.kind === 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));
|
return filters.every((filterPredicate) => filterPredicate(filterItem));
|
||||||
})
|
})
|
||||||
|
|
||||||
.map(({ item }) => {
|
.map(({ item }) => {
|
||||||
const random = Chance(item.uid);
|
const random = Chance(item.uid);
|
||||||
return {
|
return {
|
||||||
resource: 'folders',
|
resource: typeMap[item.kind],
|
||||||
name: item.uid,
|
name: item.uid,
|
||||||
title: item.title,
|
title: item.title,
|
||||||
field: {
|
field: {
|
||||||
|
|||||||
+61
-49
@@ -11,6 +11,32 @@ const baseResponse = {
|
|||||||
apiVersion: 'folder.grafana.app/v1beta1',
|
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 folderNotFoundError = getErrorResponse('folder not found', 404);
|
||||||
|
|
||||||
const getFolderHandler = () =>
|
const getFolderHandler = () =>
|
||||||
@@ -26,28 +52,9 @@ const getFolderHandler = () =>
|
|||||||
return HttpResponse.json(folderNotFoundError, { status: 404 });
|
return HttpResponse.json(folderNotFoundError, { status: 404 });
|
||||||
}
|
}
|
||||||
|
|
||||||
return HttpResponse.json({
|
const appPlatformFolder = folderToAppPlatform(response.item, undefined, namespace);
|
||||||
...baseResponse,
|
|
||||||
metadata: {
|
return HttpResponse.json(appPlatformFolder);
|
||||||
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: {},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -121,35 +128,40 @@ const createFolderHandler = () =>
|
|||||||
|
|
||||||
const parentUid = body?.metadata?.annotations?.['grafana.app/folder'];
|
const parentUid = body?.metadata?.annotations?.['grafana.app/folder'];
|
||||||
const random = Chance(title);
|
const random = Chance(title);
|
||||||
const name = random.string({ length: 10 });
|
|
||||||
const uid = random.string({ length: 45 });
|
const uid = random.string({ length: 45 });
|
||||||
const id = random.integer({ min: 1, max: 1000 });
|
const id = random.integer({ min: 1, max: 1000 });
|
||||||
|
|
||||||
return HttpResponse.json({
|
const appPlatformFolder = folderToAppPlatform(
|
||||||
...baseResponse,
|
{ uid, title, parentUID: parentUid, kind: 'folder' },
|
||||||
metadata: {
|
id,
|
||||||
name,
|
namespace
|
||||||
namespace,
|
);
|
||||||
uid,
|
return HttpResponse.json(appPlatformFolder);
|
||||||
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: {},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
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,
|
useDeleteMultipleFoldersMutationFacade,
|
||||||
useMoveMultipleFoldersMutationFacade,
|
useMoveMultipleFoldersMutationFacade,
|
||||||
} from './hooks';
|
} from './hooks';
|
||||||
import { setupCreateFolder } from './test-utils';
|
import { setupCreateFolder, setupUpdateFolder } from './test-utils';
|
||||||
|
|
||||||
import { useDeleteFolderMutation, useUpdateFolderMutation } from './index';
|
import { useDeleteFolderMutation, useUpdateFolderMutation } from './index';
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ const renderFolderHook = async () => {
|
|||||||
wrapper: getWrapper({}),
|
wrapper: getWrapper({}),
|
||||||
});
|
});
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(result.current.isLoading).toBe(false);
|
expect(result.current.data).toBeDefined();
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
@@ -246,20 +246,20 @@ describe('useMoveMultipleFoldersMutationFacade', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('useCreateFolder', () => {
|
describe.each([
|
||||||
describe.each([
|
// app platform
|
||||||
// app platform
|
true,
|
||||||
true,
|
// legacy
|
||||||
// legacy
|
false,
|
||||||
false,
|
])('folderAppPlatformAPI toggle set to: %s', (toggle) => {
|
||||||
])('folderAppPlatformAPI toggle set to: %s', (toggle) => {
|
beforeEach(() => {
|
||||||
beforeEach(() => {
|
config.featureToggles.foldersAppPlatformAPI = toggle;
|
||||||
config.featureToggles.foldersAppPlatformAPI = toggle;
|
});
|
||||||
});
|
afterEach(() => {
|
||||||
afterEach(() => {
|
config.featureToggles = originalToggles;
|
||||||
config.featureToggles = originalToggles;
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
describe('useCreateFolder', () => {
|
||||||
it('creates a folder', async () => {
|
it('creates a folder', async () => {
|
||||||
const { user } = setupCreateFolder();
|
const { user } = setupCreateFolder();
|
||||||
|
|
||||||
@@ -268,4 +268,15 @@ describe('useCreateFolder', () => {
|
|||||||
expect(await screen.findByText('Folder created')).toBeInTheDocument();
|
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,
|
useDeleteFoldersMutation as useDeleteFoldersMutationLegacy,
|
||||||
useNewFolderMutation as useLegacyNewFolderMutation,
|
useNewFolderMutation as useLegacyNewFolderMutation,
|
||||||
useMoveFoldersMutation as useMoveFoldersMutationLegacy,
|
useMoveFoldersMutation as useMoveFoldersMutationLegacy,
|
||||||
|
useSaveFolderMutation as useLegacySaveFolderMutation,
|
||||||
MoveFoldersArgs,
|
MoveFoldersArgs,
|
||||||
DeleteFoldersArgs,
|
DeleteFoldersArgs,
|
||||||
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||||
@@ -44,6 +45,8 @@ import {
|
|||||||
useUpdateFolderMutation,
|
useUpdateFolderMutation,
|
||||||
Folder,
|
Folder,
|
||||||
CreateFolderApiArg,
|
CreateFolderApiArg,
|
||||||
|
useReplaceFolderMutation,
|
||||||
|
ReplaceFolderApiArg,
|
||||||
} from './index';
|
} from './index';
|
||||||
|
|
||||||
/** Trigger necessary actions to ensure legacy folder stores are updated */
|
/** Trigger necessary actions to ensure legacy folder stores are updated */
|
||||||
@@ -320,6 +323,38 @@ export function useCreateFolder() {
|
|||||||
return [createFolderAppPlatform, result] as const;
|
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(
|
function combinedState(
|
||||||
result: ReturnType<typeof useGetFolderQuery>,
|
result: ReturnType<typeof useGetFolderQuery>,
|
||||||
resultParents: ReturnType<typeof useGetFolderParentsQuery>,
|
resultParents: ReturnType<typeof useGetFolderParentsQuery>,
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ export const {
|
|||||||
useDeleteFolderMutation,
|
useDeleteFolderMutation,
|
||||||
useCreateFolderMutation,
|
useCreateFolderMutation,
|
||||||
useUpdateFolderMutation,
|
useUpdateFolderMutation,
|
||||||
|
useReplaceFolderMutation,
|
||||||
} = folderAPIv1beta1;
|
} = folderAPIv1beta1;
|
||||||
|
|
||||||
// eslint-disable-next-line no-barrel-files/no-barrel-files
|
// 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 { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||||
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
|
import { AppNotificationList } from 'app/core/components/AppNotifications/AppNotificationList';
|
||||||
|
|
||||||
import { useCreateFolder } from './hooks';
|
import { useCreateFolder, useUpdateFolder } from './hooks';
|
||||||
|
|
||||||
const [_, { folderA }] = getFolderFixtures();
|
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 */
|
/** Renders test component with a button that will create a new folder */
|
||||||
export const setupCreateFolder = () => render(<TestCreationComponent />);
|
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 { ComponentProps } from 'react';
|
||||||
import * as React from 'react';
|
|
||||||
import { useParams } from 'react-router-dom-v5-compat';
|
import { useParams } from 'react-router-dom-v5-compat';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
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 { 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 { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
import { backendSrv } from 'app/core/services/backend_srv';
|
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 BrowseDashboardsPage from './BrowseDashboardsPage';
|
||||||
import * as permissions from './permissions';
|
import * as permissions from './permissions';
|
||||||
|
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
setupMockServer();
|
setupMockServer();
|
||||||
const [_, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures();
|
|
||||||
|
|
||||||
jest.mock('@grafana/runtime', () => ({
|
const [_, { dashbdD, folderA, folderA_folderA }] = getFolderFixtures();
|
||||||
...jest.requireActual('@grafana/runtime'),
|
|
||||||
getBackendSrv: () => backendSrv,
|
|
||||||
config: {
|
|
||||||
...jest.requireActual('@grafana/runtime').config,
|
|
||||||
unifiedAlertingEnabled: true,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
jest.mock('react-virtualized-auto-sizer', () => {
|
jest.mock('react-virtualized-auto-sizer', () => {
|
||||||
return {
|
return {
|
||||||
@@ -51,42 +41,10 @@ jest.mock('react-router-dom-v5-compat', () => ({
|
|||||||
useParams: jest.fn().mockReturnValue({}),
|
useParams: jest.fn().mockReturnValue({}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function render(...[ui, options]: Parameters<typeof rtlRender>) {
|
function render(ui: Parameters<typeof testRender>[0]) {
|
||||||
const { rerender } = rtlRender(
|
return testRender(ui, {
|
||||||
<TestProvider
|
preloadedState: { navIndex: { 'dashboards/browse': { text: 'Dashboards', id: 'dashboards/browse' } } },
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('browse-dashboards BrowseDashboardsPage', () => {
|
describe('browse-dashboards BrowseDashboardsPage', () => {
|
||||||
@@ -102,16 +60,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
server.use(
|
config.unifiedAlertingEnabled = true;
|
||||||
http.get('/api/search/sorting', () => {
|
|
||||||
return HttpResponse.json({
|
|
||||||
sortOptions: [],
|
|
||||||
});
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
||||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
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 () => {
|
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));
|
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
|
// Check the filters are now hidden
|
||||||
expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument();
|
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 () => {
|
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));
|
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
|
// Check the actions are now visible
|
||||||
expect(screen.getByRole('button', { name: 'Move' })).toBeInTheDocument();
|
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 () => {
|
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(
|
const checkbox = await screen.findByTestId(
|
||||||
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
|
selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid)
|
||||||
);
|
);
|
||||||
await userEvent.click(checkbox);
|
await user.click(checkbox);
|
||||||
|
|
||||||
// Check the filters are now hidden
|
// Check the filters are now hidden
|
||||||
expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument();
|
expect(screen.queryByText('Filter by tag')).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||||||
import { Trans } from '@grafana/i18n';
|
import { Trans } from '@grafana/i18n';
|
||||||
import { config, reportInteraction } from '@grafana/runtime';
|
import { config, reportInteraction } from '@grafana/runtime';
|
||||||
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
import { useDispatch } from 'app/types/store';
|
import { useDispatch } from 'app/types/store';
|
||||||
@@ -21,7 +21,6 @@ import { useGetResourceRepositoryView } from '../provisioning/hooks/useGetResour
|
|||||||
import { useSearchStateManager } from '../search/state/SearchStateManager';
|
import { useSearchStateManager } from '../search/state/SearchStateManager';
|
||||||
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
|
import { getSearchPlaceholder } from '../search/tempI18nPhrases';
|
||||||
|
|
||||||
import { useSaveFolderMutation } from './api/browseDashboardsAPI';
|
|
||||||
import { BrowseActions } from './components/BrowseActions/BrowseActions';
|
import { BrowseActions } from './components/BrowseActions/BrowseActions';
|
||||||
import { BrowseFilters } from './components/BrowseFilters';
|
import { BrowseFilters } from './components/BrowseFilters';
|
||||||
import { BrowseView } from './components/BrowseView';
|
import { BrowseView } from './components/BrowseView';
|
||||||
@@ -73,7 +72,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
|||||||
}, [isSearching, searchState.result, stateManager]);
|
}, [isSearching, searchState.result, stateManager]);
|
||||||
|
|
||||||
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
|
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
|
||||||
const [saveFolder] = useSaveFolderMutation();
|
const [saveFolder] = useUpdateFolder();
|
||||||
const navModel = useMemo(() => {
|
const navModel = useMemo(() => {
|
||||||
if (!folderDTO) {
|
if (!folderDTO) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { screen } from '@testing-library/react';
|
import { render, screen } from 'test/test-utils';
|
||||||
import { render } from 'test/test-utils';
|
|
||||||
|
|
||||||
import { config } from '@grafana/runtime';
|
import { config } from '@grafana/runtime';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom-v5-compat';
|
|||||||
|
|
||||||
import { t } from '@grafana/i18n';
|
import { t } from '@grafana/i18n';
|
||||||
import { Alert } from '@grafana/ui';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
import { buildNavModel, getAlertingTabID } from 'app/features/folders/state/navModel';
|
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 { stringifyErrorLike } from '../alerting/unified/utils/misc';
|
||||||
import { rulerRuleType } from '../alerting/unified/utils/rules';
|
import { rulerRuleType } from '../alerting/unified/utils/rules';
|
||||||
|
|
||||||
import { useSaveFolderMutation } from './api/browseDashboardsAPI';
|
|
||||||
import { FolderActionsButton } from './components/FolderActionsButton';
|
import { FolderActionsButton } from './components/FolderActionsButton';
|
||||||
|
|
||||||
const { useRulerNamespaceQuery } = alertRuleApi;
|
const { useRulerNamespaceQuery } = alertRuleApi;
|
||||||
@@ -31,7 +30,7 @@ export function BrowseFolderAlertingPage() {
|
|||||||
namespace: folderUID,
|
namespace: folderUID,
|
||||||
});
|
});
|
||||||
|
|
||||||
const [saveFolder] = useSaveFolderMutation();
|
const [saveFolder] = useUpdateFolder();
|
||||||
|
|
||||||
const navModel = useMemo(() => {
|
const navModel = useMemo(() => {
|
||||||
if (!folderDTO) {
|
if (!folderDTO) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { http, HttpResponse } from 'msw';
|
|||||||
import { useParams } from 'react-router-dom-v5-compat';
|
import { useParams } from 'react-router-dom-v5-compat';
|
||||||
import { render, screen } from 'test/test-utils';
|
import { render, screen } from 'test/test-utils';
|
||||||
|
|
||||||
|
import { config, setBackendSrv } from '@grafana/runtime';
|
||||||
import server, { setupMockServer } from '@grafana/test-utils/server';
|
import server, { setupMockServer } from '@grafana/test-utils/server';
|
||||||
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
import { getFolderFixtures } from '@grafana/test-utils/unstable';
|
||||||
import { contextSrv } from 'app/core/core';
|
import { contextSrv } from 'app/core/core';
|
||||||
@@ -11,15 +12,9 @@ import BrowseFolderLibraryPanelsPage from './BrowseFolderLibraryPanelsPage';
|
|||||||
import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture';
|
import { getLibraryElementsResponse } from './fixtures/libraryElements.fixture';
|
||||||
import * as permissions from './permissions';
|
import * as permissions from './permissions';
|
||||||
|
|
||||||
|
setBackendSrv(backendSrv);
|
||||||
setupMockServer();
|
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.mock('react-router-dom-v5-compat', () => ({
|
||||||
...jest.requireActual('react-router-dom-v5-compat'),
|
...jest.requireActual('react-router-dom-v5-compat'),
|
||||||
useParams: jest.fn(),
|
useParams: jest.fn(),
|
||||||
@@ -46,19 +41,15 @@ describe('browse-dashboards BrowseFolderLibraryPanelsPage', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
config.unifiedAlertingEnabled = true;
|
||||||
server.use(
|
server.use(
|
||||||
http.get('/api/library-elements', () => {
|
http.get('/api/library-elements', () => {
|
||||||
return HttpResponse.json({
|
return HttpResponse.json({
|
||||||
result: mockLibraryElementsResponse,
|
result: mockLibraryElementsResponse,
|
||||||
});
|
});
|
||||||
}),
|
|
||||||
http.get('/api/search/sorting', () => {
|
|
||||||
return HttpResponse.json({});
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
jest.spyOn(permissions, 'getFolderPermissions').mockImplementation(() => mockPermissions);
|
||||||
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { useParams } from 'react-router-dom-v5-compat';
|
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 { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
import { GrafanaRouteComponentProps } from '../../core/navigation/types';
|
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 { OpenLibraryPanelModal } from '../library-panels/components/OpenLibraryPanelModal/OpenLibraryPanelModal';
|
||||||
import { LibraryElementDTO } from '../library-panels/types';
|
import { LibraryElementDTO } from '../library-panels/types';
|
||||||
|
|
||||||
import { useGetFolderQuery, useSaveFolderMutation } from './api/browseDashboardsAPI';
|
|
||||||
|
|
||||||
export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
|
export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||||
|
|
||||||
export function BrowseFolderLibraryPanelsPage() {
|
export function BrowseFolderLibraryPanelsPage() {
|
||||||
const { uid: folderUID = '' } = useParams();
|
const { uid: folderUID = '' } = useParams();
|
||||||
const { data: folderDTO } = useGetFolderQuery(folderUID);
|
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
|
||||||
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
|
const [selected, setSelected] = useState<LibraryElementDTO | undefined>(undefined);
|
||||||
const [saveFolder] = useSaveFolderMutation();
|
const [saveFolder] = useUpdateFolder();
|
||||||
|
|
||||||
const navModel = useMemo(() => {
|
const navModel = useMemo(() => {
|
||||||
if (!folderDTO) {
|
if (!folderDTO) {
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export const browseDashboardsAPI = createApi({
|
|||||||
}),
|
}),
|
||||||
|
|
||||||
// save an existing folder (e.g. rename)
|
// 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
|
// 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
|
// 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
|
// instead let's just invalidate all the getFolder calls
|
||||||
|
|||||||
Reference in New Issue
Block a user