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,43 +1,91 @@
|
|||
import { generatedAPI } from './endpoints.gen';
|
||||
import { DescendantCount } from 'app/types/folders';
|
||||
|
||||
export const folderAPIv1beta1 = generatedAPI.enhanceEndpoints({
|
||||
endpoints: {
|
||||
getFolder: {
|
||||
providesTags: (result, error, arg) => (result ? [{ type: 'Folder', id: arg.name }] : []),
|
||||
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 }] : []),
|
||||
},
|
||||
listFolder: {
|
||||
providesTags: (result) =>
|
||||
result
|
||||
? [
|
||||
{ type: 'Folder', id: 'LIST' },
|
||||
...result.items
|
||||
.map((folder) => ({ type: 'Folder' as const, id: folder.metadata?.name }))
|
||||
.filter(Boolean),
|
||||
]
|
||||
: [{ type: 'Folder', id: 'LIST' }],
|
||||
},
|
||||
deleteFolder: {
|
||||
// We don't want delete to invalidate getFolder tags, as that would lead to unnecessary 404s
|
||||
invalidatesTags: (result, error) => (error ? [] : [{ type: 'Folder', id: 'LIST' }]),
|
||||
},
|
||||
updateFolder: {
|
||||
query: (queryArg) => ({
|
||||
url: `/folders/${queryArg.name}`,
|
||||
method: 'PATCH',
|
||||
// We need to stringify the body and set the correct header for the call to work with k8s api.
|
||||
body: JSON.stringify(queryArg.patch),
|
||||
headers: {
|
||||
'Content-Type': 'application/strategic-merge-patch+json',
|
||||
},
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
listFolder: {
|
||||
providesTags: (result) =>
|
||||
result
|
||||
? [
|
||||
{ type: 'Folder', id: 'LIST' },
|
||||
...result.items.map((folder) => ({ type: 'Folder' as const, id: folder.metadata?.name })).filter(Boolean),
|
||||
]
|
||||
: [{ type: 'Folder', id: 'LIST' }],
|
||||
},
|
||||
deleteFolder: {
|
||||
// We don't want delete to invalidate getFolder tags, as that would lead to unnecessary 404s
|
||||
invalidatesTags: (result, error) => (error ? [] : [{ type: 'Folder', id: 'LIST' }]),
|
||||
},
|
||||
updateFolder: {
|
||||
query: (queryArg) => ({
|
||||
url: `/folders/${queryArg.name}`,
|
||||
method: 'PATCH',
|
||||
// We need to stringify the body and set the correct header for the call to work with k8s api.
|
||||
body: JSON.stringify(queryArg.patch),
|
||||
headers: {
|
||||
'Content-Type': 'application/strategic-merge-patch+json',
|
||||
},
|
||||
params: {
|
||||
pretty: queryArg.pretty,
|
||||
dryRun: queryArg.dryRun,
|
||||
fieldManager: queryArg.fieldManager,
|
||||
fieldValidation: queryArg.fieldValidation,
|
||||
force: queryArg.force,
|
||||
})
|
||||
.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 promises = folderUIDs.map((folderUID) => {
|
||||
return getBackendSrv().get<DescendantCountDTO>(`/api/folders/${folderUID}/counts`);
|
||||
});
|
||||
const totalCounts: DescendantCount = {
|
||||
folders: folderUIDs.length,
|
||||
dashboards: dashboardUIDs.length,
|
||||
library_elements: 0,
|
||||
alertrules: 0,
|
||||
};
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
for (const folderCounts of results) {
|
||||
totalCounts.folders += folderCounts.folder;
|
||||
totalCounts.dashboards += folderCounts.dashboard;
|
||||
totalCounts.alertrules += folderCounts.alertrule;
|
||||
totalCounts.library_elements += folderCounts.librarypanel;
|
||||
}
|
||||
|
||||
const totalCounts = {
|
||||
folder: Object.values(selectedItems.folder).filter(isTruthy).length,
|
||||
dashboard: Object.values(selectedItems.dashboard).filter(isTruthy).length,
|
||||
libraryPanel: 0,
|
||||
alertRule: 0,
|
||||
};
|
||||
|
||||
for (const folderCounts of results) {
|
||||
totalCounts.folder += folderCounts.folder;
|
||||
totalCounts.dashboard += folderCounts.dashboard;
|
||||
totalCounts.alertRule += folderCounts.alertrule;
|
||||
totalCounts.libraryPanel += folderCounts.librarypanel;
|
||||
return { data: totalCounts };
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
return { data: totalCounts };
|
||||
},
|
||||
}),
|
||||
|
||||
|
|
|
|||
|
|
@ -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,15 +58,53 @@ 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 () => {
|
||||
props.selectedItems.folder = {
|
||||
myFolderUid: true,
|
||||
};
|
||||
render(<MoveModal {...props} />);
|
||||
describe('when a folder is selected', () => {
|
||||
describe.each([
|
||||
// app platform
|
||||
true,
|
||||
// legacy
|
||||
false,
|
||||
])('with foldersAppPlatformAPI set to %s', (toggle) => {
|
||||
beforeEach(() => {
|
||||
props.selectedItems.folder = {
|
||||
[folderA.item.uid]: true,
|
||||
};
|
||||
config.featureToggles.foldersAppPlatformAPI = toggle;
|
||||
});
|
||||
afterEach(() => {
|
||||
config.featureToggles = originalToggles;
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('status', { name: 'Moving this item may change its permissions.' })
|
||||
).toBeInTheDocument();
|
||||
it('displays a warning about permissions if a folder is selected', async () => {
|
||||
render(<MoveModal {...props} />);
|
||||
|
||||
expect(
|
||||
await screen.findByRole('status', { name: 'Moving this item may change its permissions.' })
|
||||
).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 () => {
|
||||
|
|
|
|||
|
|
@ -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