mirror of https://github.com/grafana/grafana.git
				
				
				
			Folders: Refactor hooks to (eventually) consume app platform `/counts` endpoint (#110894)
This commit is contained in:
		
							parent
							
								
									4f43761630
								
							
						
					
					
						commit
						3ea093b596
					
				|  | @ -1901,11 +1901,6 @@ | |||
|       "count": 1 | ||||
|     } | ||||
|   }, | ||||
|   "public/app/features/browse-dashboards/components/BrowseActions/MoveModal.tsx": { | ||||
|     "no-restricted-syntax": { | ||||
|       "count": 1 | ||||
|     } | ||||
|   }, | ||||
|   "public/app/features/browse-dashboards/components/NewFolderForm.tsx": { | ||||
|     "no-restricted-syntax": { | ||||
|       "count": 1 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import { HttpResponse, http } from 'msw'; | |||
| 
 | ||||
| import { treeViewersCanEdit, wellFormedTree } from '../../../fixtures/folders'; | ||||
| 
 | ||||
| const [mockTree] = wellFormedTree(); | ||||
| const [mockTree, { folderB }] = wellFormedTree(); | ||||
| const [mockTreeThatViewersCanEdit] = treeViewersCanEdit(); | ||||
| const collator = new Intl.Collator(); | ||||
| 
 | ||||
|  | @ -122,6 +122,38 @@ const saveFolderHandler = () => | |||
|     return HttpResponse.json({ ...folder.item, title: body.title }); | ||||
|   }); | ||||
| 
 | ||||
| const handlers = [listFoldersHandler(), getFolderHandler(), createFolderHandler(), saveFolderHandler()]; | ||||
| const getMockFolderCounts = (folder: number, dashboard: number, librarypanel: number, alertrule: number) => { | ||||
|   return { | ||||
|     folder, | ||||
|     dashboard, | ||||
|     librarypanel, | ||||
|     alertrule, | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const folderCountsHandler = () => | ||||
|   http.get<{ uid: string }, { title: string; version: number }>('/api/folders/:uid/counts', async ({ params }) => { | ||||
|     const { uid } = params; | ||||
|     const folder = mockTree.find((v) => v.item.uid === uid); | ||||
| 
 | ||||
|     if (!folder) { | ||||
|       // The legacy API returns 0's for a folder that doesn't exist 🤷♂️
 | ||||
|       return HttpResponse.json(getMockFolderCounts(0, 0, 0, 0)); | ||||
|     } | ||||
| 
 | ||||
|     if (uid === folderB.item.uid) { | ||||
|       return HttpResponse.json({}, { status: 500 }); | ||||
|     } | ||||
| 
 | ||||
|     return HttpResponse.json(getMockFolderCounts(1, 1, 1, 1)); | ||||
|   }); | ||||
| 
 | ||||
| const handlers = [ | ||||
|   listFoldersHandler(), | ||||
|   getFolderHandler(), | ||||
|   createFolderHandler(), | ||||
|   saveFolderHandler(), | ||||
|   folderCountsHandler(), | ||||
| ]; | ||||
| 
 | ||||
| export default handlers; | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ import { HttpResponse, http } from 'msw'; | |||
| import { wellFormedTree } from '../../../../fixtures/folders'; | ||||
| import { getErrorResponse } from '../../../helpers'; | ||||
| 
 | ||||
| const [mockTree] = wellFormedTree(); | ||||
| const [mockTree, { folderB }] = wellFormedTree(); | ||||
| 
 | ||||
| const baseResponse = { | ||||
|   kind: 'Folder', | ||||
|  | @ -164,4 +164,67 @@ const replaceFolderHandler = () => | |||
|       return HttpResponse.json(appPlatformFolder); | ||||
|     } | ||||
|   ); | ||||
| export default [getFolderHandler(), getFolderParentsHandler(), createFolderHandler(), replaceFolderHandler()]; | ||||
| 
 | ||||
| const getMockFolderCounts = (folders: number, dashboards: number, library_elements: number, alertrules: number) => { | ||||
|   return { | ||||
|     kind: 'DescendantCounts', | ||||
|     apiVersion: 'folder.grafana.app/v1beta1', | ||||
|     counts: [ | ||||
|       { | ||||
|         group: 'dashboard.grafana.app', | ||||
|         resource: 'dashboards', | ||||
|         count: dashboards, | ||||
|       }, | ||||
|       { | ||||
|         group: 'sql-fallback', | ||||
|         resource: 'alertrules', | ||||
|         count: alertrules, | ||||
|       }, | ||||
|       { | ||||
|         group: 'sql-fallback', | ||||
|         resource: 'dashboards', | ||||
|         count: dashboards, | ||||
|       }, | ||||
|       { | ||||
|         group: 'sql-fallback', | ||||
|         resource: 'folders', | ||||
|         count: folders, | ||||
|       }, | ||||
|       { | ||||
|         group: 'sql-fallback', | ||||
|         resource: 'library_elements', | ||||
|         count: library_elements, | ||||
|       }, | ||||
|     ], | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
| const folderCountsHandler = () => | ||||
|   http.get<{ folderUid: string; namespace: string }, PartialFolderPayload>( | ||||
|     '/apis/folder.grafana.app/v1beta1/namespaces/:namespace/folders/:folderUid/counts', | ||||
|     async ({ params }) => { | ||||
|       const { folderUid } = params; | ||||
|       const matchedFolder = mockTree.find(({ item }) => { | ||||
|         return item.uid === folderUid; | ||||
|       }); | ||||
| 
 | ||||
|       if (!matchedFolder) { | ||||
|         // The API returns 0's for a folder that doesn't exist 🤷♂️
 | ||||
|         return HttpResponse.json(getMockFolderCounts(0, 0, 0, 0)); | ||||
|       } | ||||
| 
 | ||||
|       if (folderUid === folderB.item.uid) { | ||||
|         return HttpResponse.json({}, { status: 500 }); | ||||
|       } | ||||
| 
 | ||||
|       return HttpResponse.json(getMockFolderCounts(1, 1, 1, 1)); | ||||
|     } | ||||
|   ); | ||||
| 
 | ||||
| export default [ | ||||
|   getFolderHandler(), | ||||
|   getFolderParentsHandler(), | ||||
|   createFolderHandler(), | ||||
|   replaceFolderHandler(), | ||||
|   folderCountsHandler(), | ||||
| ]; | ||||
|  |  | |||
|  | @ -13,10 +13,12 @@ import { | |||
|   useMoveFoldersMutation as useMoveFoldersMutationLegacy, | ||||
|   useSaveFolderMutation as useLegacySaveFolderMutation, | ||||
|   useMoveFolderMutation as useMoveFolderMutationLegacy, | ||||
|   useGetAffectedItemsQuery as useLegacyGetAffectedItemsQuery, | ||||
|   MoveFoldersArgs, | ||||
|   DeleteFoldersArgs, | ||||
|   MoveFolderArgs, | ||||
| } from 'app/features/browse-dashboards/api/browseDashboardsAPI'; | ||||
| import { DashboardTreeSelection } from 'app/features/browse-dashboards/types'; | ||||
| import { FolderDTO, NewFolder } from 'app/types/folders'; | ||||
| 
 | ||||
| import kbn from '../../../../core/utils/kbn'; | ||||
|  | @ -49,6 +51,7 @@ import { | |||
|   CreateFolderApiArg, | ||||
|   useReplaceFolderMutation, | ||||
|   ReplaceFolderApiArg, | ||||
|   useGetAffectedItemsQuery, | ||||
| } from './index'; | ||||
| 
 | ||||
| function getFolderUrl(uid: string, title: string): string { | ||||
|  | @ -393,6 +396,26 @@ function useRefreshFolders() { | |||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function useGetAffectedItems({ folder, dashboard }: Pick<DashboardTreeSelection, 'folder' | 'dashboard'>) { | ||||
|   const folderUIDs = Object.keys(folder).filter((uid) => folder[uid]); | ||||
|   const dashboardUIDs = Object.keys(dashboard).filter((uid) => dashboard[uid]); | ||||
| 
 | ||||
|   // TODO: Remove constant condition here once we have a solution for the app platform counts
 | ||||
|   // As of now, the counts are not calculated recursively, so we need to use the legacy API
 | ||||
|   const shouldUseAppPlatformAPI = false && Boolean(config.featureToggles.foldersAppPlatformAPI); | ||||
|   const hookParams: | ||||
|     | Parameters<typeof useLegacyGetAffectedItemsQuery>[0] | ||||
|     | Parameters<typeof useGetAffectedItemsQuery>[0] = { | ||||
|     folderUIDs, | ||||
|     dashboardUIDs, | ||||
|   }; | ||||
| 
 | ||||
|   const legacyResult = useLegacyGetAffectedItemsQuery(!shouldUseAppPlatformAPI ? hookParams : skipToken); | ||||
|   const appPlatformResult = useGetAffectedItemsQuery(shouldUseAppPlatformAPI ? hookParams : skipToken); | ||||
| 
 | ||||
|   return shouldUseAppPlatformAPI ? appPlatformResult : legacyResult; | ||||
| } | ||||
| 
 | ||||
| function combinedState( | ||||
|   result: ReturnType<typeof useGetFolderQuery>, | ||||
|   resultParents: ReturnType<typeof useGetFolderParentsQuery>, | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| import { generatedAPI } from './endpoints.gen'; | ||||
| import { DescendantCount } from 'app/types/folders'; | ||||
| 
 | ||||
| export const folderAPIv1beta1 = generatedAPI.enhanceEndpoints({ | ||||
| import { generatedAPI } from './endpoints.gen'; | ||||
| import { getParsedCounts } from './utils'; | ||||
| 
 | ||||
| export const folderAPIv1beta1 = generatedAPI | ||||
|   .enhanceEndpoints({ | ||||
|     endpoints: { | ||||
|       getFolder: { | ||||
|         providesTags: (result, error, arg) => (result ? [{ type: 'Folder', id: arg.name }] : []), | ||||
|  | @ -10,7 +14,9 @@ export const folderAPIv1beta1 = generatedAPI.enhanceEndpoints({ | |||
|           result | ||||
|             ? [ | ||||
|                 { type: 'Folder', id: 'LIST' }, | ||||
|               ...result.items.map((folder) => ({ type: 'Folder' as const, id: folder.metadata?.name })).filter(Boolean), | ||||
|                 ...result.items | ||||
|                   .map((folder) => ({ type: 'Folder' as const, id: folder.metadata?.name })) | ||||
|                   .filter(Boolean), | ||||
|               ] | ||||
|             : [{ type: 'Folder', id: 'LIST' }], | ||||
|       }, | ||||
|  | @ -37,7 +43,49 @@ export const folderAPIv1beta1 = generatedAPI.enhanceEndpoints({ | |||
|         }), | ||||
|       }, | ||||
|     }, | ||||
| }); | ||||
|   }) | ||||
|   .injectEndpoints({ | ||||
|     endpoints: (builder) => ({ | ||||
|       getAffectedItems: builder.query<Record<string, number>, { folderUIDs: string[]; dashboardUIDs: string[] }>({ | ||||
|         // Similar to legacy API, don't cache this data as we don't have full knowledge of the descendant entities
 | ||||
|         // and when they're created/deleted, so we can't easily know when this data is stale
 | ||||
|         keepUnusedDataFor: 0, | ||||
|         queryFn: async ({ folderUIDs, dashboardUIDs }, queryApi) => { | ||||
|           const initialCounts: DescendantCount = { | ||||
|             folders: folderUIDs.length, | ||||
|             dashboards: dashboardUIDs.length, | ||||
|             library_elements: 0, | ||||
|             alertrules: 0, | ||||
|           }; | ||||
| 
 | ||||
|           const promises = folderUIDs.map(async (folderUID) => | ||||
|             queryApi.dispatch(generatedAPI.endpoints.getFolderCounts.initiate({ name: folderUID })) | ||||
|           ); | ||||
|           try { | ||||
|             const results = await Promise.all(promises); | ||||
| 
 | ||||
|             const mapped = results.reduce((acc, result) => { | ||||
|               const { data, error } = result; | ||||
|               if (error) { | ||||
|                 throw error; | ||||
|               } | ||||
| 
 | ||||
|               const counts = getParsedCounts(data?.counts ?? []); | ||||
|               acc.folders += counts.folders; | ||||
|               acc.dashboards += counts.dashboards; | ||||
|               acc.alertrules += counts.alertrules; | ||||
|               acc.library_elements += counts.library_elements; | ||||
|               return acc; | ||||
|             }, initialCounts); | ||||
| 
 | ||||
|             return { data: mapped }; | ||||
|           } catch (error) { | ||||
|             return { error }; | ||||
|           } | ||||
|         }, | ||||
|       }), | ||||
|     }), | ||||
|   }); | ||||
| 
 | ||||
| export const { | ||||
|   useGetFolderQuery, | ||||
|  | @ -46,6 +94,7 @@ export const { | |||
|   useCreateFolderMutation, | ||||
|   useUpdateFolderMutation, | ||||
|   useReplaceFolderMutation, | ||||
|   useGetAffectedItemsQuery, | ||||
| } = folderAPIv1beta1; | ||||
| 
 | ||||
| // eslint-disable-next-line no-barrel-files/no-barrel-files
 | ||||
|  |  | |||
|  | @ -6,6 +6,8 @@ import appEvents from '../../../../core/app_events'; | |||
| import { isProvisionedFolder } from '../../../../features/browse-dashboards/api/isProvisioned'; | ||||
| import { useDispatch } from '../../../../types/store'; | ||||
| 
 | ||||
| import { ResourceStats } from './endpoints.gen'; | ||||
| 
 | ||||
| import { folderAPIv1beta1 as folderAPI } from './index'; | ||||
| 
 | ||||
| export async function isProvisionedFolderCheck( | ||||
|  | @ -34,3 +36,34 @@ export async function isProvisionedFolderCheck( | |||
|     return false; | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| const initialCounts: Record<string, number> = { | ||||
|   folder: 0, | ||||
|   dashboard: 0, | ||||
|   libraryPanel: 0, | ||||
|   alertRule: 0, | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Parses descendant counts into legacy-friendly format | ||||
|  * | ||||
|  * Takes the first count information as the source of truth, e.g. if | ||||
|  * the array has a | ||||
|  * | ||||
|  * `"group": "dashboard.grafana.app"` | ||||
|  * | ||||
|  * entry first, and a | ||||
|  * | ||||
|  * `"group": "sql-fallback"` | ||||
|  * | ||||
|  * entry later, the `dashboard.grafana.app` count will be used | ||||
|  */ | ||||
| export const getParsedCounts = (counts: ResourceStats[]) => { | ||||
|   return counts.reduce((acc, { resource, count }) => { | ||||
|     // If there's no value already, then use that count, so a fallback count is not used
 | ||||
|     if (!acc[resource]) { | ||||
|       acc[resource] = count; | ||||
|     } | ||||
|     return acc; | ||||
|   }, initialCounts); | ||||
| }; | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| import { createApi } from '@reduxjs/toolkit/query/react'; | ||||
| 
 | ||||
| import { AppEvents, isTruthy, locationUtil } from '@grafana/data'; | ||||
| import { AppEvents, locationUtil } from '@grafana/data'; | ||||
| import { t } from '@grafana/i18n'; | ||||
| import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime'; | ||||
| import { Dashboard } from '@grafana/schema'; | ||||
|  | @ -22,7 +22,6 @@ import { FolderListItemDTO, FolderDTO, DescendantCount, DescendantCountDTO } fro | |||
| import { getDashboardScenePageStateManager } from '../../dashboard-scene/pages/DashboardScenePageStateManager'; | ||||
| import { deletedDashboardsCache } from '../../search/service/deletedDashboardsCache'; | ||||
| import { refetchChildren, refreshParents } from '../state/actions'; | ||||
| import { DashboardTreeSelection } from '../types'; | ||||
| 
 | ||||
| import { isProvisionedDashboard } from './isProvisioned'; | ||||
| import { PAGE_SIZE } from './services'; | ||||
|  | @ -191,33 +190,34 @@ export const browseDashboardsAPI = createApi({ | |||
|     }), | ||||
| 
 | ||||
|     // gets the descendant counts for a folder. used in the move/delete modals.
 | ||||
|     getAffectedItems: builder.query<DescendantCount, DashboardTreeSelection>({ | ||||
|     getAffectedItems: builder.query<DescendantCount, { folderUIDs: string[]; dashboardUIDs: string[] }>({ | ||||
|       // don't cache this data for now, since library panel/alert rule creation isn't done through rtk query
 | ||||
|       keepUnusedDataFor: 0, | ||||
|       queryFn: async (selectedItems) => { | ||||
|         const folderUIDs = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]); | ||||
| 
 | ||||
|       queryFn: async ({ folderUIDs, dashboardUIDs }) => { | ||||
|         try { | ||||
|           const promises = folderUIDs.map((folderUID) => { | ||||
|             return getBackendSrv().get<DescendantCountDTO>(`/api/folders/${folderUID}/counts`); | ||||
|           }); | ||||
| 
 | ||||
|           const results = await Promise.all(promises); | ||||
| 
 | ||||
|         const totalCounts = { | ||||
|           folder: Object.values(selectedItems.folder).filter(isTruthy).length, | ||||
|           dashboard: Object.values(selectedItems.dashboard).filter(isTruthy).length, | ||||
|           libraryPanel: 0, | ||||
|           alertRule: 0, | ||||
|           const totalCounts: DescendantCount = { | ||||
|             folders: folderUIDs.length, | ||||
|             dashboards: dashboardUIDs.length, | ||||
|             library_elements: 0, | ||||
|             alertrules: 0, | ||||
|           }; | ||||
| 
 | ||||
|           for (const folderCounts of results) { | ||||
|           totalCounts.folder += folderCounts.folder; | ||||
|           totalCounts.dashboard += folderCounts.dashboard; | ||||
|           totalCounts.alertRule += folderCounts.alertrule; | ||||
|           totalCounts.libraryPanel += folderCounts.librarypanel; | ||||
|             totalCounts.folders += folderCounts.folder; | ||||
|             totalCounts.dashboards += folderCounts.dashboard; | ||||
|             totalCounts.alertrules += folderCounts.alertrule; | ||||
|             totalCounts.library_elements += folderCounts.librarypanel; | ||||
|           } | ||||
| 
 | ||||
|           return { data: totalCounts }; | ||||
|         } catch (error) { | ||||
|           return { error }; | ||||
|         } | ||||
|       }, | ||||
|     }), | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,8 +3,8 @@ import { useState } from 'react'; | |||
| import { Trans, t } from '@grafana/i18n'; | ||||
| import { config, reportInteraction } from '@grafana/runtime'; | ||||
| import { Alert, ConfirmModal, Text, Space } from '@grafana/ui'; | ||||
| import { useGetAffectedItems } from 'app/api/clients/folder/v1beta1/hooks'; | ||||
| 
 | ||||
| import { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI'; | ||||
| import { DashboardTreeSelection } from '../../types'; | ||||
| 
 | ||||
| import { DescendantCount } from './DescendantCount'; | ||||
|  | @ -17,8 +17,8 @@ export interface Props { | |||
| } | ||||
| 
 | ||||
| export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Props) => { | ||||
|   const { data } = useGetAffectedItemsQuery(selectedItems); | ||||
|   const deleteIsInvalid = Boolean(data && (data.alertRule || data.libraryPanel)); | ||||
|   const { data } = useGetAffectedItems(selectedItems); | ||||
|   const deleteIsInvalid = Boolean(data && (data.alertrules || data.library_elements)); | ||||
|   const [isDeleting, setIsDeleting] = useState(false); | ||||
| 
 | ||||
|   const onDelete = async () => { | ||||
|  |  | |||
|  | @ -2,8 +2,8 @@ import Skeleton from 'react-loading-skeleton'; | |||
| 
 | ||||
| import { t } from '@grafana/i18n'; | ||||
| import { Alert, Text } from '@grafana/ui'; | ||||
| import { useGetAffectedItems } from 'app/api/clients/folder/v1beta1/hooks'; | ||||
| 
 | ||||
| import { useGetAffectedItemsQuery } from '../../api/browseDashboardsAPI'; | ||||
| import { DashboardTreeSelection } from '../../types'; | ||||
| 
 | ||||
| import { buildBreakdownString } from './utils'; | ||||
|  | @ -13,7 +13,7 @@ export interface Props { | |||
| } | ||||
| 
 | ||||
| export const DescendantCount = ({ selectedItems }: Props) => { | ||||
|   const { data, isFetching, isLoading, error } = useGetAffectedItemsQuery(selectedItems); | ||||
|   const { data, isFetching, isLoading, error } = useGetAffectedItems(selectedItems); | ||||
| 
 | ||||
|   return error ? ( | ||||
|     <Alert | ||||
|  | @ -25,7 +25,7 @@ export const DescendantCount = ({ selectedItems }: Props) => { | |||
|     /> | ||||
|   ) : ( | ||||
|     <Text element="p" color="secondary"> | ||||
|       {data && buildBreakdownString(data.folder, data.dashboard, data.libraryPanel, data.alertRule)} | ||||
|       {data && buildBreakdownString(data.folders, data.dashboards, data.library_elements, data.alertrules)} | ||||
|       {(isFetching || isLoading) && <Skeleton width={200} />} | ||||
|     </Text> | ||||
|   ); | ||||
|  |  | |||
|  | @ -1,18 +1,19 @@ | |||
| import { HttpResponse, http } from 'msw'; | ||||
| import { render, screen } from 'test/test-utils'; | ||||
| 
 | ||||
| import { setBackendSrv } from '@grafana/runtime'; | ||||
| 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 { backendSrv } from 'app/core/services/backend_srv'; | ||||
| 
 | ||||
| import { MoveModal, Props } from './MoveModal'; | ||||
| 
 | ||||
| const [_, { folderA }] = getFolderFixtures(); | ||||
| const [_, { folderA, folderB }] = getFolderFixtures(); | ||||
| 
 | ||||
| setBackendSrv(backendSrv); | ||||
| setupMockServer(); | ||||
| 
 | ||||
| const originalToggles = { ...config.featureToggles }; | ||||
| 
 | ||||
| describe('browse-dashboards MoveModal', () => { | ||||
|   const mockOnDismiss = jest.fn(); | ||||
|   const mockOnConfirm = jest.fn(); | ||||
|  | @ -20,17 +21,6 @@ describe('browse-dashboards MoveModal', () => { | |||
|   window.HTMLElement.prototype.scrollIntoView = () => {}; | ||||
| 
 | ||||
|   beforeEach(() => { | ||||
|     server.use( | ||||
|       http.get('/api/folders/:uid/counts', () => { | ||||
|         return HttpResponse.json({ | ||||
|           folder: 1, | ||||
|           dashboard: 2, | ||||
|           librarypanel: 3, | ||||
|           alertrule: 4, | ||||
|         }); | ||||
|       }) | ||||
|     ); | ||||
| 
 | ||||
|     props = { | ||||
|       isOpen: true, | ||||
|       onConfirm: mockOnConfirm, | ||||
|  | @ -68,10 +58,24 @@ describe('browse-dashboards MoveModal', () => { | |||
|     expect(await screen.findByRole('button', { name: 'Select folder' })).toBeInTheDocument(); | ||||
|   }); | ||||
| 
 | ||||
|   it('displays a warning about permissions if a folder is selected', async () => { | ||||
|   describe('when a folder is selected', () => { | ||||
|     describe.each([ | ||||
|       // app platform
 | ||||
|       true, | ||||
|       // legacy
 | ||||
|       false, | ||||
|     ])('with foldersAppPlatformAPI set to %s', (toggle) => { | ||||
|       beforeEach(() => { | ||||
|         props.selectedItems.folder = { | ||||
|       myFolderUid: true, | ||||
|           [folderA.item.uid]: true, | ||||
|         }; | ||||
|         config.featureToggles.foldersAppPlatformAPI = toggle; | ||||
|       }); | ||||
|       afterEach(() => { | ||||
|         config.featureToggles = originalToggles; | ||||
|       }); | ||||
| 
 | ||||
|       it('displays a warning about permissions if a folder is selected', async () => { | ||||
|         render(<MoveModal {...props} />); | ||||
| 
 | ||||
|         expect( | ||||
|  | @ -79,6 +83,30 @@ describe('browse-dashboards MoveModal', () => { | |||
|         ).toBeInTheDocument(); | ||||
|       }); | ||||
| 
 | ||||
|       it('displays summary of affected items', async () => { | ||||
|         render(<MoveModal {...props} />); | ||||
| 
 | ||||
|         expect(await screen.findByText(/This action will move the following content/i)).toBeInTheDocument(); | ||||
| 
 | ||||
|         expect(await screen.findByText(/5 item/)).toBeInTheDocument(); | ||||
|         expect(screen.getByText(/2 folder/)).toBeInTheDocument(); | ||||
|         expect(screen.getByText(/1 dashboard/)).toBeInTheDocument(); | ||||
|         expect(screen.getByText(/1 library panel/)).toBeInTheDocument(); | ||||
|         expect(screen.getByText(/1 alert rule/)).toBeInTheDocument(); | ||||
|       }); | ||||
| 
 | ||||
|       it('shows an error if one of the folder counts cannot be fetched', async () => { | ||||
|         props.selectedItems.folder = { | ||||
|           [folderA.item.uid]: true, | ||||
|           [folderB.item.uid]: true, | ||||
|         }; | ||||
|         render(<MoveModal {...props} />); | ||||
| 
 | ||||
|         expect(await screen.findByRole('alert', { name: /unable to retrieve/i })).toBeInTheDocument(); | ||||
|       }); | ||||
|     }); | ||||
|   }); | ||||
| 
 | ||||
|   it('enables the `Move` button once a folder is selected', async () => { | ||||
|     const { user } = render(<MoveModal {...props} />); | ||||
| 
 | ||||
|  |  | |||
|  | @ -55,7 +55,7 @@ export const MoveModal = ({ onConfirm, onDismiss, selectedItems, ...props }: Pro | |||
| 
 | ||||
|       <Space v={3} /> | ||||
| 
 | ||||
|       <Field label={t('browse-dashboards.action.move-modal-field-label', 'Folder name')}> | ||||
|       <Field noMargin label={t('browse-dashboards.action.move-modal-field-label', 'Folder name')}> | ||||
|         <ProvisioningAwareFolderPicker | ||||
|           value={moveTarget} | ||||
|           excludeUIDs={selectedFolders} | ||||
|  |  | |||
|  | @ -47,6 +47,11 @@ export interface FolderState { | |||
|   version: number; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * API response from `/api/folders/${folderUID}/counts` | ||||
|  * @deprecated The properties here are inconsistently named with App Platform API responses. | ||||
|  * Avoid using this type as it will be removed after app platform folder migration is complete | ||||
|  */ | ||||
| export interface DescendantCountDTO { | ||||
|   folder: number; | ||||
|   dashboard: number; | ||||
|  | @ -54,12 +59,9 @@ export interface DescendantCountDTO { | |||
|   alertrule: number; | ||||
| } | ||||
| 
 | ||||
| export interface DescendantCount { | ||||
|   folder: number; | ||||
|   dashboard: number; | ||||
|   libraryPanel: number; | ||||
|   alertRule: number; | ||||
| } | ||||
| type DescendantResource = 'folders' | 'dashboards' | 'library_elements' | 'alertrules'; | ||||
| /** Summary of descendant counts by resource type, with keys matching the App Platform API response */ | ||||
| export interface DescendantCount extends Record<DescendantResource, number> {} | ||||
| 
 | ||||
| export interface FolderInfo { | ||||
|   /** | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue