Support natural sizing

Summary:
Introduced the `scrollable={false}` option to DataTable, that gives the table its natural size, while still having all the other gimmicks of DataTable, like search, filter, etc etc.

To implement this, a non-virtualizing rendering is needed, which is handled by the `StaticDataSourceRenderer`

Also introduced the option to hide the searchbar.

Reviewed By: nikoant

Differential Revision: D28036469

fbshipit-source-id: 633c4f7f3fabfa99efa2839059aaa59b0a407ada
This commit is contained in:
Michel Weststrate
2021-04-28 05:47:07 -07:00
committed by Facebook GitHub Bot
parent e423bc7959
commit d6c74c4e2f
6 changed files with 188 additions and 40 deletions

View File

@@ -46,6 +46,7 @@ import {useAssertStableRef} from '../../utils/useAssertStableRef';
import {Formatter} from '../DataFormatter'; import {Formatter} from '../DataFormatter';
import {usePluginInstance} from '../../plugin/PluginContext'; import {usePluginInstance} from '../../plugin/PluginContext';
import {debounce} from 'lodash'; import {debounce} from 'lodash';
import {StaticDataSourceRenderer} from './StaticDataSourceRenderer';
interface DataTableProps<T = any> { interface DataTableProps<T = any> {
columns: DataTableColumn<T>[]; columns: DataTableColumn<T>[];
@@ -59,6 +60,8 @@ interface DataTableProps<T = any> {
_testHeight?: number; // exposed for unit testing only _testHeight?: number; // exposed for unit testing only
onCopyRows?(records: T[]): string; onCopyRows?(records: T[]): string;
onContextMenu?: (selection: undefined | T) => React.ReactElement; onContextMenu?: (selection: undefined | T) => React.ReactElement;
searchbar?: boolean;
scrollable?: boolean;
} }
type DataTableInput<T = any> = type DataTableInput<T = any> =
@@ -403,23 +406,34 @@ export function DataTable<T extends object>(
// eslint-disable-next-line // eslint-disable-next-line
}, []); }, []);
return ( const header = (
<Layout.Container grow> <Layout.Container>
{props.searchbar !== false && (
<TableSearch
searchValue={searchValue}
useRegex={tableState.useRegex}
dispatch={dispatch as any}
contextMenu={contexMenu}
extraActions={props.extraActions}
/>
)}
<TableHead
visibleColumns={visibleColumns}
dispatch={dispatch as any}
sorting={sorting}
scrollbarSize={
props.scrollable === false
? 0
: 15 /* width on MacOS: TODO, determine dynamically */
}
/>
</Layout.Container>
);
const mainSection =
props.scrollable !== false ? (
<Layout.Top> <Layout.Top>
<Layout.Container> {header}
<TableSearch
searchValue={searchValue}
useRegex={tableState.useRegex}
dispatch={dispatch as any}
contextMenu={contexMenu}
extraActions={props.extraActions}
/>
<TableHead
visibleColumns={visibleColumns}
dispatch={dispatch as any}
sorting={sorting}
/>
</Layout.Container>
<DataSourceRenderer<T, RenderContext<T>> <DataSourceRenderer<T, RenderContext<T>>
dataSource={dataSource} dataSource={dataSource}
autoScroll={tableState.autoScroll && !dragging.current} autoScroll={tableState.autoScroll && !dragging.current}
@@ -435,6 +449,24 @@ export function DataTable<T extends object>(
_testHeight={props._testHeight} _testHeight={props._testHeight}
/> />
</Layout.Top> </Layout.Top>
) : (
<Layout.Container>
{header}
<StaticDataSourceRenderer<T, RenderContext<T>>
dataSource={dataSource}
useFixedRowHeight={!tableState.usesWrapping}
defaultRowHeight={DEFAULT_ROW_HEIGHT}
context={renderingConfig}
itemRenderer={itemRenderer}
onKeyDown={onKeyDown}
emptyRenderer={emptyRenderer}
/>
</Layout.Container>
);
return (
<Layout.Container grow={props.scrollable !== false}>
{mainSection}
{props.autoScroll && ( {props.autoScroll && (
<AutoScroller> <AutoScroller>
<PushpinFilled <PushpinFilled
@@ -481,7 +513,7 @@ function syncRecordsToDataSource<T>(ds: DataSource<T>, records: T[]) {
// TODO: optimize in the case we're only dealing with appends or replacements // TODO: optimize in the case we're only dealing with appends or replacements
records.forEach((r) => ds.append(r)); records.forEach((r) => ds.append(r));
const duration = Math.abs(Date.now() - startTime); const duration = Math.abs(Date.now() - startTime);
if (duration > 50 || records.length > 1000) { if (duration > 50 || records.length > 500) {
console.warn( console.warn(
"The 'records' props is only intended to be used on small datasets. Please use a 'dataSource' instead. See createDataSource for details: https://fbflipper.com/docs/extending/flipper-plugin#createdatasource", "The 'records' props is only intended to be used on small datasets. Please use a 'dataSource' instead. See createDataSource for details: https://fbflipper.com/docs/extending/flipper-plugin#createdatasource",
); );

View File

@@ -0,0 +1,111 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import React, {memo, useCallback, useEffect, useState} from 'react';
import {DataSource} from '../../state/DataSource';
import {useVirtual} from 'react-virtual';
import styled from '@emotion/styled';
import {RedrawContext} from './DataSourceRenderer';
export type DataSourceVirtualizer = ReturnType<typeof useVirtual>;
type DataSourceProps<T extends object, C> = {
/**
* The data source to render
*/
dataSource: DataSource<T, any, any>;
/**
* additional context that will be passed verbatim to the itemRenderer, so that it can be easily memoized
*/
context?: C;
/**
* Takes care of rendering an item
* @param item The item as stored in the dataSource
* @param index The index of the item being rendered. The index represents the offset in the _visible_ items of the dataSource
* @param context The optional context passed into this DataSourceRenderer
*/
itemRenderer(item: T, index: number, context: C): React.ReactElement;
useFixedRowHeight: boolean;
defaultRowHeight: number;
onKeyDown?: React.KeyboardEventHandler<HTMLDivElement>;
onUpdateAutoScroll?(autoScroll: boolean): void;
emptyRenderer?(dataSource: DataSource<T>): React.ReactElement;
};
/**
* This component is UI agnostic, and just takes care of rendering all items in the DataSource.
* This component does not apply virtualization, so don't use it for large datasets!
*/
export const StaticDataSourceRenderer: <T extends object, C>(
props: DataSourceProps<T, C>,
) => React.ReactElement = memo(function StaticDataSourceRenderer({
dataSource,
useFixedRowHeight,
context,
itemRenderer,
onKeyDown,
emptyRenderer,
}: DataSourceProps<any, any>) {
/**
* Virtualization
*/
// render scheduling
const [, setForceUpdate] = useState(0);
const redraw = useCallback(() => {
setForceUpdate((x) => x + 1);
}, []);
useEffect(
function subscribeToDataSource() {
let unmounted = false;
dataSource.view.setWindow(0, dataSource.limit);
dataSource.view.setListener((_event) => {
if (unmounted) {
return;
}
setForceUpdate((x) => x + 1);
});
return () => {
unmounted = true;
dataSource.view.setListener(undefined);
};
},
[dataSource, setForceUpdate, useFixedRowHeight],
);
useEffect(() => {
// initial virtualization is incorrect because the parent ref is not yet set, so trigger render after mount
setForceUpdate((x) => x + 1);
}, [setForceUpdate]);
/**
* Rendering
*/
const records = dataSource.view.output();
if (records.length > 500) {
console.warn(
"StaticDataSourceRenderer should only be used on small datasets. For large datasets the 'scrollable' flag should enabled on DataTable",
);
}
return (
<RedrawContext.Provider value={redraw}>
<div onKeyDown={onKeyDown}>
{records.length === 0
? emptyRenderer?.(dataSource)
: records.map((item, index) => (
<div key={index}>{itemRenderer(item, index, context)}</div>
))}
</div>
</RedrawContext.Provider>
);
}) as any;

View File

@@ -104,19 +104,21 @@ const TableHeadColumnContainer = styled.div<{
})); }));
TableHeadColumnContainer.displayName = 'TableHead:TableHeadColumnContainer'; TableHeadColumnContainer.displayName = 'TableHead:TableHeadColumnContainer';
const TableHeadContainer = styled.div({ const TableHeadContainer = styled.div<{scrollbarSize: number}>(
position: 'relative', ({scrollbarSize}) => ({
display: 'flex', position: 'relative',
flexDirection: 'row', display: 'flex',
borderBottom: `1px solid ${theme.dividerColor}`, flexDirection: 'row',
backgroundColor: theme.backgroundWash, borderBottom: `1px solid ${theme.dividerColor}`,
userSelect: 'none', backgroundColor: theme.backgroundWash,
whiteSpace: 'nowrap', userSelect: 'none',
borderLeft: `4px solid ${theme.backgroundWash}`, // space for selection, see TableRow whiteSpace: 'nowrap',
// hardcoded value to correct for the scrollbar in the main container. borderLeft: `4px solid ${theme.backgroundWash}`, // space for selection, see TableRow
// ideally we should measure this instead. // hardcoded value to correct for the scrollbar in the main container.
paddingRight: 15, // ideally we should measure this instead.
}); paddingRight: scrollbarSize,
}),
);
TableHeadContainer.displayName = 'TableHead:TableHeadContainer'; TableHeadContainer.displayName = 'TableHead:TableHeadContainer';
const RIGHT_RESIZABLE = {right: true}; const RIGHT_RESIZABLE = {right: true};
@@ -221,13 +223,15 @@ export const TableHead = memo(function TableHead({
visibleColumns, visibleColumns,
dispatch, dispatch,
sorting, sorting,
scrollbarSize,
}: { }: {
dispatch: DataTableDispatch<any>; dispatch: DataTableDispatch<any>;
visibleColumns: DataTableColumn<any>[]; visibleColumns: DataTableColumn<any>[];
sorting: Sorting | undefined; sorting: Sorting | undefined;
scrollbarSize: number;
}) { }) {
return ( return (
<TableHeadContainer> <TableHeadContainer scrollbarSize={scrollbarSize}>
{visibleColumns.map((column, i) => ( {visibleColumns.map((column, i) => (
<TableHeadColumn <TableHeadColumn
key={column.key} key={column.key}

View File

@@ -68,7 +68,7 @@ TableBodyRowContainer.displayName = 'TableRow:TableBodyRowContainer';
const TableBodyColumnContainer = styled.div<{ const TableBodyColumnContainer = styled.div<{
width: Width; width: Width;
multiline?: boolean; multiline?: boolean;
justifyContent: 'left' | 'right' | 'center' | 'flex-start'; justifyContent: 'left' | 'right' | 'center';
}>((props) => ({ }>((props) => ({
display: 'block', display: 'block',
flexShrink: props.width === undefined ? 1 : 0, flexShrink: props.width === undefined ? 1 : 0,
@@ -80,6 +80,7 @@ const TableBodyColumnContainer = styled.div<{
whiteSpace: props.multiline ? 'pre-wrap' : 'nowrap', whiteSpace: props.multiline ? 'pre-wrap' : 'nowrap',
wordWrap: props.multiline ? 'break-word' : 'normal', wordWrap: props.multiline ? 'break-word' : 'normal',
width: props.width, width: props.width,
textAlign: props.justifyContent,
justifyContent: props.justifyContent, justifyContent: props.justifyContent,
'&::selection': { '&::selection': {
color: 'inherit', color: 'inherit',
@@ -128,7 +129,7 @@ export const TableRow = memo(function TableRow({
<TableBodyColumnContainer <TableBodyColumnContainer
key={col.key as string} key={col.key as string}
multiline={col.wrap} multiline={col.wrap}
justifyContent={col.align ? col.align : 'flex-start'} justifyContent={col.align ? col.align : 'left'}
width={col.width}> width={col.width}>
{value} {value}
</TableBodyColumnContainer> </TableBodyColumnContainer>

View File

@@ -58,12 +58,12 @@ test('update and append', async () => {
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
> >
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
test DataTable test DataTable
</div> </div>
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
true true
</div> </div>
@@ -115,12 +115,12 @@ test('column visibility', async () => {
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
> >
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
test DataTable test DataTable
</div> </div>
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
true true
</div> </div>
@@ -140,7 +140,7 @@ test('column visibility', async () => {
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
> >
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
test DataTable test DataTable
</div> </div>

View File

@@ -49,12 +49,12 @@ test('update and append', async () => {
class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" class="css-1k3kr6b-TableBodyRowContainer e1luu51r1"
> >
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
test DataTable test DataTable
</div> </div>
<div <div
class="css-744e08-TableBodyColumnContainer e1luu51r0" class="css-esqhnb-TableBodyColumnContainer e1luu51r0"
> >
true true
</div> </div>