From d408e712e5d071bd81794fbcdd8946c9c190cb15 Mon Sep 17 00:00:00 2001 From: Sergej-Vlasov <37613182+Sergej-Vlasov@users.noreply.github.com> Date: Thu, 9 Oct 2025 16:51:36 +0300 Subject: [PATCH] Repeats: Add E2E tests for tabs layout repeats (#112109) * add e2e tests for repeated tabs * finalise tab repeat e2e tests --- .github/CODEOWNERS | 1 + .../dashboards-repeats-custom-grid.spec.ts | 1 - .../dashboards-repeats-tabs-layout.spec.ts | 487 ++++++++++ e2e-playwright/dashboard-new-layouts/utils.ts | 45 + .../dashboards/V2DashWithTabRepeats.json | 837 ++++++++++++++++++ .../src/selectors/components.ts | 5 + .../scene/layout-tabs/TabItemEditor.tsx | 1 + .../scene/layout-tabs/TabItemRenderer.tsx | 2 +- 8 files changed, 1377 insertions(+), 2 deletions(-) create mode 100644 e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts create mode 100644 e2e-playwright/dashboards/V2DashWithTabRepeats.json diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2af83368859..114a4d8feed 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -429,6 +429,7 @@ /e2e-playwright/dashboards/TestDashboard.json @grafana/dashboards-squad @grafana/grafana-search-navigate-organise /e2e-playwright/dashboards/TestV2Dashboard.json @grafana/dashboards-squad /e2e-playwright/dashboards/V2DashWithRepeats.json @grafana/dashboards-squad +/e2e-playwright/dashboards/V2DashWithTabRepeats.json @grafana/dashboards-squad /e2e-playwright/dashboards-suite/adhoc-filter-from-panel.spec.ts @grafana/datapro /e2e-playwright/dashboards-suite/dashboard-browse-nested.spec.ts @grafana/grafana-search-navigate-organise /e2e-playwright/dashboards-suite/dashboard-browse.spec.ts @grafana/grafana-search-navigate-organise diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts index 0a349df5728..2d235571f97 100644 --- a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-custom-grid.spec.ts @@ -1,6 +1,5 @@ import { test, expect } from '@grafana/plugin-e2e'; -import testV2Dashboard from '../dashboards/TestV2Dashboard.json'; import testV2DashWithRepeats from '../dashboards/V2DashWithRepeats.json'; import { diff --git a/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts new file mode 100644 index 00000000000..100ec864b0d --- /dev/null +++ b/e2e-playwright/dashboard-new-layouts/dashboards-repeats-tabs-layout.spec.ts @@ -0,0 +1,487 @@ +import { test, expect } from '@grafana/plugin-e2e'; + +import V2DashWithTabRepeats from '../dashboards/V2DashWithTabRepeats.json'; + +import { + verifyChanges, + saveDashboard, + importTestDashboard, + goToEmbeddedPanel, + checkRepeatedTabTitles, + groupIntoTab, + moveTab, + getTabPosition, +} from './utils'; + +const repeatTitleBase = 'Tab - '; +const newTitleBase = 'edited tab rep - '; +const repeatOptions = [1, 2, 3, 4]; + +test.use({ + featureToggles: { + kubernetesDashboards: true, + dashboardNewLayouts: true, + groupByVariable: true, + }, +}); + +test.use({ + viewport: { width: 1920, height: 1080 }, +}); + +test.describe( + 'Repeats - Dashboard tabs layout', + { + tag: ['@dashboards'], + }, + () => { + test('can enable tab repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard(page, selectors, 'Tabs layout repeats - add repeats'); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await groupIntoTab(page, dashboardPage, selectors); + + await dashboardPage + .getByGrafanaSelector(selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput) + .fill(`${repeatTitleBase}$c1`); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + + 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 checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + }); + + test('can update tab repeats with variable change', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update on variable change', + JSON.stringify(V2DashWithTabRepeats) + ); + + const c1Var = dashboardPage.getByGrafanaSelector(selectors.pages.Dashboard.SubMenu.submenuItemLabels('c1')); + await c1Var + .locator('..') + .getByTestId(selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(repeatOptions.join(','))) + .click(); + // deselect last variable option + await dashboardPage + .getByGrafanaSelector( + selectors.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(`${repeatOptions.at(-1)}`) + ) + .click(); + await page.locator('body').click({ position: { x: 0, y: 0 } }); // blur select + + // verify that repeats are present for first 3 values + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions.slice(0, -1)); + // verify there is no repeat with last value + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(-1)}`)) + ).toBeHidden(); + }); + test('can update repeats in edit pane', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update through edit pane', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + // select first/original repeat tab to activate edit pane + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) + .click(); + + const titleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput + ); + await titleInput.fill(`${newTitleBase}$c1`); + await titleInput.blur(); + + await checkRepeatedTabTitles(dashboardPage, selectors, newTitleBase, repeatOptions); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await checkRepeatedTabTitles(dashboardPage, selectors, newTitleBase, repeatOptions); + }); + + test('can update repeats after panel change', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update repeats after panel change', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().click(); + + const panelTitleInput = dashboardPage.getByGrafanaSelector( + selectors.components.PanelEditor.OptionsPane.fieldInput('Title') + ); + await panelTitleInput.fill('New edited panel'); + await panelTitleInput.blur(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + .click(); + + // intermediate step to verify tab switch happened + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 2 - Row 1 - Panel repeat 1')) + ).toBeVisible(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + }); + + test('can update repeats after panel change in editor', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - update repeats after panel change in editor', + JSON.stringify(V2DashWithTabRepeats) + ); + + const panel = dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).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('New edited panel'); + await panelTitleInput.blur(); + + // playwright too fast, verifying JSON diff that changes landed + await verifyChanges(dashboardPage, page, selectors, 'New edited panel'); + + // verify panel title change in panel editor UI + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title(`New edited panel`)) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.DashboardEditPaneSplitter.primaryBody) + ).toBeVisible(); // verifying that dashboard loaded + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + .click(); + + // intermediate step to verify tab switch happened + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 2 - Row 1 - Panel repeat 1')) + ).toBeVisible(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + // verify edited panel title updated in repeated tab + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New edited panel')) + ).toBeVisible(); + }); + + test('can hide canvas grid add row action in repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - hide canvas add action in repeats', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + .click(); + + await expect(dashboardPage.getByGrafanaSelector(selectors.components.CanvasGridAddActions.addRow)).toBeHidden(); + }); + + test('can move repeated tabs', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - move repeated tabs', + JSON.stringify(V2DashWithTabRepeats) + ); + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + await moveTab(dashboardPage, page, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`, 'New tab'); + + // playwright too fast - adding intermediate step so that UI has time to update + await dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab')).click(); + + // verify move by tab position + const repeatedTab = await getTabPosition(dashboardPage, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`); + const normalTab = await getTabPosition(dashboardPage, selectors, 'New tab'); + expect(normalTab?.x).toBeLessThan(repeatedTab?.x || 0); + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + const repeatedTab2 = await getTabPosition(dashboardPage, selectors, `${repeatTitleBase}${repeatOptions.at(0)}`); + const normalTab2 = await getTabPosition(dashboardPage, selectors, 'New tab'); + expect(normalTab2?.x).toBeLessThan(repeatedTab2?.x || 0); + }); + + test('can load into repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - can load into repeated tab', + JSON.stringify(V2DashWithTabRepeats) + ); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .click(); + + await page.reload(); + + await expect(page.locator('[data-testid="uplot-main-div"]').first()).toBeVisible(); + + expect( + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .getAttribute('aria-selected') + ).toBe('true'); + }); + + test('can view panels in repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - view panels in repeated tabs', + JSON.stringify(V2DashWithTabRepeats) + ); + + // non repeated panel in repeated tab + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 1 - Panel repeat 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // repeated panel in original tab repeat + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 1 - Panel repeat 1')) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.backToDashboardButton) + .click(); + + // repeated panel in repeated tab + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('v'); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 1 - Panel repeat 1')) + ).toBeHidden(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + }); + + test('can view embedded panels in repeated tab', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - view embedded panels in repeated tabs', + JSON.stringify(V2DashWithTabRepeats) + ); + + const dashUrl = page.url(); + + // non repeated panel in repeated tab + await dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')).first().hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('New panel')) + ).toBeVisible(); + await page.goto(dashUrl); + + // repeated panel in original tab repeat + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 1 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + await page.goto(dashUrl); + + // repeated panel in repeated tab + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + .click(); + await dashboardPage + .getByGrafanaSelector(selectors.components.DashboardRow.title('Row 2')) + .scrollIntoViewIfNeeded(); + await dashboardPage + .getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + .hover(); + await page.keyboard.press('p+e'); + await goToEmbeddedPanel(page); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Panels.Panel.title('Tab 3 - Row 2 - Panel repeat 2')) + ).toBeVisible(); + }); + + test('can remove repeats', async ({ dashboardPage, selectors, page }) => { + await importTestDashboard( + page, + selectors, + 'Tabs layout repeats - remove repeats', + JSON.stringify(V2DashWithTabRepeats) + ); + + // verify 5 tabs are present (4 repeats and 1 normal) + await checkRepeatedTabTitles(dashboardPage, selectors, repeatTitleBase, repeatOptions); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + + await dashboardPage.getByGrafanaSelector(selectors.components.NavToolbar.editDashboard.editButton).click(); + + await dashboardPage + .getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(0)}`)) + .click(); + + 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: 'Disable repeating' }).click(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(3)}`)) + ).toBeHidden(); + + await saveDashboard(dashboardPage, page, selectors); + await page.reload(); + + await expect( + dashboardPage.getByGrafanaSelector( + selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.join(' + ')}`) + ) + ).toBeVisible(); + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title('New tab'))).toBeVisible(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(1)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(2)}`)) + ).toBeHidden(); + await expect( + dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${repeatTitleBase}${repeatOptions.at(3)}`)) + ).toBeHidden(); + }); + } +); diff --git a/e2e-playwright/dashboard-new-layouts/utils.ts b/e2e-playwright/dashboard-new-layouts/utils.ts index 2d279b0545a..70f5aa41bf0 100644 --- a/e2e-playwright/dashboard-new-layouts/utils.ts +++ b/e2e-playwright/dashboard-new-layouts/utils.ts @@ -187,3 +187,48 @@ export async function goToEmbeddedPanel(page: Page) { page.goto(soloPanelUrl!); } + +export async function moveTab( + dashboardPage: DashboardPage, + page: Page, + selectors: E2ESelectorGroups, + sourceTab: string, + targetTab: string +) { + // Get target panel position + const targetTabElement = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(targetTab)).first(); + + // Get source panel element + const sourceTabElement = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(sourceTab)).first(); + + const targetBox = await targetTabElement.boundingBox(); + + // Perform drag and drop (dragTo() did not work in this case) + await sourceTabElement.hover(); + await page.mouse.down(); + // move to adjusted target position (relative to top left) + await page.mouse.move((targetBox?.x || 0) + (targetBox?.width || 0), targetBox?.y || 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 checkRepeatedTabTitles( + dashboardPage: DashboardPage, + selectors: E2ESelectorGroups, + title: string, + options: Array +) { + for (const option of options) { + await expect(dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(`${title}${option}`))).toBeVisible(); + } +} + +export async function getTabPosition(dashboardPage: DashboardPage, selectors: E2ESelectorGroups, tabTitle: string) { + const tab = dashboardPage.getByGrafanaSelector(selectors.components.Tab.title(tabTitle)).first(); + const boundingBox = await tab.boundingBox(); + return boundingBox; +} diff --git a/e2e-playwright/dashboards/V2DashWithTabRepeats.json b/e2e-playwright/dashboards/V2DashWithTabRepeats.json new file mode 100644 index 00000000000..99cabae8d94 --- /dev/null +++ b/e2e-playwright/dashboards/V2DashWithTabRepeats.json @@ -0,0 +1,837 @@ +{ + "apiVersion": "dashboard.grafana.app/v2beta1", + "kind": "Dashboard", + "metadata": { + "name": "addwm76", + "namespace": "default", + "uid": "TOro1wyxnlZJEWBZN3MF2MuXtdBb1GOnG6zynaCifi4X", + "resourceVersion": "3", + "generation": 4, + "creationTimestamp": "2025-09-18T08:03:45Z", + "labels": {}, + "annotations": {} + }, + "spec": { + "annotations": [], + "cursorSync": "Off", + "description": "", + "editable": true, + "elements": { + "panel-1": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 1, + "links": [], + "title": "Tab $c1 - Row $c2 - Panel repeat $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.3.0-pre" + } + } + }, + "panel-2": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": { + "scenarioId": "random_walk", + "seriesCount": 1 + }, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 2, + "links": [], + "title": "New panel", + "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.3.0-pre" + } + } + }, + "panel-3": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 3, + "links": [], + "title": "New panel", + "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.3.0-pre" + } + } + }, + "panel-4": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 4, + "links": [], + "title": "New panel", + "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.3.0-pre" + } + } + }, + "panel-5": { + "kind": "Panel", + "spec": { + "data": { + "kind": "QueryGroup", + "spec": { + "queries": [ + { + "kind": "PanelQuery", + "spec": { + "hidden": false, + "query": { + "datasource": { + "name": "PD8C576611E62080A" + }, + "group": "grafana-testdata-datasource", + "kind": "DataQuery", + "spec": {}, + "version": "v0" + }, + "refId": "A" + } + } + ], + "queryOptions": {}, + "transformations": [] + } + }, + "description": "", + "id": 5, + "links": [], + "title": "New panel", + "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.3.0-pre" + } + } + } + }, + "layout": { + "kind": "TabsLayout", + "spec": { + "tabs": [ + { + "kind": "TabsLayoutTab", + "spec": { + "layout": { + "kind": "RowsLayout", + "spec": { + "rows": [ + { + "kind": "RowsLayoutRow", + "spec": { + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-5" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "title": "New row" + } + }, + { + "kind": "RowsLayoutRow", + "spec": { + "collapse": false, + "layout": { + "kind": "GridLayout", + "spec": { + "items": [ + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-1" + }, + "height": 8, + "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": 8 + } + }, + { + "kind": "GridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-3" + }, + "height": 8, + "width": 12, + "x": 12, + "y": 8 + } + } + ] + } + }, + "repeat": { + "mode": "variable", + "value": "c2" + }, + "title": "Row $c2" + } + } + ] + } + }, + "repeat": { + "mode": "variable", + "value": "c1" + }, + "title": "Tab - $c1" + } + }, + { + "kind": "TabsLayoutTab", + "spec": { + "layout": { + "kind": "AutoGridLayout", + "spec": { + "columnWidthMode": "standard", + "items": [ + { + "kind": "AutoGridLayoutItem", + "spec": { + "element": { + "kind": "ElementReference", + "name": "panel-4" + } + } + } + ], + "maxColumnCount": 3, + "rowHeightMode": "standard" + } + }, + "title": "New tab" + } + } + ] + } + }, + "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": "tab 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": ["1", "2", "3", "4"], + "value": ["1", "2", "3", "4"] + }, + "hide": "dontHide", + "includeAll": true, + "multi": true, + "name": "c2", + "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": "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 + } + } + ] + }, + "status": {} +} diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 05b0f553578..9dc1afc8a89 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -670,6 +670,11 @@ export const versionedComponents = { '12.1.0': 'data-testid fill screen switch', }, }, + TabsLayout: { + titleInput: { + '12.2.0': 'data-testid tab title input', + }, + }, }, }, PanelInspector: { 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 ede733f4f42..3c4466e8866 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx @@ -91,6 +91,7 @@ function TabTitleInput({ tab, isNewElement, id }: { tab: TabItem; isNewElement: onFocus={() => (prevTitle.current = title || '')} onBlur={() => editTabTitleAction(tab, title || '', prevTitle.current || '')} onChange={(e) => tab.onChangeTitle(e.currentTarget.value)} + data-testid={selectors.components.PanelEditor.ElementEditPane.TabsLayout.titleInput} /> ); diff --git a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx index 06d526420a1..65621b516c4 100644 --- a/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx +++ b/public/app/features/dashboard-scene/scene/layout-tabs/TabItemRenderer.tsx @@ -24,7 +24,7 @@ export function TabItemRenderer({ model }: SceneComponentProps) { const mySlug = model.getSlug(); const urlKey = parentLayout.getUrlKey(); const isActive = mySlug === currentTabSlug; - const myIndex = parentLayout.state.tabs.findIndex((tab) => tab === model); + const myIndex = parentLayout.getTabsIncludingRepeats().findIndex((tab) => tab === model); const location = useLocation(); const href = textUtil.sanitize(locationUtil.getUrlForPartial(location, { [urlKey]: mySlug })); const styles = useStyles2(getStyles);