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
This commit is contained in:
Laura Benz 2024-06-13 09:12:10 +02:00 committed by GitHub
parent 25f65cee29
commit ca2df58ab0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 160 additions and 0 deletions

View File

@ -53,6 +53,10 @@ interface ImportOptions {
folderUid: string;
}
interface RestoreDashboardArgs {
dashboardUID: string;
}
function createBackendSrvBaseQuery({ baseURL }: { baseURL: string }): BaseQueryFn<RequestOptions> {
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<void, { folder: FolderDTO; destinationUID: string }>({
invalidatesTags: ['getFolder'],
@ -174,6 +179,7 @@ export const browseDashboardsAPI = createApi({
});
},
}),
// delete an *individual* folder. used in the folder actions menu.
deleteFolder: builder.mutation<void, FolderDTO>({
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<DescendantCount, DashboardTreeSelection>({
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<void, MoveItemsArgs>({
invalidatesTags: ['getFolder'],
@ -276,6 +284,7 @@ export const browseDashboardsAPI = createApi({
});
},
}),
// delete *multiple* items (folders and dashboards). used in the delete modal.
deleteItems: builder.mutation<void, DeleteItemsArgs>({
queryFn: async ({ selectedItems }, _api, _extraOptions, baseQuery) => {
@ -313,6 +322,7 @@ export const browseDashboardsAPI = createApi({
});
},
}),
// save an existing dashboard
saveDashboard: builder.mutation<SaveDashboardResponseDTO, SaveDashboardCommand>({
query: ({ dashboard, folderUid, message, overwrite, showErrorAlert }) => ({
@ -339,6 +349,7 @@ export const browseDashboardsAPI = createApi({
});
},
}),
importDashboard: builder.mutation<ImportDashboardResponseDTO, ImportOptions>({
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<void, RestoreDashboardArgs>({
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';

View File

@ -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}
/>
<RecentlyDeletedActions />
<AutoSizer>
{({ width, height }) => (
<SearchView

View File

@ -0,0 +1,44 @@
import React from 'react';
import { ConfirmModal, Text } from '@grafana/ui';
import { Trans, t } from '../../../core/internationalization';
import { DashboardTreeSelection } from '../../browse-dashboards/types';
interface Props {
isOpen: boolean;
onConfirm: () => Promise<void>;
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 (
<ConfirmModal
body={
<Text element="p">
<Trans i18nKey="recently-deleted.restore-modal.text" count={numberOfDashboards}>
This action will restore {{ numberOfDashboards }} dashboards.
</Trans>
</Text>
// 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}
/>
);
};

View File

@ -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 (
<div className={styles.row}>
<Button onClick={showRestoreModal} variant="secondary">
<Trans i18nKey="recently-deleted.buttons.restore">Restore</Trans>
</Button>
</div>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
row: css({
marginBottom: theme.spacing(2),
}),
});

View File

@ -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",

View File

@ -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äľ",