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,13 +246,12 @@ describe('useMoveMultipleFoldersMutationFacade', () => { | |||
|   }); | ||||
| }); | ||||
| 
 | ||||
| describe('useCreateFolder', () => { | ||||
|   describe.each([ | ||||
| describe.each([ | ||||
|   // app platform
 | ||||
|   true, | ||||
|   // legacy
 | ||||
|   false, | ||||
|   ])('folderAppPlatformAPI toggle set to: %s', (toggle) => { | ||||
| ])('folderAppPlatformAPI toggle set to: %s', (toggle) => { | ||||
|   beforeEach(() => { | ||||
|     config.featureToggles.foldersAppPlatformAPI = toggle; | ||||
|   }); | ||||
|  | @ -260,6 +259,7 @@ describe('useCreateFolder', () => { | |||
|     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