diff --git a/superset-frontend/spec/javascripts/components/AlteredSliceTag_spec.jsx b/superset-frontend/spec/javascripts/components/AlteredSliceTag_spec.jsx
index 8cb313c70..41b4fe97a 100644
--- a/superset-frontend/spec/javascripts/components/AlteredSliceTag_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/AlteredSliceTag_spec.jsx
@@ -24,7 +24,7 @@ import AlteredSliceTag from 'src/components/AlteredSliceTag';
import ModalTrigger from 'src/components/ModalTrigger';
import TooltipWrapper from 'src/components/TooltipWrapper';
import ListView from 'src/components/ListView';
-import TableCollection from 'src/components/ListView/TableCollection';
+import TableCollection from 'src/components/dataViewCommon/TableCollection';
import {
defaultProps,
diff --git a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
index 118e01bbd..f8bed6b13 100644
--- a/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
+++ b/superset-frontend/spec/javascripts/components/ListView/ListView_spec.jsx
@@ -29,9 +29,9 @@ import { CardSortSelect } from 'src/components/ListView/CardSortSelect';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
import ListView from 'src/components/ListView/ListView';
import ListViewFilters from 'src/components/ListView/Filters';
-import ListViewPagination from 'src/components/ListView/Pagination';
+import ListViewPagination from 'src/components/dataViewCommon/Pagination';
+import TableCollection from 'src/components/dataViewCommon/TableCollection';
import Pagination from 'src/components/Pagination';
-import TableCollection from 'src/components/ListView/TableCollection';
import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint';
diff --git a/superset-frontend/spec/javascripts/components/TableView/TableView_spec.tsx b/superset-frontend/spec/javascripts/components/TableView/TableView_spec.tsx
new file mode 100644
index 000000000..ee9b1ae69
--- /dev/null
+++ b/superset-frontend/spec/javascripts/components/TableView/TableView_spec.tsx
@@ -0,0 +1,143 @@
+/**
+ * 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 React from 'react';
+import { mount } from 'enzyme';
+import { supersetTheme, ThemeProvider } from '@superset-ui/core';
+import Pagination from 'src/components/Pagination';
+import TableView from '../../../../src/components/TableView';
+import { TableViewProps } from '../../../../src/components/TableView/TableView';
+
+const mockedProps: TableViewProps = {
+ columns: [
+ {
+ accessor: 'id',
+ Header: 'ID',
+ sortable: true,
+ },
+ {
+ accessor: 'age',
+ Header: 'Age',
+ },
+ {
+ accessor: 'name',
+ Header: 'Name',
+ },
+ ],
+ data: [
+ { id: 1, age: 20, name: 'Emily' },
+ { id: 2, age: 10, name: 'Kate' },
+ { id: 3, age: 40, name: 'Anna' },
+ { id: 4, age: 30, name: 'Jane' },
+ ],
+ pageSize: 1,
+};
+
+const factory = (props = mockedProps) =>
+ mount(, {
+ wrappingComponent: ThemeProvider,
+ wrappingComponentProps: { theme: supersetTheme },
+ });
+
+describe('TableView', () => {
+ it('render a table, columns and rows', () => {
+ const pageSize = 10;
+ const wrapper = factory({ ...mockedProps, pageSize });
+ expect(wrapper.find('table')).toExist();
+ expect(wrapper.find('table th')).toHaveLength(mockedProps.columns.length);
+ expect(wrapper.find('table tbody tr')).toHaveLength(
+ Math.min(mockedProps.data.length, pageSize),
+ );
+ });
+
+ it('renders pagination controls', () => {
+ const wrapper = factory();
+ expect(wrapper.find(Pagination)).toExist();
+ expect(wrapper.find(Pagination.Prev)).toExist();
+ expect(wrapper.find(Pagination.Item)).toExist();
+ expect(wrapper.find(Pagination.Next)).toExist();
+ });
+
+ it("doesn't render pagination when pagination is disabled", () => {
+ const wrapper = factory({ ...mockedProps, withPagination: false });
+ expect(wrapper.find(Pagination)).not.toExist();
+ });
+
+ it("doesn't render pagination when fewer rows than page size", () => {
+ const pageSize = 999;
+ expect(pageSize).toBeGreaterThan(mockedProps.data.length);
+
+ const wrapper = factory({ ...mockedProps, pageSize });
+ expect(wrapper.find(Pagination)).not.toExist();
+ });
+
+ it('changes page when button is clicked', () => {
+ const pageSize = 3;
+ const dataLength = mockedProps.data.length;
+
+ expect(dataLength).toBeGreaterThan(pageSize);
+ const wrapper = factory({ ...mockedProps, pageSize });
+
+ expect(wrapper.find('table tbody tr')).toHaveLength(pageSize);
+
+ wrapper.find('NEXT_PAGE_LINK span').simulate('click');
+ expect(wrapper.find('table tbody tr')).toHaveLength(
+ Math.min(dataLength - pageSize, pageSize),
+ );
+ });
+
+ it('sorts by age when header cell is clicked', () => {
+ const wrapper = factory();
+ expect(wrapper.find('table tbody td Cell').at(1).props().value).toEqual(20);
+
+ // sort ascending
+ wrapper.find('table thead th').at(1).simulate('click');
+ expect(wrapper.find('table tbody td Cell').at(1).props().value).toEqual(10);
+
+ // sort descending
+ wrapper.find('table thead th').at(1).simulate('click');
+ expect(wrapper.find('table tbody td Cell').at(1).props().value).toEqual(40);
+
+ // no sort
+ wrapper.find('table thead th').at(1).simulate('click');
+ expect(wrapper.find('table tbody td Cell').at(1).props().value).toEqual(20);
+ });
+
+ it('sorts by data when initialSortBy is passed', () => {
+ let wrapper = factory();
+ expect(wrapper.find('table tbody td Cell').at(2).props().value).toEqual(
+ 'Emily',
+ );
+
+ wrapper = factory({
+ ...mockedProps,
+ initialSortBy: [{ id: 'name', desc: true }],
+ });
+ expect(wrapper.find('table tbody td Cell').at(2).props().value).toEqual(
+ 'Kate',
+ );
+
+ wrapper = factory({
+ ...mockedProps,
+ initialSortBy: [{ id: 'name', desc: false }],
+ });
+ expect(wrapper.find('table tbody td Cell').at(2).props().value).toEqual(
+ 'Anna',
+ );
+ });
+});
diff --git a/superset-frontend/src/components/ListView/ListView.tsx b/superset-frontend/src/components/ListView/ListView.tsx
index e79261c5b..e9e65812d 100644
--- a/superset-frontend/src/components/ListView/ListView.tsx
+++ b/superset-frontend/src/components/ListView/ListView.tsx
@@ -24,9 +24,8 @@ import cx from 'classnames';
import Button from 'src/components/Button';
import Icon from 'src/components/Icon';
import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox';
-import TableCollection from './TableCollection';
+import { TableCollection, Pagination } from 'src/components/dataViewCommon';
import CardCollection from './CardCollection';
-import Pagination from './Pagination';
import FilterControls from './Filters';
import { CardSortSelect } from './CardSortSelect';
import {
diff --git a/superset-frontend/src/components/ListView/index.ts b/superset-frontend/src/components/ListView/index.ts
index 30be8a1c8..0d6753fc6 100644
--- a/superset-frontend/src/components/ListView/index.ts
+++ b/superset-frontend/src/components/ListView/index.ts
@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
-
export * from './ListView';
export * from './types';
diff --git a/superset-frontend/src/components/TableView/TableView.tsx b/superset-frontend/src/components/TableView/TableView.tsx
new file mode 100644
index 000000000..3eb11a8b3
--- /dev/null
+++ b/superset-frontend/src/components/TableView/TableView.tsx
@@ -0,0 +1,137 @@
+/**
+ * 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 React from 'react';
+import { styled, t } from '@superset-ui/core';
+import { useFilters, usePagination, useSortBy, useTable } from 'react-table';
+import { Empty } from 'src/common/components';
+import { TableCollection, Pagination } from 'src/components/dataViewCommon';
+import { SortColumns } from './types';
+
+const DEFAULT_PAGE_SIZE = 10;
+
+export interface TableViewProps {
+ columns: any[];
+ data: any[];
+ pageSize?: number;
+ initialPageIndex?: number;
+ initialSortBy?: SortColumns;
+ loading?: boolean;
+ withPagination?: boolean;
+ className?: string;
+}
+
+const EmptyWrapper = styled.div`
+ margin: ${({ theme }) => theme.gridUnit * 40}px 0;
+`;
+
+const TableViewStyles = styled.div`
+ .table-cell.table-cell {
+ vertical-align: top;
+ }
+
+ .pagination-container {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .row-count-container {
+ margin-top: ${({ theme }) => theme.gridUnit * 2}px;
+ color: ${({ theme }) => theme.colors.grayscale.base};
+ }
+`;
+
+const TableView = ({
+ columns,
+ data,
+ pageSize: initialPageSize,
+ initialPageIndex,
+ initialSortBy = [],
+ loading = false,
+ withPagination = true,
+ ...props
+}: TableViewProps) => {
+ const initialState = {
+ pageSize: initialPageSize ?? DEFAULT_PAGE_SIZE,
+ pageIndex: initialPageIndex ?? 0,
+ sortBy: initialSortBy,
+ };
+
+ const {
+ getTableProps,
+ getTableBodyProps,
+ headerGroups,
+ page,
+ rows,
+ prepareRow,
+ pageCount,
+ gotoPage,
+ state: { pageIndex, pageSize },
+ } = useTable(
+ {
+ columns,
+ data,
+ initialState,
+ },
+ useFilters,
+ useSortBy,
+ usePagination,
+ );
+
+ const content = withPagination ? page : rows;
+ return (
+
+
+ {!loading && content.length === 0 && (
+
+
+
+ )}
+ {pageCount > 1 && withPagination && (
+
+
gotoPage(p - 1)}
+ hideFirstAndLastPageLinks
+ />
+
+ {!loading &&
+ t(
+ '%s-%s of %s',
+ pageSize * pageIndex + (page.length && 1),
+ pageSize * pageIndex + page.length,
+ data.length,
+ )}
+
+
+ )}
+
+ );
+};
+
+export default TableView;
diff --git a/superset-frontend/src/components/TableView/index.ts b/superset-frontend/src/components/TableView/index.ts
new file mode 100644
index 000000000..88d57c481
--- /dev/null
+++ b/superset-frontend/src/components/TableView/index.ts
@@ -0,0 +1,19 @@
+/**
+ * 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.
+ */
+export { default } from './TableView';
diff --git a/superset-frontend/src/components/TableView/types.ts b/superset-frontend/src/components/TableView/types.ts
new file mode 100644
index 000000000..974cb2496
--- /dev/null
+++ b/superset-frontend/src/components/TableView/types.ts
@@ -0,0 +1,24 @@
+/**
+ * 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.
+ */
+export interface SortColumn {
+ id: string;
+ desc?: boolean;
+}
+
+export type SortColumns = SortColumn[];
diff --git a/superset-frontend/src/components/ListView/Pagination.tsx b/superset-frontend/src/components/dataViewCommon/Pagination.tsx
similarity index 100%
rename from superset-frontend/src/components/ListView/Pagination.tsx
rename to superset-frontend/src/components/dataViewCommon/Pagination.tsx
diff --git a/superset-frontend/src/components/ListView/TableCollection.tsx b/superset-frontend/src/components/dataViewCommon/TableCollection.tsx
similarity index 96%
rename from superset-frontend/src/components/ListView/TableCollection.tsx
rename to superset-frontend/src/components/dataViewCommon/TableCollection.tsx
index db799379b..eb638253e 100644
--- a/superset-frontend/src/components/ListView/TableCollection.tsx
+++ b/superset-frontend/src/components/dataViewCommon/TableCollection.tsx
@@ -33,8 +33,7 @@ interface TableCollectionProps {
highlightRowId?: number;
}
-const Table = styled.table`
- background-color: white;
+export const Table = styled.table`
border-collapse: separate;
border-radius: ${({ theme }) => theme.borderRadius}px;
@@ -199,6 +198,8 @@ const Table = styled.table`
}
`;
+Table.displayName = 'table';
+
export default function TableCollection({
getTableProps,
getTableBodyProps,
@@ -267,13 +268,15 @@ export default function TableCollection({
{rows.length > 0 &&
rows.map(row => {
prepareRow(row);
+ // @ts-ignore
+ const rowId = row.original.id;
return (
{row.cells.map(cell => {
diff --git a/superset-frontend/src/components/dataViewCommon/index.ts b/superset-frontend/src/components/dataViewCommon/index.ts
new file mode 100644
index 000000000..e552e5c9d
--- /dev/null
+++ b/superset-frontend/src/components/dataViewCommon/index.ts
@@ -0,0 +1,20 @@
+/**
+ * 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.
+ */
+export { default as Pagination } from './Pagination';
+export { default as TableCollection } from './TableCollection';