diff --git a/e2e-playwright/dashboard-new-layouts/dashboard-repeats-row-layout.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboard-repeats-row-layout.spec.ts new file mode 100644 index 00000000000..baa7b5a1069 --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboard-repeats-row-layout.spec.ts @@ -0,0 +1,573 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import V2DashWithRowRepeats from '../dashboards/V2DashWithRowRepeats.json'; + +import { + verifyChanges, + saveDashboard, + importTestDashboard, + goToEmbeddedPanel, + groupIntoRow, + checkRepeatedRowTitles, + moveRow, + getRowPosition, +} from './utils'; + +const repeatTitleBase = 'Row - '; +const newTitleBase = 'edited row rep - '; +const repeatOptions = [1, 2, 3, 4]; +const getRepeatedPanelTitle = (row: number, panel: number) => `repeated-row-${row}-repeated-panel-${panel}`; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.use({ + viewport: { width: 1920, height: 1080 }, +}); + +test.describe( + 'Repeats - Dashboard rows layout', + { + tag: ['@dashboards'], + }, + () => { + test('can enable row repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Row layout repeats - add repeats'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoRow(page, dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput) + .fill(`${repeatTitleBase}$c1`); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + + const repeatOptionsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('dash-row-repeat') + ); + + // expand repeat options dropdown + await repeatOptionsGroup.click(); + // find repeat variable dropdown + await repeatOptionsGroup.getByRole('combobox').click(); + await page.getByRole('option', { name: 'c1' }).click(); + + await checkRepeatedRowTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await checkRepeatedRowTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + }); + + test('can update tab repeats with variable change', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Rows layout repeats - update on variable change', + JSON.stringify(V2DashWithRowRepeats) + ); + + const c4Var = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('c4')); + await c4Var + .locator('..') + .getByTestId(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(repeatOptions.join(','))) + .click(); + // deselect last variable option + await dashboardPage + .getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(`${repeatOptions.at(0)}`) + ) + .click(); + await page.locator('body').click({ position: { x: 0, y: 0 } }); // blur select + + // verify that repeats are present for last 3 values + await checkRepeatedRowTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions.slice(1, -1)); + // verify there is no repeat with first value + expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(0)}`) + ) + ).toBeHidden(); + }); + + test('can update title for repeat rows in edit pane', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Rows layout repeats - update through edit pane', + JSON.stringify(V2DashWithRowRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + // select first/original repeat row to activate edit pane + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) + .click(); + + const titleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.RowsLayout.titleInput + ); + await titleInput.fill(`${newTitleBase}$c4`); + await titleInput.blur(); + + await checkRepeatedRowTitles(dashboardPage, selectors, newTitleBase, repeatOptions); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await checkRepeatedRowTitles(dashboardPage, selectors, newTitleBase, repeatOptions); + }); + + test('can update repeats after panel change', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - update repeats after panel change', + JSON.stringify(V2DashWithRowRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')).click(); + + const panelTitleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldInput('Title') + ); + await panelTitleInput.fill('single panel row $c4 edited'); + await panelTitleInput.blur(); + + // close first row to load the second row + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + + // verify edited panel title updated in repeated row + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 2 edited')) + ).toBeVisible(); + + // reopen first row so collapse is not saved + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + // close first row to load the second row + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 2 edited')) + ).toBeVisible(); + }); + + test('can update repeats after panel change in editor', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - update repeats after panel change in editor', + JSON.stringify(V2DashWithRowRepeats) + ); + + const editedSinglePanelName = (rowNumber: string) => `single panel row ${rowNumber} edited`; + const panel = dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')) + .first(); + await panel.hover(); + await page.keyboard.press('e'); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) + ).toBeHidden(); // verifying that panel editor loaded + + const panelTitleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldInput('Title') + ); + await panelTitleInput.fill(editedSinglePanelName('$c4')); + await panelTitleInput.blur(); + + // playwright too fast, verifying JSON diff that changes landed + await verifyChanges(dashboardPage, page, selectors, editedSinglePanelName('$c4')); + // verify panel title change in panel editor UI + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(editedSinglePanelName('1'))) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) + ).toBeVisible(); // verifying that dashboard loaded + + // close first row to make sure we are viewing second row + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + + // verify edited panel title updated in repeated row + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(editedSinglePanelName('2'))) + ).toBeVisible(); + // open first row again so collapse is not saved + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + // collapse row again so lazy loading loads 2nd row + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(editedSinglePanelName('2'))) + ).toBeVisible(); + }); + + test('can hide add panel action in repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - hide canvas add action in repeats', + JSON.stringify(V2DashWithRowRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addPanel) + ).toBeDefined(); + + // close first row to make sure second row is in viewport + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + + const secondRow = dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.wrapper(`${repeatTitleBase}2`) + ); + await expect(secondRow.getByTestId(selectors.components.CanvasGridAddActions.addPanel)).toBeHidden(); + }); + + test('can move repeated rows', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - move repeated rows', + JSON.stringify(V2DashWithRowRepeats) + ); + const singleRowTitle = 'single row'; + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // collapse rows and save + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await moveRow(dashboardPage, page, selectors, `${repeatTitleBase}1`, singleRowTitle); + + const singleRow = await getRowPosition(dashboardPage, selectors, singleRowTitle); + + const repeatedRow = await getRowPosition(dashboardPage, selectors, `${repeatTitleBase}1`); + expect(singleRow?.y).toBeLessThan(repeatedRow?.y || 0); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + for (let i = 1; i <= repeatOptions.length; i++) { + const singleRow = await getRowPosition(dashboardPage, selectors, singleRowTitle); + + // verify move by row position + const repeatedRow = await getRowPosition(dashboardPage, selectors, `${repeatTitleBase}${i}`); + expect(singleRow?.y).toBeLessThan(repeatedRow?.y || 0); + } + }); + + test('can view panels in repeated row', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - view panels in repeated rows', + JSON.stringify(V2DashWithRowRepeats) + ); + + // non repeated panel in repeated row + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')) + .first() + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 1))) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 1')) + ).toBeVisible(); + + await page.keyboard.press('Escape'); + + // repeated panel in original row repeat + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 2))) + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 1))) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 2))) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 2))) + ).toBeVisible(); + + await page.keyboard.press('Escape'); + + // repeated panel in repeated row + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(2, 2))) + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 2))) + ).toBeHidden(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(2, 2))) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(2, 2))) + ).toBeVisible(); + }); + + test('can view embedded panels in repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - view embedded panels in repeated rows', + JSON.stringify(V2DashWithRowRepeats) + ); + + const dashUrl = page.url(); + + // non repeated panel in repeated row + // collapse row to make sure row 2 is in viewport + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}1`)).click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 2')) + .first() + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('single panel row 2')) + ).toBeVisible(); + await page.goto(dashUrl); + + // repeated panel in original row + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 2))) + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(1, 2))) + ).toBeVisible(); + await page.goto(dashUrl); + + // repeated panel in repeated row + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(2, 2))) + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(getRepeatedPanelTitle(2, 2))) + ).toBeVisible(); + }); + + test('can remove repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - remove row repeats', + JSON.stringify(V2DashWithRowRepeats) + ); + + // verify both repeated and single rows are present + await checkRepeatedRowTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title('single row')) + ).toBeVisible(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) + .click(); + + const repeatOptionsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('dash-row-repeat') + ); + // expand repeat options dropdown + await repeatOptionsGroup.click(); + // find repeat variable dropdown + await repeatOptionsGroup.getByRole('combobox').click(); + await page.getByRole('option', { name: 'Disable repeating' }).click(); + + const nonRepeatedTitle = `${repeatTitleBase}${repeatOptions.join(' + ')}`; + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(nonRepeatedTitle)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title(`single panel row ${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(1)}`) + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(2)}`) + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(3)}`) + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(4)}`) + ) + ).toBeHidden(); + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(nonRepeatedTitle)) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Panels.Panel.title(`single panel row ${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(1)}`) + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(2)}`) + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(3)}`) + ) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.DashboardRow.title(`${repeatTitleBase}${repeatOptions.at(4)}`) + ) + ).toBeHidden(); + }); + test('can add tabs in repeated rows', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Row layout repeats - remove row repeats', + JSON.stringify(V2DashWithRowRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // add a tab in first row + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).first().click(); + await page.getByText('Group into tab').click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) + .fill(`tab-row-$c4`); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`tab-row-1`))).toBeVisible(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`tab-row-1`))).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`tab-row-2`))).toBeVisible(); + }); + test('can add repeat tabs in repeated rows', async ({ dashboardPage, selectors, page }) => { + const tabRepeatTitle = (tabNo: number, rowNo: number) => `tab-${tabNo}-row-${rowNo}`; + await importTestDashboard( + page, + selectors, + 'Row layout repeats - remove row repeats', + JSON.stringify(V2DashWithRowRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + // add a tab in first row + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).first().click(); + await page.getByText('Group into tab').click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) + .fill(`tab-$c1-row-$c4`); + + const repeatOptionsGroup = dashboardPage.getByGrafanaSelector( + selectors.components.OptionsGroup.group('repeat-options') + ); + // expand repeat options dropdown + await repeatOptionsGroup.getByRole('button').first().click(); + // find repeat variable dropdown + await repeatOptionsGroup.getByRole('combobox').click(); + await page.getByRole('option', { name: 'c1' }).click(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabRepeatTitle(1, 1))) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabRepeatTitle(2, 1))) + ).toBeVisible(); + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabRepeatTitle(1, 1))) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabRepeatTitle(2, 1))) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabRepeatTitle(1, 2))) + ).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabRepeatTitle(2, 2))) + ).toBeVisible(); + }); + } +); 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 7cf22108e1e..6bdd7cfdeb9 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 @@ -312,7 +312,7 @@ test.describe( await importTestDashboard( page, selectors, - 'Auto-grid repeats - move repeated panels 2', + 'Auto-grid repeats - view repeated panels 2', JSON.stringify(testV2DashWithRepeats) ); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index c1e00e1a67b..115eae8be1a 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -211,11 +211,43 @@ export async function moveTab( await page.mouse.up(); } +export async function moveRow( + dashboardPage: DashboardPage, + page: Page, + selectors: E2ESelectorGroups, + sourceRow: string, + targetRow: string +) { + // Get target panel position + const targetRowElement = dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title(targetRow)) + .first(); + + // Get source panel element + const sourceRowElement = dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title(sourceRow)) + .first(); + + const targetBox = await targetRowElement.boundingBox(); + + // Perform drag and drop (dragTo() did not work in this case) + await sourceRowElement.hover(); + await page.mouse.down(); + // move to adjusted target position (relative to top left) + await page.mouse.move(targetBox?.x || 0, (targetBox?.y || 0) + (targetBox?.height || 0), { steps: 5 }); + await page.mouse.up(); +} + export async function groupIntoTab(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); await page.getByText('Group into tab').click(); } +export async function groupIntoRow(page: Page, dashboardPage: DashboardPage, selectors: E2ESelectorGroups) { + await dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.groupPanels).click(); + await page.getByText('Group into row').click(); +} + export async function checkRepeatedTabTitles( dashboardPage: DashboardPage, selectors: E2ESelectorGroups, @@ -232,3 +264,23 @@ export async function getTabPosition(dashboardPage: DashboardPage, selectors: E2 const boundingBox = await tab.boundingBox(); return boundingBox; } + +export async function getRowPosition(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, rowTitle: string) { + const row = dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(rowTitle)).first(); + const boundingBox = await row.boundingBox(); + return boundingBox; +} + +export async function checkRepeatedRowTitles( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + title: string, + options: Array +) { + for (const option of options) { + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${title}${option}`)) + ).toBeVisible(); + await dashboardPage.getByGrafanaSelector(selectors.components.DashboardRow.title(`${title}${option}`)).click(); + } +} diff --git a/e2e-playwright/dashboards/V2DashWithRowRepeats.json b/e2e-playwright/dashboards/V2DashWithRowRepeats.json new file mode 100644 index 00000000000..cac11523a52 --- /dev/null +++ b/e2e-playwright/dashboards/V2DashWithRowRepeats.json @@ -0,0 +1,502 @@ +{ + "apiVersion": "dashboard.grafana.app/v2beta1", + "kind": "Dashboard", + "metadata": { + "name": "ad8l8fz", + "namespace": "default", + "uid": "fLb2na54K8NZHvn8LfWGL1jhZh03Hy0xpV1KzMYgAXEX", + "resourceVersion": "1", + "generation": 2, + "creationTimestamp": "2025-11-25T15:52:42Z", + "labels": { + "grafana.app/deprecatedInternalID": "20" + }, + "annotations": { + "grafana.app/createdBy": "user:aerwo725ot62od", + "grafana.app/updatedBy": "user:aerwo725ot62od", + "grafana.app/updatedTimestamp": "2025-11-25T15:52:42Z", + "grafana.app/folder": "" + } + }, + "spec": { + "annotations": [ + { + "kind": "AnnotationQuery", + "spec": { + "builtIn": true, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "query": { + "datasource": { + "name": "-- Grafana --" + }, + "group": "grafana", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + } + } + } + ], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "group": "", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "repeated-row-$c4-repeated-panel-$c3", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.4.0-pre" + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "group": "", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "single panel row $c4", + "vizConfig": { + "group": "timeseries", + "kind": "VizConfig", + "spec": { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + } + }, + "version": "12.4.0-pre" + } + } + } + }, + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 10, + "repeat": { + "direction": "h", + "mode": "variable", + "value": "c3" + }, + "width": 24, + "x": 0, + "y": 0 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-2" + }, + "height": 8, + "width": 12, + "x": 0, + "y": 10 + } + } + ] + } + }, + "repeat": { + "mode": "variable", + "value": "c4" + }, + "title": "Row - $c4" + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "collapse": false, + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "title": "single row" + } + } + ] + } + }, + "links": [], + "liveNow": false, + "preload": false, + "tags": [], + "timeSettings": { + "autoRefresh": "", + "autoRefreshIntervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "fiscalYearStartMonth": 0, + "from": "now-6h", + "hideTimepicker": false, + "timezone": "browser", + "to": "now" + }, + "title": "test-e2e-repeats", + "variables": [ + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c1", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1,2,3,4", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["A", "B", "C", "D"], + "value": ["A", "B", "C", "D"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c2", + "options": [ + { + "selected": true, + "text": "A", + "value": "A" + }, + { + "selected": true, + "text": "B", + "value": "B" + }, + { + "selected": true, + "text": "C", + "value": "C" + }, + { + "selected": true, + "text": "D", + "value": "D" + } + ], + "query": "A,B,C,D", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": false, + "multi": true, + "name": "c3", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1, 2, 3, 4", + "skipUrlSync": false + } + }, + { + "kind": "CustomVariable", + "spec": { + "allowCustomValue": true, + "current": { + "text": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": false, + "multi": true, + "name": "c4", + "options": [ + { + "selected": true, + "text": "1", + "value": "1" + }, + { + "selected": true, + "text": "2", + "value": "2" + }, + { + "selected": true, + "text": "3", + "value": "3" + }, + { + "selected": true, + "text": "4", + "value": "4" + } + ], + "query": "1, 2, 3, 4", + "skipUrlSync": false + } + } + ] + }, + "status": {} +}