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
This commit is contained in:
Beto Dealmeida
2019-06-12 10:09:04 -07:00
committed by GitHub
parent f3181ca6dc
commit 5cf06331fe
3 changed files with 133 additions and 10 deletions

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;