From d6c74c4e2f05f4655173b13e360e2d5806c28469 Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Wed, 28 Apr 2021 05:47:07 -0700 Subject: [PATCH] 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 --- .../src/ui/data-table/DataTable.tsx | 66 ++++++++--- .../data-table/StaticDataSourceRenderer.tsx | 111 ++++++++++++++++++ .../src/ui/data-table/TableHead.tsx | 32 ++--- .../src/ui/data-table/TableRow.tsx | 5 +- .../data-table/__tests__/DataTable.node.tsx | 10 +- .../__tests__/DataTableRecords.node.tsx | 4 +- 6 files changed, 188 insertions(+), 40 deletions(-) create mode 100644 desktop/flipper-plugin/src/ui/data-table/StaticDataSourceRenderer.tsx diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx index 921f27a76..81641e8f3 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx @@ -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 { columns: DataTableColumn[]; @@ -59,6 +60,8 @@ interface DataTableProps { _testHeight?: number; // exposed for unit testing only onCopyRows?(records: T[]): string; onContextMenu?: (selection: undefined | T) => React.ReactElement; + searchbar?: boolean; + scrollable?: boolean; } type DataTableInput = @@ -403,23 +406,34 @@ export function DataTable( // eslint-disable-next-line }, []); - return ( - + const header = ( + + {props.searchbar !== false && ( + + )} + + + ); + + const mainSection = + props.scrollable !== false ? ( - - - - + {header} > dataSource={dataSource} autoScroll={tableState.autoScroll && !dragging.current} @@ -435,6 +449,24 @@ export function DataTable( _testHeight={props._testHeight} /> + ) : ( + + {header} + > + dataSource={dataSource} + useFixedRowHeight={!tableState.usesWrapping} + defaultRowHeight={DEFAULT_ROW_HEIGHT} + context={renderingConfig} + itemRenderer={itemRenderer} + onKeyDown={onKeyDown} + emptyRenderer={emptyRenderer} + /> + + ); + + return ( + + {mainSection} {props.autoScroll && ( (ds: DataSource, 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", ); diff --git a/desktop/flipper-plugin/src/ui/data-table/StaticDataSourceRenderer.tsx b/desktop/flipper-plugin/src/ui/data-table/StaticDataSourceRenderer.tsx new file mode 100644 index 000000000..20d04241d --- /dev/null +++ b/desktop/flipper-plugin/src/ui/data-table/StaticDataSourceRenderer.tsx @@ -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; + +type DataSourceProps = { + /** + * The data source to render + */ + dataSource: DataSource; + /** + * 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; + onUpdateAutoScroll?(autoScroll: boolean): void; + emptyRenderer?(dataSource: DataSource): 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: ( + props: DataSourceProps, +) => React.ReactElement = memo(function StaticDataSourceRenderer({ + dataSource, + useFixedRowHeight, + context, + itemRenderer, + onKeyDown, + emptyRenderer, +}: DataSourceProps) { + /** + * 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 ( + +
+ {records.length === 0 + ? emptyRenderer?.(dataSource) + : records.map((item, index) => ( +
{itemRenderer(item, index, context)}
+ ))} +
+
+ ); +}) as any; diff --git a/desktop/flipper-plugin/src/ui/data-table/TableHead.tsx b/desktop/flipper-plugin/src/ui/data-table/TableHead.tsx index ff701dfce..ed5311ec9 100644 --- a/desktop/flipper-plugin/src/ui/data-table/TableHead.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/TableHead.tsx @@ -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; visibleColumns: DataTableColumn[]; sorting: Sorting | undefined; + scrollbarSize: number; }) { return ( - + {visibleColumns.map((column, i) => ( ((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({ {value} diff --git a/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTable.node.tsx b/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTable.node.tsx index 2d80db289..923a2abe9 100644 --- a/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTable.node.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTable.node.tsx @@ -58,12 +58,12 @@ test('update and append', async () => { class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" >
test DataTable
true
@@ -115,12 +115,12 @@ test('column visibility', async () => { class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" >
test DataTable
true
@@ -140,7 +140,7 @@ test('column visibility', async () => { class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" >
test DataTable
diff --git a/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTableRecords.node.tsx b/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTableRecords.node.tsx index 0d26bee32..d344f137c 100644 --- a/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTableRecords.node.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTableRecords.node.tsx @@ -49,12 +49,12 @@ test('update and append', async () => { class="css-1k3kr6b-TableBodyRowContainer e1luu51r1" >
test DataTable
true