From f8027e4d75064778864e5fa47d723cdf8eb5907f Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 11 Dec 2025 21:17:23 +0200 Subject: [PATCH] Dashboard: Implement modal to confirm layout change (#111093) --- .../dashboard-group-panels.spec.ts | 6 ++ .../dashboards-panel-layouts.spec.ts | 23 ++++-- .../dashboards-repeats-auto-grid.spec.ts | 26 ++++--- e2e-playwright/dashboard-new-layouts/utils.ts | 10 +++ .../DashboardLayoutSelector.test.tsx | 72 +++++++++++++++++++ .../DashboardLayoutSelector.tsx | 58 ++++++++++----- public/locales/en-US/grafana.json | 8 +++ 7 files changed, 163 insertions(+), 40 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts index c35eb03bf84..ad954267c85 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -419,6 +419,9 @@ test.describe( // Select tabs layout await page.getByLabel('layout-selection-option-Tabs').click(); + // confirm layout change + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row'))).toBeVisible(); await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New row 1'))).toBeVisible(); await expect( @@ -757,6 +760,9 @@ test.describe( // Select rows layout await page.getByLabel('layout-selection-option-Rows').click(); + // confirm layout change + await dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete).click(); + await dashboardPage .getByGrafanaSelector(selectors.components.DashboardRow.wrapper('New tab 1')) .scrollIntoViewIfNeeded(); diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts index ffacaa13912..803f9d18d46 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-panel-layouts.spec.ts @@ -4,6 +4,8 @@ import { test, expect, E2ESelectorGroups, DashboardPage } from '@grafana/plugin- import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; +import { switchToAutoGrid } from './utils'; + test.use({ featureToggles: { kubernetesDashboards: true, @@ -33,7 +35,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); await expect( dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) @@ -64,7 +67,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); // Get initial positions - standard width should have panels on different rows const firstPanelTop = await getPanelTop(dashboardPage, selectors); @@ -124,7 +128,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.minColumnWidth) @@ -181,7 +186,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); await dashboardPage .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.AutoGridLayout.maxColumns) @@ -216,7 +222,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -271,7 +278,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); const regularRowHeight = await getPanelHeight(dashboardPage, selectors); @@ -328,7 +336,8 @@ test.describe( ).toHaveCount(3); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await page.getByLabel('layout-selection-option-Auto grid').click(); + + await switchToAutoGrid(page, dashboardPage); // Set narrow column width first to ensure panels fit horizontally await dashboardPage diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts index dbba5c583c1..3e25adc6c41 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-auto-grid.spec.ts @@ -1,6 +1,6 @@ import { Page } from 'playwright-core'; -import { test, expect } from '@grafana/plugin-e2e'; +import { test, expect, DashboardPage } from '@grafana/plugin-e2e'; import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json'; @@ -12,6 +12,7 @@ import { getPanelPosition, importTestDashboard, goToEmbeddedPanel, + switchToAutoGrid, } from './utils'; const repeatTitleBase = 'repeat - '; @@ -42,7 +43,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().click(); @@ -78,7 +79,8 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); + await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -117,7 +119,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); // select first/original repeat panel to activate edit pane await dashboardPage @@ -148,7 +150,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -214,7 +216,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); // loading directly into panel editor @@ -271,7 +273,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); // this moving repeated panel between two normal panels await movePanel(dashboardPage, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`, 'New panel'); @@ -319,7 +321,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -382,7 +384,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -410,7 +412,7 @@ test.describe( await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); await dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.Sidebar.optionsButton).click(); - await switchToAutoGrid(page); + await switchToAutoGrid(page, dashboardPage); await saveDashboard(dashboardPage, page, selectors); await page.reload(); @@ -462,7 +464,3 @@ test.describe( }); } ); - -async function switchToAutoGrid(page: Page) { - await page.getByLabel('layout-selection-option-Auto grid').click(); -} diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 83508063ef5..69851994812 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -1,5 +1,6 @@ import { Page } from '@playwright/test'; +import { selectors } from '@grafana/e2e-selectors'; import { DashboardPage, E2ESelectorGroups, expect } from '@grafana/plugin-e2e'; import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; @@ -239,3 +240,12 @@ export async function getTabPosition(dashboardPage: DashboardPage, selectors: E2 const boundingBox = await tab.boundingBox(); return boundingBox; } + +export async function switchToAutoGrid(page: Page, dashboardPage: DashboardPage) { + await page.getByLabel('layout-selection-option-Auto grid').click(); + // confirm layout change if applicable + const confirmModal = dashboardPage.getByGrafanaSelector(selectors.pages.ConfirmModal.delete); + if (confirmModal) { + await confirmModal.click(); + } +} diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx new file mode 100644 index 00000000000..d363a95ddf0 --- /dev/null +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.test.tsx @@ -0,0 +1,72 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { getPanelPlugin } from '@grafana/data/test'; +import { setPluginImportUtils } from '@grafana/runtime'; +import { SceneGridLayout, VizPanel, SceneVariableSet } from '@grafana/scenes'; + +import { activateFullSceneTree } from '../../utils/test-utils'; +import { DashboardScene } from '../DashboardScene'; +import { DashboardGridItem } from '../layout-default/DashboardGridItem'; +import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager'; +import { RowItem } from '../layout-rows/RowItem'; +import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; +import { LayoutParent } from '../types/LayoutParent'; + +import { DashboardLayoutSelector } from './DashboardLayoutSelector'; + +const switchLayoutMock = jest.fn(); + +setPluginImportUtils({ + importPanelPlugin: (_) => Promise.resolve(getPanelPlugin({})), + getPanelPluginFromCache: (_) => undefined, +}); + +describe('DashboardLayoutSelector', () => { + it('should show confirmation modal when switching layouts', async () => { + const user = userEvent.setup(); + const scene = buildTestScene(); + const layoutManager = scene.state.body; + (layoutManager.parent as LayoutParent).switchLayout = switchLayoutMock; + + render(); + + await user.click(screen.getByLabelText('layout-selection-option-Tabs')); + + const confirmButton = screen.getByRole('button', { name: 'Change layout' }); + + expect(confirmButton).toBeInTheDocument(); + + await user.click(confirmButton); + expect(switchLayoutMock).toHaveBeenCalled(); + }); +}); + +const buildTestScene = () => { + const scene = new DashboardScene({ + title: 'testScene', + editable: true, + $variables: new SceneVariableSet({ + variables: [], + }), + body: new RowsLayoutManager({ + rows: [ + new RowItem({ + title: 'Row 1', + layout: new DefaultGridLayoutManager({ + grid: new SceneGridLayout({ + children: [ + new DashboardGridItem({ + body: new VizPanel({ key: 'panel-1', pluginId: 'text' }), + }), + ], + }), + }), + }), + ], + }), + }); + + activateFullSceneTree(scene); + return scene; +}; diff --git a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx index e959352986a..b32555b32f8 100644 --- a/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx +++ b/public/app/features/dashboard-scene/scene/layouts-shared/DashboardLayoutSelector.tsx @@ -1,8 +1,8 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { t } from '@grafana/i18n'; import { config } from '@grafana/runtime'; -import { RadioButtonGroup, Box } from '@grafana/ui'; +import { RadioButtonGroup, Box, ConfirmModal } from '@grafana/ui'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; @@ -20,6 +20,7 @@ export interface Props { export function DashboardLayoutSelector({ layoutManager }: Props) { const isGridLayout = layoutManager.descriptor.isGridLayout; const options = layoutRegistry.list().filter((layout) => layout.isGridLayout === isGridLayout); + const [newLayout, setNewLayout] = useState(); const disableTabs = useMemo(() => { if (config.featureToggles.unlimitedLayoutsNesting) { @@ -36,16 +37,23 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { return false; }, [layoutManager]); - const onChangeLayout = useCallback( - (newLayout: LayoutRegistryItem) => { - const layoutParent = layoutManager.parent; + const onChangeLayout = useCallback((newLayout: LayoutRegistryItem) => setNewLayout(newLayout), []); - if (layoutParent && isLayoutParent(layoutParent)) { - layoutParent.switchLayout(newLayout.createFromLayout(layoutManager)); - } - }, - [layoutManager] - ); + const onConfirmNewLayout = useCallback(() => { + if (!newLayout) { + return; + } + + const layoutParent = layoutManager.parent; + + if (layoutParent && isLayoutParent(layoutParent)) { + layoutParent.switchLayout(newLayout.createFromLayout(layoutManager)); + } + + setNewLayout(undefined); + }, [newLayout, layoutManager]); + + const onDismissNewLayout = useCallback(() => setNewLayout(undefined), []); const disabledOptions: LayoutRegistryItem[] = []; @@ -66,15 +74,27 @@ export function DashboardLayoutSelector({ layoutManager }: Props) { }); return ( - - + + + + - + ); } export function useLayoutCategory(layoutManager: DashboardLayoutManager) { diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 761ed8f51ec..90b047262a3 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -5093,6 +5093,14 @@ "layout": "Layout" }, "continue": "Continue", + "panel": { + "modal": { + "body": "Changing the layout will reset all panel positions and sizes.", + "confirm": "Change layout", + "dismiss": "Cancel", + "title": "Change layout" + } + }, "ungroup-nested-text": "This will ungroup all nested groups.", "ungroup-nested-title": "Ungroup nested groups?" },