From ca2df58ab0de2252adad3eb8678cb776a5cfa5dd Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Thu, 13 Jun 2024 09:12:10 +0200 Subject: [PATCH] RestoreDashboards: Implement restore workflow (#88753) * feat: create interactable elements * feat: add i18n * feat: add restoment process * refactor: move restore endpoint * refactor: adjust some API things * refactor: adjust i18n * Run i18n abstraction * refactor: clean up * refactor: update comment * refactor: update text in modal * refactor: correct translation keys * refactor: add changes from code review * refactor: add styling * Update go.work.sum --- .../api/browseDashboardsAPI.ts | 25 +++++++ .../manage-dashboards/RecentlyDeletedPage.tsx | 2 + .../components/RestoreModal.tsx | 44 +++++++++++++ .../state/RecentlyDeletedActions.tsx | 65 +++++++++++++++++++ public/locales/en-US/grafana.json | 12 ++++ public/locales/pseudo-LOCALE/grafana.json | 12 ++++ 6 files changed, 160 insertions(+) create mode 100644 public/app/features/manage-dashboards/components/RestoreModal.tsx create mode 100644 public/app/features/manage-dashboards/state/RecentlyDeletedActions.tsx diff --git a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts index 422eab15d93..9be53c2b10b 100644 --- a/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts +++ b/public/app/features/browse-dashboards/api/browseDashboardsAPI.ts @@ -53,6 +53,10 @@ interface ImportOptions { folderUid: string; } +interface RestoreDashboardArgs { + dashboardUID: string; +} + function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn { async function backendSrvBaseQuery(requestOptions: RequestOptions) { try { @@ -148,6 +152,7 @@ export const browseDashboardsAPI = createApi({ }); }, }), + // move an *individual* folder. used in the folder actions menu. moveFolder: builder.mutation({ invalidatesTags: ['getFolder'], @@ -174,6 +179,7 @@ export const browseDashboardsAPI = createApi({ }); }, }), + // delete an *individual* folder. used in the folder actions menu. deleteFolder: builder.mutation({ query: ({ uid }) => ({ @@ -196,6 +202,7 @@ export const browseDashboardsAPI = createApi({ }); }, }), + // gets the descendant counts for a folder. used in the move/delete modals. getAffectedItems: builder.query({ queryFn: async (selectedItems) => { @@ -225,6 +232,7 @@ export const browseDashboardsAPI = createApi({ return { data: totalCounts }; }, }), + // move *multiple* items (folders and dashboards). used in the move modal. moveItems: builder.mutation({ invalidatesTags: ['getFolder'], @@ -276,6 +284,7 @@ export const browseDashboardsAPI = createApi({ }); }, }), + // delete *multiple* items (folders and dashboards). used in the delete modal. deleteItems: builder.mutation({ queryFn: async ({ selectedItems }, _api, _extraOptions, baseQuery) => { @@ -313,6 +322,7 @@ export const browseDashboardsAPI = createApi({ }); }, }), + // save an existing dashboard saveDashboard: builder.mutation({ query: ({ dashboard, folderUid, message, overwrite, showErrorAlert }) => ({ @@ -339,6 +349,7 @@ export const browseDashboardsAPI = createApi({ }); }, }), + importDashboard: builder.mutation({ query: ({ dashboard, overwrite, inputs, folderUid }) => ({ method: 'POST', @@ -363,6 +374,19 @@ export const browseDashboardsAPI = createApi({ }); }, }), + + // restore a dashboard that got soft deleted + restoreDashboard: builder.mutation({ + query: ({ dashboardUID }) => ({ + url: `/dashboards/uid/${dashboardUID}/trash`, + method: 'PATCH', + }), + onQueryStarted: ({ dashboardUID }, { queryFulfilled, dispatch }) => { + queryFulfilled.then(() => { + dispatch(refreshParents([dashboardUID])); + }); + }, + }), }), }); @@ -377,6 +401,7 @@ export const { useNewFolderMutation, useSaveDashboardMutation, useSaveFolderMutation, + useRestoreDashboardMutation, } = browseDashboardsAPI; export { skipToken } from '@reduxjs/toolkit/query/react'; diff --git a/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx b/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx index 12f08f4bd6d..c54bb7b6688 100644 --- a/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx @@ -10,6 +10,7 @@ import { SearchView } from '../browse-dashboards/components/SearchView'; import { getFolderPermissions } from '../browse-dashboards/permissions'; import { setAllSelection } from '../browse-dashboards/state'; +import { RecentlyDeletedActions } from './state/RecentlyDeletedActions'; import { useRecentlyDeletedStateManager } from './utils/useRecentlyDeletedStateManager'; const RecentlyDeletedPage = memo(() => { @@ -47,6 +48,7 @@ const RecentlyDeletedPage = memo(() => { onPanelTypeChange={stateManager.onPanelTypeChange} onSetIncludePanels={stateManager.onSetIncludePanels} /> + {({ width, height }) => ( Promise; + onDismiss: () => void; + selectedItems: DashboardTreeSelection; + isLoading: boolean; +} + +export const RestoreModal = ({ onConfirm, onDismiss, selectedItems, isLoading, ...props }: Props) => { + const numberOfDashboards = selectedItems ? Object.keys(selectedItems.dashboard).length : 0; + const onRestore = async () => { + await onConfirm(); + onDismiss(); + }; + return ( + + + This action will restore {{ numberOfDashboards }} dashboards. + + + // TODO: replace by list of dashboards (list up to 5 dashboards) or number (from 6 dashboards)? + } + confirmText={ + isLoading + ? t('recently-deleted.restore-modal.restore-loading', 'Restoring...') + : t('recently-deleted.restore-modal.restore-button', 'Restore') + } + confirmButtonVariant="primary" + onDismiss={onDismiss} + onConfirm={onRestore} + title={t('recently-deleted.restore-modal.title', 'Restore Dashboards')} + {...props} + /> + ); +}; diff --git a/public/app/features/manage-dashboards/state/RecentlyDeletedActions.tsx b/public/app/features/manage-dashboards/state/RecentlyDeletedActions.tsx new file mode 100644 index 00000000000..a46efec3911 --- /dev/null +++ b/public/app/features/manage-dashboards/state/RecentlyDeletedActions.tsx @@ -0,0 +1,65 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data/'; +import { Button, useStyles2 } from '@grafana/ui'; + +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 { setAllSelection, useActionSelectionState } from '../../browse-dashboards/state'; +import { RestoreModal } from '../components/RestoreModal'; +import { useRecentlyDeletedStateManager } from '../utils/useRecentlyDeletedStateManager'; + +export function RecentlyDeletedActions() { + const styles = useStyles2(getStyles); + + const dispatch = useDispatch(); + const selectedItems = useActionSelectionState(); + const [, stateManager] = useRecentlyDeletedStateManager(); + + const [restoreDashboard, { isLoading: isRestoreLoading }] = useRestoreDashboardMutation(); + + const onActionComplete = () => { + dispatch(setAllSelection({ isSelected: false, folderUID: undefined })); + + stateManager.doSearchWithDebounce(); + }; + + const onRestore = async () => { + const promises = Object.entries(selectedItems.dashboard) + .filter(([_, selected]) => selected) + .map(([uid]) => restoreDashboard({ dashboardUID: uid })); + await Promise.all(promises); + onActionComplete(); + }; + + const showRestoreModal = () => { + appEvents.publish( + new ShowModalReactEvent({ + component: RestoreModal, + props: { + selectedItems, + onConfirm: onRestore, + isLoading: isRestoreLoading, + }, + }) + ); + }; + + return ( +
+ +
+ ); +} + +const getStyles = (theme: GrafanaTheme2) => ({ + row: css({ + marginBottom: theme.spacing(2), + }), +}); diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 331632b831a..6d766c64cdf 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1553,6 +1553,18 @@ }, "query-editor-not-exported": "Data source plugin does not export any Query Editor component" }, + "recently-deleted": { + "buttons": { + "restore": "Restore" + }, + "restore-modal": { + "restore-button": "Restore", + "restore-loading": "Restoring...", + "text_one": "This action will restore {{numberOfDashboards}} dashboard.", + "text_other": "This action will restore {{numberOfDashboards}} dashboards.", + "title": "Restore Dashboards" + } + }, "refresh-picker": { "aria-label": { "choose-interval": "Auto refresh turned off. Choose refresh time interval", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 59286371b22..71af32ec586 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1553,6 +1553,18 @@ }, "query-editor-not-exported": "Đäŧä şőūřčę pľūģįʼn đőęş ʼnőŧ ęχpőřŧ äʼny Qūęřy Ēđįŧőř čőmpőʼnęʼnŧ" }, + "recently-deleted": { + "buttons": { + "restore": "Ŗęşŧőřę" + }, + "restore-modal": { + "restore-button": "Ŗęşŧőřę", + "restore-loading": "Ŗęşŧőřįʼnģ...", + "text_one": "Ŧĥįş äčŧįőʼn ŵįľľ řęşŧőřę {{numberOfDashboards}} đäşĥþőäřđ.", + "text_other": "Ŧĥįş äčŧįőʼn ŵįľľ řęşŧőřę {{numberOfDashboards}} đäşĥþőäřđş.", + "title": "Ŗęşŧőřę Đäşĥþőäřđş" + } + }, "refresh-picker": { "aria-label": { "choose-interval": "Åūŧő řęƒřęşĥ ŧūřʼnęđ őƒƒ. Cĥőőşę řęƒřęşĥ ŧįmę įʼnŧęřväľ",