This is an automated email from the ASF dual-hosted git repository. beto pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push: new 5cf0633 Render columns dynamically on wide tables (#7693) 5cf0633 is described below commit 5cf06331fe182207bdc37ffcea0de00185c1def1 Author: Beto Dealmeida <robe...@dealmeida.net> AuthorDate: Wed Jun 12 10:09:04 2019 -0700 Render columns dynamically on wide tables (#7693) * Use grid for wide tables * WIP * Fix CSS issues * Add unit test * Add constant * Improve ref * Remove wrong refs; no longer need extra height * Revert number of columns --- .../FilterableTable/FilterableTable_spec.jsx | 16 ++- .../components/FilterableTable/FilterableTable.jsx | 118 +++++++++++++++++++-- .../FilterableTable/FilterableTableStyles.css | 9 +- 3 files changed, 133 insertions(+), 10 deletions(-) diff --git a/superset/assets/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx b/superset/assets/spec/javascripts/components/FilterableTable/FilterableTable_spec.jsx index e84fd16..73837b6 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(<FilterableTable {...mockedProps} />)).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(<FilterableTable {...wideTableMockedProps} />); + 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 5b94d51..fa1fc44 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 ( + <TooltipWrapper key={key} label="header" tooltip={label}> + <div + style={{ ...style, top: style.top - GRID_POSITION_ADJUSTMENT }} + className={`${className} grid-cell grid-header-cell`} + > + {label} + </div> + </TooltipWrapper> + ); + } + + renderGridCell({ columnIndex, key, rowIndex, style }) { + const columnKey = this.props.orderedColumnKeys[columnIndex]; + return ( + <div + key={key} + style={{ ...style, top: style.top - GRID_POSITION_ADJUSTMENT }} + className={`grid-cell ${this.rowClassName({ index: rowIndex })}`} + > + {this.list.get(rowIndex)[columnKey]} + </div> + ); + } + + 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 ( + <ScrollSync> + {({ onScroll, scrollTop }) => ( + <div + style={{ height }} + className="filterable-table-container Table" + ref={this.container} + > + <div className="LeftColumn"> + <Grid + cellRenderer={this.renderGridCellHeader} + columnCount={orderedColumnKeys.length} + columnWidth={getColumnWidth} + height={rowHeight} + rowCount={1} + rowHeight={rowHeight} + scrollTop={scrollTop} + width={this.totalTableWidth} + /> + </div> + <div className="RightColumn"> + <Grid + cellRenderer={this.renderGridCell} + columnCount={orderedColumnKeys.length} + columnWidth={getColumnWidth} + height={totalTableHeight - rowHeight} + onScroll={onScroll} + overscanColumnCount={overscanColumnCount} + overscanRowCount={overscanRowCount} + rowCount={this.list.size} + rowHeight={rowHeight} + width={this.totalTableWidth} + /> + </div> + </div> + )} + </ScrollSync> + ); + } + + renderTable() { const { sortBy, sortDirection } = this.state; const { filterText, @@ -203,7 +302,7 @@ export default class FilterableTable extends PureComponent { <div style={{ height }} className="filterable-table-container" - ref={(ref) => { this.container = ref; }} + ref={this.container} > {this.state.fitted && <Table @@ -224,7 +323,7 @@ export default class FilterableTable extends PureComponent { <Column dataKey={columnKey} disableSort={false} - headerRenderer={this.renderHeader} + headerRenderer={this.renderTableHeader} width={this.widthsForColumnsByKey[columnKey]} label={columnKey} key={columnKey} @@ -235,6 +334,13 @@ export default class FilterableTable extends PureComponent { </div> ); } + + 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 c890654..3db6e3a 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;