Dynamic Dashboards: Disallow adding empty row and tab titles (#113941)

This commit is contained in:
Ida Štambuk
2025-11-18 12:58:45 +01:00
committed by GitHub
parent ad9f9fc408
commit e821578ab0
4 changed files with 124 additions and 4 deletions
@@ -503,6 +503,60 @@ test.describe(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('cannot add a row without a title', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Cannot add row without title');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoRow(page, dashboardPage, selectors);
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// edit row title to a non-default and click away to trigger onBlur
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
.fill('Test row 1');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// clear the title input to simulate no title and click away to trigger onBlur
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 1')).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
.fill('');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// title should be set to a default name
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row'))
).toBeVisible();
// add another row
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow).click();
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
// edit row title to a non-default and click away to trigger onBlur
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
.fill('Test row 2');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// clear the title input to simulate no title and click away to trigger onBlur
await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('Test row 2')).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput)
.fill('');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// title should be set to a default name + 1 to avoid duplicates
await expect(
dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('New row 1'))
).toBeVisible();
});
/*
* Tabs
@@ -835,5 +889,51 @@ test.describe(
dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel'))
).toHaveCount(3);
});
test('cannot add a tab without a title', async ({ dashboardPage, selectors, page }) => {
await importTestDashboard(page, selectors, 'Cannot add tab without title');
await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click();
await groupIntoTab(page, dashboardPage, selectors);
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// edit tab title to a non-default and click away to trigger onBlur
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
.fill('Test tab 1');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// clear the title input to simulate no title and click away to trigger onBlur
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 1')).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
.fill('');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// title should be set to a default name
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible();
// add another tab
await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addTab).click();
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
// edit tab title to a non-default and click away to trigger onBlur
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
.fill('Test tab 2');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// clear the title input to simulate no title and click away to trigger onBlur
await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('Test tab 2')).click();
await dashboardPage
.getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput)
.fill('');
await dashboardPage.getByGrafanaSelector(selectors.components.EditPaneHeader.backButton).click();
// title should be set to a default name + 1 to avoid duplicates
await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab 1'))).toBeVisible();
});
}
);
@@ -683,6 +683,11 @@ export const versionedComponents = {
'12.2.0': 'data-testid tab title input',
},
},
RowsLayout: {
titleInput: {
'12.3.0': 'data-testid row title input',
},
},
},
},
PanelInspector: {
@@ -13,7 +13,7 @@ import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks
import { dashboardEditActions } from '../../edit-pane/shared';
import { getQueryRunnerFor, useDashboard } from '../../utils/utils';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { generateUniqueTitle, useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { RowItem } from './RowItem';
@@ -108,6 +108,7 @@ function RowTitleInput({ row, isNewElement }: { row: RowItem; isNewElement: bool
onFocus={() => (prevTitle.current = title || '')}
onBlur={() => editRowTitleAction(row, title || '', prevTitle.current || '')}
onChange={(e) => row.onChangeTitle(e.currentTarget.value)}
data-testid={selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput}
/>
</Field>
);
@@ -175,10 +176,18 @@ function RowRepeatSelect({ row, id }: { row: RowItem; id?: string }) {
}
function editRowTitleAction(row: RowItem, title: string, prevTitle: string) {
if (title === prevTitle) {
if (title !== '' && title === prevTitle) {
return;
}
if (title === '') {
const rowsLayout = row.getParentLayout();
const existingNames = new Set(
rowsLayout.state.rows.map((row) => row.state.title).filter((title) => title !== undefined)
);
title = generateUniqueTitle('New row', existingNames);
}
dashboardEditActions.edit({
description: t('dashboard.edit-actions.row-title', 'Change row title'),
source: row,
@@ -13,7 +13,7 @@ import { useConditionalRenderingEditor } from '../../conditional-rendering/hooks
import { dashboardEditActions } from '../../edit-pane/shared';
import { getQueryRunnerFor, useDashboard } from '../../utils/utils';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { generateUniqueTitle, useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { TabItem } from './TabItem';
@@ -147,10 +147,16 @@ function TabRepeatSelect({ tab, id }: { tab: TabItem; id?: string }) {
}
function editTabTitleAction(tab: TabItem, title: string, prevTitle: string) {
if (title === prevTitle) {
if (title !== '' && title === prevTitle) {
return;
}
if (title === '') {
const tabs = tab.getParentLayout().getTabsIncludingRepeats();
const existingNames = new Set(tabs.map((tab) => tab.state.title).filter((title) => title !== undefined));
title = generateUniqueTitle('New tab', existingNames);
}
dashboardEditActions.edit({
description: t('dashboard.edit-actions.tab-title', 'Change tab title'),
source: tab,