moving tables to react-window
Summary: Tables were using a custom virtualization, which wasn't as performant as other solutions out there. In this diff, the table component is reworked for performance. - removes `Table` component, because it was never used standalone, `ManagedTable` is what all plugins used - uses `react-window` for `ManagedTable` - reworks table highlighting and arrow-navigation to work with the new virtualization - moves actual filtering out of `ManagedTable` into `Searchable` component for a better separation of concerns. Reviewed By: jknoxville Differential Revision: D9447721 fbshipit-source-id: 15eb2eb55eed9f49a0cb1ccfb2d748b3672fa898
This commit is contained in:
committed by
Facebook Github Bot
parent
33f34650df
commit
1891e2c869
@@ -6,7 +6,6 @@
|
||||
*/
|
||||
|
||||
import type {
|
||||
TableColumnRawOrder,
|
||||
TableColumnOrder,
|
||||
TableColumnSizes,
|
||||
TableColumns,
|
||||
@@ -16,8 +15,18 @@ import type {
|
||||
TableBodyRow,
|
||||
TableOnAddFilter,
|
||||
} from './types.js';
|
||||
|
||||
import React from 'react';
|
||||
import styled from '../../styled/index.js';
|
||||
import Table from './Table.js';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import {VariableSizeList as List} from 'react-window';
|
||||
import {clipboard} from 'electron';
|
||||
import TableHead from './TableHead.js';
|
||||
import TableRow from './TableRow.js';
|
||||
import ContextMenu from '../ContextMenu.js';
|
||||
import FlexColumn from '../FlexColumn.js';
|
||||
import createPaste from '../../../utils/createPaste.js';
|
||||
import {DEFAULT_ROW_HEIGHT} from './types';
|
||||
|
||||
export type ManagedTableProps = {|
|
||||
/**
|
||||
@@ -50,7 +59,7 @@ export type ManagedTableProps = {|
|
||||
/**
|
||||
* Order of columns.
|
||||
*/
|
||||
columnOrder?: ?TableColumnRawOrder,
|
||||
columnOrder?: TableColumnOrder,
|
||||
/**
|
||||
* Size of the columns.
|
||||
*/
|
||||
@@ -99,10 +108,11 @@ export type ManagedTableProps = {|
|
||||
|};
|
||||
|
||||
type ManagedTableState = {|
|
||||
highlightedRows: TableHighlightedRows,
|
||||
highlightedRows: Set<string>,
|
||||
sortOrder: ?TableRowSortOrder,
|
||||
columnOrder: ?TableColumnRawOrder,
|
||||
columnSizes: ?TableColumnSizes,
|
||||
columnOrder: TableColumnOrder,
|
||||
columnSizes: TableColumnSizes,
|
||||
shouldScrollToBottom: boolean,
|
||||
|};
|
||||
|
||||
/**
|
||||
@@ -111,7 +121,7 @@ type ManagedTableState = {|
|
||||
* If you require lower level access to the state then use [`<Table>`]()
|
||||
* directly.
|
||||
*/
|
||||
export default class ManagedTable extends styled.StylablePureComponent<
|
||||
export default class ManagedTable extends styled.StylableComponent<
|
||||
ManagedTableProps,
|
||||
ManagedTableState,
|
||||
> {
|
||||
@@ -124,16 +134,32 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
);
|
||||
};
|
||||
|
||||
state = {
|
||||
state: ManagedTableState = {
|
||||
columnOrder:
|
||||
JSON.parse(window.localStorage.getItem(this.getTableKey()) || 'null') ||
|
||||
this.props.columnOrder,
|
||||
columnSizes: this.props.columnSizes,
|
||||
highlightedRows: [],
|
||||
this.props.columnOrder ||
|
||||
Object.keys(this.props.columns).map(key => ({key, visible: true})),
|
||||
columnSizes: this.props.columnSizes || {},
|
||||
highlightedRows: new Set(),
|
||||
sortOrder: null,
|
||||
shouldScrollToBottom: Boolean(this.props.stickyBottom),
|
||||
};
|
||||
|
||||
tableRef: ?Table;
|
||||
tableRef: {
|
||||
current: null | List,
|
||||
} = React.createRef();
|
||||
scrollRef: {
|
||||
current: null | HTMLDivElement,
|
||||
} = React.createRef();
|
||||
dragStartIndex: ?number = null;
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: ManagedTableProps) {
|
||||
// if columnSizes has changed
|
||||
@@ -152,20 +178,64 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
columnOrder: nextProps.columnOrder,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
nextProps.filter !== this.props.filter &&
|
||||
this.tableRef &&
|
||||
this.tableRef.current
|
||||
) {
|
||||
// rows were filtered, we need to recalculate heights
|
||||
this.tableRef.current.resetAfterIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
onHighlight = (highlightedRows: TableHighlightedRows) => {
|
||||
if (this.props.highlightableRows === false) {
|
||||
componentDidUpdate(prevProps: ManagedTableProps) {
|
||||
if (
|
||||
this.props.rows.length !== prevProps.rows.length &&
|
||||
this.state.shouldScrollToBottom &&
|
||||
this.state.highlightedRows.size < 2
|
||||
) {
|
||||
this.scrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
onCopy = () => {
|
||||
clipboard.writeText(this.getSelectedText());
|
||||
};
|
||||
|
||||
onKeyDown = (e: KeyboardEvent) => {
|
||||
const {highlightedRows} = this.state;
|
||||
if (highlightedRows.size === 0) {
|
||||
return;
|
||||
}
|
||||
if (this.props.multiHighlight !== true) {
|
||||
highlightedRows = highlightedRows.slice(0, 1);
|
||||
}
|
||||
|
||||
this.setState({highlightedRows});
|
||||
|
||||
if (this.props.onRowHighlighted) {
|
||||
this.props.onRowHighlighted(highlightedRows);
|
||||
if (
|
||||
((e.metaKey && process.platform === 'darwin') ||
|
||||
(e.ctrlKey && process.platform !== 'darwin')) &&
|
||||
e.keyCode === 67
|
||||
) {
|
||||
this.onCopy();
|
||||
} else if (e.keyCode === 38 || e.keyCode === 40) {
|
||||
// arrow navigation
|
||||
const {rows} = this.props;
|
||||
const {highlightedRows} = this.state;
|
||||
const lastItemKey = Array.from(this.state.highlightedRows).pop();
|
||||
const lastItemIndex = this.props.rows.findIndex(
|
||||
row => row.key === lastItemKey,
|
||||
);
|
||||
const newIndex = Math.min(
|
||||
rows.length - 1,
|
||||
Math.max(0, e.keyCode === 38 ? lastItemIndex - 1 : lastItemIndex + 1),
|
||||
);
|
||||
if (!e.shiftKey) {
|
||||
highlightedRows.clear();
|
||||
}
|
||||
highlightedRows.add(rows[newIndex].key);
|
||||
this.setState({highlightedRows}, () => {
|
||||
const {current} = this.tableRef;
|
||||
if (current) {
|
||||
current.scrollToItem(newIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -174,7 +244,6 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
};
|
||||
|
||||
onColumnOrder = (columnOrder: TableColumnOrder) => {
|
||||
// $FlowFixMe
|
||||
this.setState({columnOrder});
|
||||
// persist column order
|
||||
window.localStorage.setItem(
|
||||
@@ -187,45 +256,246 @@ export default class ManagedTable extends styled.StylablePureComponent<
|
||||
this.setState({columnSizes});
|
||||
};
|
||||
|
||||
setRef = (table: ?Table) => {
|
||||
this.tableRef = table;
|
||||
};
|
||||
|
||||
scrollToBottom() {
|
||||
const {tableRef} = this;
|
||||
if (tableRef) {
|
||||
tableRef.scrollToBottom();
|
||||
const {current} = this.tableRef;
|
||||
if (current && this.props.rows.length > 1) {
|
||||
current.scrollToItem(this.props.rows.length - 1);
|
||||
}
|
||||
}
|
||||
|
||||
onHighlight = (
|
||||
e: SyntheticMouseEvent<>,
|
||||
row: TableBodyRow,
|
||||
index: number,
|
||||
) => {
|
||||
if (e.button !== 0) {
|
||||
// Only highlight rows when using primary mouse button,
|
||||
// otherwise do nothing, to not interfere context menus.
|
||||
return;
|
||||
}
|
||||
if (e.shiftKey) {
|
||||
// prevents text selection
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
let {highlightedRows} = this.state;
|
||||
|
||||
this.dragStartIndex = index;
|
||||
document.addEventListener('mouseup', this.onStopDragSelecting);
|
||||
|
||||
if (
|
||||
(e.metaKey && process.platform === 'darwin') ||
|
||||
(e.ctrlKey && process.platform !== 'darwin')
|
||||
) {
|
||||
highlightedRows.add(row.key);
|
||||
} else if (e.shiftKey) {
|
||||
// range select
|
||||
const lastItemKey = Array.from(this.state.highlightedRows).pop();
|
||||
highlightedRows = new Set([
|
||||
...highlightedRows,
|
||||
...this.selectInRange(lastItemKey, row.key),
|
||||
]);
|
||||
} else {
|
||||
// single select
|
||||
this.state.highlightedRows.clear();
|
||||
this.state.highlightedRows.add(row.key);
|
||||
}
|
||||
|
||||
this.setState({highlightedRows});
|
||||
};
|
||||
|
||||
onStopDragSelecting = () => {
|
||||
this.dragStartIndex = null;
|
||||
document.removeEventListener('mouseup', this.onStopDragSelecting);
|
||||
};
|
||||
|
||||
selectInRange = (fromKey: string, toKey: string): Array<string> => {
|
||||
const selected = [];
|
||||
let startIndex = -1;
|
||||
let endIndex = -1;
|
||||
for (let i = 0; i < this.props.rows.length; i++) {
|
||||
if (this.props.rows[i].key === fromKey) {
|
||||
startIndex = i;
|
||||
}
|
||||
if (this.props.rows[i].key === toKey) {
|
||||
endIndex = i;
|
||||
}
|
||||
if (endIndex > -1 && startIndex > -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
let i = Math.min(startIndex, endIndex);
|
||||
i <= Math.max(startIndex, endIndex);
|
||||
i++
|
||||
) {
|
||||
try {
|
||||
selected.push(this.props.rows[i].key);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
return selected;
|
||||
};
|
||||
|
||||
onMouseEnterRow = (
|
||||
e: SyntheticMouseEvent<>,
|
||||
row: TableBodyRow,
|
||||
index: number,
|
||||
) => {
|
||||
const {dragStartIndex} = this;
|
||||
const {current} = this.tableRef;
|
||||
if (dragStartIndex && current) {
|
||||
current.scrollToItem(index + 1);
|
||||
const startKey = this.props.rows[dragStartIndex].key;
|
||||
const highlightedRows = new Set(this.selectInRange(startKey, row.key));
|
||||
this.setState({
|
||||
highlightedRows,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
buildContextMenuItems = () => {
|
||||
const {highlightedRows} = this.state;
|
||||
if (highlightedRows.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
label:
|
||||
highlightedRows.size > 1
|
||||
? `Copy ${highlightedRows.size} rows`
|
||||
: 'Copy row',
|
||||
click: this.onCopy,
|
||||
},
|
||||
{
|
||||
label: 'Create Paste',
|
||||
click: () => createPaste(this.getSelectedText()),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
getSelectedText = (): string => {
|
||||
const {highlightedRows} = this.state;
|
||||
|
||||
if (highlightedRows.size === 0) {
|
||||
return '';
|
||||
}
|
||||
return this.props.rows
|
||||
.filter(row => highlightedRows.has(row.key))
|
||||
.map(
|
||||
(row: TableBodyRow) =>
|
||||
row.copyText ||
|
||||
Array.from(
|
||||
document.querySelectorAll(`[data-key='${row.key}'] > *`) || [],
|
||||
)
|
||||
.map(node => node.textContent)
|
||||
.join('\t'),
|
||||
)
|
||||
.join('\n');
|
||||
};
|
||||
|
||||
onScroll = ({
|
||||
scrollDirection,
|
||||
scrollOffset,
|
||||
}: {
|
||||
scrollDirection: 'forward' | 'backward',
|
||||
scrollOffset: number,
|
||||
scrollUpdateWasRequested: boolean,
|
||||
}) => {
|
||||
const {current} = this.scrollRef;
|
||||
const parent = current ? current.parentElement : null;
|
||||
if (
|
||||
this.props.stickyBottom &&
|
||||
scrollDirection === 'forward' &&
|
||||
!this.state.shouldScrollToBottom &&
|
||||
current &&
|
||||
parent instanceof HTMLElement &&
|
||||
current.offsetHeight - (scrollOffset + parent.offsetHeight) <
|
||||
parent.offsetHeight
|
||||
) {
|
||||
this.setState({shouldScrollToBottom: true});
|
||||
} else if (
|
||||
this.props.stickyBottom &&
|
||||
scrollDirection === 'backward' &&
|
||||
this.state.shouldScrollToBottom
|
||||
) {
|
||||
this.setState({shouldScrollToBottom: false});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {props, state} = this;
|
||||
const {
|
||||
onAddFilter,
|
||||
columns,
|
||||
multiline,
|
||||
zebra,
|
||||
rows,
|
||||
rowLineHeight,
|
||||
} = this.props;
|
||||
|
||||
const {columnOrder, columnSizes, highlightedRows} = this.state;
|
||||
const columnKeys = columnOrder
|
||||
.map(k => (k.visible ? k.key : null))
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<Table
|
||||
ref={this.setRef}
|
||||
virtual={props.virtual}
|
||||
floating={props.floating}
|
||||
multiline={props.multiline}
|
||||
columns={props.columns}
|
||||
rows={props.rows}
|
||||
rowLineHeight={props.rowLineHeight}
|
||||
autoHeight={props.autoHeight}
|
||||
filter={props.filter}
|
||||
filterValue={props.filterValue}
|
||||
highlightedRows={state.highlightedRows}
|
||||
onHighlight={this.onHighlight}
|
||||
sortOrder={state.sortOrder}
|
||||
onSort={this.onSort}
|
||||
columnOrder={state.columnOrder}
|
||||
onColumnOrder={this.onColumnOrder}
|
||||
columnSizes={state.columnSizes}
|
||||
onColumnResize={this.onColumnResize}
|
||||
stickyBottom={props.stickyBottom}
|
||||
onAddFilter={props.onAddFilter}
|
||||
zebra={props.zebra}
|
||||
hideHeader={props.hideHeader}
|
||||
/>
|
||||
<FlexColumn style={{flexGrow: 1}}>
|
||||
<TableHead
|
||||
columnOrder={columnOrder}
|
||||
onColumnOrder={this.onColumnOrder}
|
||||
columns={columns}
|
||||
onColumnResize={this.onColumnResize}
|
||||
sortOrder={this.state.sortOrder}
|
||||
columnSizes={columnSizes}
|
||||
onSort={this.onSort}
|
||||
/>
|
||||
<FlexColumn
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
}}>
|
||||
<AutoSizer>
|
||||
{({width, height}) => (
|
||||
<ContextMenu buildItems={this.buildContextMenuItems}>
|
||||
<List
|
||||
itemCount={this.props.rows.length}
|
||||
itemSize={index =>
|
||||
(rows[index] && rows[index].height) ||
|
||||
rowLineHeight ||
|
||||
DEFAULT_ROW_HEIGHT
|
||||
}
|
||||
ref={this.tableRef}
|
||||
width={width}
|
||||
estimatedItemSize={rowLineHeight || DEFAULT_ROW_HEIGHT}
|
||||
overscanCount={5}
|
||||
innerRef={this.scrollRef}
|
||||
onScroll={this.onScroll}
|
||||
height={height}>
|
||||
{({index, style}) => (
|
||||
<TableRow
|
||||
columnSizes={columnSizes}
|
||||
columnKeys={columnKeys}
|
||||
onMouseDown={e => this.onHighlight(e, rows[index], index)}
|
||||
onMouseEnter={e =>
|
||||
this.onMouseEnterRow(e, rows[index], index)
|
||||
}
|
||||
multiline={multiline}
|
||||
rowLineHeight={24}
|
||||
highlighted={highlightedRows.has(rows[index].key)}
|
||||
row={rows[index]}
|
||||
index={index}
|
||||
style={style}
|
||||
onAddFilter={onAddFilter}
|
||||
zebra={zebra}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
</ContextMenu>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</FlexColumn>
|
||||
</FlexColumn>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user