diff --git a/superset/assets/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx b/superset/assets/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx
index e84fd16d77e..73837b68d74 100644
--- a/superset/assets/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx
+++ b/superset/assets/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx
@@ -18,7 +18,7 @@
*/
import React from 'react';
import { mount } from 'enzyme';
-import FilterableTable from '../../../../src/components/FilterableTable/FilterableTable';
+import FilterableTable, { MAX_COLUMNS_FOR_TABLE } from '../../../../src/components/FilterableTable/FilterableTable';
describe('FilterableTable', () => {
const mockedProps = {
@@ -36,10 +36,22 @@ describe('FilterableTable', () => {
it('is valid element', () => {
expect(React.isValidElement()).toBe(true);
});
- it('renders a grid with 2 rows', () => {
+ it('renders a grid with 2 Table rows', () => {
expect(wrapper.find('.ReactVirtualized__Grid')).toHaveLength(1);
expect(wrapper.find('.ReactVirtualized__Table__row')).toHaveLength(2);
});
+ it('renders a grid with 2 Grid rows for wide tables', () => {
+ const wideTableColumns = MAX_COLUMNS_FOR_TABLE + 1;
+ const wideTableMockedProps = {
+ orderedColumnKeys: Array.from(Array(wideTableColumns), (_, x) => `col_${x}`),
+ data: [
+ Object.assign(...Array.from(Array(wideTableColumns)).map((val, x) => ({ [`col_${x}`]: x }))),
+ ],
+ height: 500,
+ };
+ const wideTableWrapper = mount();
+ expect(wideTableWrapper.find('.ReactVirtualized__Grid')).toHaveLength(2);
+ });
it('filters on a string', () => {
const props = {
...mockedProps,
diff --git a/superset/assets/src/components/FilterableTable/FilterableTable.jsx b/superset/assets/src/components/FilterableTable/FilterableTable.jsx
index 5b94d512ef9..fa1fc44d2aa 100644
--- a/superset/assets/src/components/FilterableTable/FilterableTable.jsx
+++ b/superset/assets/src/components/FilterableTable/FilterableTable.jsx
@@ -22,9 +22,11 @@ import JSONbig from 'json-bigint';
import React, { PureComponent } from 'react';
import {
Column,
- Table,
+ Grid,
+ ScrollSync,
SortDirection,
SortIndicator,
+ Table,
} from 'react-virtualized';
import { getTextDimension } from '@superset-ui/dimension';
import TooltipWrapper from '../TooltipWrapper';
@@ -34,6 +36,10 @@ function getTextWidth(text, font = '12px Roboto') {
}
const SCROLL_BAR_HEIGHT = 15;
+const GRID_POSITION_ADJUSTMENT = 4;
+
+// when more than MAX_COLUMNS_FOR_TABLE are returned, switch from table to grid view
+export const MAX_COLUMNS_FOR_TABLE = 50;
const propTypes = {
orderedColumnKeys: PropTypes.array.isRequired,
@@ -41,6 +47,7 @@ const propTypes = {
height: PropTypes.number.isRequired,
filterText: PropTypes.string,
headerHeight: PropTypes.number,
+ overscanColumnCount: PropTypes.number,
overscanRowCount: PropTypes.number,
rowHeight: PropTypes.number,
striped: PropTypes.bool,
@@ -50,6 +57,7 @@ const propTypes = {
const defaultProps = {
filterText: '',
headerHeight: 32,
+ overscanColumnCount: 10,
overscanRowCount: 10,
rowHeight: 32,
striped: true,
@@ -60,7 +68,11 @@ export default class FilterableTable extends PureComponent {
constructor(props) {
super(props);
this.list = List(this.formatTableData(props.data));
- this.renderHeader = this.renderHeader.bind(this);
+ this.renderGridCell = this.renderGridCell.bind(this);
+ this.renderGridCellHeader = this.renderGridCellHeader.bind(this);
+ this.renderGrid = this.renderGrid.bind(this);
+ this.renderTableHeader = this.renderTableHeader.bind(this);
+ this.renderTable = this.renderTable.bind(this);
this.rowClassName = this.rowClassName.bind(this);
this.sort = this.sort.bind(this);
@@ -75,6 +87,8 @@ export default class FilterableTable extends PureComponent {
sortDirection: SortDirection.ASC,
fitted: false,
};
+
+ this.container = React.createRef();
}
componentDidMount() {
@@ -151,7 +165,7 @@ export default class FilterableTable extends PureComponent {
this.setState({ sortBy, sortDirection });
}
- renderHeader({ dataKey, label, sortBy, sortDirection }) {
+ renderTableHeader({ dataKey, label, sortBy, sortDirection }) {
const className = this.props.expandedColumns.indexOf(label) > -1
? 'header-style-disabled'
: 'header-style';
@@ -167,7 +181,92 @@ export default class FilterableTable extends PureComponent {
);
}
- render() {
+ renderGridCellHeader({ columnIndex, key, style }) {
+ const label = this.props.orderedColumnKeys[columnIndex];
+ const className = this.props.expandedColumns.indexOf(label) > -1
+ ? 'header-style-disabled'
+ : 'header-style';
+ return (
+
+
+ {label}
+
+
+ );
+ }
+
+ renderGridCell({ columnIndex, key, rowIndex, style }) {
+ const columnKey = this.props.orderedColumnKeys[columnIndex];
+ return (
+
+ {this.list.get(rowIndex)[columnKey]}
+
+ );
+ }
+
+ renderGrid() {
+ const { orderedColumnKeys, overscanColumnCount, overscanRowCount, rowHeight } = this.props;
+
+ let { height } = this.props;
+ let totalTableHeight = height;
+ if (this.container && this.totalTableWidth > this.container.clientWidth) {
+ // exclude the height of the horizontal scroll bar from the height of the table
+ // and the height of the table container if the content overflows
+ height -= SCROLL_BAR_HEIGHT;
+ totalTableHeight -= SCROLL_BAR_HEIGHT;
+ }
+
+ const getColumnWidth = ({ index }) => this.widthsForColumnsByKey[orderedColumnKeys[index]];
+
+ // fix height of filterable table
+ return (
+
+ {({ onScroll, scrollTop }) => (
+
+ )}
+
+ );
+ }
+
+ renderTable() {
const { sortBy, sortDirection } = this.state;
const {
filterText,
@@ -203,7 +302,7 @@ export default class FilterableTable extends PureComponent {
{ this.container = ref; }}
+ ref={this.container}
>
{this.state.fitted &&
);
}
+
+ render() {
+ if (this.props.orderedColumnKeys.length > MAX_COLUMNS_FOR_TABLE) {
+ return this.renderGrid();
+ }
+ return this.renderTable();
+ }
}
FilterableTable.propTypes = propTypes;
diff --git a/superset/assets/src/components/FilterableTable/FilterableTableStyles.css b/superset/assets/src/components/FilterableTable/FilterableTableStyles.css
index c890654ad41..3db6e3a7c76 100644
--- a/superset/assets/src/components/FilterableTable/FilterableTableStyles.css
+++ b/superset/assets/src/components/FilterableTable/FilterableTableStyles.css
@@ -30,7 +30,8 @@
display: flex;
flex-direction: row;
}
-.ReactVirtualized__Table__headerTruncatedText {
+.ReactVirtualized__Table__headerTruncatedText,
+.grid-header-cell {
display: inline-block;
max-width: 100%;
white-space: nowrap;
@@ -38,13 +39,17 @@
overflow: hidden;
}
.ReactVirtualized__Table__headerColumn,
-.ReactVirtualized__Table__rowColumn {
+.ReactVirtualized__Table__rowColumn,
+.grid-cell {
min-width: 0px;
border-right: 1px solid #ccc;
align-self: center;
padding: 12px;
font-size: 12px;
}
+.grid-header-cell {
+ font-weight: 700;
+}
.ReactVirtualized__Table__headerColumn:last-of-type,
.ReactVirtualized__Table__rowColumn:last-of-type {
border-right: 0px;