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 {usePluginInstance} from '../../plugin/PluginContext';
import {debounce} from 'lodash';
import {StaticDataSourceRenderer} from './StaticDataSourceRenderer';
interface DataTableProps<T = any> {
columns: DataTableColumn<T>[];
@@ -59,6 +60,8 @@ interface DataTableProps<T = any> {
_testHeight?: number; // exposed for unit testing only
onCopyRows?(records: T[]): string;
onContextMenu?: (selection: undefined | T) => React.ReactElement;
searchbar?: boolean;
scrollable?: boolean;
}
type DataTableInput<T = any> =
@@ -403,23 +406,34 @@ export function DataTable<T extends object>(
// eslint-disable-next-line
}, []);
return (
<Layout.Container grow>
const header = (
<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.Container>
<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>
{header}
<DataSourceRenderer<T, RenderContext<T>>
dataSource={dataSource}
autoScroll={tableState.autoScroll && !dragging.current}
@@ -435,6 +449,24 @@ export function DataTable<T extends object>(
_testHeight={props._testHeight}
/>
</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 && (
<AutoScroller>
<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
records.forEach((r) => ds.append(r));
const duration = Math.abs(Date.now() - startTime);
if (duration > 50 || records.length > 1000) {
if (duration > 50 || records.length > 500) {
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",
);

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

View File

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

View File

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

View File

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