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:
committed by
Facebook GitHub Bot
parent
e423bc7959
commit
d6c74c4e2f
@@ -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,10 +406,9 @@ export function DataTable<T extends object>(
|
||||
// eslint-disable-next-line
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout.Container grow>
|
||||
<Layout.Top>
|
||||
const header = (
|
||||
<Layout.Container>
|
||||
{props.searchbar !== false && (
|
||||
<TableSearch
|
||||
searchValue={searchValue}
|
||||
useRegex={tableState.useRegex}
|
||||
@@ -414,12 +416,24 @@ export function DataTable<T extends object>(
|
||||
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>
|
||||
{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",
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -104,7 +104,8 @@ const TableHeadColumnContainer = styled.div<{
|
||||
}));
|
||||
TableHeadColumnContainer.displayName = 'TableHead:TableHeadColumnContainer';
|
||||
|
||||
const TableHeadContainer = styled.div({
|
||||
const TableHeadContainer = styled.div<{scrollbarSize: number}>(
|
||||
({scrollbarSize}) => ({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
@@ -115,8 +116,9 @@ const TableHeadContainer = styled.div({
|
||||
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,
|
||||
});
|
||||
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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user