[sqllab] table refactor (#2587)

* make react-virtualized table work
use dynamic sizing for cell width
enable filtering
require height prop for result set component

* fix tests and linting

* move some state to props

* move getTextWidth to visUtils

* make striped rows optional

* fix striped proptype

* update name to FilterableTable

* add basic test and fix linting

* accept array of columns keys rather than an array of objects that needs to be mapped

* move container div inside the component

* rename styles

* fit table component to width if it's smaller than parent container

* move stylesheet to javascript folder otherwise it throws an error on npm run prod

* move css to index.jsx

* fix result set spec

* fix linting and test

* fix result set props

* keep list immutable
This commit is contained in:
Alanna Scott
2017-04-18 14:29:38 -07:00
committed by GitHub
parent f40499e550
commit db6cd21504
13 changed files with 302 additions and 53 deletions

View File

@@ -0,0 +1,190 @@
import { List } from 'immutable';
import React, { PropTypes, PureComponent } from 'react';
import {
Column,
Table,
SortDirection,
SortIndicator,
} from 'react-virtualized';
import { getTextWidth } from '../../modules/visUtils';
const propTypes = {
orderedColumnKeys: PropTypes.array.isRequired,
data: PropTypes.array.isRequired,
height: PropTypes.number.isRequired,
filterText: PropTypes.string,
headerHeight: PropTypes.number,
overscanRowCount: PropTypes.number,
rowHeight: PropTypes.number,
striped: PropTypes.bool,
};
const defaultProps = {
filterText: '',
headerHeight: 32,
overscanRowCount: 10,
rowHeight: 32,
striped: true,
};
export default class FilterableTable extends PureComponent {
constructor(props) {
super(props);
this.list = List(this.formatTableData(props.data));
this.headerRenderer = this.headerRenderer.bind(this);
this.rowClassName = this.rowClassName.bind(this);
this.sort = this.sort.bind(this);
this.widthsForColumnsByKey = this.getWidthsForColumns();
this.totalTableWidth = props.orderedColumnKeys
.map(key => this.widthsForColumnsByKey[key])
.reduce((curr, next) => curr + next);
this.state = {
sortBy: props.orderedColumnKeys[0],
sortDirection: SortDirection.ASC,
fitted: false,
};
}
componentDidMount() {
this.fitTableToWidthIfNeeded();
}
getDatum(list, index) {
return list.get(index % list.size);
}
getWidthsForColumns() {
const PADDING = 40; // accounts for cell padding and width of sorting icon
const widthsByColumnKey = {};
this.props.orderedColumnKeys.forEach((key) => {
const colWidths = this.list
.map(d => getTextWidth(d[key]) + PADDING) // get width for each value for a key
.push(getTextWidth(key) + PADDING); // add width of column key to end of list
// set max width as value for key
widthsByColumnKey[key] = Math.max(...colWidths);
});
return widthsByColumnKey;
}
fitTableToWidthIfNeeded() {
const containerWidth = this.container.getBoundingClientRect().width;
if (containerWidth > this.totalTableWidth) {
this.totalTableWidth = containerWidth - 2; // accomodates 1px border on container
}
this.setState({ fitted: true });
}
formatTableData(data) {
const formattedData = data.map((row) => {
const newRow = {};
for (const k in row) {
const val = row[k];
if (typeof (val) === 'string') {
newRow[k] = val;
} else {
newRow[k] = JSON.stringify(val);
}
}
return newRow;
});
return formattedData;
}
hasMatch(text, row) {
const values = [];
for (const key in row) {
if (row.hasOwnProperty(key)) {
values.push(row[key].toLowerCase());
}
}
return values.some(v => v.includes(text.toLowerCase()));
}
headerRenderer({ dataKey, label, sortBy, sortDirection }) {
return (
<div>
{label}
{sortBy === dataKey &&
<SortIndicator sortDirection={sortDirection} />
}
</div>
);
}
rowClassName({ index }) {
let className = '';
if (this.props.striped) {
className = index % 2 === 0 ? 'even-row' : 'odd-row';
}
return className;
}
sort({ sortBy, sortDirection }) {
this.setState({ sortBy, sortDirection });
}
render() {
const { sortBy, sortDirection } = this.state;
const {
filterText,
headerHeight,
height,
orderedColumnKeys,
overscanRowCount,
rowHeight,
} = this.props;
let sortedAndFilteredList = this.list;
// filter list
if (filterText) {
sortedAndFilteredList = this.list.filter(row => this.hasMatch(filterText, row));
}
// sort list
sortedAndFilteredList = sortedAndFilteredList
.sortBy(item => item[sortBy])
.update(list => sortDirection === SortDirection.DESC ? list.reverse() : list);
const rowGetter = ({ index }) => this.getDatum(sortedAndFilteredList, index);
return (
<div
style={{ height }}
className="filterable-table-container"
ref={(ref) => { this.container = ref; }}
>
{this.state.fitted &&
<Table
ref="Table"
headerHeight={headerHeight}
height={height - 2}
overscanRowCount={overscanRowCount}
rowClassName={this.rowClassName}
rowHeight={rowHeight}
rowGetter={rowGetter}
rowCount={sortedAndFilteredList.size}
sort={this.sort}
sortBy={sortBy}
sortDirection={sortDirection}
width={this.totalTableWidth}
>
{orderedColumnKeys.map(columnKey => (
<Column
dataKey={columnKey}
disableSort={false}
headerRenderer={this.headerRenderer}
width={this.widthsForColumnsByKey[columnKey]}
label={columnKey}
key={columnKey}
/>
))}
</Table>
}
</div>
);
}
}
FilterableTable.propTypes = propTypes;
FilterableTable.defaultProps = defaultProps;

View File

@@ -0,0 +1,60 @@
.ReactVirtualized__Table__headerRow {
font-weight: 700;
display: flex;
flex-direction: row;
align-items: center;
}
.ReactVirtualized__Table__row {
display: flex;
flex-direction: row;
}
.ReactVirtualized__Table__headerTruncatedText {
display: inline-block;
max-width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.ReactVirtualized__Table__headerColumn,
.ReactVirtualized__Table__rowColumn {
min-width: 0px;
border-right: 1px solid #ccc;
align-self: center;
padding: 12px;
font-size: 12px;
}
.ReactVirtualized__Table__headerColumn:last-of-type,
.ReactVirtualized__Table__rowColumn:last-of-type {
border-right: 0px;
}
.ReactVirtualized__Table__headerColumn:focus,
.ReactVirtualized__Table__Grid:focus {
outline: none;
}
.ReactVirtualized__Table__rowColumn {
text-overflow: ellipsis;
white-space: nowrap;
}
.ReactVirtualized__Table__sortableHeaderColumn {
cursor: pointer;
}
.ReactVirtualized__Table__sortableHeaderIconContainer {
display: flex;
align-items: center;
}
.ReactVirtualized__Table__sortableHeaderIcon {
flex: 0 0 24px;
height: 1em;
width: 1em;
fill: currentColor;
}
.even-row { background: #f2f2f2; }
.odd-row { background: #ffffff; }
.even-row,
.odd-row {
border: none;
}
.filterable-table-container {
overflow: auto;
border: 1px solid #ccc;
}