diff --git a/e2e/dashboards-suite/dashboard-browse-nested.spec.ts b/e2e/dashboards-suite/dashboard-browse-nested.spec.ts new file mode 100644 index 00000000000..19dae811c13 --- /dev/null +++ b/e2e/dashboards-suite/dashboard-browse-nested.spec.ts @@ -0,0 +1,147 @@ +import { e2e } from '@grafana/e2e'; +import { selectors } from '@grafana/e2e-selectors'; + +import { makeNewDashboardRequestBody } from './utils/makeDashboard'; + +const NUM_ROOT_FOLDERS = 60; +const NUM_ROOT_DASHBOARDS = 60; +const NUM_NESTED_FOLDERS = 60; +const NUM_NESTED_DASHBOARDS = 60; + +// TODO enable this test when nested folders goes live +describe.skip('Dashboard browse (nested)', () => { + const dashboardUIDsToCleanUp: string[] = []; + const folderUIDsToCleanUp: string[] = []; + + // Add nested folder structure + before(() => { + e2e.flows.login('admin', 'admin'); + + // Add root folders + for (let i = 0; i < NUM_ROOT_FOLDERS; i++) { + e2e() + .request({ + method: 'POST', + url: '/api/folders', + body: { + title: `Root folder ${i.toString().padStart(2, '0')}`, + }, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => { + folderUIDsToCleanUp.push(response.body.uid); + }); + } + + // Add root dashboards + for (let i = 0; i < NUM_ROOT_DASHBOARDS; i++) { + e2e() + .request({ + method: 'POST', + url: '/api/dashboards/db', + body: makeNewDashboardRequestBody(`Root dashboard ${i.toString().padStart(2, '0')}`), + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => { + dashboardUIDsToCleanUp.push(response.body.uid); + }); + } + + // Add folder with children + e2e() + .request({ + method: 'POST', + url: '/api/folders', + body: { + title: 'A root folder with children', + }, + headers: { + 'Content-Type': 'application/json', + }, + }) + .then((response) => { + const folderUid = response.body.uid; + folderUIDsToCleanUp.push(folderUid); + // Add nested folders + for (let i = 0; i < NUM_NESTED_FOLDERS; i++) { + e2e().request({ + method: 'POST', + url: '/api/folders', + body: { + title: `Nested folder ${i.toString().padStart(2, '0')}`, + parentUid: folderUid, + }, + headers: { + 'Content-Type': 'application/json', + }, + }); + } + // Add nested dashboards + for (let i = 0; i < NUM_NESTED_DASHBOARDS; i++) { + e2e().request({ + method: 'POST', + url: '/api/dashboards/db', + body: makeNewDashboardRequestBody(`Nested dashboard ${i.toString().padStart(2, '0')}`, folderUid), + headers: { + 'Content-Type': 'application/json', + }, + }); + } + }); + }); + + // Remove nested folder structure + after(() => { + // Clean up root dashboards + for (const dashboardUID of dashboardUIDsToCleanUp) { + e2e.flows.deleteDashboard({ + uid: dashboardUID, + quick: true, + title: '', + }); + } + // Clean up root folders (cascading delete will remove any nested folders and dashboards) + for (const folderUID of folderUIDsToCleanUp) { + e2e().request({ + method: 'DELETE', + url: `/api/folders/${folderUID}`, + qs: { + forceDeleteRules: false, + }, + }); + } + }); + + it('pagination works correctly for folders and root', () => { + e2e.pages.Dashboards.visit(); + + e2e().contains('A root folder with children').should('be.visible'); + + // Expand A root folder with children + e2e().get('[aria-label="Expand folder A root folder with children"]').click(); + e2e().contains('Nested folder 00').should('be.visible'); + + // Scroll the page and check visibility of next set of items + e2e().get(`[data-testid="${selectors.pages.BrowseDashboards.table.body}"] > div`).scrollTo(0, 1700); + e2e().contains('Nested folder 59').should('be.visible'); + e2e().contains('Nested dashboard 00').should('be.visible'); + + // Scroll the page and check visibility of next set of items + e2e().get(`[data-testid="${selectors.pages.BrowseDashboards.table.body}"] > div`).scrollTo(0, 3800); + e2e().contains('Nested dashboard 59').should('be.visible'); + e2e().contains('Root folder 00').should('be.visible'); + + // Scroll the page and check visibility of next set of items + e2e().get(`[data-testid="${selectors.pages.BrowseDashboards.table.body}"] > div`).scrollTo(0, 5900); + e2e().contains('Root folder 59').should('be.visible'); + e2e().contains('Root dashboard 00').should('be.visible'); + + // Scroll the page and check visibility of next set of items + e2e().get(`[data-testid="${selectors.pages.BrowseDashboards.table.body}"] > div`).scrollTo(0, 8000); + e2e().contains('Root dashboard 59').should('be.visible'); + }); +}); diff --git a/e2e/dashboards-suite/utils/makeDashboard.ts b/e2e/dashboards-suite/utils/makeDashboard.ts new file mode 100644 index 00000000000..e9d8c14cb7d --- /dev/null +++ b/e2e/dashboards-suite/utils/makeDashboard.ts @@ -0,0 +1,59 @@ +export function makeNewDashboardRequestBody(dashboardName: string, folderUid?: string) { + return { + dashboard: { + annotations: { + list: [ + { + builtIn: 1, + datasource: { type: 'grafana', uid: '-- Grafana --' }, + enable: true, + hide: true, + iconColor: 'rgba(0, 211, 255, 1)', + name: 'Annotations & Alerts', + type: 'dashboard', + }, + ], + }, + editable: true, + fiscalYearStartMonth: 0, + graphTooltip: 0, + links: [], + liveNow: false, + panels: [ + { + datasource: { type: 'testdata', uid: '89_jzlT4k' }, + gridPos: { h: 9, w: 12, x: 0, y: 0 }, + id: 2, + options: { + code: { + language: 'plaintext', + showLineNumbers: false, + showMiniMap: false, + }, + content: '***A nice little happy empty dashboard***', + mode: 'markdown', + }, + pluginVersion: '9.4.0-pre', + title: 'Nothing to see here', + type: 'text', + }, + ], + refresh: '', + revision: 1, + schemaVersion: 38, + style: 'dark', + tags: [], + templating: { list: [] }, + time: { from: 'now-6h', to: 'now' }, + timepicker: {}, + timezone: '', + title: dashboardName, + version: 0, + weekStart: '', + uid: '', + }, + message: '', + overwrite: false, + folderUid, + } as const; +} diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 82d7c605e83..769467c59af 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -270,8 +270,9 @@ export const Pages = { interval: 'Playlist interval', itemDelete: 'Delete playlist item', }, - BrowseDashbards: { + BrowseDashboards: { table: { + body: 'data-testid browse-dashboards-table', row: (uid: string) => `data-testid ${uid} row`, checkbox: (uid: string) => `data-testid ${uid} checkbox`, }, diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx index bd15896c0b1..22e331ae3c8 100644 --- a/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx +++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.test.tsx @@ -214,7 +214,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { it('selecting an item hides the filters and shows the actions instead', async () => { render(); - const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid)); + const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid)); await userEvent.click(checkbox); // Check the filters are now hidden @@ -229,7 +229,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { it('navigating into a child item resets the selected state', async () => { const { rerender } = render(); - const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(folderA.item.uid)); + const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid)); await userEvent.click(checkbox); // Check the actions are now visible @@ -340,7 +340,7 @@ describe('browse-dashboards BrowseDashboardsPage', () => { render(); const checkbox = await screen.findByTestId( - selectors.pages.BrowseDashbards.table.checkbox(folderA_folderA.item.uid) + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderA.item.uid) ); await userEvent.click(checkbox); diff --git a/public/app/features/browse-dashboards/components/BrowseView.test.tsx b/public/app/features/browse-dashboards/components/BrowseView.test.tsx index b0a1ade87f1..34271fdbc70 100644 --- a/public/app/features/browse-dashboards/components/BrowseView.test.tsx +++ b/public/app/features/browse-dashboards/components/BrowseView.test.tsx @@ -57,7 +57,7 @@ describe('browse-dashboards BrowseView', () => { it('checks items when selected', async () => { render(); - const checkbox = await screen.findByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashbdD.item.uid)); + const checkbox = await screen.findByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashbdD.item.uid)); expect(checkbox).not.toBeChecked(); await userEvent.click(checkbox); @@ -76,7 +76,7 @@ describe('browse-dashboards BrowseView', () => { const directChildren = mockTree.filter((v) => v.item.kind !== 'ui' && v.item.parentUID === folderA.item.uid); for (const child of directChildren) { - const childCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(child.item.uid)); + const childCheckbox = screen.queryByTestId(selectors.pages.BrowseDashboards.table.checkbox(child.item.uid)); expect(childCheckbox).toBeChecked(); } }); @@ -96,7 +96,7 @@ describe('browse-dashboards BrowseView', () => { const grandchildren = mockTree.filter((v) => v.item.kind !== 'ui' && v.item.parentUID === folderA_folderB.item.uid); for (const child of grandchildren) { - const childCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(child.item.uid)); + const childCheckbox = screen.queryByTestId(selectors.pages.BrowseDashboards.table.checkbox(child.item.uid)); expect(childCheckbox).toBeChecked(); } }); @@ -112,16 +112,16 @@ describe('browse-dashboards BrowseView', () => { await clickCheckbox(folderA_folderB_dashbdB.item.uid); const itemCheckbox = screen.queryByTestId( - selectors.pages.BrowseDashbards.table.checkbox(folderA_folderB_dashbdB.item.uid) + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderB_dashbdB.item.uid) ); expect(itemCheckbox).not.toBeChecked(); const parentCheckbox = screen.queryByTestId( - selectors.pages.BrowseDashbards.table.checkbox(folderA_folderB.item.uid) + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderB.item.uid) ); expect(parentCheckbox).not.toBeChecked(); - const grandparentCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(folderA.item.uid)); + const grandparentCheckbox = screen.queryByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid)); expect(grandparentCheckbox).not.toBeChecked(); }); @@ -135,12 +135,12 @@ describe('browse-dashboards BrowseView', () => { await clickCheckbox(folderA_folderB_dashbdB.item.uid); const parentCheckbox = screen.queryByTestId( - selectors.pages.BrowseDashbards.table.checkbox(folderA_folderB.item.uid) + selectors.pages.BrowseDashboards.table.checkbox(folderA_folderB.item.uid) ); expect(parentCheckbox).not.toBeChecked(); expect(parentCheckbox).toBePartiallyChecked(); - const grandparentCheckbox = screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(folderA.item.uid)); + const grandparentCheckbox = screen.queryByTestId(selectors.pages.BrowseDashboards.table.checkbox(folderA.item.uid)); expect(grandparentCheckbox).not.toBeChecked(); expect(grandparentCheckbox).toBePartiallyChecked(); }); @@ -159,18 +159,18 @@ describe('browse-dashboards BrowseView', () => { }); async function expandFolder(uid: string) { - const row = screen.getByTestId(selectors.pages.BrowseDashbards.table.row(uid)); - const expandButton = getByLabelText(row, 'Expand folder'); + const row = screen.getByTestId(selectors.pages.BrowseDashboards.table.row(uid)); + const expandButton = getByLabelText(row, /Expand folder/); await userEvent.click(expandButton); } async function collapseFolder(uid: string) { - const row = screen.getByTestId(selectors.pages.BrowseDashbards.table.row(uid)); - const expandButton = getByLabelText(row, 'Collapse folder'); + const row = screen.getByTestId(selectors.pages.BrowseDashboards.table.row(uid)); + const expandButton = getByLabelText(row, /Collapse folder/); await userEvent.click(expandButton); } async function clickCheckbox(uid: string) { - const checkbox = screen.getByTestId(selectors.pages.BrowseDashbards.table.checkbox(uid)); + const checkbox = screen.getByTestId(selectors.pages.BrowseDashboards.table.checkbox(uid)); await userEvent.click(checkbox); } diff --git a/public/app/features/browse-dashboards/components/CheckboxCell.tsx b/public/app/features/browse-dashboards/components/CheckboxCell.tsx index d00cb921c0e..3338f0436e5 100644 --- a/public/app/features/browse-dashboards/components/CheckboxCell.tsx +++ b/public/app/features/browse-dashboards/components/CheckboxCell.tsx @@ -31,7 +31,7 @@ export default function CheckboxCell({ return ( onItemSelectionChange?.(item, ev.currentTarget.checked)} diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx index 20709235e0b..8a58cd9c421 100644 --- a/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx +++ b/public/app/features/browse-dashboards/components/DashboardsTree.test.tsx @@ -44,7 +44,7 @@ describe('browse-dashboards DashboardsTree', () => { ); expect(screen.queryByText(dashboard.item.title)).toBeInTheDocument(); expect(screen.queryByText(assertIsDefined(dashboard.item.tags)[0])).toBeInTheDocument(); - expect(screen.getByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashboard.item.uid))).toBeInTheDocument(); + expect(screen.getByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashboard.item.uid))).toBeInTheDocument(); }); it('does not render checkbox when disabled', () => { @@ -63,7 +63,7 @@ describe('browse-dashboards DashboardsTree', () => { /> ); expect( - screen.queryByTestId(selectors.pages.BrowseDashbards.table.checkbox(dashboard.item.uid)) + screen.queryByTestId(selectors.pages.BrowseDashboards.table.checkbox(dashboard.item.uid)) ).not.toBeInTheDocument(); }); @@ -101,7 +101,7 @@ describe('browse-dashboards DashboardsTree', () => { requestLoadMore={requestLoadMore} /> ); - const folderButton = screen.getByLabelText('Expand folder'); + const folderButton = screen.getByLabelText(`Expand folder ${folder.item.title}`); await userEvent.click(folderButton); expect(handler).toHaveBeenCalledWith(folder.item.uid, true); diff --git a/public/app/features/browse-dashboards/components/DashboardsTree.tsx b/public/app/features/browse-dashboards/components/DashboardsTree.tsx index a44ef0b3a0b..5623f29b99c 100644 --- a/public/app/features/browse-dashboards/components/DashboardsTree.tsx +++ b/public/app/features/browse-dashboards/components/DashboardsTree.tsx @@ -147,7 +147,7 @@ export function DashboardsTree({ ); })} -
+
{row.cells.map((cell) => { const { key, ...cellProps } = cell.getCellProps(); diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx index 2ea5e119cfe..b1762839cf6 100644 --- a/public/app/features/browse-dashboards/components/NameCell.tsx +++ b/public/app/features/browse-dashboards/components/NameCell.tsx @@ -59,7 +59,7 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro onFolderClick(item.uid, !isOpen); }} name={isOpen ? 'angle-down' : 'angle-right'} - aria-label={isOpen ? 'Collapse folder' : 'Expand folder'} + aria-label={isOpen ? `Collapse folder ${item.title}` : `Expand folder ${item.title}`} /> ) : (