Skip to content

Commit

Permalink
[sqllab] table refactor (#2587)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Alanna Scott authored Apr 18, 2017
1 parent f40499e commit db6cd21
Show file tree
Hide file tree
Showing 13 changed files with 302 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,13 @@ class DataPreviewModal extends React.PureComponent {
</Modal.Title>
</Modal.Header>
<Modal.Body>
<ResultSet query={query} visualize={false} csv={false} actions={this.props.actions} />
<ResultSet
query={query}
visualize={false}
csv={false}
actions={this.props.actions}
height={400}
/>
</Modal.Body>
</Modal>
);
Expand Down
4 changes: 3 additions & 1 deletion superset/assets/javascripts/SqlLab/components/QueryTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ class QueryTable extends React.PureComponent {
modalTitle={'Data preview'}
beforeOpen={this.openAsyncResults.bind(this, query)}
onExit={this.clearQueryResults.bind(this, query)}
modalBody={<ResultSet showSql query={query} actions={this.props.actions} />}
modalBody={
<ResultSet showSql query={query} actions={this.props.actions} height={400} />
}
/>
);
} else {
Expand Down
47 changes: 13 additions & 34 deletions superset/assets/javascripts/SqlLab/components/ResultSet.jsx
Original file line number Diff line number Diff line change
@@ -1,42 +1,40 @@
import React from 'react';
import { Alert, Button, ButtonGroup, ProgressBar } from 'react-bootstrap';
import { Table } from 'reactable';
import shortid from 'shortid';

import VisualizeModal from './VisualizeModal';
import HighlightedSql from './HighlightedSql';

const RESULTS_CONTROLS_HEIGHT = 36;
import FilterableTable from '../../components/FilterableTable/FilterableTable';

const propTypes = {
actions: React.PropTypes.object,
csv: React.PropTypes.bool,
query: React.PropTypes.object,
search: React.PropTypes.bool,
searchText: React.PropTypes.string,
showSql: React.PropTypes.bool,
visualize: React.PropTypes.bool,
cache: React.PropTypes.bool,
resultSetHeight: React.PropTypes.number,
height: React.PropTypes.number.isRequired,
};
const defaultProps = {
search: true,
visualize: true,
showSql: false,
csv: true,
searchText: '',
actions: {},
cache: false,
};

const RESULT_SET_CONTROLS_HEIGHT = 46;

class ResultSet extends React.PureComponent {
export default class ResultSet extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
searchText: '',
showModal: false,
data: [],
height: props.search ? props.height - RESULT_SET_CONTROLS_HEIGHT : props.height,
};
}
componentWillReceiveProps(nextProps) {
Expand All @@ -54,6 +52,7 @@ class ResultSet extends React.PureComponent {
this.fetchResults(nextProps.query);
}
}

getControls() {
if (this.props.search || this.props.visualize || this.props.csv) {
let csvButton;
Expand Down Expand Up @@ -132,6 +131,7 @@ class ResultSet extends React.PureComponent {
reFetchQueryResults(query) {
this.props.actions.reFetchQueryResults(query);
}

render() {
const query = this.props.query;
const results = query.results;
Expand Down Expand Up @@ -195,31 +195,12 @@ class ResultSet extends React.PureComponent {
/>
{this.getControls.bind(this)()}
{sql}
<div
className="ResultSet"
style={{ height: `${this.props.resultSetHeight - RESULTS_CONTROLS_HEIGHT}px` }}
>
<Table
data={data.map(function (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;
})}
columns={results.columns.map(col => col.name)}
sortable
className="table table-condensed table-bordered"
filterBy={this.state.searchText}
filterable={results.columns.map(c => c.name)}
hideFilterInput
/>
</div>
<FilterableTable
data={data}
orderedColumnKeys={results.columns.map(col => col.name)}
height={this.state.height}
filterText={this.state.searchText}
/>
</div>
);
}
Expand All @@ -240,5 +221,3 @@ class ResultSet extends React.PureComponent {
}
ResultSet.propTypes = propTypes;
ResultSet.defaultProps = defaultProps;

export default ResultSet;
4 changes: 2 additions & 2 deletions superset/assets/javascripts/SqlLab/components/SouthPane.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ class SouthPane extends React.PureComponent {
search
query={latestQuery}
actions={props.actions}
resultSetHeight={this.state.innerTabHeight}
height={this.state.innerTabHeight}
/>
);
} else {
Expand All @@ -90,7 +90,7 @@ class SouthPane extends React.PureComponent {
csv={false}
actions={props.actions}
cache
resultSetHeight={this.state.innerTabHeight}
height={this.state.innerTabHeight}
/>
</Tab>
));
Expand Down
4 changes: 3 additions & 1 deletion superset/assets/javascripts/SqlLab/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import { initEnhancer } from '../reduxUtils';
import { initJQueryAjaxCSRF } from '../modules/utils';
import App from './components/App';
import { appSetup } from '../common';
import './main.css';

require('./main.css');
require('../components/FilterableTable/FilterableTableStyles.css');

appSetup();
initJQueryAjaxCSRF();
Expand Down
10 changes: 0 additions & 10 deletions superset/assets/javascripts/SqlLab/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,6 @@ div.tablePopover:hover {
padding-bottom: 3px;
padding-top: 3px;
}
.ResultSet {
overflow: auto;
border-bottom: 1px solid #ccc;
}
.ResultSet table {
margin-bottom: 0px;
}
.ResultSet table tr.last {
border-bottom: none;
}
.ace_editor {
border: 1px solid #ccc;
margin: 0px 0px 10px 0px;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit db6cd21

Please sign in to comment.