diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 49f91d2bcef..6a6e96e97ae 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -57,6 +57,10 @@ interface RestoreDashboardArgs { dashboardUID: string; } +interface HardDeleteDashboardArgs { + dashboardUID: string; +} + function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { async function backendSrvBaseQuery(requestOptions: RequestOptions) { try { @@ -382,6 +386,19 @@ export const browseDashboardsAPI = createApi({ method: 'PATCH', }), }), + + // permanently delete a dashboard. used in PermanentlyDeleteModal. + hardDeleteDashboard: builder.mutation({ + query: ({ dashboardUID }) => ({ + url: `/dashboards/uid/${dashboardUID}/trash`, + method: 'DELETE', + }), + onQueryStarted: ({ dashboardUID }, { queryFulfilled, dispatch }) => { + queryFulfilled.then(() => { + dispatch(refreshParents([dashboardUID])); + }); + }, + }), }), }); @@ -397,6 +414,7 @@ export const { useSaveDashboardMutation, useSaveFolderMutation, useRestoreDashboardMutation, + useHardDeleteDashboardMutation, } = browseDashboardsAPI; export { skipToken } from '@reduxjs/toolkit/query/react'; diff --git a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx index a45d6446934..937da5d0924 100644 --- a/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx +++ b/public/app/features/browse-dashboards/components/BrowseActions/DeleteModal.tsx @@ -35,6 +35,18 @@ export const DeleteModal = ({ onConfirm, onDismiss, selectedItems, ...props }: P + {config.featureToggles.dashboardRestore && ( + <> + + + This action will delete the selected folders immediately but the selected dashboards will be marked + for deletion in 30 days. You can restore the dashboards anytime before the 30 days expires. Folders + cannot be restored. + + + + + )} This action will delete the following content: diff --git a/public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx b/public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx new file mode 100644 index 00000000000..0c0b89cb708 --- /dev/null +++ b/public/app/features/browse-dashboards/components/PermanentlyDeleteModal.tsx @@ -0,0 +1,44 @@ +import React from 'react'; + +import { ConfirmModal, Text } from '@grafana/ui'; + +import { Trans, t } from '../../../core/internationalization'; + +import { Props as ModalProps } from './RestoreModal'; + +export const PermanentlyDeleteModal = ({ + onConfirm, + onDismiss, + selectedDashboards, + isLoading, + ...props +}: ModalProps) => { + const numberOfDashboards = selectedDashboards.length; + + const onDelete = async () => { + await onConfirm(); + onDismiss(); + }; + return ( + + + This action will delete {{ numberOfDashboards }} dashboards. + + + } + title={t('recently-deleted.permanently-delete-modal.title', 'Permanently Delete Dashboards')} + confirmationText={t('recently-deleted.permanently-delete-modal.confirm-text', 'Delete')} + confirmText={ + isLoading + ? t('recently-deleted.permanently-delete-modal.delete-loading', 'Deleting...') + : t('recently-deleted.permanently-delete-modal.delete-button', 'Delete') + } + confirmButtonVariant="destructive" + onConfirm={onDelete} + onDismiss={onDismiss} + {...props} + /> + ); +}; diff --git a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx index c1ceaa07289..b2f23879ab6 100644 --- a/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx +++ b/public/app/features/browse-dashboards/components/RecentlyDeletedActions.tsx @@ -9,10 +9,12 @@ import appEvents from '../../../core/app_events'; import { Trans } from '../../../core/internationalization'; import { useDispatch } from '../../../types'; import { ShowModalReactEvent } from '../../../types/events'; -import { useRestoreDashboardMutation } from '../../browse-dashboards/api/browseDashboardsAPI'; -import { clearFolders, setAllSelection, useActionSelectionState } from '../../browse-dashboards/state'; +import { useHardDeleteDashboardMutation, useRestoreDashboardMutation } from '../api/browseDashboardsAPI'; import { useRecentlyDeletedStateManager } from '../api/useRecentlyDeletedStateManager'; -import { RestoreModal } from '../components/RestoreModal'; +import { clearFolders, setAllSelection, useActionSelectionState } from '../state'; + +import { PermanentlyDeleteModal } from './PermanentlyDeleteModal'; +import { RestoreModal } from './RestoreModal'; export function RecentlyDeletedActions() { const styles = useStyles2(getStyles); @@ -22,6 +24,7 @@ export function RecentlyDeletedActions() { const [, stateManager] = useRecentlyDeletedStateManager(); const [restoreDashboard, { isLoading: isRestoreLoading }] = useRestoreDashboardMutation(); + const [deleteDashboard, { isLoading: isDeleteLoading }] = useHardDeleteDashboardMutation(); const selectedDashboards = useMemo(() => { return Object.entries(selectedItemsState.dashboard) @@ -64,6 +67,13 @@ export function RecentlyDeletedActions() { onActionComplete(); }; + const onDelete = async () => { + const promises = selectedDashboards.map((uid) => deleteDashboard({ dashboardUID: uid })); + + await Promise.all(promises); + onActionComplete(); + }; + const showRestoreModal = () => { appEvents.publish( new ShowModalReactEvent({ @@ -77,17 +87,36 @@ export function RecentlyDeletedActions() { ); }; + const showDeleteModal = () => { + appEvents.publish( + new ShowModalReactEvent({ + component: PermanentlyDeleteModal, + props: { + selectedDashboards, + onConfirm: onDelete, + isLoading: isDeleteLoading, + }, + }) + ); + }; + return (
+
); } const getStyles = (theme: GrafanaTheme2) => ({ row: css({ + display: 'flex', + flexDirection: 'row', + gap: theme.spacing(1), marginBottom: theme.spacing(2), }), }); diff --git a/public/app/features/browse-dashboards/components/RestoreModal.tsx b/public/app/features/browse-dashboards/components/RestoreModal.tsx index 520825861fb..9b16765b963 100644 --- a/public/app/features/browse-dashboards/components/RestoreModal.tsx +++ b/public/app/features/browse-dashboards/components/RestoreModal.tsx @@ -4,7 +4,7 @@ import { ConfirmModal, Text } from '@grafana/ui'; import { Trans, t } from '../../../core/internationalization'; -interface Props { +export interface Props { isOpen: boolean; onConfirm: () => Promise; onDismiss: () => void; diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 0f00b256579..2f81df6b661 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -78,6 +78,7 @@ "delete-button": "Delete", "delete-modal-invalid-text": "One or more folders contain library panels or alert rules. Delete these first in order to proceed.", "delete-modal-invalid-title": "Cannot delete folder", + "delete-modal-restore-dashboards-text": "This action will delete the selected folders immediately but the selected dashboards will be marked for deletion in 30 days. You can restore the dashboards anytime before the 30 days expires. Folders cannot be restored.", "delete-modal-text": "This action will delete the following content:", "delete-modal-title": "Delete", "deleting": "Deleting...", @@ -1574,8 +1575,17 @@ }, "recently-deleted": { "buttons": { + "delete": "Delete permanently", "restore": "Restore" }, + "permanently-delete-modal": { + "confirm-text": "Delete", + "delete-button": "Delete", + "delete-loading": "Deleting...", + "text_one": "This action will delete {{numberOfDashboards}} dashboards.", + "text_other": "This action will delete {{numberOfDashboards}} dashboards.", + "title": "Permanently Delete Dashboards" + }, "restore-modal": { "restore-button": "Restore", "restore-loading": "Restoring...", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 065ea9ab9b5..288b9f25abc 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -78,6 +78,7 @@ "delete-button": "Đęľęŧę", "delete-modal-invalid-text": "Øʼnę őř mőřę ƒőľđęřş čőʼnŧäįʼn ľįþřäřy päʼnęľş őř äľęřŧ řūľęş. Đęľęŧę ŧĥęşę ƒįřşŧ įʼn őřđęř ŧő přőčęęđ.", "delete-modal-invalid-title": "Cäʼnʼnőŧ đęľęŧę ƒőľđęř", + "delete-modal-restore-dashboards-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę şęľęčŧęđ ƒőľđęřş įmmęđįäŧęľy þūŧ ŧĥę şęľęčŧęđ đäşĥþőäřđş ŵįľľ þę mäřĸęđ ƒőř đęľęŧįőʼn įʼn 30 đäyş. Ÿőū čäʼn řęşŧőřę ŧĥę đäşĥþőäřđş äʼnyŧįmę þęƒőřę ŧĥę 30 đäyş ęχpįřęş. Főľđęřş čäʼnʼnőŧ þę řęşŧőřęđ.", "delete-modal-text": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę ŧĥę ƒőľľőŵįʼnģ čőʼnŧęʼnŧ:", "delete-modal-title": "Đęľęŧę", "deleting": "Đęľęŧįʼnģ...", @@ -1574,8 +1575,17 @@ }, "recently-deleted": { "buttons": { + "delete": "Đęľęŧę pęřmäʼnęʼnŧľy", "restore": "Ŗęşŧőřę" }, + "permanently-delete-modal": { + "confirm-text": "Đęľęŧę", + "delete-button": "Đęľęŧę", + "delete-loading": "Đęľęŧįʼnģ...", + "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđş.", + "text_other": "Ŧĥįş äčŧįőʼn ŵįľľ đęľęŧę {{numberOfDashboards}} đäşĥþőäřđş.", + "title": "Pęřmäʼnęʼnŧľy Đęľęŧę Đäşĥþőäřđş" + }, "restore-modal": { "restore-button": "Ŗęşŧőřę", "restore-loading": "Ŗęşŧőřįʼnģ...",