mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
25f65cee29
commit
ca2df58ab0
@ -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';
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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),
|
||||
}),
|
||||
});
|
@ -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",
|
||||
|
@ -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äľ",
|
||||
|
Loading…
Reference in New Issue
Block a user