Dynamic dashboards: Extra confirmation before deleting tab or row with content (#103589)
* Extra confirmation before deleting tab with content * also confirm rows * make i18n-extract * remake how the confirm works * reset panel menu behavior * update i18n, fix missing event * fix lint
This commit is contained in:
@@ -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) {
|
||||
</Stack>
|
||||
<Stack direction="row" gap={1}>
|
||||
{element.renderActions && element.renderActions()}
|
||||
{(onCopy || onDelete) && (
|
||||
{(onCopy || onDuplicate) && (
|
||||
<Dropdown
|
||||
overlay={
|
||||
<Menu>
|
||||
@@ -69,22 +70,15 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{onDelete && (
|
||||
<ConfirmButton
|
||||
onConfirm={onDelete}
|
||||
confirmText="Confirm"
|
||||
confirmVariant="destructive"
|
||||
{(onDelete || onConfirmDelete) && (
|
||||
<Button
|
||||
onClick={onConfirmDelete || onDelete}
|
||||
size="sm"
|
||||
closeOnConfirm={true}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
fill="outline"
|
||||
icon="trash-alt"
|
||||
tooltip={t('dashboard.layout.common.delete', 'Delete')}
|
||||
/>
|
||||
</ConfirmButton>
|
||||
variant="destructive"
|
||||
fill="outline"
|
||||
icon="trash-alt"
|
||||
tooltip={t('dashboard.layout.common.delete', 'Delete')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
</div>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
+15
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -134,9 +134,13 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> 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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -152,10 +152,15 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> 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();
|
||||
|
||||
@@ -30,6 +30,11 @@ export interface EditableDashboardElement {
|
||||
*/
|
||||
onDelete?(): void;
|
||||
|
||||
/**
|
||||
* Should confirm delete action
|
||||
*/
|
||||
onConfirmDelete?(): void;
|
||||
|
||||
/**
|
||||
* Supports duplicate action
|
||||
*/
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user