From 00e4d2440d27f26386556aaea20c60eab31f730f Mon Sep 17 00:00:00 2001 From: Michel Weststrate Date: Tue, 27 Apr 2021 14:52:34 -0700 Subject: [PATCH] Support providing records directly to DataTable Summary: This diff allows using a DataTable without setting up a full datasource, which is simpler in case the dataset is small and/or fixed. For an continuously growing dataset a DataSource should still be set up. Reviewed By: jknoxville Differential Revision: D28037897 fbshipit-source-id: 2e56b1970e19f967c3752a78737b8f7a3f934b87 --- .../src/ui/data-table/DataTable.tsx | 49 +++++++- .../__tests__/DataTableRecords.node.tsx | 108 ++++++++++++++++++ 2 files changed, 154 insertions(+), 3 deletions(-) create mode 100644 desktop/flipper-plugin/src/ui/data-table/__tests__/DataTableRecords.node.tsx diff --git a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx index 5adc2ec03..921f27a76 100644 --- a/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx +++ b/desktop/flipper-plugin/src/ui/data-table/DataTable.tsx @@ -49,7 +49,7 @@ import {debounce} from 'lodash'; interface DataTableProps { columns: DataTableColumn[]; - dataSource: DataSource; + autoScroll?: boolean; extraActions?: React.ReactElement; onSelect?(record: T | undefined, records: T[]): void; @@ -61,6 +61,12 @@ interface DataTableProps { onContextMenu?: (selection: undefined | T) => React.ReactElement; } +type DataTableInput = + | {dataSource: DataSource} + | { + records: T[]; + }; + export type DataTableColumn = { key: keyof T & string; // possible future extension: getValue(row) (and free-form key) to support computed columns @@ -94,9 +100,10 @@ export interface RenderContext { } export function DataTable( - props: DataTableProps, + props: DataTableInput & DataTableProps, ): React.ReactElement { - const {dataSource, onRowStyle, onSelect, onCopyRows, onContextMenu} = props; + const {onRowStyle, onSelect, onCopyRows, onContextMenu} = props; + const dataSource = normalizeDataSourceInput(props); useAssertStableRef(dataSource, 'dataSource'); useAssertStableRef(onRowStyle, 'onRowStyle'); useAssertStableRef(props.onSelect, 'onRowSelect'); @@ -445,6 +452,42 @@ export function DataTable( ); } +/* eslint-disable react-hooks/rules-of-hooks */ +function normalizeDataSourceInput(props: DataTableInput): DataSource { + if ('dataSource' in props) { + return props.dataSource; + } + if ('records' in props) { + const [dataSource] = useState(() => { + const ds = new DataSource(undefined); + syncRecordsToDataSource(ds, props.records); + return ds; + }); + useEffect(() => { + syncRecordsToDataSource(dataSource, props.records); + }, [dataSource, props.records]); + + return dataSource; + } + throw new Error( + `Either the 'dataSource' or 'records' prop should be provided to DataTable`, + ); +} +/* eslint-enable */ + +function syncRecordsToDataSource(ds: DataSource, records: T[]) { + const startTime = Date.now(); + ds.clear(); + // 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) { + 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", + ); + } +} + function emptyRenderer(dataSource: DataSource) { return ; } 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 new file mode 100644 index 000000000..0d26bee32 --- /dev/null +++ b/desktop/flipper-plugin/src/ui/data-table/__tests__/DataTableRecords.node.tsx @@ -0,0 +1,108 @@ +/** + * 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 from 'react'; +import {DataTable, DataTableColumn} from '../DataTable'; +import {render} from '@testing-library/react'; + +type Todo = { + title: string; + done: boolean; +}; + +function createTestRecords(): Todo[] { + return [ + { + title: 'test DataTable', + done: true, + }, + ]; +} + +const columns: DataTableColumn[] = [ + { + key: 'title', + wrap: false, + }, + { + key: 'done', + wrap: false, + }, +]; + +test('update and append', async () => { + let records = createTestRecords(); + const rendering = render( + , + ); + { + const elem = await rendering.findAllByText('test DataTable'); + expect(elem.length).toBe(1); + expect(elem[0].parentElement).toMatchInlineSnapshot(` +
+
+ test DataTable +
+
+ true +
+
+ `); + } + + function rerender() { + rendering.rerender( + , + ); + } + + // append + { + records = [ + ...records, + { + title: 'Drink coffee', + done: false, + }, + ]; + rerender(); + const elem = await rendering.findAllByText('Drink coffee'); + expect(elem.length).toBe(1); + } + + // update + { + records = [ + { + title: 'DataTable tested', + done: false, + }, + ...records.slice(1), + ]; + rerender(); + const elem = await rendering.findAllByText('DataTable tested'); + expect(elem.length).toBe(1); + expect(rendering.queryByText('test DataTable')).toBeNull(); + } + + // remove + { + records = [records[1]]; + rerender(); + const elem = await rendering.findAllByText('Drink coffee'); + expect(elem.length).toBe(1); + expect(rendering.queryByText('DataTable tested')).toBeNull(); + } +});