diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx index f9d9b924878..f470f18e50a 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.test.tsx @@ -19,10 +19,12 @@ import fetchMock from 'fetch-mock'; import { QueryFormData, SupersetClient } from '@superset-ui/core'; import { + fireEvent, render, screen, userEvent, waitFor, + within, } from 'spec/helpers/testing-library'; import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore'; import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries'; @@ -119,6 +121,34 @@ const fetchWithData = () => { }); }; +const fetchWithPaginatedData = () => { + setupDatasetEndpoint(); + fetchMock.post(SAMPLES_ENDPOINT, { + result: { + total_count: 100, + data: [ + { + year: 1996, + na_sales: 11.27, + eu_sales: 8.89, + }, + { + year: 1989, + na_sales: 23.2, + eu_sales: 2.26, + }, + { + year: 1999, + na_sales: 9, + eu_sales: 6.18, + }, + ], + colnames: ['year', 'na_sales', 'eu_sales'], + coltypes: [0, 0, 0], + }, + }); +}; + afterEach(() => { fetchMock.clearHistory().removeRoutes(); supersetGetCache.clear(); @@ -254,6 +284,54 @@ describe('download actions', () => { }); }); +test('should render pagination when results exceed page size', async () => { + // The "should render the error" test above leaves a SupersetClient.post + // rejection spy active (matching the existing pattern; "should use + // verbose_map" further down does the same cleanup). Reset it here so the + // fetch in this test actually returns data. + jest.restoreAllMocks(); + fetchWithPaginatedData(); + await waitForRender(); + // With total_count=100 and page size=50, pagination should render + await waitFor(() => { + const pagination = document.querySelector('.ant-pagination'); + expect(pagination).toBeTruthy(); + }); +}); + +test('should offer the full set of page-size options', async () => { + fetchWithPaginatedData(); + await waitForRender(); + + // The page-size changer renders as an antd Select. In jsdom, antd opens + // its overlay on mouseDown of the .ant-select-selector element rather + // than via a click on the inner combobox input. + const selector = await waitFor(() => { + const el = document.querySelector( + '.ant-pagination-options-size-changer .ant-select-selector', + ) as HTMLElement | null; + expect(el).toBeTruthy(); + return el!; + }); + fireEvent.mouseDown(selector); + + // The opened listbox lives in a body portal; collect its options and assert + // exactly the canonical [5, 15, 25, 50, 100] set is offered. Without this + // guard, regressing to a single hardcoded option (the pre-rework approach) + // would silently pass CI. + const listbox = await screen.findByRole('listbox'); + const offeredSizes = within(listbox) + .getAllByRole('option') + .map(el => el.getAttribute('title')); + expect(offeredSizes).toEqual([ + '5 / page', + '15 / page', + '25 / page', + '50 / page', + '100 / page', + ]); +}); + test('should use verbose_map for column headers when available', async () => { jest.restoreAllMocks(); diff --git a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx index 9326b63b485..901f9d2421b 100644 --- a/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx +++ b/superset-frontend/src/components/Chart/DrillDetail/DrillDetailPane.tsx @@ -60,7 +60,7 @@ import { getDrillPayload } from './utils'; import { ResultsPage } from './types'; import { datasetLabelLower } from 'src/features/semanticLayers/label'; -const PAGE_SIZE = 50; +const DEFAULT_PAGE_SIZE = 50; interface DataType { [key: string]: any; @@ -94,6 +94,7 @@ export default function DrillDetailPane({ }) { const theme = useTheme(); const [pageIndex, setPageIndex] = useState(0); + const [pageSize, setPageSize] = useState(DEFAULT_PAGE_SIZE); const lastPageIndex = useRef(pageIndex); const [filters, setFilters] = useState(initialFilters); const [isLoading, setIsLoading] = useState(false); @@ -307,13 +308,13 @@ export default function DrillDetailPane({ if (!responseError && !isLoading && !resultsPages.has(pageIndex)) { setIsLoading(true); const jsonPayload = getDrillPayload(formData, filters) ?? {}; - const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / PAGE_SIZE); + const cachePageLimit = Math.ceil(SAMPLES_ROW_LIMIT / pageSize); getDatasourceSamples( datasourceType as DatasourceType, Number(datasourceId), false, jsonPayload, - PAGE_SIZE, + pageSize, pageIndex + 1, dashboardId, ) @@ -349,6 +350,7 @@ export default function DrillDetailPane({ formData, isLoading, pageIndex, + pageSize, responseError, resultsPages, ]); @@ -384,13 +386,20 @@ export default function DrillDetailPane({ data={data} columns={mappedColumns} size={TableSize.Small} - defaultPageSize={PAGE_SIZE} + defaultPageSize={DEFAULT_PAGE_SIZE} recordCount={resultsPage?.total} usePagination loading={isLoading} - onChange={pagination => - setPageIndex(pagination.current ? pagination.current - 1 : 0) - } + onChange={pagination => { + const newPageSize = pagination.pageSize ?? pageSize; + if (newPageSize !== pageSize) { + setPageSize(newPageSize); + setResultsPages(new Map()); + setPageIndex(0); + } else { + setPageIndex(pagination.current ? pagination.current - 1 : 0); + } + }} resizable virtualize allowHTML={allowHTML}