From 0396b26fd04796a1d5580cf59ba73fc377fa3c42 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:05:58 -0500 Subject: [PATCH] [EuiDataGrid] Add new `cellContext` prop/API (#7374) Co-authored-by: Cee Chen --- changelogs/upcoming/7374.md | 1 + src-docs/src/views/datagrid/_props_table.tsx | 13 +++ src-docs/src/views/datagrid/_snippets.tsx | 5 + src-docs/src/views/datagrid/basics/_props.tsx | 1 + .../datagrid/cells_popovers/cell_context.tsx | 102 ++++++++++++++++++ .../cells_popovers/datagrid_cells_example.js | 30 ++++++ .../datagrid/body/cell/data_grid_cell.tsx | 4 + .../body/cell/data_grid_cell_wrapper.tsx | 3 + .../datagrid/body/data_grid_body_custom.tsx | 2 + .../body/data_grid_body_virtualized.tsx | 2 + src/components/datagrid/data_grid.test.tsx | 38 ++++++- src/components/datagrid/data_grid.tsx | 2 + src/components/datagrid/data_grid_types.ts | 22 +++- 13 files changed, 221 insertions(+), 4 deletions(-) create mode 100644 changelogs/upcoming/7374.md create mode 100644 src-docs/src/views/datagrid/cells_popovers/cell_context.tsx diff --git a/changelogs/upcoming/7374.md b/changelogs/upcoming/7374.md new file mode 100644 index 00000000000..e4914244c09 --- /dev/null +++ b/changelogs/upcoming/7374.md @@ -0,0 +1 @@ +- Added new `EuiDataGrid` new prop: `cellContext`, an optional object of additional props passed to the cell render function. diff --git a/src-docs/src/views/datagrid/_props_table.tsx b/src-docs/src/views/datagrid/_props_table.tsx index 92305342d0c..2866d179cdd 100644 --- a/src-docs/src/views/datagrid/_props_table.tsx +++ b/src-docs/src/views/datagrid/_props_table.tsx @@ -29,6 +29,19 @@ export const DataGridPropsTable: FunctionComponent<{ .filter((i) => !exclude?.includes(i)) .sort(); + // Manually move the cellContext prop after renderCellValue + const cellContext = gridPropsKeys.splice( + gridPropsKeys.findIndex((prop) => prop === 'cellContext'), + 1 + )[0]; + if (cellContext) { + gridPropsKeys.splice( + gridPropsKeys.findIndex((prop) => prop === 'renderCellValue') + 1, + 0, + cellContext + ); + } + const items: BasicItem[] = gridPropsKeys.map((prop) => { return { id: prop, diff --git a/src-docs/src/views/datagrid/_snippets.tsx b/src-docs/src/views/datagrid/_snippets.tsx index 72b86c826bf..a8b3bc7d0fd 100644 --- a/src-docs/src/views/datagrid/_snippets.tsx +++ b/src-docs/src/views/datagrid/_snippets.tsx @@ -53,6 +53,11 @@ inMemory={{ level: 'sorting' }}`, }, ]}`, renderCellValue: 'renderCellValue={({ rowIndex, columnId }) => {}}', + cellContext: `cellContext={{ + // Will be passed to your \`renderCellValue\` function/component as a prop + yourData, +}} +renderCellValue={({ rowIndex, columnId, yourData }) => {}}`, renderCellPopover: `renderCellPopover={({ children, cellActions }) => ( <> I'm a custom popover! diff --git a/src-docs/src/views/datagrid/basics/_props.tsx b/src-docs/src/views/datagrid/basics/_props.tsx index 875890ca701..23f57beb330 100644 --- a/src-docs/src/views/datagrid/basics/_props.tsx +++ b/src-docs/src/views/datagrid/basics/_props.tsx @@ -24,6 +24,7 @@ const gridLinks = { ref: '/tabular-content/data-grid-advanced#ref-methods', renderCustomGridBody: '/tabular-content/data-grid-advanced#custom-body-renderer', + cellContext: '/tabular-content/data-grid-cells-popovers#cell-context', }; export const DataGridTopProps = () => { diff --git a/src-docs/src/views/datagrid/cells_popovers/cell_context.tsx b/src-docs/src/views/datagrid/cells_popovers/cell_context.tsx new file mode 100644 index 00000000000..97ec37f0b6d --- /dev/null +++ b/src-docs/src/views/datagrid/cells_popovers/cell_context.tsx @@ -0,0 +1,102 @@ +import React, { useState, useEffect, useCallback, ReactNode } from 'react'; +import { faker } from '@faker-js/faker'; + +import { + EuiDataGrid, + EuiDataGridColumn, + type RenderCellValue, + EuiButton, + EuiSpacer, + EuiSkeletonText, +} from '../../../../../src'; + +type DataType = Array<{ [key: string]: ReactNode }>; + +const columns: EuiDataGridColumn[] = [ + { id: 'firstName' }, + { id: 'lastName' }, + { id: 'suffix' }, + { id: 'boolean' }, +]; + +const CellValue: RenderCellValue = ({ + rowIndex, + columnId, + // Props from cellContext + data, + isLoading, +}) => { + if (isLoading) { + return ; + } + + const value = data[rowIndex][columnId]; + return value; +}; + +export default () => { + const [visibleColumns, setVisibleColumns] = useState( + columns.map(({ id }) => id) + ); + + const [data, setData] = useState([]); + const [cellContext, setCellContext] = useState({ + data, + isLoading: false, + }); + + // Mock fetching data from an async API + const mockLoading = useCallback(() => { + setCellContext((context) => ({ + ...context, + isLoading: true, + })); + + // End the loading state after 3 seconds + const timeout = setTimeout(() => { + setCellContext((context) => ({ + ...context, + isLoading: false, + })); + }, 3000); + return () => clearTimeout(timeout); + }, []); + + const fetchData = useCallback(() => { + mockLoading(); + + const data: DataType = []; + for (let i = 1; i < 5; i++) { + data.push({ + firstName: faker.person.firstName(), + lastName: faker.person.lastName(), + suffix: faker.person.suffix(), + boolean: `${faker.datatype.boolean()}`, + }); + } + setData(data); + setCellContext((context) => ({ ...context, data })); + }, [mockLoading]); + + // Fetch data on page load + useEffect(() => { + fetchData(); + }, [fetchData]); + + return ( + <> + + Fetch grid data + + + + + ); +}; diff --git a/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js b/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js index afa8a3bc6db..1b5352f3192 100644 --- a/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js +++ b/src-docs/src/views/datagrid/cells_popovers/datagrid_cells_example.js @@ -21,6 +21,9 @@ import { DataGridCellPopoverExample } from './datagrid_cell_popover_example'; import DataGridFocus from './focus'; const dataGridFocusSource = require('!!raw-loader!./focus'); +import CellContext from './cell_context'; +const cellContextSource = require('!!raw-loader!./cell_context'); + import { EuiDataGridColumn, EuiDataGridColumnCellAction, @@ -218,5 +221,32 @@ export const DataGridCellsExample = { ), demo: , }, + { + title: 'Cell context', + source: [ + { + type: GuideSectionTypes.TSX, + code: cellContextSource, + }, + ], + text: ( + <> +

+ The cellContext prop is an easy way of passing + your custom data or context from the top level of{' '} + EuiDataGrid down to the cell content rendered by + your renderCellValue function component. +

+

+ The primary use of the cell context API is performance: if your data + relies on state from your app, it allows you to more easily define + your renderCellValue function statically, instead + of within your app, which in turn reduces the number of rerenders + within your data grid. +

+ + ), + demo: , + }, ], }; diff --git a/src/components/datagrid/body/cell/data_grid_cell.tsx b/src/components/datagrid/body/cell/data_grid_cell.tsx index 357edb444ff..059a2f90493 100644 --- a/src/components/datagrid/body/cell/data_grid_cell.tsx +++ b/src/components/datagrid/body/cell/data_grid_cell.tsx @@ -59,6 +59,7 @@ const EuiDataGridCellContent: FunctionComponent< > = memo( ({ renderCellValue, + cellContext, column, setCellContentsRef, rowIndex, @@ -99,6 +100,7 @@ const EuiDataGridCellContent: FunctionComponent< rowIndex={rowIndex} colIndex={colIndex} schema={column?.schema || rest.columnType} + {...cellContext} {...rest} /> @@ -465,6 +467,7 @@ export class EuiDataGridCell extends Component< const { renderCellPopover, renderCellValue, + cellContext, rowIndex, colIndex, column, @@ -492,6 +495,7 @@ export class EuiDataGridCell extends Component< setCellPopoverProps={setCellPopoverProps} > = ({ columnWidths, defaultColumnWidth, renderCellValue, + cellContext, renderCellPopover, interactiveCellId, setRowHeight, @@ -120,6 +122,7 @@ export const Cell: FunctionComponent = ({ rowManager, popoverContext, pagination, + cellContext, }; if (isLeadingControlColumn) { diff --git a/src/components/datagrid/body/data_grid_body_custom.tsx b/src/components/datagrid/body/data_grid_body_custom.tsx index d77428bed66..df119b6b84b 100644 --- a/src/components/datagrid/body/data_grid_body_custom.tsx +++ b/src/components/datagrid/body/data_grid_body_custom.tsx @@ -38,6 +38,7 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< schemaDetectors, visibleRows, renderCellValue, + cellContext, renderCellPopover, renderFooterCellValue, interactiveCellId, @@ -130,6 +131,7 @@ export const EuiDataGridBodyCustomRender: FunctionComponent< columnWidths, defaultColumnWidth, renderCellValue, + cellContext, renderCellPopover, interactiveCellId, setRowHeight, diff --git a/src/components/datagrid/body/data_grid_body_virtualized.tsx b/src/components/datagrid/body/data_grid_body_virtualized.tsx index e49b1e9dadd..fc70ed71b3a 100644 --- a/src/components/datagrid/body/data_grid_body_virtualized.tsx +++ b/src/components/datagrid/body/data_grid_body_virtualized.tsx @@ -113,6 +113,7 @@ export const EuiDataGridBodyVirtualized: FunctionComponent< rowCount, visibleRows: { startRow, endRow, visibleRowCount }, renderCellValue, + cellContext, renderCellPopover, renderFooterCellValue, interactiveCellId, @@ -326,6 +327,7 @@ export const EuiDataGridBodyVirtualized: FunctionComponent< columnWidths, defaultColumnWidth, renderCellValue, + cellContext, renderCellPopover, interactiveCellId, rowHeightsOptions, diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index c0fd34833b7..367a157a13a 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -9,7 +9,7 @@ import React, { useEffect, useState } from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { EuiDataGrid } from './'; -import { EuiDataGridProps } from './data_grid_types'; +import type { EuiDataGridProps, RenderCellValue } from './data_grid_types'; import { findTestSubject, requiredProps } from '../../test'; import { render } from '../../test/rtl'; import { EuiDataGridColumnResizer } from './body/header/data_grid_column_resizer'; @@ -976,6 +976,42 @@ describe('EuiDataGrid', () => { ] `); }); + + it('passes `cellContext` as props to the renderCellValue component', () => { + const dataGridProps = { + 'aria-label': 'test', + columns: [{ id: 'Column' }], + columnVisibility: { + visibleColumns: ['Column'], + setVisibleColumns: () => {}, + }, + rowCount: 1, + }; + + const RenderCellValueWithContext: RenderCellValue = ({ someContext }) => ( +
+ {someContext ? 'hello' : 'world'} +
+ ); + + const { getByTestSubject, rerender } = render( + + ); + expect(getByTestSubject('renderedCell')).toHaveTextContent('hello'); + + rerender( + + ); + expect(getByTestSubject('renderedCell')).toHaveTextContent('world'); + }); }); describe('pagination', () => { diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 9344454f317..b8d9bcc75ec 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -110,6 +110,7 @@ export const EuiDataGrid = memo( schemaDetectors, rowCount, renderCellValue, + cellContext, renderCellPopover, renderFooterCellValue, className, @@ -443,6 +444,7 @@ export const EuiDataGrid = memo( schemaDetectors={allSchemaDetectors} pagination={pagination} renderCellValue={renderCellValue} + cellContext={cellContext} renderCellPopover={renderCellPopover} renderFooterCellValue={renderFooterCellValue} rowCount={rowCount} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 7a082117e13..8608552e6c0 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -268,6 +268,12 @@ export type CommonGridProps = CommonProps & * as its only argument. */ renderCellValue: EuiDataGridCellProps['renderCellValue']; + /** + * An optional object of props passed to the `renderCellValue` component. + * This API exists to make it easier to define your `renderCellValue` function + * component statically, and not rerender due to other dependent state. + */ + cellContext?: EuiDataGridCellProps['cellContext']; /** * An optional function that can be used to completely customize the rendering of cell popovers. * @@ -453,6 +459,7 @@ export interface EuiDataGridBodyProps { rowCount: number; visibleRows: EuiDataGridVisibleRows; renderCellValue: EuiDataGridCellProps['renderCellValue']; + cellContext?: EuiDataGridCellProps['cellContext']; renderCellPopover?: EuiDataGridCellProps['renderCellPopover']; renderFooterCellValue?: EuiDataGridCellProps['renderCellValue']; renderCustomGridBody?: EuiDataGridProps['renderCustomGridBody']; @@ -597,6 +604,16 @@ export interface EuiDataGridCellPopoverElementProps ) => void; } +type CellContext = Omit< + Record, + keyof EuiDataGridCellValueElementProps +>; +type CellPropsWithContext = CellContext & EuiDataGridCellValueElementProps; + +export type RenderCellValue = + | ((props: CellPropsWithContext) => ReactNode) + | ComponentClass; + export interface EuiDataGridCellProps { rowIndex: number; visibleRowIndex: number; @@ -609,9 +626,8 @@ export interface EuiDataGridCellProps { isExpandable: boolean; className?: string; popoverContext: DataGridCellPopoverContextShape; - renderCellValue: - | ((props: EuiDataGridCellValueElementProps) => ReactNode) - | ComponentClass; + renderCellValue: RenderCellValue; + cellContext?: CellContext; renderCellPopover?: | JSXElementConstructor | ((props: EuiDataGridCellPopoverElementProps) => ReactNode);