mirror of
https://github.com/grafana/grafana.git
synced 2026-02-03 20:49:50 -05:00
Folders: Migrate bulk delete actions (#109525)
* Split delete items call and use new API * add test * Add translation * Remove unnecessary array spread * move check provisioned into util function * wip conflict fix * Undo file rename * Remove unneeded handler for now * Fix delete invalidating getFolder calls * Don't call hooks conditionally --------- Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
parent
1e0587001d
commit
dcea3315fa
10 changed files with 221 additions and 53 deletions
|
|
@ -1,12 +1,34 @@
|
|||
import { renderHook, getWrapper, waitFor } from 'test/test-utils';
|
||||
|
||||
import { AppEvents } from '@grafana/data';
|
||||
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 { useDeleteFoldersMutation as useDeleteFoldersMutationLegacy } from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
|
||||
import { useGetFolderQueryFacade } from './hooks';
|
||||
import { useGetFolderQueryFacade, useDeleteMultipleFoldersMutationFacade } from './hooks';
|
||||
|
||||
import { useDeleteFolderMutation } from './index';
|
||||
|
||||
// Mocks for the hooks used inside useGetFolderQueryFacade
|
||||
jest.mock('./index', () => ({
|
||||
...jest.requireActual('./index'),
|
||||
useDeleteFolderMutation: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getAppEvents: jest.fn(() => ({
|
||||
publish: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
const mockGetAppEvents = jest.mocked(require('@grafana/runtime').getAppEvents);
|
||||
|
||||
jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({
|
||||
...jest.requireActual('app/features/browse-dashboards/api/browseDashboardsAPI'),
|
||||
useDeleteFoldersMutation: jest.fn(),
|
||||
}));
|
||||
setBackendSrv(backendSrv);
|
||||
setupMockServer();
|
||||
|
||||
|
|
@ -105,3 +127,61 @@ describe('useGetFolderQueryFacade', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('useDeleteMultipleFoldersMutationFacade', () => {
|
||||
const dispatchMock = jest.fn();
|
||||
const mockDeleteFolder = jest.fn(() => ({ error: undefined }));
|
||||
const mockDeleteFolderLegacy = jest.fn(() => ({ error: undefined }));
|
||||
const publishMock = jest.fn();
|
||||
|
||||
const oldToggleValue = config.featureToggles.foldersAppPlatformAPI;
|
||||
|
||||
afterAll(() => {
|
||||
config.featureToggles.foldersAppPlatformAPI = oldToggleValue;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockDeleteFolder.mockClear();
|
||||
mockDeleteFolderLegacy.mockClear();
|
||||
(useDeleteFolderMutation as jest.Mock).mockReturnValue([mockDeleteFolder]);
|
||||
(useDeleteFoldersMutationLegacy as jest.Mock).mockReturnValue([mockDeleteFolderLegacy]);
|
||||
|
||||
// Mock useDispatch
|
||||
jest.spyOn(require('../../../../types/store'), 'useDispatch').mockReturnValue(dispatchMock);
|
||||
});
|
||||
|
||||
it('deletes multiple folders and publishes success alert', async () => {
|
||||
mockGetAppEvents.mockReturnValue({
|
||||
publish: publishMock,
|
||||
});
|
||||
config.featureToggles.foldersAppPlatformAPI = true;
|
||||
const folderUIDs = ['uid1', 'uid2'];
|
||||
const deleteFolders = useDeleteMultipleFoldersMutationFacade();
|
||||
await deleteFolders({ folderUIDs });
|
||||
|
||||
// Should call deleteFolder for each UID
|
||||
expect(mockDeleteFolder).toHaveBeenCalledTimes(folderUIDs.length);
|
||||
expect(mockDeleteFolder).toHaveBeenCalledWith({ name: 'uid1' });
|
||||
expect(mockDeleteFolder).toHaveBeenCalledWith({ name: 'uid2' });
|
||||
|
||||
// Should publish success alert
|
||||
expect(publishMock).toHaveBeenCalledWith({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: ['Folder deleted'],
|
||||
});
|
||||
|
||||
// Should dispatch refreshParents
|
||||
expect(dispatchMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses legacy call when flag is false', async () => {
|
||||
config.featureToggles.foldersAppPlatformAPI = false;
|
||||
const folderUIDs = ['uid1', 'uid2'];
|
||||
const deleteFolders = useDeleteMultipleFoldersMutationFacade();
|
||||
await deleteFolders({ folderUIDs });
|
||||
|
||||
// Should call deleteFolder for each UID
|
||||
expect(mockDeleteFolderLegacy).toHaveBeenCalledTimes(1);
|
||||
expect(mockDeleteFolderLegacy).toHaveBeenCalledWith({ folderUIDs });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { config, getAppEvents } from '@grafana/runtime';
|
|||
import {
|
||||
useDeleteFolderMutation as useDeleteFolderMutationLegacy,
|
||||
useGetFolderQuery as useGetFolderQueryLegacy,
|
||||
useDeleteFoldersMutation as useDeleteFoldersMutationLegacy,
|
||||
} from 'app/features/browse-dashboards/api/browseDashboardsAPI';
|
||||
import { FolderDTO } from 'app/types/folders';
|
||||
|
||||
|
|
@ -20,11 +21,12 @@ import {
|
|||
ManagerKind,
|
||||
} from '../../../../features/apiserver/types';
|
||||
import { PAGE_SIZE } from '../../../../features/browse-dashboards/api/services';
|
||||
import { refetchChildren } from '../../../../features/browse-dashboards/state/actions';
|
||||
import { refetchChildren, refreshParents } from '../../../../features/browse-dashboards/state/actions';
|
||||
import { GENERAL_FOLDER_UID } from '../../../../features/search/constants';
|
||||
import { useDispatch } from '../../../../types/store';
|
||||
import { useGetDisplayMappingQuery } from '../../iam/v0alpha1';
|
||||
|
||||
import { isProvisionedFolderCheck } from './utils';
|
||||
import { rootFolder, sharedWithMeFolder } from './virtualFolders';
|
||||
|
||||
import { useGetFolderQuery, useGetFolderParentsQuery, useDeleteFolderMutation } from './index';
|
||||
|
|
@ -190,6 +192,38 @@ export function useDeleteFolderMutationFacade() {
|
|||
};
|
||||
}
|
||||
|
||||
export function useDeleteMultipleFoldersMutationFacade() {
|
||||
const [deleteFolders] = useDeleteFoldersMutationLegacy();
|
||||
const [deleteFolder] = useDeleteFolderMutation();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
if (!config.featureToggles.foldersAppPlatformAPI) {
|
||||
return deleteFolders;
|
||||
}
|
||||
|
||||
return async function deleteFolders({ folderUIDs }: { folderUIDs: string[] }) {
|
||||
// Delete all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of folderUIDs) {
|
||||
// This also shows warning alert
|
||||
if (await isProvisionedFolderCheck(dispatch, folderUID)) {
|
||||
continue;
|
||||
}
|
||||
const result = await deleteFolder({ name: folderUID });
|
||||
if (!result.error) {
|
||||
// Before this was done in backend srv automatically because the old API sent a message wiht 200 request. see
|
||||
// public/app/core/services/backend_srv.ts#L341-L361. New API does not do that so we do it here.
|
||||
getAppEvents().publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [t('folders.api.folder-deleted-success', 'Folder deleted')],
|
||||
});
|
||||
dispatch(refreshParents(folderUIDs));
|
||||
}
|
||||
}
|
||||
return { data: undefined };
|
||||
};
|
||||
}
|
||||
|
||||
function combinedState(
|
||||
result: ReturnType<typeof useGetFolderQuery>,
|
||||
resultParents: ReturnType<typeof useGetFolderParentsQuery>,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,25 @@
|
|||
import { generatedAPI } from './endpoints.gen';
|
||||
|
||||
export const folderAPIv1beta1 = generatedAPI.enhanceEndpoints({});
|
||||
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' }]),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { useGetFolderQuery, useGetFolderParentsQuery, useDeleteFolderMutation } = folderAPIv1beta1;
|
||||
|
||||
|
|
|
|||
31
public/app/api/clients/folder/v1beta1/utils.ts
Normal file
31
public/app/api/clients/folder/v1beta1/utils.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { AppEvents } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import appEvents from '../../../../core/app_events';
|
||||
import { isProvisionedFolder } from '../../../../features/browse-dashboards/api/isProvisioned';
|
||||
import { useDispatch } from '../../../../types/store';
|
||||
|
||||
import { folderAPIv1beta1 as folderAPI } from './index';
|
||||
|
||||
export async function isProvisionedFolderCheck(dispatch: ReturnType<typeof useDispatch>, folderUID: string) {
|
||||
if (config.featureToggles.provisioning) {
|
||||
const folder = await dispatch(folderAPI.endpoints.getFolder.initiate({ name: folderUID }));
|
||||
// TODO: taken from browseDashboardAPI as it is, but this error handling should be moved up to UI code.
|
||||
if (folder.data && isProvisionedFolder(folder.data)) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertWarning.name,
|
||||
payload: [
|
||||
t(
|
||||
'folders.api.folder-delete-error-provisioned',
|
||||
'Cannot delete provisioned folder. To remove it, delete it from the repository and synchronise to apply the changes.'
|
||||
),
|
||||
],
|
||||
});
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import { config, getBackendSrv, isFetchError, locationService } from '@grafana/r
|
|||
import { Dashboard } from '@grafana/schema';
|
||||
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { folderAPIv1beta1 as folderAPI } from 'app/api/clients/folder/v1beta1';
|
||||
import { isProvisionedFolderCheck } from 'app/api/clients/folder/v1beta1/utils';
|
||||
import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
|
|
@ -26,12 +27,17 @@ import { DashboardTreeSelection } from '../types';
|
|||
import { isProvisionedDashboard, isProvisionedFolder } from './isProvisioned';
|
||||
import { PAGE_SIZE } from './services';
|
||||
|
||||
interface DeleteItemsArgs {
|
||||
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>;
|
||||
interface DeleteFoldersArgs {
|
||||
folderUIDs: string[];
|
||||
}
|
||||
|
||||
interface MoveItemsArgs extends DeleteItemsArgs {
|
||||
interface DeleteDashboardsArgs {
|
||||
dashboardUIDs: string[];
|
||||
}
|
||||
|
||||
interface MoveItemsArgs {
|
||||
destinationUID: string;
|
||||
selectedItems: Omit<DashboardTreeSelection, 'panel' | '$all'>;
|
||||
}
|
||||
|
||||
export interface ImportInputs {
|
||||
|
|
@ -281,27 +287,16 @@ export const browseDashboardsAPI = createApi({
|
|||
},
|
||||
}),
|
||||
|
||||
// delete *multiple* items (folders and dashboards). used in the delete modal.
|
||||
deleteItems: builder.mutation<void, DeleteItemsArgs>({
|
||||
// delete *multiple* folders. used in the delete modal.
|
||||
deleteFolders: builder.mutation<void, DeleteFoldersArgs>({
|
||||
invalidatesTags: ['getFolder'],
|
||||
queryFn: async ({ selectedItems }, _api, _extraOptions, baseQuery) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
const pageStateManager = getDashboardScenePageStateManager();
|
||||
queryFn: async ({ folderUIDs }, _api, _extraOptions, baseQuery) => {
|
||||
// Delete all the folders sequentially
|
||||
// TODO error handling here
|
||||
for (const folderUID of selectedFolders) {
|
||||
if (config.featureToggles.provisioning) {
|
||||
const folder = await dispatch(folderAPI.endpoints.getFolder.initiate({ name: folderUID }));
|
||||
if (isProvisionedFolder(folder.data)) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertWarning.name,
|
||||
payload: [
|
||||
'Cannot delete provisioned folder. To remove it, delete it from the repository and synchronise to apply the changes.',
|
||||
],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
for (const folderUID of folderUIDs) {
|
||||
// This also shows warning alert
|
||||
if (await isProvisionedFolderCheck(dispatch, folderUID)) {
|
||||
continue;
|
||||
}
|
||||
await baseQuery({
|
||||
url: `/folders/${folderUID}`,
|
||||
|
|
@ -313,9 +308,23 @@ export const browseDashboardsAPI = createApi({
|
|||
},
|
||||
});
|
||||
}
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ folderUIDs }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(refreshParents(folderUIDs));
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
||||
// delete *multiple* dashboards. used in the delete modal.
|
||||
deleteDashboards: builder.mutation<void, DeleteDashboardsArgs>({
|
||||
invalidatesTags: ['getFolder'],
|
||||
queryFn: async ({ dashboardUIDs }, _api, _extraOptions, baseQuery) => {
|
||||
const pageStateManager = getDashboardScenePageStateManager();
|
||||
// Delete all the dashboards sequentially
|
||||
// TODO error handling here
|
||||
for (const dashboardUID of selectedDashboards) {
|
||||
for (const dashboardUID of dashboardUIDs) {
|
||||
if (config.featureToggles.provisioning) {
|
||||
const dto = await getDashboardAPI().getDashboardDTO(dashboardUID);
|
||||
if (isProvisionedDashboard(dto)) {
|
||||
|
|
@ -346,11 +355,9 @@ export const browseDashboardsAPI = createApi({
|
|||
}
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ selectedItems }, { queryFulfilled, dispatch }) => {
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
onQueryStarted: ({ dashboardUIDs }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(refreshParents([...selectedFolders, ...selectedDashboards]));
|
||||
dispatch(refreshParents(dashboardUIDs));
|
||||
});
|
||||
},
|
||||
}),
|
||||
|
|
@ -487,7 +494,8 @@ export const browseDashboardsAPI = createApi({
|
|||
export const {
|
||||
endpoints,
|
||||
useDeleteFolderMutation,
|
||||
useDeleteItemsMutation,
|
||||
useDeleteFoldersMutation,
|
||||
useDeleteDashboardsMutation,
|
||||
useGetAffectedItemsQuery,
|
||||
useGetFolderQuery,
|
||||
useLazyGetFolderQuery,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,8 @@ import { ShowModalReactEvent } from 'app/types/events';
|
|||
import { FolderDTO } from 'app/types/folders';
|
||||
import { useDispatch } from 'app/types/store';
|
||||
|
||||
import { useDeleteItemsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI';
|
||||
import { useDeleteMultipleFoldersMutationFacade } from '../../../../api/clients/folder/v1beta1/hooks';
|
||||
import { useDeleteDashboardsMutation, useMoveItemsMutation } from '../../api/browseDashboardsAPI';
|
||||
import { useActionSelectionState } from '../../state/hooks';
|
||||
import { setAllSelection } from '../../state/slice';
|
||||
import { DashboardTreeSelection } from '../../types';
|
||||
|
|
@ -32,7 +33,8 @@ export function BrowseActions({ folderDTO }: Props) {
|
|||
|
||||
const dispatch = useDispatch();
|
||||
const selectedItems = useActionSelectionState();
|
||||
const [deleteItems] = useDeleteItemsMutation();
|
||||
const [deleteDashboards] = useDeleteDashboardsMutation();
|
||||
const deleteFolders = useDeleteMultipleFoldersMutationFacade();
|
||||
const [moveItems] = useMoveItemsMutation();
|
||||
const [, stateManager] = useSearchStateManager();
|
||||
const provisioningEnabled = config.featureToggles.provisioning;
|
||||
|
|
@ -54,7 +56,10 @@ export function BrowseActions({ folderDTO }: Props) {
|
|||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
await deleteItems({ selectedItems });
|
||||
const selectedDashboards = Object.keys(selectedItems.dashboard).filter((uid) => selectedItems.dashboard[uid]);
|
||||
const selectedFolders = Object.keys(selectedItems.folder).filter((uid) => selectedItems.folder[uid]);
|
||||
await deleteDashboards({ dashboardUIDs: selectedDashboards });
|
||||
await deleteFolders({ folderUIDs: selectedFolders });
|
||||
trackAction('delete', selectedItems);
|
||||
onActionComplete();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@ import { CellProps, Column, HeaderProps } from 'react-table';
|
|||
|
||||
import { DashboardViewItem, DashboardViewItemKind } from 'app/features/search/types';
|
||||
|
||||
/**
|
||||
* Object of what is selected in the tree. It is record where keys are categories from DashboardViewItemKind and
|
||||
* each category is a record where the key is the UID of the object and value is whether it is selected or not.
|
||||
*/
|
||||
export type DashboardTreeSelection = Record<DashboardViewItemKind, Record<string, boolean | undefined>> & {
|
||||
$all: boolean;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { config, reportInteraction } from '@grafana/runtime';
|
|||
import { Button, ConfirmModal, Modal, Space, Text, TextLink } from '@grafana/ui';
|
||||
import { DeleteProvisionedDashboardDrawer } from 'app/features/provisioning/components/Dashboards/DeleteProvisionedDashboardDrawer';
|
||||
|
||||
import { useDeleteItemsMutation } from '../../browse-dashboards/api/browseDashboardsAPI';
|
||||
import { useDeleteDashboardsMutation } from '../../browse-dashboards/api/browseDashboardsAPI';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
interface ButtonProps {
|
||||
|
|
@ -26,7 +26,7 @@ interface DeleteModalProps {
|
|||
|
||||
export function DeleteDashboardButton({ dashboard }: ButtonProps) {
|
||||
const [showModal, toggleModal] = useToggle(false);
|
||||
const [deleteItems] = useDeleteItemsMutation();
|
||||
const [deleteDashboards] = useDeleteDashboardsMutation();
|
||||
|
||||
const [, onConfirm] = useAsyncFn(async () => {
|
||||
reportInteraction('grafana_manage_dashboards_delete_clicked', {
|
||||
|
|
@ -38,14 +38,7 @@ export function DeleteDashboardButton({ dashboard }: ButtonProps) {
|
|||
});
|
||||
toggleModal();
|
||||
if (dashboard.state.uid) {
|
||||
await deleteItems({
|
||||
selectedItems: {
|
||||
dashboard: {
|
||||
[dashboard.state.uid]: true,
|
||||
},
|
||||
folder: {},
|
||||
},
|
||||
});
|
||||
await deleteDashboards({ dashboardUIDs: [dashboard.state.uid] });
|
||||
}
|
||||
await dashboard.onDashboardDelete();
|
||||
}, [dashboard, toggleModal]);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { Modal, Button, Text, Space, TextLink } from '@grafana/ui';
|
|||
import { DashboardModel } from 'app/features/dashboard/state/DashboardModel';
|
||||
import { cleanUpDashboardAndVariables } from 'app/features/dashboard/state/actions';
|
||||
|
||||
import { useDeleteItemsMutation } from '../../../browse-dashboards/api/browseDashboardsAPI';
|
||||
import { useDeleteDashboardsMutation } from '../../../browse-dashboards/api/browseDashboardsAPI';
|
||||
import { DeleteDashboardModal as DeleteModal } from '../../../dashboard-scene/settings/DeleteDashboardButton';
|
||||
|
||||
type DeleteDashboardModalProps = {
|
||||
|
|
@ -26,7 +26,7 @@ type Props = DeleteDashboardModalProps & ConnectedProps<typeof connector>;
|
|||
|
||||
const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariables, dashboard }: Props) => {
|
||||
const isProvisioned = dashboard.meta.provisioned;
|
||||
const [deleteItems] = useDeleteItemsMutation();
|
||||
const [deleteDashboards] = useDeleteDashboardsMutation();
|
||||
|
||||
const [, onConfirm] = useAsyncFn(async () => {
|
||||
reportInteraction('grafana_manage_dashboards_delete_clicked', {
|
||||
|
|
@ -36,14 +36,7 @@ const DeleteDashboardModalUnconnected = ({ hideModal, cleanUpDashboardAndVariabl
|
|||
source: 'dashboard_settings',
|
||||
restore_enabled: Boolean(config.featureToggles.restoreDashboards),
|
||||
});
|
||||
await deleteItems({
|
||||
selectedItems: {
|
||||
dashboard: {
|
||||
[dashboard.uid]: true,
|
||||
},
|
||||
folder: {},
|
||||
},
|
||||
});
|
||||
await deleteDashboards({ dashboardUIDs: [dashboard.uid] });
|
||||
cleanUpDashboardAndVariables();
|
||||
hideModal();
|
||||
locationService.replace('/');
|
||||
|
|
|
|||
|
|
@ -7559,6 +7559,7 @@
|
|||
},
|
||||
"folders": {
|
||||
"api": {
|
||||
"folder-delete-error-provisioned": "Cannot delete provisioned folder. To remove it, delete it from the repository and synchronise to apply the changes.",
|
||||
"folder-deleted-success": "Folder deleted"
|
||||
},
|
||||
"get-loading-nav": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue