From 250b12148da225b5479cd6aee4bbf2d8bc1d56eb Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 27 Sep 2019 13:14:47 -0600 Subject: [PATCH] [EuiDataGrid] InMemory Performance (#2374) * Automatically detect data schema for in-memory datagrid * Merge in described schema for field formatting * Better column type detection * Tests for euidatagrid schema / column type * refactor datagrid schema code, add datetime type detection * some comments * Allow extra type detectors for EuiDataGrid * cleanup of docs and type formatting * Fix datagrid unit test * Update currency detector * Allow EuiDataGrid's inMemory prop to be {true} * Added ability to provide extra props for the containing cell div * Added test for cell props * Performance cleanups * Clean up datagrid doc's inMemory selection * Merged in feature branch * EuiDataGrid in-memory options * Performance refactor for in-memory values * added a comment * Fix sorting on in-memory and schema datagrid docs --- src-docs/src/views/datagrid/datagrid.js | 215 ++++++++++++------ src-docs/src/views/datagrid/in_memory.js | 2 +- src-docs/src/views/datagrid/schema.js | 17 +- .../__snapshots__/data_grid.test.tsx.snap | 186 +++------------ src/components/datagrid/column_selector.tsx | 13 +- src/components/datagrid/data_grid.test.tsx | 28 +-- src/components/datagrid/data_grid.tsx | 54 +++-- src/components/datagrid/data_grid_body.tsx | 12 +- src/components/datagrid/data_grid_cell.tsx | 67 +++--- .../datagrid/data_grid_inmemory_renderer.tsx | 126 +++++++--- src/components/datagrid/data_grid_schema.ts | 1 + src/components/datagrid/data_grid_types.ts | 34 +-- src/components/datagrid/style_selector.tsx | 8 +- 13 files changed, 421 insertions(+), 342 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 070ac54aa45..fca7e084203 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -1,12 +1,21 @@ -import React, { Component, Fragment, useEffect } from 'react'; +import React, { + Fragment, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { fake } from 'faker'; import { + EuiButton, EuiDataGrid, EuiButtonIcon, EuiLink, + EuiPopover, } from '../../../../src/components/'; import { iconTypes } from '../../../../src-docs/src/views/icon/icons'; +import { EuiRadioGroup } from '../../../../src/components/form/radio'; const columns = [ { @@ -38,10 +47,10 @@ const columns = [ }, ]; -const data = []; +const raw_data = []; -for (let i = 1; i < 100; i++) { - data.push({ +for (let i = 1; i < 1000; i++) { + raw_data.push({ name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), email: {fake('{{internet.email}}')}, location: ( @@ -72,22 +81,39 @@ for (let i = 1; i < 100; i++) { }); } -export default class DataGridContainer extends Component { - constructor(props) { - super(props); - - this.state = { - sortingColumns: [], - data, - pagination: { - pageIndex: 0, - pageSize: 50, - }, - }; - } +export default () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + // ** Pagination config + const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); + const onChangeItemsPerPage = useCallback( + pageSize => setPagination(pagination => ({ ...pagination, pageSize })), + [setPagination] + ); + const onChangePage = useCallback( + pageIndex => setPagination(pagination => ({ ...pagination, pageIndex })), + [setPagination] + ); + + // ** Sorting config + const [sortingColumns, setSortingColumns] = useState([]); + const onSort = useCallback( + sortingColumns => { + setSortingColumns(sortingColumns); + }, + [setSortingColumns] + ); - setSorting = sortingColumns => { - const sortedData = [...data].sort((a, b) => { + const [inMemoryLevel, setInMemoryLevel] = useState(''); + + // Sort data + let data = useMemo(() => { + // the grid itself is responsible for sorting if inMemory is `sorting` + if (inMemoryLevel === 'sorting') { + return raw_data; + } + + return [...raw_data].sort((a, b) => { for (let i = 0; i < sortingColumns.length; i++) { const column = sortingColumns[i]; const aValue = a[column.id]; @@ -99,58 +125,117 @@ export default class DataGridContainer extends Component { return 0; }); - this.setState({ sortingColumns, data: sortedData }); - }; - - setPageIndex = pageIndex => - this.setState(({ pagination }) => ({ - pagination: { ...pagination, pageIndex }, - })); - - setPageSize = pageSize => - this.setState(({ pagination }) => ({ - pagination: { ...pagination, pageSize }, - })); - - dummyIcon = () => ( - - ); + }, [raw_data, sortingColumns, inMemoryLevel]); + + // Pagination + data = useMemo(() => { + // the grid itself is responsible for sorting if inMemory is sorting or pagination + if (inMemoryLevel === 'sorting' || inMemoryLevel === 'pagination') { + return data; + } + + const rowStart = pagination.pageIndex * pagination.pageSize; + const rowEnd = Math.min(rowStart + pagination.pageSize, data.length); + return data.slice(rowStart, rowEnd); + }, [data, pagination, inMemoryLevel]); + + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId, setCellProps }) => { + let adjustedRowIndex = rowIndex; - render() { - const { data, pagination, sortingColumns } = this.state; + // If we are doing the pagination (instead of leaving that to the grid) + // then the row index must be adjusted as `data` has already been pruned to the page size + if (inMemoryLevel !== 'sorting' && inMemoryLevel !== 'pagination') { + adjustedRowIndex = + rowIndex - pagination.pageIndex * pagination.pageSize; + } + + useEffect(() => { + if (columnId === 'amount') { + if (data.hasOwnProperty(adjustedRowIndex)) { + const numeric = parseFloat( + data[adjustedRowIndex][columnId].match(/\d+\.\d+/)[0], + 10 + ); + setCellProps({ + style: { + backgroundColor: `rgba(0, ${(numeric / 1000) * 255}, 0, 0.2)`, + }, + }); + } + } + }, [adjustedRowIndex, columnId, setCellProps]); + + return data.hasOwnProperty(adjustedRowIndex) + ? data[adjustedRowIndex][columnId] + : null; + }; + }, [data, inMemoryLevel]); + + const inMemoryProps = {}; + if (inMemoryLevel !== '') { + inMemoryProps.inMemory = { + level: inMemoryLevel, + skipColumns: ['actions'], + }; + } + + return ( +
+ setIsPopoverOpen(state => !state)}> + inMemory options + + } + closePopover={() => setIsPopoverOpen(false)}> + { + setInMemoryLevel(value === '' ? undefined : value); + setIsPopoverOpen(false); + }} + /> + - return ( { - useEffect(() => { - if (columnId === 'amount') { - const numeric = parseFloat( - data[rowIndex][columnId].match(/\d+\.\d+/)[0], - 10 - ); - setCellProps({ - style: { - backgroundColor: `rgba(0, ${(numeric / 1000) * 255}, 0, 0.2)`, - }, - }); - } - }, [rowIndex, columnId, setCellProps]); - return data[rowIndex][columnId]; - }} - sorting={{ columns: sortingColumns, onSort: this.setSorting }} + rowCount={raw_data.length} + renderCellValue={renderCellValue} + {...inMemoryProps} + sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, - pageSizeOptions: [5, 10, 25], - onChangeItemsPerPage: this.setPageSize, - onChangePage: this.setPageIndex, + pageSizeOptions: [10, 25, 50, 100], + onChangeItemsPerPage: onChangeItemsPerPage, + onChangePage: onChangePage, }} /> - ); - } -} +
+ ); +}; diff --git a/src-docs/src/views/datagrid/in_memory.js b/src-docs/src/views/datagrid/in_memory.js index 6b68a9943e7..67d9d011290 100644 --- a/src-docs/src/views/datagrid/in_memory.js +++ b/src-docs/src/views/datagrid/in_memory.js @@ -91,7 +91,7 @@ export default class InMemoryDataGrid extends Component { const value = data[rowIndex][columnId]; return value; }} - inMemory="sorting" + inMemory={{ level: 'sorting' }} sorting={{ columns: sortingColumns, onSort: this.setSorting }} pagination={{ ...pagination, diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 200170927c4..f1feccf3629 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -74,7 +74,22 @@ export default class InMemoryDataGrid extends Component { }; } - setSorting = sortingColumns => this.setState({ sortingColumns }); + setSorting = sortingColumns => { + const data = [...this.state.data].sort((a, b) => { + for (let i = 0; i < sortingColumns.length; i++) { + const column = sortingColumns[i]; + const aValue = a[column.id]; + const bValue = b[column.id]; + + if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; + if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + } + + return 0; + }); + + this.setState({ data, sortingColumns }); + }; setPageIndex = pageIndex => this.setState(({ pagination }) => ({ diff --git a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap index 3dd2294ec54..a8fcfe29e0e 100644 --- a/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap +++ b/src/components/datagrid/__snapshots__/data_grid.test.tsx.snap @@ -319,33 +319,14 @@ Array [ role="gridcell" tabindex="0" > -
-
-
-
-
- 0, A -
+
+
+ 0, A
-
-
-
-
-
-
- 0, B -
+
+
+ 0, B
-
-
-
-
-
-
- 1, A -
+
+
+ 1, A
-
-
-
-
-
-
- 1, B -
+
+
+ 1, B
-
-
-
-
-
-
- 2, A -
+
+
+ 2, A
-
-
-
-
-
-
- 2, B -
+
+
+ 2, B
-
diff --git a/src/components/datagrid/column_selector.tsx b/src/components/datagrid/column_selector.tsx index d5486b08ca0..cda4989c528 100644 --- a/src/components/datagrid/column_selector.tsx +++ b/src/components/datagrid/column_selector.tsx @@ -1,9 +1,4 @@ -import React, { - Fragment, - FunctionComponent, - useState, - ReactChild, -} from 'react'; +import React, { Fragment, useState, ReactChild, ReactElement } from 'react'; import classNames from 'classnames'; import { EuiDataGridColumn } from './data_grid_types'; // @ts-ignore-next-line @@ -24,7 +19,7 @@ import { EuiIcon } from '../icon'; export const useColumnSelector = ( availableColumns: EuiDataGridColumn[] -): [FunctionComponent, EuiDataGridColumn[]] => { +): [ReactElement, EuiDataGridColumn[]] => { const [sortedColumns, setSortedColumns] = useState(availableColumns); const [visibleColumns, setVisibleColumns] = useState(availableColumns); @@ -59,7 +54,7 @@ export const useColumnSelector = ( 'euiDataGrid__controlBtn--active': numberOfHiddenFields > 0, }); - const ColumnSelector = () => ( + const columnSelector = ( ); - return [ColumnSelector, visibleColumns]; + return [columnSelector, visibleColumns]; }; diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 70646e046a5..59b57f921a3 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { mount, ReactWrapper, render } from 'enzyme'; import { EuiDataGrid } from './'; import { @@ -315,11 +315,13 @@ describe('EuiDataGrid', () => { columns={[{ id: 'A' }, { id: 'B' }]} rowCount={2} renderCellValue={({ rowIndex, columnId, setCellProps }) => { - setCellProps({ - className: 'customClass', - 'data-test-subj': `cell-${rowIndex}-${columnId}`, - style: { color: columnId === 'A' ? 'red' : 'blue' }, - }); + useEffect(() => { + setCellProps({ + className: 'customClass', + 'data-test-subj': `cell-${rowIndex}-${columnId}`, + style: { color: columnId === 'A' ? 'red' : 'blue' }, + }); + }, []); return `${rowIndex}, ${columnId}`; }} @@ -418,7 +420,7 @@ Array [ { if (columnId === 'A') { @@ -452,7 +454,7 @@ Array [ columnId === 'A' ? 5.5 : 'true' @@ -487,7 +489,7 @@ Array [ ({ id }))} - inMemory="pagination" + inMemory={{ level: 'pagination' }} rowCount={1} renderCellValue={({ columnId }) => values[columnId]} /> @@ -528,7 +530,7 @@ Array [ }, }, ]} - inMemory="pagination" + inMemory={{ level: 'pagination' }} rowCount={1} renderCellValue={({ columnId }) => values[columnId]} /> @@ -937,7 +939,7 @@ Array [ // render A 0->4 and B 9->5 columnId === 'A' ? rowIndex : 9 - rowIndex } - inMemory="sorting" + inMemory={{ level: 'sorting' }} sorting={{ columns: [{ id: 'A', direction: 'desc' }], onSort: () => {}, @@ -965,7 +967,7 @@ Array [ // render A as 0, 1, 0, 1, 0 and B as 9->5 columnId === 'A' ? rowIndex % 2 : 9 - rowIndex } - inMemory="sorting" + inMemory={{ level: 'sorting' }} sorting={{ columns: [ { id: 'A', direction: 'desc' }, @@ -1001,7 +1003,7 @@ Array [ // render A as 0, 1, 0, 1, 0 and B as 9->5 columnId === 'A' ? rowIndex % 2 : 9 - rowIndex } - inMemory="sorting" + inMemory={{ level: 'sorting' }} sorting={{ columns: [], onSort, diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index e7ead4bfea6..6b685472eb0 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -270,7 +270,10 @@ function useOnResize( ); } -function useInMemoryValues(): [ +function useInMemoryValues( + inMemory: EuiDataGridInMemory | undefined, + rowCount: number +): [ EuiDataGridInMemoryValues, (rowIndex: number, column: EuiDataGridColumn, value: string) => void ] { @@ -281,15 +284,21 @@ function useInMemoryValues(): [ const onCellRender = useCallback( (rowIndex, column, value) => { setInMemoryValues(inMemoryValues => { - const nextInMemoryVaues = { ...inMemoryValues }; - nextInMemoryVaues[rowIndex] = nextInMemoryVaues[rowIndex] || {}; - nextInMemoryVaues[rowIndex][column.id] = value; - return nextInMemoryVaues; + const nextInMemoryValues = { ...inMemoryValues }; + nextInMemoryValues[rowIndex] = nextInMemoryValues[rowIndex] || {}; + nextInMemoryValues[rowIndex][column.id] = value; + return nextInMemoryValues; }); }, - [inMemoryValues, setInMemoryValues] + [setInMemoryValues] ); + // if `inMemory.level` or `rowCount` changes reset the values + const inMemoryLevel = inMemory && inMemory.level; + useEffect(() => { + setInMemoryValues({}); + }, [inMemoryLevel, rowCount]); + return [inMemoryValues, onCellRender]; } @@ -324,27 +333,27 @@ function createKeyDownHandler( if (isGridNavigationEnabled) { switch (keyCode) { case keyCodes.DOWN: - if (y < rowCount) { - event.preventDefault(); + event.preventDefault(); + if (y < rowCount - 1) { setFocusedCell([x, y + 1]); } break; case keyCodes.LEFT: + event.preventDefault(); if (x > 0) { - event.preventDefault(); setFocusedCell([x - 1, y]); } break; case keyCodes.UP: + event.preventDefault(); // TODO sort out when a user can arrow up into the column headers if (y > 0) { - event.preventDefault(); setFocusedCell([x, y - 1]); } break; case keyCodes.RIGHT: + event.preventDefault(); if (x < colCount) { - event.preventDefault(); setFocusedCell([x + 1, y]); } break; @@ -389,15 +398,15 @@ export const EuiDataGrid: FunctionComponent = props => { gridStyle, pagination, sorting, - inMemory = false, + inMemory, ...rest } = props; // apply style props on top of defaults const gridStyleWithDefaults = { ...startingStyles, ...gridStyle }; - const [ColumnSelector, visibleColumns] = useColumnSelector(columns); - const [StyleSelector, gridStyles] = useStyleSelector(gridStyleWithDefaults); + const [columnSelector, visibleColumns] = useColumnSelector(columns); + const [styleSelector, gridStyles] = useStyleSelector(gridStyleWithDefaults); // compute the default column width from the container's clientWidth and count of visible columns const defaultColumnWidth = useDefaultColumnWidth( @@ -429,20 +438,20 @@ export const EuiDataGrid: FunctionComponent = props => { className ); - const [inMemoryValues, onCellRender] = useInMemoryValues(); + const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory, rowCount); const detectedSchema = useDetectSchema( inMemoryValues, schemaDetectors, - inMemory !== false + inMemory != null ); const mergedSchema = getMergedSchema(detectedSchema, columns); // These grid controls will only show when there is room. Check the resize observer callback const gridControls = ( - - + {columnSelector} + {styleSelector} ); @@ -513,9 +522,16 @@ export const EuiDataGrid: FunctionComponent = props => {
{inMemory ? ( ) : null} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 6d5a1c04af0..4a225733129 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -28,7 +28,7 @@ interface EuiDataGridBodyProps { onCellFocus: EuiDataGridDataRowProps['onCellFocus']; rowCount: number; renderCellValue: EuiDataGridCellProps['renderCellValue']; - inMemory: EuiDataGridInMemory; + inMemory?: EuiDataGridInMemory; inMemoryValues: EuiDataGridInMemoryValues; isGridNavigationEnabled: EuiDataGridCellProps['isGridNavigationEnabled']; interactiveCellId: EuiDataGridCellProps['interactiveCellId']; @@ -74,7 +74,8 @@ export const EuiDataGridBody: FunctionComponent< const rowMap: { [key: number]: number } = {}; if ( - inMemory === 'sorting' && + inMemory && + inMemory.level === 'sorting' && sorting != null && sorting.columns.length > 0 ) { @@ -123,9 +124,14 @@ export const EuiDataGridBody: FunctionComponent< break; } } + + // map the row into the visible rows + if (pagination) { + reverseMappedIndex -= pagination.pageIndex * pagination.pageSize; + } onCellFocus([colIndex, reverseMappedIndex]); }, - [onCellFocus, rowMap] + [onCellFocus, rowMap, pagination] ); const rows = useMemo(() => { diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index f15e3e07029..312c9bf7009 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -8,8 +8,6 @@ import React, { HTMLAttributes, } from 'react'; import classnames from 'classnames'; -// @ts-ignore -import { EuiFocusTrap } from '../focus_trap'; import { CommonProps, Omit } from '../common'; import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; @@ -41,7 +39,11 @@ interface EuiDataGridCellState { type EuiDataGridCellValueProps = Omit< EuiDataGridCellProps, - 'width' | 'isFocusable' | 'isGridNavigationEnabled' | 'interactiveCellId' + | 'width' + | 'isFocusable' + | 'isGridNavigationEnabled' + | 'interactiveCellId' + | 'onCellFocus' >; const EuiDataGridCellContent: FunctionComponent< @@ -167,7 +169,10 @@ export class EuiDataGridCell extends Component< } } - shouldComponentUpdate(nextProps: EuiDataGridCellProps) { + shouldComponentUpdate( + nextProps: EuiDataGridCellProps, + nextState: EuiDataGridCellState + ) { if (nextProps.rowIndex !== this.props.rowIndex) return true; if (nextProps.colIndex !== this.props.colIndex) return true; if (nextProps.columnId !== this.props.columnId) return true; @@ -181,6 +186,9 @@ export class EuiDataGridCell extends Component< return true; if (nextProps.interactiveCellId !== this.props.interactiveCellId) return true; + + if (nextState.cellProps !== this.state.cellProps) return true; + return false; } @@ -195,9 +203,10 @@ export class EuiDataGridCell extends Component< isGridNavigationEnabled, interactiveCellId, columnType, + onCellFocus, ...rest } = this.props; - const { colIndex, rowIndex, onCellFocus } = rest; + const { colIndex, rowIndex } = rest; const isInteractive = this.isInteractiveCell(); const isInteractiveCell = { [CELL_CONTENTS_ATTR]: isInteractive, @@ -232,32 +241,30 @@ export class EuiDataGridCell extends Component< {...cellProps} data-test-subj="dataGridRowCell" onFocus={() => onCellFocus([colIndex, rowIndex])}> - - { - this.updateFocus(); - this.setTabbablesTabIndex(); - }} - observerOptions={{ - childList: true, - subtree: true, - }}> - {ref => ( -
-
- -
+ { + this.updateFocus(); + this.setTabbablesTabIndex(); + }} + observerOptions={{ + childList: true, + subtree: true, + }}> + {ref => ( +
+
+
- )} - - +
+ )} +
); } diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index 64b8cf1e400..fa320900585 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -6,12 +6,12 @@ import React, { useMemo, useState, } from 'react'; -import { createPortal } from 'react-dom'; +import { createPortal, unstable_batchedUpdates } from 'react-dom'; import { CellValueElementProps, EuiDataGridCellProps } from './data_grid_cell'; -import { EuiDataGridColumn } from './data_grid_types'; -import { EuiInnerText } from '../inner_text'; +import { EuiDataGridColumn, EuiDataGridInMemory } from './data_grid_types'; interface EuiDataGridInMemoryRendererProps { + inMemory: EuiDataGridInMemory; columns: EuiDataGridColumn[]; rowCount: number; renderCellValue: EuiDataGridCellProps['renderCellValue']; @@ -24,49 +24,113 @@ interface EuiDataGridInMemoryRendererProps { function noop() {} +const _queue: Function[] = []; + +function processQueue() { + // the queued functions trigger react setStates which, if unbatched, + // each cause a full update->render->dom pass _per function_ + // instead, tell React to wait until all updates are finished before re-rendering + unstable_batchedUpdates(() => { + for (let i = 0; i < _queue.length; i++) { + _queue[i](); + } + _queue.length = 0; + }); +} + +function enqueue(fn: Function) { + if (_queue.length === 0) { + setTimeout(processQueue); + } + _queue.push(fn); +} + +function getElementText(element: HTMLElement) { + return 'innerText' in element + ? element.innerText + : // TS thinks element.innerText always exists, however it doesn't in jest/jsdom enviornment + // @ts-ignore-next-line + element.textContent || undefined; +} + +const ObservedCell: FunctionComponent<{ + renderCellValue: EuiDataGridInMemoryRendererProps['renderCellValue']; + onCellRender: EuiDataGridInMemoryRendererProps['onCellRender']; + i: number; + column: EuiDataGridColumn; +}> = ({ renderCellValue, i, column, onCellRender }) => { + const [ref, setRef] = useState(); + + useEffect(() => { + if (ref) { + // this is part of React's component lifecycle, onCellRender->setState are automatically batched + onCellRender(i, column, getElementText(ref)); + const observer = new MutationObserver(() => { + // onMutation callbacks aren't in the component lifecycle, intentionally batch any effects + enqueue(onCellRender.bind(null, i, column, getElementText(ref))); + }); + observer.observe(ref, { + characterData: true, + subtree: true, + attributes: true, + childList: true, + }); + + return () => { + observer.disconnect(); + }; + } + }, [ref]); + + const CellElement = renderCellValue as JSXElementConstructor< + CellValueElementProps + >; + + return ( +
+ +
+ ); +}; + export const EuiDataGridInMemoryRenderer: FunctionComponent< EuiDataGridInMemoryRendererProps -> = ({ columns, rowCount, renderCellValue, onCellRender }) => { +> = ({ inMemory, columns, rowCount, renderCellValue, onCellRender }) => { const [documentFragment] = useState(() => document.createDocumentFragment()); const rows = useMemo(() => { const rows = []; - const CellElement = renderCellValue as JSXElementConstructor< - CellValueElementProps - >; - for (let i = 0; i < rowCount; i++) { rows.push( - {columns.map(column => ( - - - {(ref, text) => { - useEffect(() => { - if (text != null) { - onCellRender(i, column, text); - } - }, [text]); - return ( -
- -
- ); - }} -
-
- ))} + {columns + .map(column => { + const skipThisColumn = + inMemory.skipColumns && + inMemory.skipColumns.indexOf(column.id) !== -1; + + if (skipThisColumn) { + return null; + } + + return ( + + ); + }) + .filter(cell => cell != null)}
); } return rows; - }, [columns, rowCount, renderCellValue]); + }, [columns, rowCount, renderCellValue, onCellRender]); return createPortal( {rows}, diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index 9dcc26f1b2e..68c629d8fb7 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -113,6 +113,7 @@ export function useDetectSchema( // for each row, score each value by each detector and put the results on `columnSchemas` const rowIndices = Object.keys(inMemoryValues); + for (let i = 0; i < rowIndices.length; i++) { const rowIndex = rowIndices[i]; const rowData = inMemoryValues[rowIndex]; diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 5691e1bf472..9fd29ce20fe 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -41,23 +41,25 @@ export interface EuiDataGridSorting { columns: Array<{ id: string; direction: 'asc' | 'desc' }>; } -/* -Given the data flow Filtering->Sorting->Pagination: -Each step can be performed by service calls or in-memory by the grid. -However, we cannot allow any service calls after an in-memory operation. -E.g. if Pagination requires a service call the grid cannot perform -in-memory Filtering or Sorting. This means a single value representing the -service / in-memory boundary can be used. Thus there are four states for in-memory: -* false - all service calls -* "pagination" - only pagination is performed in-memory -* "sorting" - sorting & pagination is performed in-memory -* "filtering" - all operations are performed in-memory, no service calls +export interface EuiDataGridInMemory { + /** + Given the data flow Filtering->Sorting->Pagination: + Each step can be performed by service calls or in-memory by the grid. + However, we cannot allow any service calls after an in-memory operation. + E.g. if Pagination requires a service call the grid cannot perform + in-memory Filtering or Sorting. This means a single value representing the + service / in-memory boundary can be used. Thus there are four states for in-memory's level: + * "enhancements" - no in-memory operations, but use the available data to enhance the grid + * "pagination" - only pagination is performed in-memory + * "sorting" - sorting & pagination is performed in-memory + * "filtering" - all operations are performed in-memory, no service calls */ -export type EuiDataGridInMemory = - | boolean - | 'pagination' - | 'sorting' - | 'filtering'; + level: 'enhancements' | 'pagination' | 'sorting' | 'filtering'; + /** + * Array of column ids for in-memory processing to skip + */ + skipColumns?: string[]; +} export interface EuiDataGridInMemoryValues { [key: string]: { [key: string]: string }; diff --git a/src/components/datagrid/style_selector.tsx b/src/components/datagrid/style_selector.tsx index 2d41696cb94..e58f77f41b6 100644 --- a/src/components/datagrid/style_selector.tsx +++ b/src/components/datagrid/style_selector.tsx @@ -1,4 +1,4 @@ -import React, { FunctionComponent, useState } from 'react'; +import React, { ReactElement, useState } from 'react'; import { EuiDataGridStyle } from './data_grid_types'; import { EuiI18n } from '../i18n'; // @ts-ignore-next-line @@ -32,7 +32,7 @@ const densityStyles: { [key: string]: Partial } = { export const useStyleSelector = ( initialStyles: EuiDataGridStyle -): [FunctionComponent<{}>, EuiDataGridStyle] => { +): [ReactElement, EuiDataGridStyle] => { // track styles specified by the user at run time const [userGridStyles, setUserGridStyles] = useState({}); @@ -54,7 +54,7 @@ export const useStyleSelector = ( ...userGridStyles, }; - const StyleSelector = () => ( + const styleSelector = ( ); - return [StyleSelector, gridStyles]; + return [styleSelector, gridStyles]; };