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;