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
alexjonspencer1/prototype-two-phase-datagrid-init
Oscar Kilhed 3 months ago committed by GitHub
parent 081d7dc3b2
commit bc08e9cb4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 28
      public/app/features/dashboard-scene/edit-pane/EditPaneHeader.tsx
  2. 15
      public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
  3. 15
      public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
  4. 18
      public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
  5. 28
      public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
  6. 6
      public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
  7. 32
      public/app/features/dashboard-scene/scene/layout-tabs/TabItem.tsx
  8. 7
      public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
  9. 5
      public/app/features/dashboard-scene/scene/types/EditableDashboardElement.ts
  10. 13
      public/locales/en-US/grafana.json

@ -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());
}

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

Loading…
Cancel
Save