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 = {
 | 
					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()];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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: {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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,13 +246,12 @@ 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;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
| 
						 | 
					@ -260,6 +259,7 @@ describe('useCreateFolder', () => {
 | 
				
			||||||
    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
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue