diff --git a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx
index 9605d479df9..e7aa01f9ede 100644
--- a/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
-import { Button, Menu, Stack, Text, useStyles2, ConfirmButton, Dropdown, Icon, IconButton } from '@grafana/ui';
+import { Button, Menu, Stack, Text, useStyles2, Dropdown, Icon, IconButton } from '@grafana/ui';
import { t } from 'app/core/internationalization';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
@@ -20,6 +20,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
const onCopy = element.onCopy?.bind(element);
const onDuplicate = element.onDuplicate?.bind(element);
const onDelete = element.onDelete?.bind(element);
+ const onConfirmDelete = element.onConfirmDelete?.bind(element);
// temporary simple solution, should select parent element
const onGoBack = () => editPane.clearSelection();
const canGoBack = editPane.state.selection;
@@ -40,7 +41,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
{element.renderActions && element.renderActions()}
- {(onCopy || onDelete) && (
+ {(onCopy || onDuplicate) && (
@@ -69,22 +70,15 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
)}
- {onDelete && (
-
-
-
+ variant="destructive"
+ fill="outline"
+ icon="trash-alt"
+ tooltip={t('dashboard.layout.common.delete', 'Delete')}
+ />
)}
diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
index 25bcdd807c9..860e7be2aaa 100644
--- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
@@ -1,5 +1,7 @@
+import { appEvents } from 'app/core/core';
import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
+import { ShowConfirmModalEvent } from 'app/types/events';
import { BulkActionElement } from '../scene/types/BulkActionElement';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
@@ -17,6 +19,19 @@ export class MultiSelectedObjectsEditableElement implements EditableDashboardEle
return { typeName: t('dashboard.edit-pane.elements.objects', 'Objects'), icon: 'folder', instanceName: '' };
}
+ public onConfirmDelete() {
+ appEvents.publish(
+ new ShowConfirmModalEvent({
+ title: t('dashboard.edit-pane.elements.multiple-elements', 'Multiple elements'),
+ text: t(
+ 'dashboard.edit-pane.elements.multiple-elements-delete-text',
+ 'Are you sure you want to delete these elements?'
+ ),
+ onConfirm: () => this.onDelete(),
+ })
+ );
+ }
+
public onDelete() {
this._elements.forEach((item) => item.onDelete());
}
diff --git a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
index cbdd794d3bd..32449565bac 100644
--- a/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
@@ -1,7 +1,9 @@
import { v4 as uuidv4 } from 'uuid';
+import { appEvents } from 'app/core/core';
import { t } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
+import { ShowConfirmModalEvent } from 'app/types/events';
import { EditableDashboardElement, EditableDashboardElementInfo } from '../scene/types/EditableDashboardElement';
@@ -28,6 +30,19 @@ export class MultiSelectedVizPanelsEditableElement implements EditableDashboardE
return [header];
}
+ public onConfirmDelete() {
+ appEvents.publish(
+ new ShowConfirmModalEvent({
+ title: t('dashboard.edit-pane.elements.multiple-panels', 'Multiple panels'),
+ text: t(
+ 'dashboard.edit-pane.elements.multiple-panels-delete-text',
+ 'Are you sure you want to delete these panels? All queries will be removed.'
+ ),
+ onConfirm: () => this.onDelete(),
+ })
+ );
+ }
+
public onDelete() {
this._panels.forEach((panel) => {
panel.onDelete();
diff --git a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
index b6a40e9dadf..a488e832da2 100644
--- a/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
+++ b/public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
@@ -3,9 +3,11 @@ import { useMemo } from 'react';
import { locationService } from '@grafana/runtime';
import { sceneGraph, VizPanel } from '@grafana/scenes';
import { Stack, Button } from '@grafana/ui';
+import { appEvents } from 'app/core/core';
import { t, Trans } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
+import { ShowConfirmModalEvent } from 'app/types/events';
import {
PanelBackgroundSwitch,
@@ -84,6 +86,22 @@ export class VizPanelEditableElement implements EditableDashboardElement, BulkAc
layout.removePanel?.(this.panel);
}
+ public onConfirmDelete() {
+ appEvents.publish(
+ new ShowConfirmModalEvent({
+ title: t('dashboard.viz-panel.delete-panel-title', 'Delete panel?'),
+ text: t(
+ 'dashboard.viz-panel.delete-panel-text',
+ 'Deleting this panel will also remove all queries. Are you sure you want to continue?'
+ ),
+ yesText: t('dashboard.viz-panel.delete-panel-yes', 'Delete'),
+ onConfirm: () => {
+ this.onDelete();
+ },
+ })
+ );
+ }
+
public onDuplicate() {
const layout = dashboardSceneGraph.getLayoutManagerFor(this.panel);
layout.duplicatePanel?.(this.panel);
diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
index 135ebb139c1..9ca3eb2061b 100644
--- a/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
@@ -9,11 +9,13 @@ import {
VizPanel,
} from '@grafana/scenes';
import { RowsLayoutRowKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
+import appEvents from 'app/core/app_events';
import { LS_ROW_COPY_KEY } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import store from 'app/core/store';
import kbn from 'app/core/utils/kbn';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
+import { ShowConfirmModalEvent } from 'app/types/events';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { serializeRow } from '../../serialization/layoutSerializers/RowsLayoutSerializer';
@@ -110,6 +112,32 @@ export class RowItem
this.getParentLayout().removeRow(this);
}
+ public onConfirmDelete() {
+ if (this.getLayout().getVizPanels().length === 0) {
+ this.onDelete();
+ return;
+ }
+
+ if (this.getParentLayout().shouldUngroup()) {
+ this.onDelete();
+ return;
+ }
+
+ appEvents.publish(
+ new ShowConfirmModalEvent({
+ title: t('dashboard.rows-layout.delete-row-title', 'Delete row?'),
+ text: t(
+ 'dashboard.rows-layout.delete-row-text',
+ 'Deleting this row will also remove all panels. Are you sure you want to continue?'
+ ),
+ yesText: t('dashboard.rows-layout.delete-row-yes', 'Delete'),
+ onConfirm: () => {
+ this.onDelete();
+ },
+ })
+ );
+ }
+
public createMultiSelectedElement(items: SceneObject[]): RowItems {
return new RowItems(items.filter((item) => item instanceof RowItem));
}
diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
index 4a99a719e5e..ad82737cb81 100644
--- a/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
@@ -134,9 +134,13 @@ export class RowsLayoutManager extends SceneObjectBase i
});
}
+ public shouldUngroup(): boolean {
+ return this.state.rows.length === 1;
+ }
+
public removeRow(row: RowItem) {
// When removing last row replace ourselves with the inner row layout
- if (this.state.rows.length === 1) {
+ if (this.shouldUngroup()) {
ungroupLayout(this, row.state.layout);
return;
}
diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx
index 0074347ebb2..f9eb4b5dddd 100644
--- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx
@@ -10,10 +10,12 @@ import {
} from '@grafana/scenes';
import { TabsLayoutTabKind } from '@grafana/schema/dist/esm/schema/dashboard/v2alpha1/types.spec.gen';
import { LS_TAB_COPY_KEY } from 'app/core/constants';
+import { appEvents } from 'app/core/core';
import { t } from 'app/core/internationalization';
import store from 'app/core/store';
import kbn from 'app/core/utils/kbn';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
+import { ShowConfirmModalEvent } from 'app/types/events';
import { ConditionalRendering } from '../../conditional-rendering/ConditionalRendering';
import { serializeTab } from '../../serialization/layoutSerializers/TabsLayoutSerializer';
@@ -107,10 +109,38 @@ export class TabItem
}
public onDelete() {
- const layout = sceneGraph.getAncestor(this, TabsLayoutManager);
+ const layout = this.getParentLayout();
layout.removeTab(this);
}
+ public onConfirmDelete() {
+ const layout = this.getParentLayout();
+
+ if (layout.shouldUngroup()) {
+ layout.removeTab(this);
+ return;
+ }
+
+ if (this.getLayout().getVizPanels().length === 0) {
+ this.onDelete();
+ return;
+ }
+
+ appEvents.publish(
+ new ShowConfirmModalEvent({
+ title: t('dashboard.tabs-layout.delete-tab-title', 'Delete tab?'),
+ text: t(
+ 'dashboard.tabs-layout.delete-tab-text',
+ 'Deleting this tab will also remove all panels. Are you sure you want to continue?'
+ ),
+ yesText: t('dashboard.tabs-layout.delete-tab-yes', 'Delete'),
+ onConfirm: () => {
+ this.onDelete();
+ },
+ })
+ );
+ }
+
public serialize(): TabsLayoutTabKind {
return serializeTab(this);
}
diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
index 5a22c6aaabf..194d49c1d11 100644
--- a/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
+++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
@@ -152,10 +152,15 @@ export class TabsLayoutManager extends SceneObjectBase i
this.state.tabs.forEach((tab) => tab.getLayout().activateRepeaters?.());
}
+ public shouldUngroup(): boolean {
+ return this.state.tabs.length === 1;
+ }
+
public removeTab(tabToRemove: TabItem) {
// When removing last tab replace ourselves with the inner tab layout
- if (this.state.tabs.length === 1) {
+ if (this.shouldUngroup()) {
ungroupLayout(this, tabToRemove.state.layout);
+ return;
}
const currentTab = this.getCurrentTab();
diff --git a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts
index 3f22a6fd60f..3455eb42bff 100644
--- a/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts
+++ b/public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts
@@ -30,6 +30,11 @@ export interface EditableDashboardElement {
*/
onDelete?(): void;
+ /**
+ * Should confirm delete action
+ */
+ onConfirmDelete?(): void;
+
/**
* Supports duplicate action
*/
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index d0892ab1856..60d4da1ad59 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -2775,6 +2775,10 @@
"edit-pane": {
"elements": {
"dashboard": "Dashboard",
+ "multiple-elements": "Multiple elements",
+ "multiple-elements-delete-text": "Are you sure you want to delete these elements?",
+ "multiple-panels": "Multiple panels",
+ "multiple-panels-delete-text": "Are you sure you want to delete these panels? All queries will be removed.",
"objects": "Objects",
"panel": "Panel",
"panels": "Panels",
@@ -2973,6 +2977,9 @@
"title-row-options": "Row options"
},
"rows-layout": {
+ "delete-row-text": "Deleting this row will also remove all panels. Are you sure you want to continue?",
+ "delete-row-title": "Delete row?",
+ "delete-row-yes": "Delete",
"description": "Collapsable panel groups with headings",
"header-hidden-tooltip": "Row header only visible in edit mode",
"name": "Rows",
@@ -3047,6 +3054,9 @@
"aria-label-template-variables": "Template variables"
},
"tabs-layout": {
+ "delete-tab-text": "Deleting this tab will also remove all panels. Are you sure you want to continue?",
+ "delete-tab-title": "Delete tab?",
+ "delete-tab-yes": "Delete",
"description": "Organize panels into horizontal tabs",
"name": "Tabs",
"tab": {
@@ -3257,6 +3267,9 @@
"title-close": "Close"
},
"viz-panel": {
+ "delete-panel-text": "Deleting this panel will also remove all queries. Are you sure you want to continue?",
+ "delete-panel-title": "Delete panel?",
+ "delete-panel-yes": "Delete",
"options": {
"configure-button-tooltip": "Edit queries and visualization options",
"description": "Description",