Dashboard: Implement modal to confirm layout change (#111093)

This commit is contained in:
Bogdan Matei
2025-12-11 19:17:23 +00:00
committed by GitHub
parent f5b2dde4a1
commit f8027e4d75
7 changed files with 163 additions and 40 deletions
@@ -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();
@@ -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
@@ -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();
}
@@ -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();
}
}
@@ -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(<DashboardLayoutSelector layoutManager={layoutManager} />);
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;
};
@@ -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<LayoutRegistryItem | undefined>();
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 (
<Box paddingBottom={2} display="flex" grow={1} alignItems="stretch" gap={2} direction={'column'}>
<RadioButtonGroup
fullWidth
value={layoutManager.descriptor}
options={radioOptions}
onChange={onChangeLayout}
disabledOptions={disabledOptions}
<>
<Box paddingBottom={2} display="flex" grow={1} alignItems="stretch" gap={2} direction={'column'}>
<RadioButtonGroup
fullWidth
value={layoutManager.descriptor}
options={radioOptions}
onChange={onChangeLayout}
disabledOptions={disabledOptions}
/>
</Box>
<ConfirmModal
isOpen={!!newLayout}
title={t('dashboard.layout.panel.modal.title', 'Change layout')}
body={t('dashboard.layout.panel.modal.body', 'Changing the layout will reset all panel positions and sizes.')}
confirmText={t('dashboard.layout.panel.modal.confirm', 'Change layout')}
dismissText={t('dashboard.layout.panel.modal.dismiss', 'Cancel')}
confirmButtonVariant="primary"
onConfirm={onConfirmNewLayout}
onDismiss={onDismissNewLayout}
/>
</Box>
</>
);
}
export function useLayoutCategory(layoutManager: DashboardLayoutManager) {
+8
View File
@@ -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?"
},