You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2025/09/12 21:15:11 UTC
(superset) branch master updated: fix(ListView): implement AntD pagination for ListView component (#35057)
This is an automated email from the ASF dual-hosted git repository.
elizabeth pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git
The following commit(s) were added to refs/heads/master by this push:
new 36daa2dc3f fix(ListView): implement AntD pagination for ListView component (#35057)
36daa2dc3f is described below
commit 36daa2dc3f9cebd6ff09c646b2faa19f3cec03c1
Author: Gabriel Torres Ruiz <ga...@gmail.com>
AuthorDate: Fri Sep 12 18:15:05 2025 -0300
fix(ListView): implement AntD pagination for ListView component (#35057)
---
.../cypress/e2e/dashboard/drillby.test.ts | 766 ---------------------
superset-frontend/package-lock.json | 11 -
superset-frontend/package.json | 1 -
.../src/components/Pagination/Ellipsis.test.tsx | 37 -
.../src/components/Pagination/Ellipsis.tsx | 38 -
.../src/components/Pagination/Item.test.tsx | 47 --
.../src/components/Pagination/Item.tsx | 45 --
.../src/components/Pagination/Next.test.tsx | 37 -
.../src/components/Pagination/Next.tsx | 38 -
.../src/components/Pagination/Prev.test.tsx | 37 -
.../src/components/Pagination/Prev.tsx | 38 -
.../src/components/Pagination/Wrapper.test.tsx | 75 --
.../src/components/Pagination/Wrapper.tsx | 90 ---
.../src/components/Pagination/index.tsx | 47 --
.../src/components/Pagination/types.ts | 25 -
.../TableCollection/TableCollection.test.tsx | 106 +++
.../src/components/TableCollection/index.tsx | 141 +++-
.../src/components/TableView/TableView.test.tsx | 206 +++++-
.../src/components/TableView/TableView.tsx | 233 ++++---
.../components/Chart/DrillBy/DrillByModal.test.tsx | 212 ++++++
.../src/components/FilterableTable/index.tsx | 1 -
.../src/components/ListView/ListView.test.jsx | 31 +-
.../src/components/ListView/ListView.tsx | 27 +-
superset-frontend/src/components/ListView/utils.ts | 27 +-
.../DataTablesPane/components/SamplesPane.tsx | 11 +-
.../components/SingleQueryResultPane.tsx | 9 +-
.../DataTablesPane/components/useResultsPane.tsx | 37 +-
27 files changed, 832 insertions(+), 1541 deletions(-)
diff --git a/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts b/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts
deleted file mode 100644
index edc44a6fdb..0000000000
--- a/superset-frontend/cypress-base/cypress/e2e/dashboard/drillby.test.ts
+++ /dev/null
@@ -1,766 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-// eslint-disable-next-line import/no-extraneous-dependencies
-import { Interception } from 'cypress/types/net-stubbing';
-import { waitForChartLoad } from 'cypress/utils';
-import { SUPPORTED_CHARTS_DASHBOARD } from 'cypress/utils/urls';
-import {
- openTopLevelTab,
- SUPPORTED_TIER1_CHARTS,
- SUPPORTED_TIER2_CHARTS,
-} from './utils';
-import {
- interceptExploreJson,
- interceptV1ChartData,
- interceptFormDataKey,
-} from '../explore/utils';
-
-const interceptDrillInfo = () => {
- cy.intercept('GET', '**/api/v1/dataset/*/drill_info/*', {
- statusCode: 200,
- body: {
- result: {
- id: 1,
- changed_on_humanized: '2 days ago',
- created_on_humanized: 'a week ago',
- table_name: 'birth_names',
- changed_by: {
- first_name: 'Admin',
- last_name: 'User',
- },
- created_by: {
- first_name: 'Admin',
- last_name: 'User',
- },
- owners: [
- {
- first_name: 'Admin',
- last_name: 'User',
- },
- ],
- columns: [
- {
- column_name: 'gender',
- verbose_name: null,
- },
- {
- column_name: 'state',
- verbose_name: null,
- },
- {
- column_name: 'name',
- verbose_name: null,
- },
- {
- column_name: 'ds',
- verbose_name: null,
- },
- ],
- },
- },
- }).as('drillInfo');
-};
-
-const closeModal = () => {
- cy.get('body').then($body => {
- if ($body.find('[data-test="close-drill-by-modal"]').length) {
- cy.getBySel('close-drill-by-modal').click({ force: true });
- }
- });
-};
-
-const openTableContextMenu = (
- cellContent: string,
- tableSelector = "[data-test-viz-type='table']",
-) => {
- cy.get(tableSelector).scrollIntoView();
- cy.get(tableSelector).contains(cellContent).first().rightclick();
-};
-
-const drillBy = (targetDrillByColumn: string, isLegacy = false) => {
- if (isLegacy) {
- interceptExploreJson('legacyData');
- } else {
- interceptV1ChartData();
- }
-
- cy.get('.ant-dropdown:not(.ant-dropdown-hidden)', { timeout: 15000 })
- .should('be.visible')
- .find("[role='menu'] [role='menuitem']")
- .contains(/^Drill by$/)
- .trigger('mouseover', { force: true });
-
- cy.get(
- '.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
- { timeout: 15000 },
- )
- .should('be.visible')
- .find('[role="menuitem"]')
- .contains(new RegExp(`^${targetDrillByColumn}$`))
- .click();
-
- cy.get(
- '.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
- ).trigger('mouseout', { clientX: 0, clientY: 0, force: true });
-
- cy.get(
- '.ant-dropdown-menu-submenu:not(.ant-dropdown-menu-submenu-hidden) [data-test="drill-by-submenu"]',
- ).should('not.exist');
-
- if (isLegacy) {
- return cy.wait('@legacyData');
- }
- return cy.wait('@v1Data');
-};
-
-const verifyExpectedFormData = (
- interceptedRequest: Interception,
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- expectedFormData: Record<string, any>,
-) => {
- const actualFormData = interceptedRequest.request.body?.form_data;
- Object.entries(expectedFormData).forEach(([key, val]) => {
- expect(actualFormData?.[key]).to.eql(val);
- });
-};
-
-const testEchart = (
- vizType: string,
- chartName: string,
- drillClickCoordinates: [[number, number], [number, number]],
- furtherDrillDimension = 'name',
-) => {
- cy.get(`[data-test-viz-type='${vizType}'] canvas`).then($canvas => {
- // click 'boy'
- cy.wrap($canvas).scrollIntoView();
- cy.wrap($canvas).trigger(
- 'mouseover',
- drillClickCoordinates[0][0],
- drillClickCoordinates[0][1],
- );
- cy.wrap($canvas).rightclick(
- drillClickCoordinates[0][0],
- drillClickCoordinates[0][1],
- );
-
- drillBy('state').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupby: ['state'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- ],
- });
- });
-
- cy.getBySel(`"Drill by: ${chartName}-modal"`).as('drillByModal');
-
- cy.get('@drillByModal')
- .find('.draggable-trigger')
- .should('contain', chartName);
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'state');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible');
-
- // further drill
- cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
- // click 'other'
- cy.wrap($canvas).scrollIntoView();
- cy.wrap($canvas).trigger(
- 'mouseover',
- drillClickCoordinates[1][0],
- drillClickCoordinates[1][1],
- );
- cy.wrap($canvas).rightclick(
- drillClickCoordinates[1][0],
- drillClickCoordinates[1][1],
- );
-
- drillBy(furtherDrillDimension).then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupby: [furtherDrillDimension],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- {
- clause: 'WHERE',
- comparator: 'other',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'state',
- },
- ],
- });
- });
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible');
-
- // undo - back to drill by state
- interceptV1ChartData('drillByUndo');
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'state (other)')
- .and('contain', furtherDrillDimension)
- .contains('state (other)')
- .click();
- cy.wait('@drillByUndo').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupby: ['state'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- ],
- });
- });
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('not.contain', 'state (other)')
- .and('not.contain', furtherDrillDimension)
- .and('contain', 'state');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible');
- });
- });
-};
-
-describe('Drill by modal', () => {
- beforeEach(() => {
- closeModal();
- });
- before(() => {
- interceptDrillInfo();
- cy.visit(SUPPORTED_CHARTS_DASHBOARD);
- });
-
- describe('Modal actions + Table', () => {
- before(() => {
- closeModal();
- interceptDrillInfo();
- openTopLevelTab('Tier 1');
- SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
- });
-
- it.only('opens the modal from the context menu', () => {
- openTableContextMenu('boy');
- drillBy('state').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupby: ['state'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- ],
- });
- });
-
- cy.getBySel('"Drill by: Table-modal"').as('drillByModal');
-
- cy.get('@drillByModal')
- .find('.draggable-trigger')
- .should('contain', 'Drill by: Table');
-
- cy.get('@drillByModal')
- .find('[data-test="metadata-bar"]')
- .should('be.visible');
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'state');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible')
- .and('contain', 'state')
- .and('contain', 'sum__num');
-
- // further drilling
- openTableContextMenu('CA', '[data-test="drill-by-chart"]');
- drillBy('name').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupby: ['name'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- {
- clause: 'WHERE',
- comparator: 'CA',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'state',
- },
- ],
- });
- });
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible')
- .and('not.contain', 'state')
- .and('contain', 'name')
- .and('contain', 'sum__num');
-
- // undo - back to drill by state
- interceptV1ChartData('drillByUndo');
- interceptFormDataKey();
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'state (CA)')
- .and('contain', 'name')
- .contains('state (CA)')
- .click();
- cy.wait('@drillByUndo').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupby: ['state'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- ],
- });
- });
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible')
- .and('not.contain', 'name')
- .and('contain', 'state')
- .and('contain', 'sum__num');
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('not.contain', 'state (CA)')
- .and('not.contain', 'name')
- .and('contain', 'state');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-display-toggle"]')
- .contains('Table')
- .click();
-
- cy.getBySel('drill-by-chart').should('not.exist');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-results-table"]')
- .should('be.visible');
-
- cy.wait('@formDataKey').then(intercept => {
- cy.get('@drillByModal')
- .contains('Edit chart')
- .should('have.attr', 'href')
- .and(
- 'contain',
- `/explore/?form_data_key=${intercept.response?.body?.key}`,
- );
- });
- });
- });
-
- describe('Tier 1 charts', () => {
- before(() => {
- closeModal();
- interceptDrillInfo();
- openTopLevelTab('Tier 1');
- SUPPORTED_TIER1_CHARTS.forEach(waitForChartLoad);
- });
-
- it('Pivot Table', () => {
- openTableContextMenu('boy', "[data-test-viz-type='pivot_table_v2']");
- drillBy('name').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupbyRows: ['state'],
- groupbyColumns: ['name'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- ],
- });
- });
-
- cy.getBySel('"Drill by: Pivot Table-modal"').as('drillByModal');
-
- cy.get('@drillByModal')
- .find('.draggable-trigger')
- .should('contain', 'Drill by: Pivot Table');
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'name');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible')
- .and('contain', 'state')
- .and('contain', 'name')
- .and('contain', 'sum__num')
- .and('not.contain', 'Gender');
-
- openTableContextMenu('CA', '[data-test="drill-by-chart"]');
- drillBy('ds').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupbyColumns: ['name'],
- groupbyRows: ['ds'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- {
- clause: 'WHERE',
- comparator: 'CA',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'state',
- },
- ],
- });
- });
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible')
- .and('contain', 'name')
- .and('contain', 'ds')
- .and('contain', 'sum__num')
- .and('not.contain', 'state');
-
- interceptV1ChartData('drillByUndo');
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'name (CA)')
- .and('contain', 'ds')
- .contains('name (CA)')
- .click();
- cy.wait('@drillByUndo').then(intercepted => {
- verifyExpectedFormData(intercepted, {
- groupbyRows: ['state'],
- groupbyColumns: ['name'],
- adhoc_filters: [
- {
- clause: 'WHERE',
- comparator: 'boy',
- expressionType: 'SIMPLE',
- operator: '==',
- operatorId: 'EQUALS',
- subject: 'gender',
- },
- ],
- });
- });
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible')
- .and('not.contain', 'ds')
- .and('contain', 'state')
- .and('contain', 'name')
- .and('contain', 'sum__num');
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('not.contain', 'name (CA)')
- .and('not.contain', 'ds')
- .and('contain', 'name');
- });
-
- it('Line chart', () => {
- testEchart('echarts_timeseries_line', 'Line Chart', [
- [85, 93],
- [85, 93],
- ]);
- });
-
- it('Area Chart', () => {
- testEchart('echarts_area', 'Area Chart', [
- [85, 93],
- [85, 93],
- ]);
- });
-
- it('Scatter Chart', () => {
- testEchart('echarts_timeseries_scatter', 'Scatter Chart', [
- [85, 93],
- [85, 93],
- ]);
- });
-
- it.skip('Bar Chart', () => {
- testEchart('echarts_timeseries_bar', 'Bar Chart', [
- [85, 94],
- [490, 68],
- ]);
- });
-
- it('Pie Chart', () => {
- testEchart('pie', 'Pie Chart', [
- [243, 167],
- [534, 248],
- ]);
- });
- });
-
- describe('Tier 2 charts', () => {
- before(() => {
- closeModal();
- interceptDrillInfo();
- openTopLevelTab('Tier 2');
- SUPPORTED_TIER2_CHARTS.forEach(waitForChartLoad);
- });
-
- it('Box Plot Chart', () => {
- testEchart(
- 'box_plot',
- 'Box Plot Chart',
- [
- [139, 277],
- [787, 441],
- ],
- 'ds',
- );
- });
-
- it('Generic Chart', () => {
- testEchart('echarts_timeseries', 'Generic Chart', [
- [85, 93],
- [85, 93],
- ]);
- });
-
- it('Smooth Line Chart', () => {
- testEchart('echarts_timeseries_smooth', 'Smooth Line Chart', [
- [85, 93],
- [85, 93],
- ]);
- });
-
- it('Step Line Chart', () => {
- testEchart('echarts_timeseries_step', 'Step Line Chart', [
- [85, 93],
- [85, 93],
- ]);
- });
-
- it('Funnel Chart', () => {
- testEchart('funnel', 'Funnel Chart', [
- [154, 80],
- [421, 39],
- ]);
- });
-
- it('Gauge Chart', () => {
- testEchart('gauge_chart', 'Gauge Chart', [
- [151, 95],
- [300, 143],
- ]);
- });
-
- it.skip('Radar Chart', () => {
- testEchart('radar', 'Radar Chart', [
- [182, 49],
- [423, 91],
- ]);
- });
-
- it('Treemap V2 Chart', () => {
- testEchart('treemap_v2', 'Treemap V2 Chart', [
- [145, 84],
- [220, 105],
- ]);
- });
-
- it.skip('Mixed Chart', () => {
- cy.get('[data-test-viz-type="mixed_timeseries"] canvas').then($canvas => {
- // click 'boy'
- cy.wrap($canvas).scrollIntoView();
- cy.wrap($canvas).trigger('mouseover', 85, 93);
- cy.wrap($canvas).rightclick(85, 93);
-
- drillBy('name').then(intercepted => {
- const { queries } = intercepted.request.body;
- expect(queries[0].columns).to.eql(['name']);
- expect(queries[0].filters).to.eql([
- { col: 'gender', op: '==', val: 'boy' },
- ]);
- expect(queries[1].columns).to.eql(['state']);
- expect(queries[1].filters).to.eql([]);
- });
-
- cy.getBySel('"Drill by: Mixed Chart-modal"').as('drillByModal');
-
- cy.get('@drillByModal')
- .find('.draggable-trigger')
- .should('contain', 'Mixed Chart');
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'name');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible');
-
- // further drill
- cy.get(`[data-test="drill-by-chart"] canvas`).then($canvas => {
- // click second query
- cy.wrap($canvas).scrollIntoView();
- cy.wrap($canvas).trigger('mouseover', 261, 114);
- cy.wrap($canvas).rightclick(261, 114);
-
- drillBy('ds').then(intercepted => {
- const { queries } = intercepted.request.body;
- expect(queries[0].columns).to.eql(['name']);
- expect(queries[0].filters).to.eql([
- { col: 'gender', op: '==', val: 'boy' },
- ]);
- expect(queries[1].columns).to.eql(['ds']);
- expect(queries[1].filters).to.eql([
- { col: 'state', op: '==', val: 'other' },
- ]);
- });
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible');
-
- // undo - back to drill by state
- interceptV1ChartData('drillByUndo');
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('contain', 'name (other)')
- .and('contain', 'ds')
- .contains('name (other)')
- .click();
-
- cy.wait('@drillByUndo').then(intercepted => {
- const { queries } = intercepted.request.body;
- expect(queries[0].columns).to.eql(['name']);
- expect(queries[0].filters).to.eql([
- { col: 'gender', op: '==', val: 'boy' },
- ]);
- expect(queries[1].columns).to.eql(['state']);
- expect(queries[1].filters).to.eql([]);
- });
-
- cy.get('@drillByModal')
- .find('.ant-breadcrumb')
- .should('be.visible')
- .and('contain', 'gender (boy)')
- .and('contain', '/')
- .and('not.contain', 'name (other)')
- .and('not.contain', 'ds')
- .and('contain', 'name');
-
- cy.get('@drillByModal')
- .find('[data-test="drill-by-chart"]')
- .should('be.visible');
- });
- });
- });
- });
-});
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index d4c0646fbb..207cb69f11 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -191,7 +191,6 @@
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
- "@types/react-ultimate-pagination": "^1.2.4",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"@types/redux-localstorage": "^1.0.8",
@@ -16078,16 +16077,6 @@
"@types/react": "*"
}
},
- "node_modules/@types/react-ultimate-pagination": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@types/react-ultimate-pagination/-/react-ultimate-pagination-1.2.4.tgz",
- "integrity": "sha512-1y9jLt3KEFGzFD+99qVpJUI/Eu4cEx48sClB957eGoepWRLVVi+r1UBj0157Mg7HYZcIF4I1/qGZYaBBQWhaqg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/react": "*"
- }
- },
"node_modules/@types/react-virtualized-auto-sizer": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.8.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index 8ab2eb6408..39920371e1 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -259,7 +259,6 @@
"@types/react-resizable": "^3.0.8",
"@types/react-router-dom": "^5.3.3",
"@types/react-transition-group": "^4.4.12",
- "@types/react-ultimate-pagination": "^1.2.4",
"@types/react-virtualized-auto-sizer": "^1.0.8",
"@types/react-window": "^1.8.8",
"@types/redux-localstorage": "^1.0.8",
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Ellipsis.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Ellipsis.test.tsx
deleted file mode 100644
index bb26d7848e..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Ellipsis.test.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { render, screen, userEvent } from '@superset-ui/core/spec';
-import { Ellipsis } from './Ellipsis';
-
-test('Ellipsis - click when the button is enabled', async () => {
- const click = jest.fn();
- render(<Ellipsis onClick={click} />);
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(1);
-});
-
-test('Ellipsis - click when the button is disabled', async () => {
- const click = jest.fn();
- render(<Ellipsis onClick={click} disabled />);
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(0);
-});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Ellipsis.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Ellipsis.tsx
deleted file mode 100644
index 4f1e744cd1..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Ellipsis.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import classNames from 'classnames';
-import { PaginationButtonProps } from './types';
-
-export function Ellipsis({ disabled, onClick }: PaginationButtonProps) {
- return (
- <li className={classNames({ disabled })}>
- <span
- role="button"
- tabIndex={disabled ? -1 : 0}
- onClick={e => {
- e.preventDefault();
- if (!disabled) onClick(e);
- }}
- >
- …
- </span>
- </li>
- );
-}
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Item.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Item.test.tsx
deleted file mode 100644
index 9be49cf68c..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Item.test.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { render, screen, userEvent } from '@superset-ui/core/spec';
-import { Item } from './Item';
-
-test('Item - click when the item is not active', async () => {
- const click = jest.fn();
- render(
- <Item onClick={click}>
- <div data-test="test" />
- </Item>,
- );
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(1);
- expect(screen.getByTestId('test')).toBeInTheDocument();
-});
-
-test('Item - click when the item is active', async () => {
- const click = jest.fn();
- render(
- <Item onClick={click} active>
- <div data-test="test" />
- </Item>,
- );
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(0);
- expect(screen.getByTestId('test')).toBeInTheDocument();
-});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Item.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Item.tsx
deleted file mode 100644
index b71da3c939..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Item.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { ReactNode } from 'react';
-import classNames from 'classnames';
-import { PaginationButtonProps } from './types';
-
-interface PaginationItemButton extends PaginationButtonProps {
- active?: boolean;
- children: ReactNode;
-}
-
-export function Item({ active, children, onClick }: PaginationItemButton) {
- return (
- <li className={classNames({ active })}>
- <span
- role="button"
- tabIndex={0}
- aria-current={active ? 'page' : undefined}
- onClick={e => {
- e.preventDefault();
- if (!active) onClick(e);
- }}
- >
- {children}
- </span>
- </li>
- );
-}
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Next.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Next.test.tsx
deleted file mode 100644
index 7b17857ff4..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Next.test.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { render, screen, userEvent } from '@superset-ui/core/spec';
-import { Next } from './Next';
-
-test('Next - click when the button is enabled', async () => {
- const click = jest.fn();
- render(<Next onClick={click} />);
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(1);
-});
-
-test('Next - click when the button is disabled', async () => {
- const click = jest.fn();
- render(<Next onClick={click} disabled />);
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(0);
-});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Next.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Next.tsx
deleted file mode 100644
index 689bc479b6..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Next.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import classNames from 'classnames';
-import { PaginationButtonProps } from './types';
-
-export function Next({ disabled, onClick }: PaginationButtonProps) {
- return (
- <li className={classNames({ disabled })}>
- <span
- role="button"
- tabIndex={disabled ? -1 : 0}
- onClick={e => {
- e.preventDefault();
- if (!disabled) onClick(e);
- }}
- >
- »
- </span>
- </li>
- );
-}
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Prev.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Prev.test.tsx
deleted file mode 100644
index 576817b377..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Prev.test.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { render, screen, userEvent } from '@superset-ui/core/spec';
-import { Prev } from './Prev';
-
-test('Prev - click when the button is enabled', async () => {
- const click = jest.fn();
- render(<Prev onClick={click} />);
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(1);
-});
-
-test('Prev - click when the button is disabled', async () => {
- const click = jest.fn();
- render(<Prev onClick={click} disabled />);
- expect(click).toHaveBeenCalledTimes(0);
- await userEvent.click(screen.getByRole('button'));
- expect(click).toHaveBeenCalledTimes(0);
-});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Prev.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Prev.tsx
deleted file mode 100644
index 0de334ae74..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Prev.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import classNames from 'classnames';
-import { PaginationButtonProps } from './types';
-
-export function Prev({ disabled, onClick }: PaginationButtonProps) {
- return (
- <li className={classNames({ disabled })}>
- <span
- role="button"
- tabIndex={disabled ? -1 : 0}
- onClick={e => {
- e.preventDefault();
- if (!disabled) onClick(e);
- }}
- >
- «
- </span>
- </li>
- );
-}
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Wrapper.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Wrapper.test.tsx
deleted file mode 100644
index 1dc61e59d7..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Wrapper.test.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { render, screen, cleanup } from '@superset-ui/core/spec';
-import Wrapper from './Wrapper';
-
-// Add cleanup after each test
-afterEach(async () => {
- cleanup();
- // Wait for any pending effects to complete
- await new Promise(resolve => setTimeout(resolve, 0));
-});
-
-jest.mock('./Next', () => ({
- Next: () => <div data-test="next" />,
-}));
-jest.mock('./Prev', () => ({
- Prev: () => <div data-test="prev" />,
-}));
-jest.mock('./Item', () => ({
- Item: () => <div data-test="item" />,
-}));
-jest.mock('./Ellipsis', () => ({
- Ellipsis: () => <div data-test="ellipsis" />,
-}));
-
-test('Pagination rendering correctly', async () => {
- render(
- <Wrapper>
- <li data-test="test" />
- </Wrapper>,
- );
- expect(screen.getByRole('navigation')).toBeInTheDocument();
- expect(screen.getByTestId('test')).toBeInTheDocument();
-});
-
-test('Next attribute', async () => {
- render(<Wrapper.Next onClick={jest.fn()} />);
- expect(screen.getByTestId('next')).toBeInTheDocument();
-});
-
-test('Prev attribute', async () => {
- render(<Wrapper.Next onClick={jest.fn()} />);
- expect(screen.getByTestId('next')).toBeInTheDocument();
-});
-
-test('Item attribute', async () => {
- render(
- <Wrapper.Item onClick={jest.fn()}>
- <></>
- </Wrapper.Item>,
- );
- expect(screen.getByTestId('item')).toBeInTheDocument();
-});
-
-test('Ellipsis attribute', async () => {
- render(<Wrapper.Ellipsis onClick={jest.fn()} />);
- expect(screen.getByTestId('ellipsis')).toBeInTheDocument();
-});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Wrapper.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/Wrapper.tsx
deleted file mode 100644
index a7ee66feac..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/Wrapper.tsx
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { styled } from '@superset-ui/core';
-import { Next } from './Next';
-import { Prev } from './Prev';
-import { Item } from './Item';
-import { Ellipsis } from './Ellipsis';
-
-interface PaginationProps {
- children: JSX.Element | JSX.Element[];
-}
-
-const PaginationList = styled.ul`
- ${({ theme }) => `
- display: inline-block;
- padding: ${theme.sizeUnit * 3}px;
-
- li {
- display: inline;
- margin: 0 4px;
-
- > span {
- padding: 8px 12px;
- text-decoration: none;
- background-color: ${theme.colorBgContainer};
- border: 1px solid ${theme.colorBorder};
- border-radius: ${theme.borderRadius}px;
- color: ${theme.colorText};
-
- &:hover,
- &:focus {
- z-index: 2;
- color: ${theme.colorText};
- background-color: ${theme.colorBgLayout};
- }
- }
-
- &.disabled {
- span {
- background-color: transparent;
- cursor: default;
-
- &:focus {
- outline: none;
- }
- }
- }
- &.active {
- span {
- z-index: 3;
- color: ${theme.colorBgLayout};
- cursor: default;
- background-color: ${theme.colorPrimary};
-
- &:focus {
- outline: none;
- }
- }
- }
- }
-`}
-`;
-
-function Pagination({ children }: PaginationProps) {
- return <PaginationList role="navigation">{children}</PaginationList>;
-}
-
-Pagination.Next = Next;
-Pagination.Prev = Prev;
-Pagination.Item = Item;
-Pagination.Ellipsis = Ellipsis;
-
-export default Pagination;
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx
deleted file mode 100644
index c1ba0da0df..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/index.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-import Pagination from '@superset-ui/core/components/Pagination/Wrapper';
-import {
- createUltimatePagination,
- ITEM_TYPES,
-} from 'react-ultimate-pagination';
-
-const ListViewPagination = createUltimatePagination({
- WrapperComponent: Pagination,
- itemTypeToComponent: {
- [ITEM_TYPES.PAGE]: ({ value, isActive, onClick }) => (
- <Pagination.Item active={isActive} onClick={onClick}>
- {value}
- </Pagination.Item>
- ),
- [ITEM_TYPES.ELLIPSIS]: ({ isActive, onClick }) => (
- <Pagination.Ellipsis disabled={isActive} onClick={onClick} />
- ),
- [ITEM_TYPES.PREVIOUS_PAGE_LINK]: ({ isActive, onClick }) => (
- <Pagination.Prev disabled={isActive} onClick={onClick} />
- ),
- [ITEM_TYPES.NEXT_PAGE_LINK]: ({ isActive, onClick }) => (
- <Pagination.Next disabled={isActive} onClick={onClick} />
- ),
- [ITEM_TYPES.FIRST_PAGE_LINK]: () => null,
- [ITEM_TYPES.LAST_PAGE_LINK]: () => null,
- },
-});
-
-export default ListViewPagination;
diff --git a/superset-frontend/packages/superset-ui-core/src/components/Pagination/types.ts b/superset-frontend/packages/superset-ui-core/src/components/Pagination/types.ts
deleted file mode 100644
index 483f88d5cd..0000000000
--- a/superset-frontend/packages/superset-ui-core/src/components/Pagination/types.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/**
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements. See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership. The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied. See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-import { EventHandler, SyntheticEvent } from 'react';
-
-export interface PaginationButtonProps {
- disabled?: boolean;
- onClick: EventHandler<SyntheticEvent<HTMLElement>>;
-}
diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx
index 52461fff66..a99b1d29e7 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx
@@ -100,3 +100,109 @@ test('Should the loading-indicator be visible during loading', () => {
expect(screen.getByTestId('loading-indicator')).toBeVisible();
});
+
+test('Pagination controls should be rendered when pageSize is provided', () => {
+ const paginationProps = {
+ ...defaultProps,
+ pageSize: 2,
+ totalCount: 3,
+ pageIndex: 0,
+ onPageChange: jest.fn(),
+ };
+ render(<TableCollection {...paginationProps} />);
+
+ expect(screen.getByRole('list')).toBeInTheDocument();
+});
+
+test('Pagination should call onPageChange when page is changed', async () => {
+ const onPageChange = jest.fn();
+ const paginationProps = {
+ ...defaultProps,
+ pageSize: 2,
+ totalCount: 3,
+ pageIndex: 0,
+ onPageChange,
+ };
+ const { rerender } = render(<TableCollection {...paginationProps} />);
+
+ // Simulate pagination change
+ await screen.findByTitle('Next Page');
+
+ // Verify onPageChange would be called with correct arguments
+ // The actual AntD pagination will handle the click internally
+ expect(onPageChange).toBeDefined();
+
+ // Verify that re-rendering with new pageIndex works
+ rerender(<TableCollection {...paginationProps} pageIndex={1} />);
+ expect(screen.getByRole('list')).toBeInTheDocument();
+});
+
+test('Pagination callback should be stable across re-renders', () => {
+ const onPageChange = jest.fn();
+ const paginationProps = {
+ ...defaultProps,
+ pageSize: 2,
+ totalCount: 3,
+ pageIndex: 0,
+ onPageChange,
+ };
+
+ const { rerender } = render(<TableCollection {...paginationProps} />);
+
+ // Re-render with same props
+ rerender(<TableCollection {...paginationProps} />);
+
+ // onPageChange should not have been called during re-render
+ expect(onPageChange).not.toHaveBeenCalled();
+});
+
+test('Should display correct page info when showRowCount is true', () => {
+ const paginationProps = {
+ ...defaultProps,
+ pageSize: 2,
+ totalCount: 3,
+ pageIndex: 0,
+ onPageChange: jest.fn(),
+ showRowCount: true,
+ };
+ render(<TableCollection {...paginationProps} />);
+
+ // AntD pagination shows page info
+ expect(screen.getByText('1-2 of 3')).toBeInTheDocument();
+});
+
+test('Should not display page info when showRowCount is false', () => {
+ const paginationProps = {
+ ...defaultProps,
+ pageSize: 2,
+ totalCount: 3,
+ pageIndex: 0,
+ onPageChange: jest.fn(),
+ showRowCount: false,
+ };
+ render(<TableCollection {...paginationProps} />);
+
+ // Page info should not be shown
+ expect(screen.queryByText('1-2 of 3')).not.toBeInTheDocument();
+});
+
+test('Bulk selection should work with pagination', () => {
+ const toggleRowSelected = jest.fn();
+ const toggleAllRowsSelected = jest.fn();
+ const selectionProps = {
+ ...defaultProps,
+ bulkSelectEnabled: true,
+ selectedFlatRows: [],
+ toggleRowSelected,
+ toggleAllRowsSelected,
+ pageSize: 2,
+ totalCount: 3,
+ pageIndex: 0,
+ onPageChange: jest.fn(),
+ };
+ render(<TableCollection {...selectionProps} />);
+
+ // Check that selection checkboxes are rendered
+ const checkboxes = screen.getAllByRole('checkbox');
+ expect(checkboxes.length).toBeGreaterThan(0);
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx
index 3f18719e68..9620e32361 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/index.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { HTMLAttributes, memo, useMemo } from 'react';
+import { HTMLAttributes, memo, useMemo, useCallback } from 'react';
import {
ColumnInstance,
HeaderGroup,
@@ -47,15 +47,25 @@ interface TableCollectionProps<T extends object> {
toggleAllRowsSelected?: (value?: boolean) => void;
sticky?: boolean;
size?: TableSize;
+ pageIndex?: number;
+ pageSize?: number;
+ totalCount?: number;
+ onPageChange?: (page: number, pageSize: number) => void;
+ isPaginationSticky?: boolean;
+ showRowCount?: boolean;
}
-const StyledTable = styled(Table)`
- ${({ theme }) => `
+const StyledTable = styled(Table)<{
+ isPaginationSticky?: boolean;
+ showRowCount?: boolean;
+}>`
+ ${({ theme, isPaginationSticky, showRowCount }) => `
th.ant-column-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
+
.actions {
opacity: 0;
font-size: ${theme.fontSizeXL}px;
@@ -72,15 +82,18 @@ const StyledTable = styled(Table)`
}
}
}
+
.ant-table-column-title {
line-height: initial;
}
+
.ant-table-row:hover {
.actions {
opacity: 1;
transition: opacity ease-in ${theme.motionDurationMid};
}
}
+
.ant-table-cell {
max-width: 320px;
font-feature-settings: 'tnum' 1;
@@ -91,10 +104,37 @@ const StyledTable = styled(Table)`
padding-left: ${theme.sizeUnit * 4}px;
white-space: nowrap;
}
+
.ant-table-placeholder .ant-table-cell {
border-bottom: 0;
}
+ &.ant-table-wrapper .ant-table-pagination.ant-pagination {
+ display: flex;
+ justify-content: center;
+ margin: ${showRowCount ? theme.sizeUnit * 4 : 0}px 0 ${showRowCount ? theme.sizeUnit * 14 : 0}px 0;
+ position: relative;
+
+ .ant-pagination-total-text {
+ color: ${theme.colorTextBase};
+ margin-inline-end: 0;
+ position: absolute;
+ top: ${theme.sizeUnit * 12}px;
+ }
+
+ ${
+ isPaginationSticky &&
+ `
+ position: sticky;
+ bottom: 0;
+ left: 0;
+ z-index: 1;
+ background-color: ${theme.colorBgElevated};
+ padding: ${theme.sizeUnit * 2}px 0;
+ `
+ }
+ }
+
// Hotfix - antd doesn't apply background color to overflowing cells
& table {
background-color: ${theme.colorBgContainer};
@@ -116,13 +156,22 @@ function TableCollection<T extends object>({
prepareRow,
sticky,
size = TableSize.Middle,
+ pageIndex = 0,
+ pageSize = 25,
+ totalCount = 0,
+ onPageChange,
+ isPaginationSticky = false,
+ showRowCount = true,
}: TableCollectionProps<T>) {
- const mappedColumns = mapColumns<T>(
- columns,
- headerGroups,
- columnsForWrapText,
+ const mappedColumns = useMemo(
+ () => mapColumns<T>(columns, headerGroups, columnsForWrapText),
+ [columns, headerGroups, columnsForWrapText],
+ );
+
+ const mappedRows = useMemo(
+ () => mapRows(rows, prepareRow),
+ [rows, prepareRow],
);
- const mappedRows = mapRows(rows, prepareRow);
const selectedRowKeys = useMemo(
() => selectedFlatRows?.map(row => row.id) || [],
@@ -147,6 +196,68 @@ function TableCollection<T extends object>({
toggleRowSelected,
toggleAllRowsSelected,
]);
+
+ const handlePaginationChange = useCallback(
+ (page: number, size: number) => {
+ const validPage = Math.max(0, (page || 1) - 1);
+ const validSize = size || pageSize;
+ onPageChange?.(validPage, validSize);
+ },
+ [pageSize, onPageChange],
+ );
+
+ const showTotalFunc = useCallback(
+ (total: number, range: [number, number]) =>
+ `${range[0]}-${range[1]} of ${total}`,
+ [],
+ );
+
+ const handleTableChange = useCallback(
+ (_pagination: any, _filters: any, sorter: SorterResult) => {
+ if (sorter && sorter.field) {
+ setSortBy?.([
+ {
+ id: sorter.field,
+ desc: sorter.order === 'descend',
+ },
+ ] as SortingRule<T>[]);
+ }
+ },
+ [setSortBy],
+ );
+
+ const paginationConfig = useMemo(() => {
+ if (totalCount === 0) return false;
+
+ const config: any = {
+ pageSize,
+ size: 'default' as const,
+ showSizeChanger: false,
+ showQuickJumper: false,
+ align: 'center' as const,
+ showTotal: showRowCount ? showTotalFunc : undefined,
+ };
+
+ if (onPageChange) {
+ config.current = pageIndex + 1;
+ config.total = totalCount;
+ config.onChange = handlePaginationChange;
+ } else {
+ if (pageIndex > 0) config.defaultCurrent = pageIndex + 1;
+ config.total = totalCount;
+ }
+
+ return config;
+ }, [
+ pageSize,
+ totalCount,
+ showRowCount,
+ showTotalFunc,
+ pageIndex,
+ handlePaginationChange,
+ onPageChange,
+ ]);
+
return (
<StyledTable
loading={loading}
@@ -155,12 +266,15 @@ function TableCollection<T extends object>({
data={mappedRows}
size={size}
data-test="listview-table"
- pagination={false}
+ pagination={paginationConfig}
+ scroll={{ x: 'max-content' }}
tableLayout="auto"
rowKey="rowId"
rowSelection={rowSelection}
locale={{ emptyText: null }}
sortDirections={['ascend', 'descend', 'ascend']}
+ isPaginationSticky={isPaginationSticky}
+ showRowCount={showRowCount}
components={{
header: {
cell: (props: HTMLAttributes<HTMLTableCellElement>) => (
@@ -176,14 +290,7 @@ function TableCollection<T extends object>({
),
},
}}
- onChange={(_pagination, _filters, sorter: SorterResult) => {
- setSortBy?.([
- {
- id: sorter.field,
- desc: sorter.order === 'descend',
- },
- ] as SortingRule<T>[]);
- }}
+ onChange={handleTableChange}
/>
);
}
diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.test.tsx
index 228ba4dbe3..7d0b9a3000 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.test.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.test.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { render, screen, userEvent } from '@superset-ui/core/spec';
+import { render, screen, userEvent, waitFor } from '@superset-ui/core/spec';
import { TableView, TableViewProps } from '.';
const mockedProps: TableViewProps = {
@@ -30,6 +30,7 @@ const mockedProps: TableViewProps = {
{
accessor: 'age',
Header: 'Age',
+ sortable: true,
id: 'age',
},
{
@@ -78,10 +79,10 @@ test('should render the cells', () => {
test('should render the pagination', () => {
render(<TableView {...mockedProps} />);
- expect(screen.getByRole('navigation')).toBeInTheDocument();
- expect(screen.getAllByRole('button')).toHaveLength(4);
- expect(screen.getByText('«')).toBeInTheDocument();
- expect(screen.getByText('»')).toBeInTheDocument();
+ expect(screen.getByRole('list')).toBeInTheDocument();
+ expect(screen.getAllByRole('button')).toHaveLength(2);
+ expect(screen.getByTitle('Previous Page')).toBeInTheDocument();
+ expect(screen.getByTitle('Next Page')).toBeInTheDocument();
});
test('should show the row count by default', () => {
@@ -104,45 +105,63 @@ test('should NOT render the pagination when disabled', () => {
withPagination: false,
};
render(<TableView {...withoutPaginationProps} />);
- expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
});
-test('should NOT render the pagination when fewer rows than page size', () => {
+test('should render the pagination even when fewer rows than page size', () => {
const withoutPaginationProps = {
...mockedProps,
pageSize: 3,
};
render(<TableView {...withoutPaginationProps} />);
- expect(screen.queryByRole('navigation')).not.toBeInTheDocument();
+ expect(screen.getByRole('list')).toBeInTheDocument();
});
-test('should change page when « and » buttons are clicked', async () => {
+test('should change page when pagination is clicked', async () => {
render(<TableView {...mockedProps} />);
- const nextBtn = screen.getByText('»');
- const prevBtn = screen.getByText('«');
-
- await userEvent.click(nextBtn);
- expect(screen.getAllByRole('cell')).toHaveLength(3);
- expect(screen.getByText('321')).toBeInTheDocument();
- expect(screen.getByText('10')).toBeInTheDocument();
- expect(screen.getByText('Kate')).toBeInTheDocument();
- expect(screen.queryByText('Emily')).not.toBeInTheDocument();
- await userEvent.click(prevBtn);
- expect(screen.getAllByRole('cell')).toHaveLength(3);
expect(screen.getByText('123')).toBeInTheDocument();
expect(screen.getByText('27')).toBeInTheDocument();
expect(screen.getByText('Emily')).toBeInTheDocument();
expect(screen.queryByText('Kate')).not.toBeInTheDocument();
+
+ const page2 = screen.getByRole('listitem', { name: '2' });
+ await userEvent.click(page2);
+
+ await waitFor(() => {
+ expect(screen.getAllByRole('cell')).toHaveLength(3);
+ expect(screen.getByText('321')).toBeInTheDocument();
+ expect(screen.getByText('10')).toBeInTheDocument();
+ expect(screen.getByText('Kate')).toBeInTheDocument();
+ expect(screen.queryByText('Emily')).not.toBeInTheDocument();
+ });
+
+ const page1 = screen.getByRole('listitem', { name: '1' });
+ await userEvent.click(page1);
+
+ await waitFor(() => {
+ expect(screen.getAllByRole('cell')).toHaveLength(3);
+ expect(screen.getByText('123')).toBeInTheDocument();
+ expect(screen.getByText('27')).toBeInTheDocument();
+ expect(screen.getByText('Emily')).toBeInTheDocument();
+ expect(screen.queryByText('Kate')).not.toBeInTheDocument();
+ });
});
test('should sort by age', async () => {
render(<TableView {...mockedProps} />);
await userEvent.click(screen.getAllByTestId('sort-header')[1]);
- expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('10');
+
+ await waitFor(() => {
+ expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('10');
+ });
+
await userEvent.click(screen.getAllByTestId('sort-header')[1]);
- expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('27');
+
+ await waitFor(() => {
+ expect(screen.getAllByTestId('table-row-cell')[1]).toHaveTextContent('27');
+ });
});
test('should sort by initialSortBy DESC', () => {
@@ -208,3 +227,146 @@ test('should render the right wrap content text by columnsForWrapText', () => {
'ant-table-cell-ellipsis',
);
});
+
+test('should handle server-side pagination', async () => {
+ const onServerPagination = jest.fn();
+ const serverPaginationProps = {
+ ...mockedProps,
+ serverPagination: true,
+ onServerPagination,
+ totalCount: 10,
+ pageSize: 2,
+ };
+ render(<TableView {...serverPaginationProps} />);
+
+ // Click next page
+ const page2 = screen.getByRole('listitem', { name: '2' });
+ await userEvent.click(page2);
+
+ await waitFor(() => {
+ expect(onServerPagination).toHaveBeenCalledWith({
+ pageIndex: 1,
+ });
+ });
+});
+
+test('should handle server-side sorting', async () => {
+ const onServerPagination = jest.fn();
+ const serverPaginationProps = {
+ ...mockedProps,
+ serverPagination: true,
+ onServerPagination,
+ };
+ render(<TableView {...serverPaginationProps} />);
+
+ // Click on sortable column
+ await userEvent.click(screen.getAllByTestId('sort-header')[0]);
+
+ await waitFor(() => {
+ expect(onServerPagination).toHaveBeenCalledWith({
+ pageIndex: 0,
+ sortBy: [{ id: 'id', desc: false }],
+ });
+ });
+});
+
+test('pagination callbacks should be stable across re-renders', () => {
+ const onServerPagination = jest.fn();
+ const serverPaginationProps = {
+ ...mockedProps,
+ serverPagination: true,
+ onServerPagination,
+ totalCount: 10,
+ pageSize: 2,
+ };
+
+ const { rerender } = render(<TableView {...serverPaginationProps} />);
+
+ // Re-render with same props
+ rerender(<TableView {...serverPaginationProps} />);
+
+ // onServerPagination should not have been called during re-render
+ expect(onServerPagination).not.toHaveBeenCalled();
+});
+
+test('should scroll to top when scrollTopOnPagination is true', async () => {
+ const scrollToSpy = jest
+ .spyOn(window, 'scrollTo')
+ .mockImplementation(() => {});
+
+ const scrollProps = {
+ ...mockedProps,
+ scrollTopOnPagination: true,
+ pageSize: 1,
+ };
+ render(<TableView {...scrollProps} />);
+
+ // Click next page
+ const page2 = screen.getByRole('listitem', { name: '2' });
+ await userEvent.click(page2);
+
+ await waitFor(() => {
+ expect(scrollToSpy).toHaveBeenCalledWith({ top: 0, behavior: 'smooth' });
+ });
+
+ scrollToSpy.mockRestore();
+});
+
+test('should NOT scroll to top when scrollTopOnPagination is false', async () => {
+ const scrollToSpy = jest
+ .spyOn(window, 'scrollTo')
+ .mockImplementation(() => {});
+
+ const scrollProps = {
+ ...mockedProps,
+ scrollTopOnPagination: false,
+ pageSize: 1,
+ };
+ render(<TableView {...scrollProps} />);
+
+ // Click next page
+ const page2 = screen.getByRole('listitem', { name: '2' });
+ await userEvent.click(page2);
+
+ await waitFor(() => {
+ expect(screen.getByText('321')).toBeInTheDocument();
+ });
+
+ expect(scrollToSpy).not.toHaveBeenCalled();
+
+ scrollToSpy.mockRestore();
+});
+
+test('should handle totalCount of 0 correctly', () => {
+ const emptyProps = {
+ ...mockedProps,
+ data: [],
+ totalCount: 0,
+ };
+ render(<TableView {...emptyProps} />);
+
+ // Pagination should not be shown when totalCount is 0
+ expect(screen.queryByRole('list')).not.toBeInTheDocument();
+});
+
+test('should handle large datasets with pagination', () => {
+ const largeDataset = Array.from({ length: 100 }, (_, i) => ({
+ id: i,
+ age: 20 + i,
+ name: `Person ${i}`,
+ }));
+
+ const largeDataProps = {
+ ...mockedProps,
+ data: largeDataset,
+ pageSize: 10,
+ };
+ render(<TableView {...largeDataProps} />);
+
+ // Should show only first page (10 items)
+ expect(screen.getAllByTestId('table-row')).toHaveLength(10);
+
+ // Should show pagination with correct page count
+ expect(screen.getByRole('list')).toBeInTheDocument();
+ expect(screen.getByText('1-10 of 100')).toBeInTheDocument();
+});
diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.tsx
index ce99de4649..a3d87f7a16 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.tsx
+++ b/superset-frontend/packages/superset-ui-core/src/components/TableView/TableView.tsx
@@ -16,16 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { memo, useEffect, useRef } from 'react';
+import { memo, useEffect, useRef, useMemo, useCallback } from 'react';
import { isEqual } from 'lodash';
-import { styled, t } from '@superset-ui/core';
+import { styled } from '@superset-ui/core';
import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
import { Empty } from '@superset-ui/core/components';
-import Pagination from '@superset-ui/core/components/Pagination';
import TableCollection from '@superset-ui/core/components/TableCollection';
import { TableSize } from '@superset-ui/core/components/Table';
import { SortByType, ServerPagination } from './types';
+const NOOP_SERVER_PAGINATION = () => {};
+
const DEFAULT_PAGE_SIZE = 10;
export enum EmptyWrapperType {
@@ -96,29 +97,6 @@ const TableViewStyles = styled.div<{
}
`;
-const PaginationStyles = styled.div<{
- isPaginationSticky?: boolean;
-}>`
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- background-color: ${({ theme }) => theme.colorBgElevated};
-
- ${({ isPaginationSticky }) =>
- isPaginationSticky &&
- `
- position: sticky;
- bottom: 0;
- left: 0;
- `};
-
- .row-count-container {
- margin-top: ${({ theme }) => theme.sizeUnit * 2}px;
- color: ${({ theme }) => theme.colorText};
- }
-`;
-
const RawTableView = ({
columns,
data,
@@ -133,16 +111,21 @@ const RawTableView = ({
showRowCount = true,
serverPagination = false,
columnsForWrapText,
- onServerPagination = () => {},
- scrollTopOnPagination = false,
+ onServerPagination = NOOP_SERVER_PAGINATION,
+ scrollTopOnPagination = true,
size = TableSize.Middle,
...props
}: TableViewProps) => {
- const initialState = {
- pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
- pageIndex: initialPageIndex ?? 0,
- sortBy: initialSortBy,
- };
+ const tableRef = useRef<HTMLTableElement>(null);
+
+ const initialState = useMemo(
+ () => ({
+ pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
+ pageIndex: initialPageIndex ?? 0,
+ sortBy: initialSortBy,
+ }),
+ [initialPageSize, initialPageIndex, initialSortBy],
+ );
const {
getTableProps,
@@ -151,10 +134,9 @@ const RawTableView = ({
page,
rows,
prepareRow,
- pageCount,
gotoPage,
setSortBy,
- state: { pageIndex, pageSize, sortBy },
+ state: { pageIndex, sortBy },
} = useTable(
{
columns,
@@ -162,36 +144,94 @@ const RawTableView = ({
initialState,
manualPagination: serverPagination,
manualSortBy: serverPagination,
- pageCount: Math.ceil(totalCount / initialState.pageSize),
+ pageCount: serverPagination
+ ? Math.ceil(totalCount / initialState.pageSize)
+ : undefined,
+ autoResetSortBy: false,
},
useFilters,
useSortBy,
- usePagination,
+ ...(withPagination ? [usePagination] : []),
);
- const content = withPagination ? page : rows;
+ const EmptyWrapperComponent = useMemo(() => {
+ switch (emptyWrapperType) {
+ case EmptyWrapperType.Small:
+ return ({ children }: any) => <>{children}</>;
+ case EmptyWrapperType.Default:
+ default:
+ return ({ children }: any) => <EmptyWrapper>{children}</EmptyWrapper>;
+ }
+ }, [emptyWrapperType]);
- let EmptyWrapperComponent;
- switch (emptyWrapperType) {
- case EmptyWrapperType.Small:
- EmptyWrapperComponent = ({ children }: any) => <>{children}</>;
- break;
- case EmptyWrapperType.Default:
- default:
- EmptyWrapperComponent = ({ children }: any) => (
- <EmptyWrapper>{children}</EmptyWrapper>
- );
- }
+ const content = useMemo(
+ () => (withPagination ? page : rows),
+ [withPagination, page, rows],
+ );
- const isEmpty = !loading && content.length === 0;
- const hasPagination = pageCount > 1 && withPagination;
- const tableRef = useRef<HTMLTableElement>(null);
- const handleGotoPage = (p: number) => {
+ const isEmpty = useMemo(
+ () => !loading && content.length === 0,
+ [loading, content.length],
+ );
+
+ const handleScrollToTop = useCallback(() => {
if (scrollTopOnPagination) {
- tableRef?.current?.scroll(0, 0);
+ if (tableRef?.current) {
+ if (typeof tableRef.current.scrollTo === 'function') {
+ tableRef.current.scrollTo({ top: 0, behavior: 'smooth' });
+ } else if (typeof tableRef.current.scroll === 'function') {
+ tableRef.current.scroll(0, 0);
+ }
+ }
+
+ if (typeof window !== 'undefined' && window.scrollTo)
+ window.scrollTo({ top: 0, behavior: 'smooth' });
}
- gotoPage(p);
- };
+ }, [scrollTopOnPagination]);
+
+ const handlePageChange = useCallback(
+ (p: number) => {
+ if (scrollTopOnPagination) handleScrollToTop();
+
+ gotoPage(p);
+ },
+ [scrollTopOnPagination, handleScrollToTop, gotoPage],
+ );
+
+ const paginationProps = useMemo(() => {
+ if (!withPagination) {
+ return {
+ pageIndex: 0,
+ pageSize: data.length,
+ totalCount: 0,
+ onPageChange: undefined,
+ };
+ }
+
+ if (serverPagination) {
+ return {
+ pageIndex,
+ pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
+ totalCount,
+ onPageChange: handlePageChange,
+ };
+ }
+
+ return {
+ pageIndex,
+ pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
+ totalCount: data.length,
+ onPageChange: handlePageChange,
+ };
+ }, [
+ withPagination,
+ serverPagination,
+ pageIndex,
+ initialPageSize,
+ totalCount,
+ data.length,
+ handlePageChange,
+ ]);
useEffect(() => {
if (serverPagination && pageIndex !== initialState.pageIndex) {
@@ -199,7 +239,7 @@ const RawTableView = ({
pageIndex,
});
}
- }, [pageIndex]);
+ }, [initialState.pageIndex, onServerPagination, pageIndex, serverPagination]);
useEffect(() => {
if (serverPagination && !isEqual(sortBy, initialState.sortBy)) {
@@ -208,61 +248,38 @@ const RawTableView = ({
sortBy,
});
}
- }, [sortBy]);
+ }, [initialState.sortBy, onServerPagination, serverPagination, sortBy]);
return (
- <>
- <TableViewStyles {...props} ref={tableRef}>
- <TableCollection
- getTableProps={getTableProps}
- getTableBodyProps={getTableBodyProps}
- prepareRow={prepareRow}
- headerGroups={headerGroups}
- rows={content}
- columns={columns}
- loading={loading}
- setSortBy={setSortBy}
- size={size}
- columnsForWrapText={columnsForWrapText}
- />
- {isEmpty && (
- <EmptyWrapperComponent>
- {noDataText ? (
- <Empty
- image={Empty.PRESENTED_IMAGE_SIMPLE}
- description={noDataText}
- />
- ) : (
- <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
- )}
- </EmptyWrapperComponent>
- )}
- </TableViewStyles>
- {hasPagination && (
- <PaginationStyles
- className="pagination-container"
- isPaginationSticky={props.isPaginationSticky}
- >
- <Pagination
- totalPages={pageCount || 0}
- currentPage={pageCount ? pageIndex + 1 : 0}
- onChange={(p: number) => handleGotoPage(p - 1)}
- hideFirstAndLastPageLinks
- />
- {showRowCount && (
- <div className="row-count-container">
- {!loading &&
- t(
- '%s-%s of %s',
- pageSize * pageIndex + (page.length && 1),
- pageSize * pageIndex + page.length,
- totalCount,
- )}
- </div>
+ <TableViewStyles {...props} ref={tableRef}>
+ <TableCollection
+ getTableProps={getTableProps}
+ getTableBodyProps={getTableBodyProps}
+ prepareRow={prepareRow}
+ headerGroups={headerGroups}
+ rows={content}
+ columns={columns}
+ loading={loading}
+ setSortBy={setSortBy}
+ size={size}
+ columnsForWrapText={columnsForWrapText}
+ isPaginationSticky={props.isPaginationSticky}
+ showRowCount={showRowCount}
+ {...paginationProps}
+ />
+ {isEmpty && (
+ <EmptyWrapperComponent>
+ {noDataText ? (
+ <Empty
+ image={Empty.PRESENTED_IMAGE_SIMPLE}
+ description={noDataText}
+ />
+ ) : (
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
- </PaginationStyles>
+ </EmptyWrapperComponent>
)}
- </>
+ </TableViewStyles>
);
};
diff --git a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
index 264157c69d..3119c16570 100644
--- a/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
+++ b/superset-frontend/src/components/Chart/DrillBy/DrillByModal.test.tsx
@@ -356,3 +356,215 @@ describe('Embedded mode behavior', () => {
);
});
});
+
+describe('Table view with pagination', () => {
+ beforeEach(() => {
+ // Mock a large dataset response for pagination testing
+ const mockLargeDataset = {
+ result: [
+ {
+ data: Array.from({ length: 100 }, (_, i) => ({
+ state: `State${i}`,
+ sum__num: 1000 + i,
+ })),
+ colnames: ['state', 'sum__num'],
+ coltypes: [1, 0],
+ },
+ ],
+ };
+
+ fetchMock.post(CHART_DATA_ENDPOINT, mockLargeDataset, {
+ overwriteRoutes: true,
+ });
+ });
+
+ afterEach(() => {
+ fetchMock.restore();
+ });
+
+ test('should render table view when Table radio is selected', async () => {
+ await renderModal({
+ column: { column_name: 'state', verbose_name: null },
+ drillByConfig: {
+ filters: [{ col: 'gender', op: '==', val: 'boy' }],
+ groupbyFieldName: 'groupby',
+ },
+ });
+
+ // Switch to table view
+ const tableRadio = await screen.findByRole('radio', { name: /table/i });
+ userEvent.click(tableRadio);
+
+ // Wait for table to render
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+
+ // Check that pagination is rendered (there's also a breadcrumb list)
+ const lists = screen.getAllByRole('list');
+ const paginationList = lists.find(list =>
+ list.className?.includes('pagination'),
+ );
+ expect(paginationList).toBeInTheDocument();
+ });
+
+ test('should handle pagination in table view', async () => {
+ await renderModal({
+ column: { column_name: 'state', verbose_name: null },
+ drillByConfig: {
+ filters: [{ col: 'gender', op: '==', val: 'boy' }],
+ groupbyFieldName: 'groupby',
+ },
+ });
+
+ // Switch to table view
+ const tableRadio = await screen.findByRole('radio', { name: /table/i });
+ userEvent.click(tableRadio);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+
+ // Check that first page data is shown
+ expect(screen.getByText('State0')).toBeInTheDocument();
+
+ // Check pagination controls exist
+ const nextPageButton = screen.getByTitle('Next Page');
+ expect(nextPageButton).toBeInTheDocument();
+
+ // Click next page
+ userEvent.click(nextPageButton);
+
+ // Verify page changed (State0 should not be visible on page 2)
+ await waitFor(() => {
+ expect(screen.queryByText('State0')).not.toBeInTheDocument();
+ });
+ });
+
+ test('should maintain table state when switching between Chart and Table views', async () => {
+ await renderModal({
+ column: { column_name: 'state', verbose_name: null },
+ drillByConfig: {
+ filters: [{ col: 'gender', op: '==', val: 'boy' }],
+ groupbyFieldName: 'groupby',
+ },
+ });
+
+ const chartRadio = screen.getByRole('radio', { name: /chart/i });
+ const tableRadio = screen.getByRole('radio', { name: /table/i });
+
+ // Switch to table view
+ userEvent.click(tableRadio);
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+
+ // Switch back to chart view
+ userEvent.click(chartRadio);
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-chart')).toBeInTheDocument();
+ });
+
+ // Switch back to table view - should maintain state
+ userEvent.click(tableRadio);
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+ });
+
+ test('should not cause infinite re-renders with pagination', async () => {
+ // Mock console.error to catch potential infinite loop warnings
+ const originalError = console.error;
+ const consoleErrorSpy = jest.fn();
+ console.error = consoleErrorSpy;
+
+ await renderModal({
+ column: { column_name: 'state', verbose_name: null },
+ drillByConfig: {
+ filters: [{ col: 'gender', op: '==', val: 'boy' }],
+ groupbyFieldName: 'groupby',
+ },
+ });
+
+ // Switch to table view
+ const tableRadio = await screen.findByRole('radio', { name: /table/i });
+ userEvent.click(tableRadio);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+
+ // Check that no infinite loop errors were logged
+ expect(consoleErrorSpy).not.toHaveBeenCalledWith(
+ expect.stringContaining('Maximum update depth exceeded'),
+ );
+
+ console.error = originalError;
+ });
+
+ test('should handle empty results in table view', async () => {
+ // Mock empty dataset response
+ fetchMock.post(
+ CHART_DATA_ENDPOINT,
+ {
+ result: [
+ {
+ data: [],
+ colnames: ['state', 'sum__num'],
+ coltypes: [1, 0],
+ },
+ ],
+ },
+ { overwriteRoutes: true },
+ );
+
+ await renderModal({
+ column: { column_name: 'state', verbose_name: null },
+ drillByConfig: {
+ filters: [{ col: 'gender', op: '==', val: 'boy' }],
+ groupbyFieldName: 'groupby',
+ },
+ });
+
+ // Switch to table view
+ const tableRadio = await screen.findByRole('radio', { name: /table/i });
+ userEvent.click(tableRadio);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+
+ // Should show empty state
+ expect(screen.getByText('No data')).toBeInTheDocument();
+ });
+
+ test('should handle sorting in table view', async () => {
+ await renderModal({
+ column: { column_name: 'state', verbose_name: null },
+ drillByConfig: {
+ filters: [{ col: 'gender', op: '==', val: 'boy' }],
+ groupbyFieldName: 'groupby',
+ },
+ });
+
+ // Switch to table view
+ const tableRadio = await screen.findByRole('radio', { name: /table/i });
+ userEvent.click(tableRadio);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+
+ // Find sortable column header
+ const sortableHeaders = screen.getAllByTestId('sort-header');
+ expect(sortableHeaders.length).toBeGreaterThan(0);
+
+ // Click to sort
+ userEvent.click(sortableHeaders[0]);
+
+ // Table should still be rendered without crashes
+ await waitFor(() => {
+ expect(screen.getByTestId('drill-by-results-table')).toBeInTheDocument();
+ });
+ });
+});
diff --git a/superset-frontend/src/components/FilterableTable/index.tsx b/superset-frontend/src/components/FilterableTable/index.tsx
index e1f98d94d0..70c939af7e 100644
--- a/superset-frontend/src/components/FilterableTable/index.tsx
+++ b/superset-frontend/src/components/FilterableTable/index.tsx
@@ -123,7 +123,6 @@ export const FilterableTable = ({
<div className="filterable-table-container" data-test="table-container">
<GridTable
size={GridSize.Small}
- usePagination={false}
height={height}
columns={columns}
data={data}
diff --git a/superset-frontend/src/components/ListView/ListView.test.jsx b/superset-frontend/src/components/ListView/ListView.test.jsx
index 2cd517f40b..6c03c975ec 100644
--- a/superset-frontend/src/components/ListView/ListView.test.jsx
+++ b/superset-frontend/src/components/ListView/ListView.test.jsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { render, screen, within } from 'spec/helpers/testing-library';
+import { render, screen, within, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { QueryParamProvider } from 'use-query-params';
import thunk from 'redux-thunk';
@@ -172,23 +172,28 @@ describe('ListView', () => {
});
});
- // Update pagination control tests to use button role
+ // Update pagination control tests for Ant Design pagination
it('renders pagination controls', () => {
- expect(screen.getByRole('navigation')).toBeInTheDocument();
- expect(screen.getByRole('button', { name: '«' })).toBeInTheDocument();
- expect(screen.getByRole('button', { name: '»' })).toBeInTheDocument();
+ const paginationList = screen.getByRole('list');
+ expect(paginationList).toBeInTheDocument();
+
+ const pageOneItem = screen.getByRole('listitem', { name: '1' });
+ expect(pageOneItem).toBeInTheDocument();
});
it('calls fetchData on page change', async () => {
- const nextButton = screen.getByRole('button', { name: '»' });
- await userEvent.click(nextButton);
+ const pageTwoItem = screen.getByRole('listitem', { name: '2' });
+ await userEvent.click(pageTwoItem);
- // Remove sortBy expectation since it's not part of the initial state
- expect(mockedProps.fetchData).toHaveBeenCalledWith({
- filters: [],
- pageIndex: 1,
- pageSize: 1,
- sortBy: [],
+ await waitFor(() => {
+ const { calls } = mockedProps.fetchData.mock;
+ const pageChangeCall = calls.find(
+ call =>
+ call[0].pageIndex === 1 &&
+ call[0].filters.length === 0 &&
+ call[0].pageSize === 1,
+ );
+ expect(pageChangeCall).toBeDefined();
});
});
diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx
index 574327a247..b156a3e371 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -19,7 +19,6 @@
import { t, styled } from '@superset-ui/core';
import { useCallback, useEffect, useRef, useState, ReactNode } from 'react';
import cx from 'classnames';
-import Pagination from '@superset-ui/core/components/Pagination';
import TableCollection from '@superset-ui/core/components/TableCollection';
import BulkTagModal from 'src/features/tags/BulkTagModal';
import {
@@ -71,6 +70,7 @@ const ListViewStyles = styled.div`
.body {
overflow-x: auto;
+ overflow-y: hidden;
}
.ant-empty {
@@ -482,6 +482,12 @@ export function ListView<T extends object = any>({
}
}}
toggleAllRowsSelected={toggleAllRowsSelected}
+ pageIndex={pageIndex}
+ pageSize={pageSize}
+ totalCount={count}
+ onPageChange={newPageIndex => {
+ gotoPage(newPageIndex);
+ }}
/>
)}
</>
@@ -509,25 +515,6 @@ export function ListView<T extends object = any>({
)}
</div>
</div>
- {rows.length > 0 && (
- <div className="pagination-container">
- <Pagination
- totalPages={pageCount || 0}
- currentPage={pageCount && pageIndex < pageCount ? pageIndex + 1 : 0}
- onChange={(p: number) => gotoPage(p - 1)}
- hideFirstAndLastPageLinks
- />
- <div className="row-count-container">
- {!loading &&
- t(
- '%s-%s of %s',
- pageSize * pageIndex + (rows.length && 1),
- pageSize * pageIndex + rows.length,
- count,
- )}
- </div>
- </div>
- )}
</ListViewStyles>
);
}
diff --git a/superset-frontend/src/components/ListView/utils.ts b/superset-frontend/src/components/ListView/utils.ts
index 443d57c7d5..5329f2fa89 100644
--- a/superset-frontend/src/components/ListView/utils.ts
+++ b/superset-frontend/src/components/ListView/utils.ts
@@ -48,15 +48,22 @@ import {
// Define custom RisonParam for proper encoding/decoding; note that
// %, &, +, and # must be encoded to avoid breaking the url
const RisonParam: QueryParamConfig<string, any> = {
- encode: (data?: any | null) =>
- data === undefined
- ? undefined
- : rison
- .encode(data)
- .replace(/%/g, '%25')
- .replace(/&/g, '%26')
- .replace(/\+/g, '%2B')
- .replace(/#/g, '%23'),
+ encode: (data?: any | null) => {
+ if (data === undefined || data === null) return undefined;
+
+ const cleanData = JSON.parse(
+ JSON.stringify(data, (key, value) =>
+ value === undefined ? null : value,
+ ),
+ );
+
+ return rison
+ .encode(cleanData)
+ .replace(/%/g, '%25')
+ .replace(/&/g, '%26')
+ .replace(/\+/g, '%2B')
+ .replace(/#/g, '%23');
+ },
decode: (dataStr?: string | string[]) =>
dataStr === undefined || Array.isArray(dataStr)
? undefined
@@ -319,7 +326,7 @@ export function useListViewState({
filters: Object.keys(filterObj).length ? filterObj : undefined,
pageIndex,
};
- if (sortBy[0]) {
+ if (sortBy?.[0]?.id !== undefined && sortBy[0].id !== null) {
queryParams.sortColumn = sortBy[0].id;
queryParams.sortOrder = sortBy[0].desc ? 'desc' : 'asc';
}
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
index 9969dde32e..c32fc90086 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SamplesPane.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useState, useEffect, useMemo } from 'react';
+import { useState, useEffect, useMemo, useCallback } from 'react';
import { ensureIsArray, GenericDataType, styled, t } from '@superset-ui/core';
import {
TableView,
@@ -104,6 +104,11 @@ export const SamplesPane = ({
);
const filteredData = useFilteredTableData(filterText, data);
+ const handleInputChange = useCallback(
+ (input: string) => setFilterText(input),
+ [],
+ );
+
if (isLoading) {
return <Loading />;
}
@@ -117,7 +122,7 @@ export const SamplesPane = ({
columnTypes={coltypes}
rowcount={rowcount}
datasourceId={datasourceId}
- onInputChange={input => setFilterText(input)}
+ onInputChange={handleInputChange}
isLoading={isLoading}
canDownload={canDownload}
/>
@@ -139,7 +144,7 @@ export const SamplesPane = ({
columnTypes={coltypes}
rowcount={rowcount}
datasourceId={datasourceId}
- onInputChange={input => setFilterText(input)}
+ onInputChange={handleInputChange}
isLoading={isLoading}
canDownload={canDownload}
/>
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
index 55da157295..e2cc2f79c3 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/SingleQueryResultPane.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useState } from 'react';
+import { useState, useCallback } from 'react';
import { t } from '@superset-ui/core';
import {
TableView,
@@ -55,6 +55,11 @@ export const SingleQueryResultPane = ({
);
const filteredData = useFilteredTableData(filterText, data);
+ const handleInputChange = useCallback(
+ (input: string) => setFilterText(input),
+ [],
+ );
+
return (
<>
<TableControls
@@ -63,7 +68,7 @@ export const SingleQueryResultPane = ({
columnTypes={coltypes}
rowcount={rowcount}
datasourceId={datasourceId}
- onInputChange={input => setFilterText(input)}
+ onInputChange={handleInputChange}
isLoading={false}
canDownload={canDownload}
/>
diff --git a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
index 625e490388..cf7bc09699 100644
--- a/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
+++ b/superset-frontend/src/explore/components/DataTablesPane/components/useResultsPane.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { useState, useEffect, ReactElement } from 'react';
+import { useState, useEffect, ReactElement, useCallback } from 'react';
import {
ensureIsArray,
@@ -35,6 +35,14 @@ const Error = styled.pre`
margin-top: ${({ theme }) => `${theme.sizeUnit * 4}px`};
`;
+const StyledDiv = styled.div`
+ ${() => `
+ display: flex;
+ height: 100%;
+ flex-direction: column;
+ `}
+`;
+
const cache = new WeakMap();
export const useResultsPane = ({
@@ -58,6 +66,8 @@ export const useResultsPane = ({
const queryCount = metadata?.queryObjectCount ?? 1;
const isQueryCountDynamic = metadata?.dynamicQueryObjectCount;
+ const noOpInputChange = useCallback(() => {}, []);
+
useEffect(() => {
// it's an invalid formData when gets a errorMessage
if (errorMessage) return;
@@ -123,7 +133,7 @@ export const useResultsPane = ({
columnTypes={[]}
rowcount={0}
datasourceId={queryFormData.datasource}
- onInputChange={() => {}}
+ onInputChange={noOpInputChange}
isLoading={false}
canDownload={canDownload}
/>
@@ -144,16 +154,17 @@ export const useResultsPane = ({
: resultResp.slice(0, queryCount);
return resultRespToDisplay.map((result, idx) => (
- <SingleQueryResultPane
- data={result.data}
- colnames={result.colnames}
- coltypes={result.coltypes}
- rowcount={result.rowcount}
- dataSize={dataSize}
- datasourceId={queryFormData.datasource}
- key={idx}
- isVisible={isVisible}
- canDownload={canDownload}
- />
+ <StyledDiv key={idx}>
+ <SingleQueryResultPane
+ data={result.data}
+ colnames={result.colnames}
+ coltypes={result.coltypes}
+ rowcount={result.rowcount}
+ dataSize={dataSize}
+ datasourceId={queryFormData.datasource}
+ isVisible={isVisible}
+ canDownload={canDownload}
+ />
+ </StyledDiv>
));
};