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 d9cde06cc69..ffdd52e24f6 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboard-group-panels.spec.ts @@ -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(); + }); } ); diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index c31b474588f..c8cfb1d47cb 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -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: { diff --git a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx index 15a8bf1c089..2df803da9fd 100644 --- a/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx @@ -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} /> ); @@ -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, diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx index 3c4466e8866..745f612a8cc 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -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,