-
Notifications
You must be signed in to change notification settings - Fork 14.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[sqllab] table refactor #2587
[sqllab] table refactor #2587
Changes from 14 commits
0ebb392
09de72e
b054ac0
ceb8165
46d0e26
c8d375d
a500a57
14f077c
579e760
88394b2
6e734be
b42b33b
5c0286d
bf2112b
f6a18d8
5e5cb2c
aa14833
ba530f2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
}; | ||
} | ||
|
||
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())); | ||
} | ||
|
||
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; | ||
} | ||
|
||
getWidthsForColumns() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm concerned about the potential cost of this method. |
||
const PADDING = 40; // accounts for cell padding and width of sorting icon | ||
const widthsByColumnKey = {}; | ||
this.props.orderedColumnKeys.forEach((key) => { | ||
const colWidths = this.list.toArray().map(d => getTextWidth(d[key]) + PADDING); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. actually I think you could reduce right off this line already with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
// push width of column key to array as well | ||
colWidths.push(getTextWidth(key) + PADDING); | ||
// 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 }); | ||
} | ||
|
||
componentDidMount() { | ||
this.fitTableToWidthIfNeeded(); | ||
} | ||
|
||
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> | ||
); | ||
} | ||
|
||
getDatum(list, index) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm used to |
||
return list.get(index % list.size); | ||
} | ||
|
||
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 }); | ||
} | ||
} | ||
|
||
FilterableTable.propTypes = propTypes; | ||
FilterableTable.defaultProps = defaultProps; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how does this react when it overflow? I saw react-virtualized has a dynamicHeight option too, would that be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@mistercrunch and i talked offline about this, we'll keep the static row height and add a max column width. for columns that require truncating the text, we will show a modal or popover... currently there is a tooltip when hovering on any cell which shows the content, but i'm not sure if this is clear enough to the user.
i'll tackle setting a max column width in a follow up PR.