From 28acea402b2e668cd24fa3f097ed9bd78e8acb07 Mon Sep 17 00:00:00 2001 From: "grafana-delivery-bot[bot]" <132647405+grafana-delivery-bot[bot]@users.noreply.github.com> Date: Tue, 23 Apr 2024 12:51:22 +0200 Subject: [PATCH] [v11.0.x] DashboardScene: Fixes deleting dirty dashboard (#86757) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DashboardScene: Fixes deleting dirty dashboard (#86479) * DashboardScene: Fixes deleting dirty dashboard * refactor + unit test (cherry picked from commit 835b968b086a99fd6615cdbc78f848563945e693) Co-authored-by: Torkel Ödegaard --- .../scene/DashboardScene.test.tsx | 19 ++++ .../dashboard-scene/scene/DashboardScene.tsx | 8 ++ .../settings/DeleteDashboardButton.tsx | 90 +++++++++++++++++++ .../settings/GeneralSettingsEditView.tsx | 13 +-- 4 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 0bbe46104db..041c692036f 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,4 +1,5 @@ import { CoreApp, LoadingState, getDefaultTimeRange } from '@grafana/data'; +import { locationService } from '@grafana/runtime'; import { sceneGraph, SceneGridLayout, @@ -71,6 +72,12 @@ jest.mock('app/features/playlist/PlaylistSrv', () => ({ stop: jest.fn(), }, })); + +jest.mock('app/features/manage-dashboards/state/actions', () => ({ + ...jest.requireActual('app/features/manage-dashboards/state/actions'), + deleteDashboard: jest.fn().mockResolvedValue({}), +})); + const worker = createWorker(); mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }); @@ -884,6 +891,18 @@ describe('DashboardScene', () => { }); }); + describe('Deleting dashboard', () => { + it('Should mark it non dirty before navigating to root', async () => { + const scene = buildTestScene(); + scene.setState({ isDirty: true }); + + locationService.push('/d/adsdas'); + await scene.deleteDashboard(); + + expect(scene.state.isDirty).toBe(false); + }); + }); + describe('Enriching data requests', () => { let scene: DashboardScene; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index eef7d822012..eebc3be983e 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -24,6 +24,7 @@ import store from 'app/core/store'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; +import { deleteDashboard } from 'app/features/manage-dashboards/state/actions'; import { VariablesChanged } from 'app/features/variables/types'; import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types'; import { ShowConfirmModalEvent } from 'app/types/events'; @@ -832,6 +833,13 @@ export class DashboardScene extends SceneObjectBase { public setInitialSaveModel(saveModel: Dashboard) { this._initialSaveModel = saveModel; } + + public async deleteDashboard() { + await deleteDashboard(this.state.uid!, true); + // Need to mark it non dirty to navigate away without unsaved changes warning + this.setState({ isDirty: false }); + locationService.replace('/'); + } } export class DashboardVariableDependency implements SceneVariableDependencyConfigLike { diff --git a/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx new file mode 100644 index 00000000000..a896816dc7a --- /dev/null +++ b/public/app/features/dashboard-scene/settings/DeleteDashboardButton.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { useAsyncFn, useToggle } from 'react-use'; + +import { Button, ConfirmModal, Modal } from '@grafana/ui'; +import { Trans } from 'app/core/internationalization'; + +import { DashboardScene } from '../scene/DashboardScene'; + +interface ButtonProps { + dashboard: DashboardScene; +} + +export function DeleteDashboardButton({ dashboard }: ButtonProps) { + const [showModal, toggleModal] = useToggle(false); + + return ( + <> + + + {showModal && } + + ); +} + +interface ModalProps { + dashboard: DashboardScene; + onClose: () => void; +} + +function DeleteDashboardModal({ dashboard, onClose }: ModalProps) { + const [, onConfirm] = useAsyncFn(async () => { + onClose(); + await dashboard.deleteDashboard(); + }, [dashboard, onClose]); + + if (dashboard.state.meta.provisioned) { + return ; + } + + return ( + +

Do you want to delete this dashboard?

+

{dashboard.state.title}

+ + } + onConfirm={onConfirm} + onDismiss={onClose} + title="Delete" + icon="trash-alt" + confirmText="Delete" + /> + ); +} + +function ProvisionedDeleteModal({ dashboard, onClose }: ModalProps) { + return ( + +

+ This dashboard is managed by Grafana provisioning and cannot be deleted. Remove the dashboard from the config + file to delete it. +

+

+ + See{' '} + + documentation + {' '} + for more information about provisioning. + +
+ File path: {dashboard.state.meta.provisionedExternalId} +

+ + + +
+ ); +} diff --git a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx index 669dcc109d8..ec68a8db964 100644 --- a/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx +++ b/public/app/features/dashboard-scene/settings/GeneralSettingsEditView.tsx @@ -19,7 +19,6 @@ import { Page } from 'app/core/components/Page/Page'; import { FolderPicker } from 'app/core/components/Select/FolderPicker'; import { t, Trans } from 'app/core/internationalization'; import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings'; -import { DeleteDashboardButton } from 'app/features/dashboard/components/DeleteDashboard/DeleteDashboardButton'; import { GenAIDashDescriptionButton } from 'app/features/dashboard/components/GenAI/GenAIDashDescriptionButton'; import { GenAIDashTitleButton } from 'app/features/dashboard/components/GenAI/GenAIDashTitleButton'; @@ -29,6 +28,7 @@ import { NavToolbarActions } from '../scene/NavToolbarActions'; import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; import { getDashboardSceneFor } from '../utils/utils'; +import { DeleteDashboardButton } from './DeleteDashboardButton'; import { DashboardEditView, DashboardEditViewState, useDashboardEditPageNav } from './utils'; export interface GeneralSettingsEditViewState extends DashboardEditViewState {} @@ -161,9 +161,12 @@ export class GeneralSettingsEditView this.getCursorSync()?.setState({ sync: value }); }; + public onDeleteDashboard = () => {}; + static Component = ({ model }: SceneComponentProps) => { - const { navModel, pageNav } = useDashboardEditPageNav(model.getDashboard(), model.getUrlKey()); - const { title, description, tags, meta, editable } = model.getDashboard().useState(); + const dashboard = model.getDashboard(); + const { navModel, pageNav } = useDashboardEditPageNav(dashboard, model.getUrlKey()); + const { title, description, tags, meta, editable } = dashboard.useState(); const { sync: graphTooltip } = model.getCursorSync()?.useState() || {}; const { timeZone, weekStart, UNSAFE_nowDelay: nowDelay } = model.getTimeRange().useState(); const { intervals } = model.getRefreshPicker().useState(); @@ -172,7 +175,7 @@ export class GeneralSettingsEditView return ( - +
- {meta.canDelete && } + {meta.canDelete && }
);