From 8fe2cd6456d23122ed7af4d61bde409ddace5aff Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 13 Sep 2019 09:14:43 -0600 Subject: [PATCH 01/20] Automatically detect data schema for in-memory datagrid --- .../src/views/datagrid/datagrid_example.js | 2 +- src-docs/src/views/datagrid/in_memory.js | 164 +++++------------- .../datagrid/_data_grid_data_row.scss | 8 + src/components/datagrid/data_grid.tsx | 149 ++++++++++++++++ src/components/datagrid/data_grid_body.tsx | 4 + src/components/datagrid/data_grid_cell.tsx | 12 +- .../datagrid/data_grid_data_row.tsx | 4 + 7 files changed, 220 insertions(+), 123 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index 2735a39f96f..ad478608c5f 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -19,7 +19,7 @@ const dataGridStylingHtml = renderToHtml(DataGridStyling); import InMemoryDataGrid from './in_memory'; const inMemoryDataGridSource = require('!!raw-loader!./in_memory'); -const inMemoryDataGridHtml = renderToHtml(DataGridStyling); +const inMemoryDataGridHtml = renderToHtml(InMemoryDataGrid); export const DataGridExample = { title: 'Data grid', diff --git a/src-docs/src/views/datagrid/in_memory.js b/src-docs/src/views/datagrid/in_memory.js index e171e9d167d..972e3ff4b78 100644 --- a/src-docs/src/views/datagrid/in_memory.js +++ b/src-docs/src/views/datagrid/in_memory.js @@ -1,4 +1,5 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; +import { fake } from 'faker'; import { EuiDataGrid, @@ -17,142 +18,65 @@ const columns = [ id: 'name', }, { - id: 'avatar_url', + id: 'email', }, { - id: 'url', + id: 'location', }, { - id: 'contributions', + id: 'account', }, { - id: 'actions', - }, -]; - -const data = [ - { - name: 'cjcenizal', - avatar_url: 'https://avatars2.githubusercontent.com/u/1238659?v=4', - url: 'https://api.github.com/users/cjcenizal', - contributions: 392, - }, - { - name: 'snide', - avatar_url: 'https://avatars3.githubusercontent.com/u/324519?v=4', - url: 'https://api.github.com/users/snide', - contributions: 361, - }, - { - name: 'chandlerprall', - avatar_url: 'https://avatars3.githubusercontent.com/u/313125?v=4', - url: 'https://api.github.com/users/chandlerprall', - contributions: 274, - }, - { - name: 'cchaos', - avatar_url: 'https://avatars3.githubusercontent.com/u/549577?v=4', - url: 'https://api.github.com/users/cchaos', - contributions: 156, - }, - { - name: 'bevacqua', - avatar_url: 'https://avatars3.githubusercontent.com/u/934293?v=4', - url: 'https://api.github.com/users/bevacqua', - contributions: 128, - }, - { - name: 'thompsongl', - avatar_url: 'https://avatars0.githubusercontent.com/u/2728212?v=4', - url: 'https://api.github.com/users/thompsongl', - contributions: 106, - }, - { - name: 'pugnascotia', - avatar_url: 'https://avatars1.githubusercontent.com/u/8696382?v=4', - url: 'https://api.github.com/users/pugnascotia', - contributions: 82, - }, - { - name: 'nreese', - avatar_url: 'https://avatars0.githubusercontent.com/u/373691?v=4', - url: 'https://api.github.com/users/nreese', - contributions: 58, - }, - { - name: 'dmeiss', - avatar_url: 'https://avatars3.githubusercontent.com/u/45879454?v=4', - url: 'https://api.github.com/users/dmeiss', - contributions: 52, - }, - { - name: 'ryankeairns', - avatar_url: 'https://avatars2.githubusercontent.com/u/446285?v=4', - url: 'https://api.github.com/users/ryankeairns', - contributions: 32, - }, - { - name: 'stacey-gammon', - avatar_url: 'https://avatars3.githubusercontent.com/u/16563603?v=4', - url: 'https://api.github.com/users/stacey-gammon', - contributions: 24, - }, - { - name: 'theodesp', - avatar_url: 'https://avatars0.githubusercontent.com/u/328805?v=4', - url: 'https://api.github.com/users/theodesp', - contributions: 22, - }, - { - name: 'uboness', - avatar_url: 'https://avatars3.githubusercontent.com/u/211019?v=4', - url: 'https://api.github.com/users/uboness', - contributions: 17, - }, - { - name: 'weltenwort', - avatar_url: 'https://avatars3.githubusercontent.com/u/973741?v=4', - url: 'https://api.github.com/users/weltenwort', - contributions: 16, - }, - { - name: 'jen-huang', - avatar_url: 'https://avatars0.githubusercontent.com/u/1965714?v=4', - url: 'https://api.github.com/users/jen-huang', - contributions: 13, - }, - { - name: 'PopradiArpad', - avatar_url: 'https://avatars3.githubusercontent.com/u/4144816?v=4', - url: 'https://api.github.com/users/PopradiArpad', - contributions: 11, + id: 'date', }, { - name: 'chrisronline', - avatar_url: 'https://avatars1.githubusercontent.com/u/56682?v=4', - url: 'https://api.github.com/users/chrisronline', - contributions: 10, + id: 'amount', }, { - name: 'timroes', - avatar_url: 'https://avatars0.githubusercontent.com/u/877229?v=4', - url: 'https://api.github.com/users/timroes', - contributions: 10, + id: 'phone', }, { - name: 'daveyholler', - avatar_url: 'https://avatars2.githubusercontent.com/u/739960?v=4', - url: 'https://api.github.com/users/daveyholler', - contributions: 9, + id: 'version', }, { - name: 'sqren', - avatar_url: 'https://avatars3.githubusercontent.com/u/209966?v=4', - url: 'https://api.github.com/users/sqren', - contributions: 9, + id: 'actions', }, ]; +const data = []; + +for (let i = 1; i < 100; i++) { + data.push({ + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: {fake('{{internet.email}}')}, + location: ( + + {`${fake('{{address.city}}')}, `} + + {fake('{{address.country}}')} + + + ), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('{{finance.currencySymbol}}{{finance.amount}}'), + phone: fake('{{phone.phoneNumber}}'), + version: fake('{{system.semver}}'), + actions: ( + + + + + ), + }); +} + export default class InMemoryDataGrid extends Component { constructor(props) { super(props); diff --git a/src/components/datagrid/_data_grid_data_row.scss b/src/components/datagrid/_data_grid_data_row.scss index b97e97c46bc..a5e65e717a2 100644 --- a/src/components/datagrid/_data_grid_data_row.scss +++ b/src/components/datagrid/_data_grid_data_row.scss @@ -41,6 +41,14 @@ // Needed because the focus state adds a border, which needs to be subtracted from padding padding-left: $euiDataGridCellPaddingM - 1px; } + + &.euiDataGridRowCell__columnType--numeric { + font-family: monospace; + } + + &.euiDataGridRowCell__columnType--currency { + color: $euiCodeBlockRegexpColor; + } } .euiDataGridRowCell__content { diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index f44d163520b..8f0b3cf8854 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -7,6 +7,7 @@ import React, { useEffect, Fragment, ReactChild, + useMemo, } from 'react'; import classNames from 'classnames'; import { EuiI18n } from '../i18n'; @@ -287,6 +288,151 @@ function useInMemoryValues(): [ return [inMemoryValues, onCellRender]; } +const schemaDetectors = [ + { + type: 'numeric' as 'numeric', + detector: (value: string) => { + const matchLength = (value.match(/[%-(]*[\d,]+(\.\d*)?[%)]*/) || [''])[0] + .length; + return matchLength / value.length; + }, + }, + { + type: 'currency' as 'currency', + detector: (value: string) => { + const matchLength = (value.match(/[$-(]*[\d,]+(\.\d*)?[$)]*/) || [''])[0] + .length; + return matchLength / value.length; + }, + }, + { + type: 'boolean' as 'boolean', + detector: (value: string) => { + return value === 'true' || value === 'false' ? 1 : 0; + }, + }, +]; + +export type EuiDataGridSchemaType = typeof schemaDetectors[number]['type']; + +export interface EuiDataGridSchema { + [columnId: string]: EuiDataGridSchemaType | null; +} + +interface SchemaTypeScore { + type: EuiDataGridSchemaType; + score: number; +} + +interface SchemaTypeScoreComposite { + type: EuiDataGridSchemaType; + minScore: number; + maxScore: number; +} + +function scoreValueBySchemaType(value: string) { + const scores: SchemaTypeScore[] = []; + + for (let i = 0; i < schemaDetectors.length; i++) { + const { type, detector } = schemaDetectors[i]; + const score = detector(value); + scores.push({ type, score }); + } + + return scores; +} + +// completely arbitrary minimum match I came up with +// represents lowest score a type detector can have to be considered valid +const MINIMUM_SCORE_MATCH = 0.2; + +function useDetectSchema( + inMemoryValues: EuiDataGridInMemoryValues, + autoDetectSchema: boolean +) { + const schema = useMemo(() => { + const schema: EuiDataGridSchema = {}; + if (autoDetectSchema === false) { + return schema; + } + + const columnSchemas: { + [columnId: string]: { [type: string]: SchemaTypeScoreComposite }; + } = {}; + + const rowIndices = Object.keys(inMemoryValues); + for (let i = 0; i < rowIndices.length; i++) { + const rowIndex = rowIndices[i]; + const rowData = inMemoryValues[rowIndex]; + const columnIds = Object.keys(rowData); + + for (let j = 0; j < columnIds.length; j++) { + const columnId = columnIds[j]; + + const schemaColumn = (columnSchemas[columnId] = + columnSchemas[columnId] || {}); + + const columnValue = rowData[columnId].trim(); + const valueScores = scoreValueBySchemaType(columnValue); + + for (let k = 0; k < valueScores.length; k++) { + const valueScore = valueScores[k]; + if (schemaColumn.hasOwnProperty(valueScore.type)) { + const existingScore = schemaColumn[valueScore.type]; + existingScore.minScore = Math.min( + existingScore.minScore, + valueScore.score + ); + existingScore.maxScore = Math.max( + existingScore.maxScore, + valueScore.score + ); + } else { + // first entry for this column + schemaColumn[valueScore.type] = { + type: valueScore.type, + minScore: valueScore.score, + maxScore: valueScore.score, + }; + } + } + } + } + + return Object.keys(columnSchemas).reduce( + (schema, columnId) => { + const columnScores = columnSchemas[columnId]; + const columnIds = Object.keys(columnScores); + + let bestMatch: SchemaTypeScoreComposite | null = null; + + for (let i = 0; i < columnIds.length; i++) { + const columnId = columnIds[i]; + const columnScore = columnScores[columnId]; + + if (columnScore.minScore >= MINIMUM_SCORE_MATCH) { + if (bestMatch == null) { + bestMatch = columnScore; + } else if (bestMatch.minScore < columnScore.minScore) { + bestMatch = columnScore; + } else if ( + bestMatch.minScore === columnScore.minScore && + bestMatch.maxScore < columnScore.maxScore + ) { + bestMatch = columnScore; + } + } + } + + schema[columnId] = bestMatch ? bestMatch.type : null; + return schema; + }, + {} + ); + }, [inMemoryValues]); + return schema; +} + function createKeyDownHandler( props: EuiDataGridProps, visibleColumns: EuiDataGridProps['columns'], @@ -424,6 +570,8 @@ export const EuiDataGrid: FunctionComponent = props => { const [inMemoryValues, onCellRender] = useInMemoryValues(); + const detectedSchema = useDetectSchema(inMemoryValues, true); + // These grid controls will only show when there is room. Check the resize observer callback const gridControls = ( @@ -521,6 +669,7 @@ export const EuiDataGrid: FunctionComponent = props => { inMemoryValues={inMemoryValues} inMemory={inMemory} columns={visibleColumns} + schema={detectedSchema} focusedCell={focusedCell} onCellFocus={setFocusedCell} pagination={pagination} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 2d0ff590c1a..a3b6de0940f 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -17,11 +17,13 @@ import { EuiDataGridDataRow, EuiDataGridDataRowProps, } from './data_grid_data_row'; +import { EuiDataGridSchema } from './data_grid'; interface EuiDataGridBodyProps { columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; columns: EuiDataGridColumn[]; + schema: EuiDataGridSchema; focusedCell: EuiDataGridDataRowProps['focusedCell']; onCellFocus: EuiDataGridDataRowProps['onCellFocus']; rowCount: number; @@ -41,6 +43,7 @@ export const EuiDataGridBody: FunctionComponent< columnWidths, defaultColumnWidth, columns, + schema, focusedCell, onCellFocus, rowCount, @@ -137,6 +140,7 @@ export const EuiDataGridBody: FunctionComponent< ; const EuiDataGridCellContent: FunctionComponent< - EuiDataGridCellValueProps + Omit > = memo(props => { const { renderCellValue, ...rest } = props; @@ -179,6 +182,7 @@ export class EuiDataGridCell extends Component< isFocusable, isGridNavigationEnabled, interactiveCellId, + columnType, ...rest } = this.props; const { colIndex, rowIndex, onCellFocus } = rest; @@ -187,13 +191,17 @@ export class EuiDataGridCell extends Component< [CELL_CONTENTS_ATTR]: isInteractive, }; + const className = classnames('euiDataGridRowCell', { + [`euiDataGridRowCell__columnType--${columnType}`]: columnType, + }); + return (
onCellFocus([colIndex, rowIndex])} style={width != null ? { width: `${width}px` } : {}}> diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index 4a0aa1525de..46ede81a54e 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -4,11 +4,13 @@ import { EuiDataGridColumn, EuiDataGridColumnWidths } from './data_grid_types'; import { CommonProps } from '../common'; import { EuiDataGridCell, EuiDataGridCellProps } from './data_grid_cell'; +import { EuiDataGridSchema } from './data_grid'; export type EuiDataGridDataRowProps = CommonProps & HTMLAttributes & { rowIndex: number; columns: EuiDataGridColumn[]; + schema: EuiDataGridSchema; columnWidths: EuiDataGridColumnWidths; defaultColumnWidth?: number | null; focusedCell: [number, number]; @@ -24,6 +26,7 @@ const EuiDataGridDataRow: FunctionComponent< > = props => { const { columns, + schema, columnWidths, defaultColumnWidth, className, @@ -57,6 +60,7 @@ const EuiDataGridDataRow: FunctionComponent< rowIndex={rowIndex} colIndex={i} columnId={id} + columnType={schema[id]} width={width || undefined} renderCellValue={renderCellValue} onCellFocus={onCellFocus} From d7d68e77f6b4224ee1c1ddbabeaf4f47b78b3873 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 13 Sep 2019 13:02:47 -0600 Subject: [PATCH 02/20] Merge in described schema for field formatting --- .../src/views/datagrid/datagrid_example.js | 22 + src-docs/src/views/datagrid/schema.js | 380 ++++++++++++++++++ src/components/datagrid/data_grid.tsx | 29 +- src/components/datagrid/data_grid_cell.tsx | 4 +- .../datagrid/data_grid_data_row.tsx | 2 +- src/components/datagrid/data_grid_types.ts | 3 + 6 files changed, 433 insertions(+), 7 deletions(-) create mode 100644 src-docs/src/views/datagrid/schema.js diff --git a/src-docs/src/views/datagrid/datagrid_example.js b/src-docs/src/views/datagrid/datagrid_example.js index ad478608c5f..1709908d7d5 100644 --- a/src-docs/src/views/datagrid/datagrid_example.js +++ b/src-docs/src/views/datagrid/datagrid_example.js @@ -17,6 +17,10 @@ import DataGridStyling from './styling'; const dataGridStylingSource = require('!!raw-loader!./styling'); const dataGridStylingHtml = renderToHtml(DataGridStyling); +import DataGridSchema from './schema'; +const dataGridSchemaSource = require('!!raw-loader!./schema'); +const dataGridSchemaHtml = renderToHtml(DataGridSchema); + import InMemoryDataGrid from './in_memory'; const inMemoryDataGridSource = require('!!raw-loader!./in_memory'); const inMemoryDataGridHtml = renderToHtml(InMemoryDataGrid); @@ -88,6 +92,24 @@ export const DataGridExample = { demo: , props: { EuiDataGrid }, }, + { + source: [ + { + type: GuideSectionTypes.JS, + code: dataGridSchemaSource, + }, + { + type: GuideSectionTypes.HTML, + code: dataGridSchemaHtml, + }, + ], + title: 'Schema', + text: ( +

Column type information can be included on the column definition.

+ ), + components: { DataGridSchema }, + demo: , + }, { source: [ { diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js new file mode 100644 index 00000000000..daa42521bc4 --- /dev/null +++ b/src-docs/src/views/datagrid/schema.js @@ -0,0 +1,380 @@ +import React, { Component, Fragment } from 'react'; +import { fake } from 'faker'; + +import { + EuiDataGrid, + EuiButtonGroup, + EuiSpacer, + EuiFormRow, + EuiPopover, + EuiButton, + EuiButtonIcon, + EuiLink, +} from '../../../../src/components/'; +import { iconTypes } from '../icon/icons'; + +const columns = [ + { + id: 'name', + }, + { + id: 'email', + }, + { + id: 'location', + }, + { + id: 'account', + dataType: 'numeric', + }, + { + id: 'date', + }, + { + id: 'amount', + dataType: 'currency', + }, + { + id: 'phone', + }, + { + id: 'version', + }, + { + id: 'actions', + }, +]; + +const data = []; + +for (let i = 1; i < 100; i++) { + data.push({ + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + email: {fake('{{internet.email}}')}, + location: ( + + {`${fake('{{address.city}}')}, `} + + {fake('{{address.country}}')} + + + ), + date: fake('{{date.past}}'), + account: fake('{{finance.account}}'), + amount: fake('{{finance.currencySymbol}}{{finance.amount}}'), + phone: fake('{{phone.phoneNumber}}'), + version: fake('{{system.semver}}'), + actions: ( + + + + + ), + }); +} + +export default class InMemoryDataGrid extends Component { + constructor(props) { + super(props); + this.borderOptions = [ + { + id: 'all', + label: 'All', + }, + { + id: 'horizontal', + label: 'Horizontal only', + }, + { + id: 'none', + label: 'None', + }, + ]; + + this.fontSizeOptions = [ + { + id: 's', + label: 'Small', + }, + { + id: 'm', + label: 'Medium', + }, + { + id: 'l', + label: 'Large', + }, + ]; + + this.cellPaddingOptions = [ + { + id: 's', + label: 'Small', + }, + { + id: 'm', + label: 'Medium', + }, + { + id: 'l', + label: 'Large', + }, + ]; + + this.stripeOptions = [ + { + id: 'true', + label: 'Stripes on', + }, + { + id: 'false', + label: 'Stripes off', + }, + ]; + + this.rowHoverOptions = [ + { + id: 'none', + label: 'None', + }, + { + id: 'highlight', + label: 'Highlight', + }, + ]; + + this.headerOptions = [ + { + id: 'shade', + label: 'Shade', + }, + { + id: 'underline', + label: 'Underline', + }, + ]; + + this.state = { + borderSelected: 'all', + fontSizeSelected: 'm', + cellPaddingSelected: 'm', + stripes: false, + stripesSelected: 'false', + rowHoverSelected: 'highlight', + isPopoverOpen: false, + headerSelected: 'shade', + + data, + sortingColumns: [{ id: 'contributions', direction: 'asc' }], + + pagination: { + pageIndex: 0, + pageSize: 10, + }, + }; + } + + onBorderChange = optionId => { + this.setState({ + borderSelected: optionId, + }); + }; + + onFontSizeChange = optionId => { + this.setState({ + fontSizeSelected: optionId, + }); + }; + + onCellPaddingChange = optionId => { + this.setState({ + cellPaddingSelected: optionId, + }); + }; + + onStripesChange = optionId => { + this.setState({ + stripesSelected: optionId, + stripes: !this.state.stripes, + }); + }; + + onRowHoverChange = optionId => { + this.setState({ + rowHoverSelected: optionId, + }); + }; + + onHeaderChange = optionId => { + this.setState({ + headerSelected: optionId, + }); + }; + + onPopoverButtonClick() { + this.setState({ + isPopoverOpen: !this.state.isPopoverOpen, + }); + } + + closePopover() { + this.setState({ + isPopoverOpen: false, + }); + } + + setSorting = sortingColumns => this.setState({ sortingColumns }); + + setPageIndex = pageIndex => + this.setState(({ pagination }) => ({ + pagination: { ...pagination, pageIndex }, + })); + + setPageSize = pageSize => + this.setState(({ pagination }) => ({ + pagination: { ...pagination, pageSize }, + })); + + dummyIcon = () => ( + + ); + + render() { + const { data, pagination, sortingColumns } = this.state; + + const button = ( + + Table styling + + ); + + return ( +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + { + const value = data[rowIndex][columnId]; + + if (columnId === 'actions') { + return ( + <> + {this.dummyIcon()} + {this.dummyIcon()} + + ); + } + + if (columnId === 'url') { + return {value}; + } + + if (columnId === 'avatar_url') { + return ( +

+ Avatar: {value} +

+ ); + } + + return value; + }} + sorting={{ columns: sortingColumns, onSort: this.setSorting }} + pagination={{ + ...pagination, + pageSizeOptions: [5, 10, 25], + onChangeItemsPerPage: this.setPageSize, + onChangePage: this.setPageIndex, + }} + /> +
+ ); + } +} diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 8f0b3cf8854..64411716279 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -316,7 +316,7 @@ const schemaDetectors = [ export type EuiDataGridSchemaType = typeof schemaDetectors[number]['type']; export interface EuiDataGridSchema { - [columnId: string]: EuiDataGridSchemaType | null; + [columnId: string]: { columnType: EuiDataGridSchemaType | null }; } interface SchemaTypeScore { @@ -424,7 +424,7 @@ function useDetectSchema( } } - schema[columnId] = bestMatch ? bestMatch.type : null; + schema[columnId] = { columnType: bestMatch ? bestMatch.type : null }; return schema; }, {} @@ -433,6 +433,26 @@ function useDetectSchema( return schema; } +function getMergedSchema( + detectedSchema: EuiDataGridSchema, + columns: EuiDataGridColumn[] +) { + const mergedSchema = { ...detectedSchema }; + + for (let i = 0; i < columns.length; i++) { + const { id, dataType } = columns[i]; + if (dataType != null) { + if (detectedSchema.hasOwnProperty(id)) { + detectedSchema[id] = { ...detectedSchema[id], columnType: dataType }; + } else { + mergedSchema[id] = { columnType: dataType }; + } + } + } + + return mergedSchema; +} + function createKeyDownHandler( props: EuiDataGridProps, visibleColumns: EuiDataGridProps['columns'], @@ -570,7 +590,8 @@ export const EuiDataGrid: FunctionComponent = props => { const [inMemoryValues, onCellRender] = useInMemoryValues(); - const detectedSchema = useDetectSchema(inMemoryValues, true); + const detectedSchema = useDetectSchema(inMemoryValues, inMemory !== false); + const mergedSchema = getMergedSchema(detectedSchema, columns); // These grid controls will only show when there is room. Check the resize observer callback const gridControls = ( @@ -669,7 +690,7 @@ export const EuiDataGrid: FunctionComponent = props => { inMemoryValues={inMemoryValues} inMemory={inMemory} columns={visibleColumns} - schema={detectedSchema} + schema={mergedSchema} focusedCell={focusedCell} onCellFocus={setFocusedCell} pagination={pagination} diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 85685ce5baf..f602c459222 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -42,7 +42,7 @@ type EuiDataGridCellValueProps = Omit< >; const EuiDataGridCellContent: FunctionComponent< - Omit + EuiDataGridCellValueProps > = memo(props => { const { renderCellValue, ...rest } = props; @@ -221,7 +221,7 @@ export class EuiDataGridCell extends Component< {...isInteractiveCell} ref={this.cellContentsRef} className="euiDataGridRowCell__content"> - +
)} diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index 46ede81a54e..4613a88697d 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -60,7 +60,7 @@ const EuiDataGridDataRow: FunctionComponent< rowIndex={rowIndex} colIndex={i} columnId={id} - columnType={schema[id]} + columnType={schema[id] ? schema[id].columnType : null} width={width || undefined} renderCellValue={renderCellValue} onCellFocus={onCellFocus} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index d52a213d609..2cd825e47d6 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -1,5 +1,8 @@ +import { EuiDataGridSchema } from './data_grid'; + export interface EuiDataGridColumn { id: string; + dataType?: EuiDataGridSchema['schema']['columnType']; } export interface EuiDataGridColumnWidths { From 1818acb1d3a681fbd6edb3b9274f7041cb64d9f8 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 16 Sep 2019 08:45:26 -0600 Subject: [PATCH 03/20] Better column type detection --- src/components/datagrid/data_grid.tsx | 100 +++++++++++++++----------- 1 file changed, 57 insertions(+), 43 deletions(-) diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 64411716279..2a7939d8c20 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -294,7 +294,7 @@ const schemaDetectors = [ detector: (value: string) => { const matchLength = (value.match(/[%-(]*[\d,]+(\.\d*)?[%)]*/) || [''])[0] .length; - return matchLength / value.length; + return matchLength / value.length || 0; }, }, { @@ -302,7 +302,7 @@ const schemaDetectors = [ detector: (value: string) => { const matchLength = (value.match(/[$-(]*[\d,]+(\.\d*)?[$)]*/) || [''])[0] .length; - return matchLength / value.length; + return matchLength / value.length || 0; }, }, { @@ -324,12 +324,6 @@ interface SchemaTypeScore { score: number; } -interface SchemaTypeScoreComposite { - type: EuiDataGridSchemaType; - minScore: number; - maxScore: number; -} - function scoreValueBySchemaType(value: string) { const scores: SchemaTypeScore[] = []; @@ -357,7 +351,7 @@ function useDetectSchema( } const columnSchemas: { - [columnId: string]: { [type: string]: SchemaTypeScoreComposite }; + [columnId: string]: { [type: string]: number[] }; } = {}; const rowIndices = Object.keys(inMemoryValues); @@ -379,52 +373,72 @@ function useDetectSchema( const valueScore = valueScores[k]; if (schemaColumn.hasOwnProperty(valueScore.type)) { const existingScore = schemaColumn[valueScore.type]; - existingScore.minScore = Math.min( - existingScore.minScore, - valueScore.score - ); - existingScore.maxScore = Math.max( - existingScore.maxScore, - valueScore.score - ); + existingScore.push(valueScore.score); } else { // first entry for this column - schemaColumn[valueScore.type] = { - type: valueScore.type, - minScore: valueScore.score, - maxScore: valueScore.score, - }; + schemaColumn[valueScore.type] = [valueScore.score]; } } } } - return Object.keys(columnSchemas).reduce( + return Object.keys(columnSchemas).reduce( (schema, columnId) => { const columnScores = columnSchemas[columnId]; - const columnIds = Object.keys(columnScores); - - let bestMatch: SchemaTypeScoreComposite | null = null; - - for (let i = 0; i < columnIds.length; i++) { - const columnId = columnIds[i]; - const columnScore = columnScores[columnId]; - - if (columnScore.minScore >= MINIMUM_SCORE_MATCH) { - if (bestMatch == null) { - bestMatch = columnScore; - } else if (bestMatch.minScore < columnScore.minScore) { - bestMatch = columnScore; - } else if ( - bestMatch.minScore === columnScore.minScore && - bestMatch.maxScore < columnScore.maxScore - ) { - bestMatch = columnScore; + const typeIds = Object.keys(columnScores); + + // + const typeSummaries: { + [type: string]: { + minScore: number; + maxScore: number; + mean: number; + sd: number; + }; + } = {}; + + let bestType = null; + let bestScore = 0; + + for (let i = 0; i < typeIds.length; i++) { + const typeId = typeIds[i]; + + const typeScores = columnScores[typeId]; + + let minScore = 1; + let maxScore = 0; + + let totalScore = 0; + for (let j = 0; j < typeScores.length; j++) { + const score = typeScores[j]; + totalScore += score; + minScore = Math.min(minScore, score); + maxScore = Math.max(maxScore, score); + } + const mean = totalScore / typeScores.length; + + let sdSum = 0; + for (let j = 0; j < typeScores.length; j++) { + const score = typeScores[j]; + sdSum += (score - mean) * (score - mean); + } + // console.log(sdSum, typeScores.length - 1); + const sd = Math.sqrt(sdSum / typeScores.length); + + const summary = { minScore, maxScore, mean, sd }; + + const score = summary.mean - summary.sd; + if (score > MINIMUM_SCORE_MATCH) { + if (bestType == null || score > bestScore) { + bestType = typeId; + bestScore = score; } } + + typeSummaries[typeId] = summary; } + schema[columnId] = { columnType: bestType }; - schema[columnId] = { columnType: bestMatch ? bestMatch.type : null }; return schema; }, {} @@ -443,7 +457,7 @@ function getMergedSchema( const { id, dataType } = columns[i]; if (dataType != null) { if (detectedSchema.hasOwnProperty(id)) { - detectedSchema[id] = { ...detectedSchema[id], columnType: dataType }; + mergedSchema[id] = { ...detectedSchema[id], columnType: dataType }; } else { mergedSchema[id] = { columnType: dataType }; } From dd8855613fa7ccb5ec1a4b26c5db62cc5cd5e08e Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Tue, 17 Sep 2019 11:42:46 -0600 Subject: [PATCH 04/20] Tests for euidatagrid schema / column type --- src/components/datagrid/data_grid.test.tsx | 92 ++++++++++++++++++++++ src/components/datagrid/data_grid.tsx | 7 +- src/components/datagrid/data_grid_cell.tsx | 3 +- src/components/datagrid/data_grid_types.ts | 3 +- 4 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 4ebe1346f4b..438e8ae0053 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -307,6 +307,98 @@ describe('EuiDataGrid', () => { expect($element.children().length).toBe(allCells.length); }); }); + + describe('schema datatype classnames', () => { + it('applies classnames from explicit datatypes', () => { + const component = mount( + + `${rowIndex}, ${columnId}` + } + /> + ); + + const gridCellClassNames = component + .find('[className*="euiDataGridRowCell__columnType--"]') + .map(x => x.props().className); + expect(gridCellClassNames).toMatchInlineSnapshot(` +Array [ + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--customFormatName", + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--customFormatName", + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--customFormatName", +] +`); + }); + + it('automatically detects column types and applies classnames', () => { + const component = mount( + { + if (columnId === 'A') { + return 5.5; + } else if (columnId === 'B') { + return 'true'; + } else { + return 'asdf'; + } + }} + /> + ); + + const gridCellClassNames = component + .find('[className~="euiDataGridRowCell"]') + .map(x => x.props().className); + expect(gridCellClassNames).toMatchInlineSnapshot(` +Array [ + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--boolean", + "euiDataGridRowCell", + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--boolean", + "euiDataGridRowCell", +] +`); + }); + + it('overrides automatically detected column types with supplied schema', () => { + const component = mount( + + columnId === 'A' ? 5.5 : 'true' + } + /> + ); + + const gridCellClassNames = component + .find('[className~="euiDataGridRowCell"]') + .map(x => x.props().className); + expect(gridCellClassNames).toMatchInlineSnapshot(` +Array [ + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--alphanumeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--alphanumeric", +] +`); + }); + }); }); describe('cell rendering', () => { diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 2a7939d8c20..9218c3cf930 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -313,14 +313,12 @@ const schemaDetectors = [ }, ]; -export type EuiDataGridSchemaType = typeof schemaDetectors[number]['type']; - export interface EuiDataGridSchema { - [columnId: string]: { columnType: EuiDataGridSchemaType | null }; + [columnId: string]: { columnType: string | null }; } interface SchemaTypeScore { - type: EuiDataGridSchemaType; + type: string; score: number; } @@ -427,6 +425,7 @@ function useDetectSchema( const summary = { minScore, maxScore, mean, sd }; + // the mean-standard_deviation calculation is fairly arbitrary but fits the patterns I've thrown at it const score = summary.mean - summary.sd; if (score > MINIMUM_SCORE_MATCH) { if (bestType == null || score > bestScore) { diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index f602c459222..1f6425ca81e 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -12,7 +12,6 @@ import { EuiFocusTrap } from '../focus_trap'; import { Omit } from '../common'; import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; -import { EuiDataGridSchemaType } from './data_grid'; export interface CellValueElementProps { rowIndex: number; @@ -23,7 +22,7 @@ export interface EuiDataGridCellProps { rowIndex: number; colIndex: number; columnId: string; - columnType?: EuiDataGridSchemaType | null; + columnType?: string | null; width?: number; isFocusable: boolean; onCellFocus: Function; diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 2cd825e47d6..50b8f61835a 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -2,7 +2,8 @@ import { EuiDataGridSchema } from './data_grid'; export interface EuiDataGridColumn { id: string; - dataType?: EuiDataGridSchema['schema']['columnType']; + // allow devs to pass arbitrary dataType strings, but internally keep the code matching against the known types + dataType?: EuiDataGridSchema['schema']['columnType'] | string; } export interface EuiDataGridColumnWidths { From 0fa201796e9faf9984a768c8596463f83e12274d Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 18 Sep 2019 08:46:58 -0600 Subject: [PATCH 05/20] refactor datagrid schema code, add datetime type detection --- src/components/datagrid/data_grid.test.tsx | 36 +++ src/components/datagrid/data_grid.tsx | 180 +-------------- src/components/datagrid/data_grid_body.tsx | 2 +- .../datagrid/data_grid_data_row.tsx | 2 +- src/components/datagrid/data_grid_schema.ts | 213 ++++++++++++++++++ src/components/datagrid/data_grid_types.ts | 4 +- 6 files changed, 254 insertions(+), 183 deletions(-) create mode 100644 src/components/datagrid/data_grid_schema.ts diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 438e8ae0053..b114ca68bc8 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -396,6 +396,42 @@ Array [ "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", "euiDataGridRowCell euiDataGridRowCell__columnType--alphanumeric", ] +`); + }); + + it('detects all of the supported types', () => { + const values: { [key: string]: string } = { + A: '-5.80', + B: 'false', + C: '$-5.80', + E: '2019-09-18T12:31:28', + F: '2019-09-18T12:31:28Z', + G: '2019-09-18T12:31:28.234', + H: '2019-09-18T12:31:28.234+0300', + }; + const component = mount( + ({ id }))} + inMemory="pagination" + rowCount={1} + renderCellValue={({ columnId }) => values[columnId]} + /> + ); + + const gridCellClassNames = component + .find('[className~="euiDataGridRowCell"]') + .map(x => x.props().className); + expect(gridCellClassNames).toMatchInlineSnapshot(` +Array [ + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--boolean", + "euiDataGridRowCell euiDataGridRowCell__columnType--currency", + "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", + "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", + "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", + "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", +] `); }); }); diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 9218c3cf930..509db7fa0bf 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -7,7 +7,6 @@ import React, { useEffect, Fragment, ReactChild, - useMemo, } from 'react'; import classNames from 'classnames'; import { EuiI18n } from '../i18n'; @@ -41,6 +40,7 @@ import { EuiFocusTrap } from '../focus_trap'; import { EuiResizeObserver } from '../observer/resize_observer'; import { CELL_CONTENTS_ATTR } from './utils'; import { EuiDataGridInMemoryRenderer } from './data_grid_inmemory_renderer'; +import { getMergedSchema, useDetectSchema } from './data_grid_schema'; // When below this number the grid only shows the full screen button const MINIMUM_WIDTH_FOR_GRID_CONTROLS = 479; @@ -288,184 +288,6 @@ function useInMemoryValues(): [ return [inMemoryValues, onCellRender]; } -const schemaDetectors = [ - { - type: 'numeric' as 'numeric', - detector: (value: string) => { - const matchLength = (value.match(/[%-(]*[\d,]+(\.\d*)?[%)]*/) || [''])[0] - .length; - return matchLength / value.length || 0; - }, - }, - { - type: 'currency' as 'currency', - detector: (value: string) => { - const matchLength = (value.match(/[$-(]*[\d,]+(\.\d*)?[$)]*/) || [''])[0] - .length; - return matchLength / value.length || 0; - }, - }, - { - type: 'boolean' as 'boolean', - detector: (value: string) => { - return value === 'true' || value === 'false' ? 1 : 0; - }, - }, -]; - -export interface EuiDataGridSchema { - [columnId: string]: { columnType: string | null }; -} - -interface SchemaTypeScore { - type: string; - score: number; -} - -function scoreValueBySchemaType(value: string) { - const scores: SchemaTypeScore[] = []; - - for (let i = 0; i < schemaDetectors.length; i++) { - const { type, detector } = schemaDetectors[i]; - const score = detector(value); - scores.push({ type, score }); - } - - return scores; -} - -// completely arbitrary minimum match I came up with -// represents lowest score a type detector can have to be considered valid -const MINIMUM_SCORE_MATCH = 0.2; - -function useDetectSchema( - inMemoryValues: EuiDataGridInMemoryValues, - autoDetectSchema: boolean -) { - const schema = useMemo(() => { - const schema: EuiDataGridSchema = {}; - if (autoDetectSchema === false) { - return schema; - } - - const columnSchemas: { - [columnId: string]: { [type: string]: number[] }; - } = {}; - - const rowIndices = Object.keys(inMemoryValues); - for (let i = 0; i < rowIndices.length; i++) { - const rowIndex = rowIndices[i]; - const rowData = inMemoryValues[rowIndex]; - const columnIds = Object.keys(rowData); - - for (let j = 0; j < columnIds.length; j++) { - const columnId = columnIds[j]; - - const schemaColumn = (columnSchemas[columnId] = - columnSchemas[columnId] || {}); - - const columnValue = rowData[columnId].trim(); - const valueScores = scoreValueBySchemaType(columnValue); - - for (let k = 0; k < valueScores.length; k++) { - const valueScore = valueScores[k]; - if (schemaColumn.hasOwnProperty(valueScore.type)) { - const existingScore = schemaColumn[valueScore.type]; - existingScore.push(valueScore.score); - } else { - // first entry for this column - schemaColumn[valueScore.type] = [valueScore.score]; - } - } - } - } - - return Object.keys(columnSchemas).reduce( - (schema, columnId) => { - const columnScores = columnSchemas[columnId]; - const typeIds = Object.keys(columnScores); - - // - const typeSummaries: { - [type: string]: { - minScore: number; - maxScore: number; - mean: number; - sd: number; - }; - } = {}; - - let bestType = null; - let bestScore = 0; - - for (let i = 0; i < typeIds.length; i++) { - const typeId = typeIds[i]; - - const typeScores = columnScores[typeId]; - - let minScore = 1; - let maxScore = 0; - - let totalScore = 0; - for (let j = 0; j < typeScores.length; j++) { - const score = typeScores[j]; - totalScore += score; - minScore = Math.min(minScore, score); - maxScore = Math.max(maxScore, score); - } - const mean = totalScore / typeScores.length; - - let sdSum = 0; - for (let j = 0; j < typeScores.length; j++) { - const score = typeScores[j]; - sdSum += (score - mean) * (score - mean); - } - // console.log(sdSum, typeScores.length - 1); - const sd = Math.sqrt(sdSum / typeScores.length); - - const summary = { minScore, maxScore, mean, sd }; - - // the mean-standard_deviation calculation is fairly arbitrary but fits the patterns I've thrown at it - const score = summary.mean - summary.sd; - if (score > MINIMUM_SCORE_MATCH) { - if (bestType == null || score > bestScore) { - bestType = typeId; - bestScore = score; - } - } - - typeSummaries[typeId] = summary; - } - schema[columnId] = { columnType: bestType }; - - return schema; - }, - {} - ); - }, [inMemoryValues]); - return schema; -} - -function getMergedSchema( - detectedSchema: EuiDataGridSchema, - columns: EuiDataGridColumn[] -) { - const mergedSchema = { ...detectedSchema }; - - for (let i = 0; i < columns.length; i++) { - const { id, dataType } = columns[i]; - if (dataType != null) { - if (detectedSchema.hasOwnProperty(id)) { - mergedSchema[id] = { ...detectedSchema[id], columnType: dataType }; - } else { - mergedSchema[id] = { columnType: dataType }; - } - } - } - - return mergedSchema; -} - function createKeyDownHandler( props: EuiDataGridProps, visibleColumns: EuiDataGridProps['columns'], diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index a3b6de0940f..6d5a1c04af0 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -17,7 +17,7 @@ import { EuiDataGridDataRow, EuiDataGridDataRowProps, } from './data_grid_data_row'; -import { EuiDataGridSchema } from './data_grid'; +import { EuiDataGridSchema } from './data_grid_schema'; interface EuiDataGridBodyProps { columnWidths: EuiDataGridColumnWidths; diff --git a/src/components/datagrid/data_grid_data_row.tsx b/src/components/datagrid/data_grid_data_row.tsx index 4613a88697d..c38d31f23dc 100644 --- a/src/components/datagrid/data_grid_data_row.tsx +++ b/src/components/datagrid/data_grid_data_row.tsx @@ -4,7 +4,7 @@ import { EuiDataGridColumn, EuiDataGridColumnWidths } from './data_grid_types'; import { CommonProps } from '../common'; import { EuiDataGridCell, EuiDataGridCellProps } from './data_grid_cell'; -import { EuiDataGridSchema } from './data_grid'; +import { EuiDataGridSchema } from './data_grid_schema'; export type EuiDataGridDataRowProps = CommonProps & HTMLAttributes & { diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts new file mode 100644 index 00000000000..54d8aed2dd8 --- /dev/null +++ b/src/components/datagrid/data_grid_schema.ts @@ -0,0 +1,213 @@ +import { useMemo } from 'react'; +import { + EuiDataGridColumn, + EuiDataGridInMemoryValues, +} from './data_grid_types'; + +const schemaDetectors = [ + { + type: 'boolean', + detector(value: string) { + return value === 'true' || value === 'false' ? 1 : 0; + }, + }, + { + type: 'currency', + detector(value: string) { + const matchLength = (value.match(/[$-(]*[\d,]+(\.\d*)?[$)]*/) || [''])[0] + .length; + + // if there is no currency symbol then reduce the score + const hasCurrency = value.indexOf('$') !== -1; + const currencyAdjustment = hasCurrency ? 1 : 0.75; + + return (matchLength / value.length) * currencyAdjustment || 0; + }, + }, + { + type: 'datetime', + detector(value: string) { + // matches the most common forms of ISO-8601 + const isoTimestampMatch = value.match( + // 2019 - 09 - 17 T 12 : 18 : 32 .853 Z or -0600 + /^\d{2,4}-\d{1,2}-\d{1,2}(T?\d{1,2}:\d{1,2}:\d{1,2}(\.\d{3})?(Z|[+-]\d{4})?)?/ + ); + + // matches 9 digits (seconds) or 13 digits (milliseconds) since unix epoch + const unixTimestampMatch = value.match(/^(\d{9}|\d{13})$/); + + const isoMatchLength = isoTimestampMatch + ? isoTimestampMatch[0].length + : 0; + + // reduce the confidence of a unix timestamp match to 75% + // (a column of all unix timestamps should be numeric instead) + const unixMatchLength = unixTimestampMatch + ? unixTimestampMatch[0].length * 0.75 + : 0; + + return Math.max(isoMatchLength, unixMatchLength) / value.length || 0; + }, + }, + { + type: 'numeric', + detector(value: string) { + const matchLength = (value.match(/[%-(]*[\d,]+(\.\d*)?[%)]*/) || [''])[0] + .length; + return matchLength / value.length || 0; + }, + }, +]; + +export interface EuiDataGridSchema { + [columnId: string]: { columnType: string | null }; +} + +interface SchemaTypeScore { + type: string; + score: number; +} + +function scoreValueBySchemaType(value: string) { + const scores: SchemaTypeScore[] = []; + + for (let i = 0; i < schemaDetectors.length; i++) { + const { type, detector } = schemaDetectors[i]; + const score = detector(value); + scores.push({ type, score }); + } + + return scores; +} + +// completely arbitrary minimum match I came up with +// represents lowest score a type detector can have to be considered valid +const MINIMUM_SCORE_MATCH = 0.2; + +export function useDetectSchema( + inMemoryValues: EuiDataGridInMemoryValues, + autoDetectSchema: boolean +) { + const schema = useMemo(() => { + const schema: EuiDataGridSchema = {}; + if (autoDetectSchema === false) { + return schema; + } + + const columnSchemas: { + [columnId: string]: { [type: string]: number[] }; + } = {}; + + const rowIndices = Object.keys(inMemoryValues); + for (let i = 0; i < rowIndices.length; i++) { + const rowIndex = rowIndices[i]; + const rowData = inMemoryValues[rowIndex]; + const columnIds = Object.keys(rowData); + + for (let j = 0; j < columnIds.length; j++) { + const columnId = columnIds[j]; + + const schemaColumn = (columnSchemas[columnId] = + columnSchemas[columnId] || {}); + + const columnValue = rowData[columnId].trim(); + const valueScores = scoreValueBySchemaType(columnValue); + + for (let k = 0; k < valueScores.length; k++) { + const valueScore = valueScores[k]; + if (schemaColumn.hasOwnProperty(valueScore.type)) { + const existingScore = schemaColumn[valueScore.type]; + existingScore.push(valueScore.score); + } else { + // first entry for this column + schemaColumn[valueScore.type] = [valueScore.score]; + } + } + } + } + + return Object.keys(columnSchemas).reduce( + (schema, columnId) => { + const columnScores = columnSchemas[columnId]; + const typeIds = Object.keys(columnScores); + + // + const typeSummaries: { + [type: string]: { + minScore: number; + maxScore: number; + mean: number; + sd: number; + }; + } = {}; + + let bestType = null; + let bestScore = 0; + + for (let i = 0; i < typeIds.length; i++) { + const typeId = typeIds[i]; + + const typeScores = columnScores[typeId]; + + let minScore = 1; + let maxScore = 0; + + let totalScore = 0; + for (let j = 0; j < typeScores.length; j++) { + const score = typeScores[j]; + totalScore += score; + minScore = Math.min(minScore, score); + maxScore = Math.max(maxScore, score); + } + const mean = totalScore / typeScores.length; + + let sdSum = 0; + for (let j = 0; j < typeScores.length; j++) { + const score = typeScores[j]; + sdSum += (score - mean) * (score - mean); + } + // console.log(sdSum, typeScores.length - 1); + const sd = Math.sqrt(sdSum / typeScores.length); + + const summary = { minScore, maxScore, mean, sd }; + + // the mean-standard_deviation calculation is fairly arbitrary but fits the patterns I've thrown at it + const score = summary.mean - summary.sd; + if (score > MINIMUM_SCORE_MATCH) { + if (bestType == null || score > bestScore) { + bestType = typeId; + bestScore = score; + } + } + + typeSummaries[typeId] = summary; + } + schema[columnId] = { columnType: bestType }; + + return schema; + }, + {} + ); + }, [inMemoryValues]); + return schema; +} + +export function getMergedSchema( + detectedSchema: EuiDataGridSchema, + columns: EuiDataGridColumn[] +) { + const mergedSchema = { ...detectedSchema }; + + for (let i = 0; i < columns.length; i++) { + const { id, dataType } = columns[i]; + if (dataType != null) { + if (detectedSchema.hasOwnProperty(id)) { + mergedSchema[id] = { ...detectedSchema[id], columnType: dataType }; + } else { + mergedSchema[id] = { columnType: dataType }; + } + } + } + + return mergedSchema; +} diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 50b8f61835a..f8e8b6e567c 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -1,9 +1,9 @@ -import { EuiDataGridSchema } from './data_grid'; +import { EuiDataGridSchema } from './data_grid_schema'; export interface EuiDataGridColumn { id: string; // allow devs to pass arbitrary dataType strings, but internally keep the code matching against the known types - dataType?: EuiDataGridSchema['schema']['columnType'] | string; + dataType?: EuiDataGridSchema['*']['columnType']; } export interface EuiDataGridColumnWidths { From be62bf6b15e1713633a2ea16ce11a6bbdae42027 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 18 Sep 2019 09:03:27 -0600 Subject: [PATCH 06/20] some comments --- src/components/datagrid/data_grid_schema.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index 54d8aed2dd8..2beae082d82 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -82,7 +82,7 @@ function scoreValueBySchemaType(value: string) { // completely arbitrary minimum match I came up with // represents lowest score a type detector can have to be considered valid -const MINIMUM_SCORE_MATCH = 0.2; +const MINIMUM_SCORE_MATCH = 0.5; export function useDetectSchema( inMemoryValues: EuiDataGridInMemoryValues, @@ -98,6 +98,7 @@ export function useDetectSchema( [columnId: string]: { [type: string]: number[] }; } = {}; + // 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]; @@ -126,16 +127,14 @@ export function useDetectSchema( } } + // for each column, reduce each detector type's score to a single value and find the best fit return Object.keys(columnSchemas).reduce( (schema, columnId) => { const columnScores = columnSchemas[columnId]; const typeIds = Object.keys(columnScores); - // const typeSummaries: { [type: string]: { - minScore: number; - maxScore: number; mean: number; sd: number; }; @@ -149,29 +148,26 @@ export function useDetectSchema( const typeScores = columnScores[typeId]; - let minScore = 1; - let maxScore = 0; - + // find the mean let totalScore = 0; for (let j = 0; j < typeScores.length; j++) { const score = typeScores[j]; totalScore += score; - minScore = Math.min(minScore, score); - maxScore = Math.max(maxScore, score); } const mean = totalScore / typeScores.length; + // compute standard deviation let sdSum = 0; for (let j = 0; j < typeScores.length; j++) { const score = typeScores[j]; sdSum += (score - mean) * (score - mean); } - // console.log(sdSum, typeScores.length - 1); const sd = Math.sqrt(sdSum / typeScores.length); - const summary = { minScore, maxScore, mean, sd }; + const summary = { mean, sd }; // the mean-standard_deviation calculation is fairly arbitrary but fits the patterns I've thrown at it + // it is meant to represent the scores' average and distribution const score = summary.mean - summary.sd; if (score > MINIMUM_SCORE_MATCH) { if (bestType == null || score > bestScore) { From d43c422d2990ea60783c3ab5ded64827b5f6327e Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 18 Sep 2019 09:11:40 -0600 Subject: [PATCH 07/20] Allow extra type detectors for EuiDataGrid --- src/components/datagrid/data_grid.test.tsx | 36 +++++++++++++++++++++ src/components/datagrid/data_grid.tsx | 14 ++++++-- src/components/datagrid/data_grid_schema.ts | 23 ++++++++++--- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index b114ca68bc8..d833f855df5 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -432,6 +432,42 @@ Array [ "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", ] +`); + }); + + it('accepts extra detectors', () => { + const values: { [key: string]: string } = { + A: '-5.80', + B: '127.0.0.1', + }; + const component = mount( + ({ id }))} + schemaDetectors={[ + { + type: 'ipaddress', + detector(value: string) { + return value.match(/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/) + ? 1 + : 0; + }, + }, + ]} + inMemory="pagination" + rowCount={1} + renderCellValue={({ columnId }) => values[columnId]} + /> + ); + + const gridCellClassNames = component + .find('[className~="euiDataGridRowCell"]') + .map(x => x.props().className); + expect(gridCellClassNames).toMatchInlineSnapshot(` +Array [ + "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", + "euiDataGridRowCell euiDataGridRowCell__columnType--ipaddress", +] `); }); }); diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 509db7fa0bf..fa9b2e3b21b 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -40,7 +40,11 @@ import { EuiFocusTrap } from '../focus_trap'; import { EuiResizeObserver } from '../observer/resize_observer'; import { CELL_CONTENTS_ATTR } from './utils'; import { EuiDataGridInMemoryRenderer } from './data_grid_inmemory_renderer'; -import { getMergedSchema, useDetectSchema } from './data_grid_schema'; +import { + getMergedSchema, + SchemaDetector, + useDetectSchema, +} from './data_grid_schema'; // When below this number the grid only shows the full screen button const MINIMUM_WIDTH_FOR_GRID_CONTROLS = 479; @@ -48,6 +52,7 @@ const MINIMUM_WIDTH_FOR_GRID_CONTROLS = 479; type CommonGridProps = CommonProps & HTMLAttributes & { columns: EuiDataGridColumn[]; + schemaDetectors?: SchemaDetector[]; rowCount: number; renderCellValue: EuiDataGridCellProps['renderCellValue']; gridStyle?: EuiDataGridStyle; @@ -377,6 +382,7 @@ export const EuiDataGrid: FunctionComponent = props => { const { columns, + schemaDetectors, rowCount, renderCellValue, className, @@ -425,7 +431,11 @@ export const EuiDataGrid: FunctionComponent = props => { const [inMemoryValues, onCellRender] = useInMemoryValues(); - const detectedSchema = useDetectSchema(inMemoryValues, inMemory !== false); + const detectedSchema = useDetectSchema( + inMemoryValues, + schemaDetectors, + inMemory !== false + ); const mergedSchema = getMergedSchema(detectedSchema, columns); // These grid controls will only show when there is room. Check the resize observer callback diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index 2beae082d82..7c0a71f8b30 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -4,7 +4,12 @@ import { EuiDataGridInMemoryValues, } from './data_grid_types'; -const schemaDetectors = [ +export interface SchemaDetector { + type: string; + detector: (value: string) => number; +} + +const schemaDetectors: SchemaDetector[] = [ { type: 'boolean', detector(value: string) { @@ -68,11 +73,15 @@ interface SchemaTypeScore { score: number; } -function scoreValueBySchemaType(value: string) { +function scoreValueBySchemaType( + value: string, + extraSchemaDetectors: SchemaDetector[] = [] +) { const scores: SchemaTypeScore[] = []; + const detectors = [...schemaDetectors, ...extraSchemaDetectors]; - for (let i = 0; i < schemaDetectors.length; i++) { - const { type, detector } = schemaDetectors[i]; + for (let i = 0; i < detectors.length; i++) { + const { type, detector } = detectors[i]; const score = detector(value); scores.push({ type, score }); } @@ -86,6 +95,7 @@ const MINIMUM_SCORE_MATCH = 0.5; export function useDetectSchema( inMemoryValues: EuiDataGridInMemoryValues, + schemaDetectors: SchemaDetector[] | undefined, autoDetectSchema: boolean ) { const schema = useMemo(() => { @@ -112,7 +122,10 @@ export function useDetectSchema( columnSchemas[columnId] || {}); const columnValue = rowData[columnId].trim(); - const valueScores = scoreValueBySchemaType(columnValue); + const valueScores = scoreValueBySchemaType( + columnValue, + schemaDetectors + ); for (let k = 0; k < valueScores.length; k++) { const valueScore = valueScores[k]; From f56201e1718562ce626ed49157a4dce63744d465 Mon Sep 17 00:00:00 2001 From: Dave Snider Date: Wed, 18 Sep 2019 18:48:36 -0700 Subject: [PATCH 08/20] cleanup of docs and type formatting --- src-docs/src/views/datagrid/in_memory.js | 310 +----------------- src-docs/src/views/datagrid/schema.js | 298 +---------------- src/components/datagrid/_data_grid.scss | 4 +- .../datagrid/_data_grid_data_row.scss | 12 +- .../datagrid/_data_grid_header_row.scss | 10 +- src/components/datagrid/data_grid.test.tsx | 45 +-- src/components/datagrid/data_grid.tsx | 1 + src/components/datagrid/data_grid_cell.tsx | 2 +- .../datagrid/data_grid_header_row.tsx | 11 +- 9 files changed, 85 insertions(+), 608 deletions(-) diff --git a/src-docs/src/views/datagrid/in_memory.js b/src-docs/src/views/datagrid/in_memory.js index 972e3ff4b78..6b68a9943e7 100644 --- a/src-docs/src/views/datagrid/in_memory.js +++ b/src-docs/src/views/datagrid/in_memory.js @@ -1,17 +1,7 @@ import React, { Component, Fragment } from 'react'; import { fake } from 'faker'; -import { - EuiDataGrid, - EuiButtonGroup, - EuiSpacer, - EuiFormRow, - EuiPopover, - EuiButton, - EuiButtonIcon, - EuiLink, -} from '../../../../src/components/'; -import { iconTypes } from '../icon/icons'; +import { EuiDataGrid, EuiLink } from '../../../../src/components/'; const columns = [ { @@ -38,9 +28,6 @@ const columns = [ { id: 'version', }, - { - id: 'actions', - }, ]; const data = []; @@ -62,112 +49,14 @@ for (let i = 1; i < 100; i++) { amount: fake('{{finance.currencySymbol}}{{finance.amount}}'), phone: fake('{{phone.phoneNumber}}'), version: fake('{{system.semver}}'), - actions: ( - - - - - ), }); } export default class InMemoryDataGrid extends Component { constructor(props) { super(props); - this.borderOptions = [ - { - id: 'all', - label: 'All', - }, - { - id: 'horizontal', - label: 'Horizontal only', - }, - { - id: 'none', - label: 'None', - }, - ]; - - this.fontSizeOptions = [ - { - id: 's', - label: 'Small', - }, - { - id: 'm', - label: 'Medium', - }, - { - id: 'l', - label: 'Large', - }, - ]; - - this.cellPaddingOptions = [ - { - id: 's', - label: 'Small', - }, - { - id: 'm', - label: 'Medium', - }, - { - id: 'l', - label: 'Large', - }, - ]; - - this.stripeOptions = [ - { - id: 'true', - label: 'Stripes on', - }, - { - id: 'false', - label: 'Stripes off', - }, - ]; - - this.rowHoverOptions = [ - { - id: 'none', - label: 'None', - }, - { - id: 'highlight', - label: 'Highlight', - }, - ]; - - this.headerOptions = [ - { - id: 'shade', - label: 'Shade', - }, - { - id: 'underline', - label: 'Underline', - }, - ]; this.state = { - borderSelected: 'all', - fontSizeSelected: 'm', - cellPaddingSelected: 'm', - stripes: false, - stripesSelected: 'false', - rowHoverSelected: 'highlight', - isPopoverOpen: false, - headerSelected: 'shade', - data, sortingColumns: [{ id: 'contributions', direction: 'asc' }], @@ -178,55 +67,6 @@ export default class InMemoryDataGrid extends Component { }; } - onBorderChange = optionId => { - this.setState({ - borderSelected: optionId, - }); - }; - - onFontSizeChange = optionId => { - this.setState({ - fontSizeSelected: optionId, - }); - }; - - onCellPaddingChange = optionId => { - this.setState({ - cellPaddingSelected: optionId, - }); - }; - - onStripesChange = optionId => { - this.setState({ - stripesSelected: optionId, - stripes: !this.state.stripes, - }); - }; - - onRowHoverChange = optionId => { - this.setState({ - rowHoverSelected: optionId, - }); - }; - - onHeaderChange = optionId => { - this.setState({ - headerSelected: optionId, - }); - }; - - onPopoverButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - setSorting = sortingColumns => this.setState({ sortingColumns }); setPageIndex = pageIndex => @@ -239,141 +79,27 @@ export default class InMemoryDataGrid extends Component { pagination: { ...pagination, pageSize }, })); - dummyIcon = () => ( - - ); - render() { const { data, pagination, sortingColumns } = this.state; - const button = ( - - Table styling - - ); - return ( -
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - { - const value = data[rowIndex][columnId]; - - if (columnId === 'actions') { - return ( - <> - {this.dummyIcon()} - {this.dummyIcon()} - - ); - } - - if (columnId === 'url') { - return {value}; - } - - if (columnId === 'avatar_url') { - return ( -

- Avatar: {value} -

- ); - } - - return value; - }} - inMemory="sorting" - sorting={{ columns: sortingColumns, onSort: this.setSorting }} - pagination={{ - ...pagination, - pageSizeOptions: [5, 10, 25], - onChangeItemsPerPage: this.setPageSize, - onChangePage: this.setPageIndex, - }} - /> -
+ { + const value = data[rowIndex][columnId]; + return value; + }} + inMemory="sorting" + sorting={{ columns: sortingColumns, onSort: this.setSorting }} + pagination={{ + ...pagination, + pageSizeOptions: [5, 10, 25], + onChangeItemsPerPage: this.setPageSize, + onChangePage: this.setPageIndex, + }} + /> ); } } diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index daa42521bc4..200170927c4 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -3,11 +3,6 @@ import { fake } from 'faker'; import { EuiDataGrid, - EuiButtonGroup, - EuiSpacer, - EuiFormRow, - EuiPopover, - EuiButton, EuiButtonIcon, EuiLink, } from '../../../../src/components/'; @@ -40,14 +35,11 @@ const columns = [ { id: 'version', }, - { - id: 'actions', - }, ]; const data = []; -for (let i = 1; i < 100; i++) { +for (let i = 1; i < 5; i++) { data.push({ name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), email: {fake('{{internet.email}}')}, @@ -61,115 +53,17 @@ for (let i = 1; i < 100; i++) { ), date: fake('{{date.past}}'), account: fake('{{finance.account}}'), - amount: fake('{{finance.currencySymbol}}{{finance.amount}}'), + amount: fake('${{finance.amount}}'), phone: fake('{{phone.phoneNumber}}'), version: fake('{{system.semver}}'), - actions: ( - - - - - ), }); } export default class InMemoryDataGrid extends Component { constructor(props) { super(props); - this.borderOptions = [ - { - id: 'all', - label: 'All', - }, - { - id: 'horizontal', - label: 'Horizontal only', - }, - { - id: 'none', - label: 'None', - }, - ]; - - this.fontSizeOptions = [ - { - id: 's', - label: 'Small', - }, - { - id: 'm', - label: 'Medium', - }, - { - id: 'l', - label: 'Large', - }, - ]; - - this.cellPaddingOptions = [ - { - id: 's', - label: 'Small', - }, - { - id: 'm', - label: 'Medium', - }, - { - id: 'l', - label: 'Large', - }, - ]; - - this.stripeOptions = [ - { - id: 'true', - label: 'Stripes on', - }, - { - id: 'false', - label: 'Stripes off', - }, - ]; - - this.rowHoverOptions = [ - { - id: 'none', - label: 'None', - }, - { - id: 'highlight', - label: 'Highlight', - }, - ]; - - this.headerOptions = [ - { - id: 'shade', - label: 'Shade', - }, - { - id: 'underline', - label: 'Underline', - }, - ]; this.state = { - borderSelected: 'all', - fontSizeSelected: 'm', - cellPaddingSelected: 'm', - stripes: false, - stripesSelected: 'false', - rowHoverSelected: 'highlight', - isPopoverOpen: false, - headerSelected: 'shade', - data, sortingColumns: [{ id: 'contributions', direction: 'asc' }], @@ -180,55 +74,6 @@ export default class InMemoryDataGrid extends Component { }; } - onBorderChange = optionId => { - this.setState({ - borderSelected: optionId, - }); - }; - - onFontSizeChange = optionId => { - this.setState({ - fontSizeSelected: optionId, - }); - }; - - onCellPaddingChange = optionId => { - this.setState({ - cellPaddingSelected: optionId, - }); - }; - - onStripesChange = optionId => { - this.setState({ - stripesSelected: optionId, - stripes: !this.state.stripes, - }); - }; - - onRowHoverChange = optionId => { - this.setState({ - rowHoverSelected: optionId, - }); - }; - - onHeaderChange = optionId => { - this.setState({ - headerSelected: optionId, - }); - }; - - onPopoverButtonClick() { - this.setState({ - isPopoverOpen: !this.state.isPopoverOpen, - }); - } - - closePopover() { - this.setState({ - isPopoverOpen: false, - }); - } - setSorting = sortingColumns => this.setState({ sortingColumns }); setPageIndex = pageIndex => @@ -251,130 +96,23 @@ export default class InMemoryDataGrid extends Component { render() { const { data, pagination, sortingColumns } = this.state; - const button = ( - - Table styling - - ); - return ( -
- -
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - { - const value = data[rowIndex][columnId]; - - if (columnId === 'actions') { - return ( - <> - {this.dummyIcon()} - {this.dummyIcon()} - - ); - } - - if (columnId === 'url') { - return {value}; - } - - if (columnId === 'avatar_url') { - return ( -

- Avatar: {value} -

- ); - } - - return value; - }} - sorting={{ columns: sortingColumns, onSort: this.setSorting }} - pagination={{ - ...pagination, - pageSizeOptions: [5, 10, 25], - onChangeItemsPerPage: this.setPageSize, - onChangePage: this.setPageIndex, - }} - /> -
+ { + const value = data[rowIndex][columnId]; + return value; + }} + sorting={{ columns: sortingColumns, onSort: this.setSorting }} + pagination={{ + ...pagination, + pageSizeOptions: [5, 10, 25], + onChangeItemsPerPage: this.setPageSize, + onChangePage: this.setPageIndex, + }} + /> ); } } diff --git a/src/components/datagrid/_data_grid.scss b/src/components/datagrid/_data_grid.scss index 6106a0470a5..51050e37984 100644 --- a/src/components/datagrid/_data_grid.scss +++ b/src/components/datagrid/_data_grid.scss @@ -25,8 +25,10 @@ .euiDataGrid__content { @include euiScrollBar; - @include euiYScrollWithShadows; + @include euiScrollBar; + height: 100%; + overflow-y: auto; font-feature-settings: 'tnum' 1; // Tabular numbers overflow-x: auto; scroll-padding: 0; diff --git a/src/components/datagrid/_data_grid_data_row.scss b/src/components/datagrid/_data_grid_data_row.scss index a5e65e717a2..cb6a3c6ceca 100644 --- a/src/components/datagrid/_data_grid_data_row.scss +++ b/src/components/datagrid/_data_grid_data_row.scss @@ -18,6 +18,7 @@ // Hack to allow for all the focus guard stuff > * { max-width: 100%; + width: 100%; } &:first-of-type { @@ -42,12 +43,12 @@ padding-left: $euiDataGridCellPaddingM - 1px; } - &.euiDataGridRowCell__columnType--numeric { - font-family: monospace; + &.euiDataGridRowCell--numeric { + text-align: right; } - &.euiDataGridRowCell__columnType--currency { - color: $euiCodeBlockRegexpColor; + &.euiDataGridRowCell--currency { + text-align: right; } } @@ -80,7 +81,8 @@ // Border alternates @include euiDataGridStyles(bordersNone) { @include euiDataGridRowCell { - border-color: transparent; + // sass-lint:disable-block no-important + border-color: transparent !important; } } diff --git a/src/components/datagrid/_data_grid_header_row.scss b/src/components/datagrid/_data_grid_header_row.scss index 5c8269d6071..a329f73ea0a 100644 --- a/src/components/datagrid/_data_grid_header_row.scss +++ b/src/components/datagrid/_data_grid_header_row.scss @@ -14,6 +14,14 @@ .euiDataGridHeaderCell__content { @include euiTextTruncate; } + + &.euiDataGridHeaderCell--numeric { + text-align: right; + } + + &.euiDataGridHeaderCell--currency { + text-align: right; + } } // Header alternates @@ -94,4 +102,4 @@ @include euiDataGridHeaderCell { padding: $euiDataGridCellPaddingL; } -} \ No newline at end of file +} diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index d833f855df5..6c099c34902 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -327,16 +327,7 @@ describe('EuiDataGrid', () => { const gridCellClassNames = component .find('[className*="euiDataGridRowCell__columnType--"]') .map(x => x.props().className); - expect(gridCellClassNames).toMatchInlineSnapshot(` -Array [ - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--customFormatName", - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--customFormatName", - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--customFormatName", -] -`); + expect(gridCellClassNames).toMatchInlineSnapshot(`Array []`); }); it('automatically detects column types and applies classnames', () => { @@ -363,11 +354,11 @@ Array [ .map(x => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--boolean", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--boolean", "euiDataGridRowCell", - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--boolean", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--boolean", "euiDataGridRowCell", ] `); @@ -391,10 +382,10 @@ Array [ .map(x => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--alphanumeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--alphanumeric", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--alphanumeric", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--alphanumeric", ] `); }); @@ -424,13 +415,13 @@ Array [ .map(x => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--boolean", - "euiDataGridRowCell euiDataGridRowCell__columnType--currency", - "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", - "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", - "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", - "euiDataGridRowCell euiDataGridRowCell__columnType--datetime", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--boolean", + "euiDataGridRowCell euiDataGridRowCell--currency", + "euiDataGridRowCell euiDataGridRowCell--datetime", + "euiDataGridRowCell euiDataGridRowCell--datetime", + "euiDataGridRowCell euiDataGridRowCell--datetime", + "euiDataGridRowCell euiDataGridRowCell--datetime", ] `); }); @@ -465,8 +456,8 @@ Array [ .map(x => x.props().className); expect(gridCellClassNames).toMatchInlineSnapshot(` Array [ - "euiDataGridRowCell euiDataGridRowCell__columnType--numeric", - "euiDataGridRowCell euiDataGridRowCell__columnType--ipaddress", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--ipaddress", ] `); }); diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index fa9b2e3b21b..e7ead4bfea6 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -528,6 +528,7 @@ export const EuiDataGrid: FunctionComponent = props => { columnWidths={columnWidths} defaultColumnWidth={defaultColumnWidth} setColumnWidth={setColumnWidth} + schema={mergedSchema} /> & { columns: EuiDataGridColumn[]; columnWidths: EuiDataGridColumnWidths; + schema: EuiDataGridSchema; defaultColumnWidth?: number | null; setColumnWidth: (columnId: string, width: number) => void; sorting?: EuiDataGridSorting; @@ -24,6 +26,7 @@ const EuiDataGridHeaderRow: FunctionComponent< > = props => { const { columns, + schema, columnWidths, defaultColumnWidth, className, @@ -77,12 +80,18 @@ const EuiDataGridHeaderRow: FunctionComponent< } } + const columnType = schema[id] ? schema[id].columnType : null; + + const classes = classnames('euiDataGridHeaderCell', { + [`euiDataGridHeaderCell--${columnType}`]: columnType, + }); + return (
{width ? ( From 692ed6e37bc06d54b0d51f4e7a009a295f37a0b4 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 19 Sep 2019 09:38:18 -0600 Subject: [PATCH 09/20] Fix datagrid unit test --- src/components/datagrid/data_grid.test.tsx | 13 +++++++++++-- src/components/datagrid/data_grid_schema.ts | 4 ++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 6c099c34902..6e355df2bab 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -325,9 +325,18 @@ describe('EuiDataGrid', () => { ); const gridCellClassNames = component - .find('[className*="euiDataGridRowCell__columnType--"]') + .find('[className*="euiDataGridRowCell--"]') .map(x => x.props().className); - expect(gridCellClassNames).toMatchInlineSnapshot(`Array []`); + expect(gridCellClassNames).toMatchInlineSnapshot(` +Array [ + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--customFormatName", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--customFormatName", + "euiDataGridRowCell euiDataGridRowCell--numeric", + "euiDataGridRowCell euiDataGridRowCell--customFormatName", +] +`); }); it('automatically detects column types and applies classnames', () => { diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index 7c0a71f8b30..e91082e6666 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -24,9 +24,9 @@ const schemaDetectors: SchemaDetector[] = [ // if there is no currency symbol then reduce the score const hasCurrency = value.indexOf('$') !== -1; - const currencyAdjustment = hasCurrency ? 1 : 0.75; + const confidenceAdjustment = hasCurrency ? 1 : 0.95; - return (matchLength / value.length) * currencyAdjustment || 0; + return (matchLength / value.length) * confidenceAdjustment || 0; }, }, { From b4a83f262fb09a3b9ad505c3361d2ade69875225 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 19 Sep 2019 10:30:12 -0600 Subject: [PATCH 10/20] Update currency detector --- src/components/datagrid/data_grid_schema.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/datagrid/data_grid_schema.ts b/src/components/datagrid/data_grid_schema.ts index e91082e6666..9dcc26f1b2e 100644 --- a/src/components/datagrid/data_grid_schema.ts +++ b/src/components/datagrid/data_grid_schema.ts @@ -19,8 +19,11 @@ const schemaDetectors: SchemaDetector[] = [ { type: 'currency', detector(value: string) { - const matchLength = (value.match(/[$-(]*[\d,]+(\.\d*)?[$)]*/) || [''])[0] - .length; + const matchLength = (value.match( + // currency prefers starting with 1-3 characters for the currency symbol + // then it matches against numerical data + $ + /(^[^-(]{1,3})?[$-(]*[\d,]+(\.\d*)?[$)]*/ + ) || [''])[0].length; // if there is no currency symbol then reduce the score const hasCurrency = value.indexOf('$') !== -1; From 66a4a558e749f8fc729bf347f2c9eef17f64f690 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 19 Sep 2019 10:48:47 -0600 Subject: [PATCH 11/20] Allow EuiDataGrid's inMemory prop to be {true} --- src/components/datagrid/data_grid_types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index f8e8b6e567c..5691e1bf472 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -54,7 +54,7 @@ service / in-memory boundary can be used. Thus there are four states for in-memo * "filtering" - all operations are performed in-memory, no service calls */ export type EuiDataGridInMemory = - | false + | boolean | 'pagination' | 'sorting' | 'filtering'; From 8aa38f56ee7feedd0a402c19908170b679f29c7e Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 19 Sep 2019 12:56:33 -0600 Subject: [PATCH 12/20] Added ability to provide extra props for the containing cell div --- src-docs/src/views/datagrid/datagrid.js | 21 +++++++-- src/components/datagrid/data_grid_cell.tsx | 46 ++++++++++++++++--- .../datagrid/data_grid_inmemory_renderer.tsx | 8 +++- 3 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 611c8fb11a7..070ac54aa45 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -1,4 +1,4 @@ -import React, { Component, Fragment } from 'react'; +import React, { Component, Fragment, useEffect } from 'react'; import { fake } from 'faker'; import { @@ -124,10 +124,25 @@ export default class DataGridContainer extends Component { return ( data[rowIndex][columnId]} + renderCellValue={({ rowIndex, columnId, setCellProps }) => { + 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 }} pagination={{ ...pagination, diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index b772c9a77ee..52c8633d277 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -5,17 +5,19 @@ import React, { memo, ReactNode, createRef, + HTMLAttributes, } from 'react'; import classnames from 'classnames'; // @ts-ignore import { EuiFocusTrap } from '../focus_trap'; -import { Omit } from '../common'; +import { CommonProps, Omit } from '../common'; import { getTabbables, CELL_CONTENTS_ATTR } from './utils'; import { EuiMutationObserver } from '../observer/mutation_observer'; export interface CellValueElementProps { rowIndex: number; columnId: string; + setCellProps: (props: HTMLAttributes) => void; } export interface EuiDataGridCellProps { @@ -33,7 +35,9 @@ export interface EuiDataGridCellProps { | ((props: CellValueElementProps) => ReactNode); } -interface EuiDataGridCellState {} +interface EuiDataGridCellState { + cellProps: CommonProps & HTMLAttributes; +} type EuiDataGridCellValueProps = Omit< EuiDataGridCellProps, @@ -41,7 +45,9 @@ type EuiDataGridCellValueProps = Omit< >; const EuiDataGridCellContent: FunctionComponent< - EuiDataGridCellValueProps + EuiDataGridCellValueProps & { + setCellProps: CellValueElementProps['setCellProps']; + } > = memo(props => { const { renderCellValue, ...rest } = props; @@ -61,6 +67,9 @@ export class EuiDataGridCell extends Component< > { cellRef = createRef(); cellContentsRef = createRef(); + state: EuiDataGridCellState = { + cellProps: {}, + }; isInteractiveCell() { const cellContents = this.cellContentsRef.current; @@ -175,6 +184,10 @@ export class EuiDataGridCell extends Component< return false; } + setCellProps = (cellProps: HTMLAttributes) => { + this.setState({ cellProps }); + }; + render() { const { width, @@ -194,16 +207,31 @@ export class EuiDataGridCell extends Component< [`euiDataGridRowCell--${columnType}`]: columnType, }); + const cellProps: CommonProps & HTMLAttributes = { + ...this.state.cellProps, + 'data-test-subj': classnames( + 'dataGridRowCell', + this.state.cellProps['data-test-subj'] + ), + className: classnames(className, this.state.cellProps.className), + }; + + const widthStyle = width != null ? { width: `${width}px` } : {}; + if (cellProps.hasOwnProperty('style')) { + cellProps.style = { ...cellProps.style, ...widthStyle }; + } else { + cellProps.style = widthStyle; + } + return (
onCellFocus([colIndex, rowIndex])} - style={width != null ? { width: `${width}px` } : {}}> + onFocus={() => onCellFocus([colIndex, rowIndex])}> { @@ -220,7 +248,11 @@ export class EuiDataGridCell extends Component< {...isInteractiveCell} ref={this.cellContentsRef} className="euiDataGridRowCell__content"> - +
)} diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index a666c7747a3..64b8cf1e400 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -22,6 +22,8 @@ interface EuiDataGridInMemoryRendererProps { ) => void; } +function noop() {} + export const EuiDataGridInMemoryRenderer: FunctionComponent< EuiDataGridInMemoryRendererProps > = ({ columns, rowCount, renderCellValue, onCellRender }) => { @@ -48,7 +50,11 @@ export const EuiDataGridInMemoryRenderer: FunctionComponent< }, [text]); return (
- +
); }} From 205446085fbeae806d134403c470b87a500a4744 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 20 Sep 2019 09:49:57 -0600 Subject: [PATCH 13/20] Added test for cell props --- src/components/datagrid/data_grid.test.tsx | 74 ++++++++++++++++++++++ src/components/datagrid/data_grid_cell.tsx | 4 +- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 6e355df2bab..70646e046a5 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -308,6 +308,80 @@ describe('EuiDataGrid', () => { }); }); + it('renders and applies custom props', () => { + const component = mount( + { + setCellProps({ + className: 'customClass', + 'data-test-subj': `cell-${rowIndex}-${columnId}`, + style: { color: columnId === 'A' ? 'red' : 'blue' }, + }); + + return `${rowIndex}, ${columnId}`; + }} + /> + ); + + expect( + component.find('.euiDataGridRowCell').map(cell => { + const props = cell.props(); + delete props.children; + return props; + }) + ).toMatchInlineSnapshot(` +Array [ + Object { + "className": "euiDataGridRowCell customClass", + "data-test-subj": "dataGridRowCell", + "onFocus": [Function], + "role": "gridcell", + "style": Object { + "color": "red", + "width": "100px", + }, + "tabIndex": 0, + }, + Object { + "className": "euiDataGridRowCell customClass", + "data-test-subj": "dataGridRowCell", + "onFocus": [Function], + "role": "gridcell", + "style": Object { + "color": "blue", + "width": "100px", + }, + "tabIndex": -1, + }, + Object { + "className": "euiDataGridRowCell customClass", + "data-test-subj": "dataGridRowCell", + "onFocus": [Function], + "role": "gridcell", + "style": Object { + "color": "red", + "width": "100px", + }, + "tabIndex": -1, + }, + Object { + "className": "euiDataGridRowCell customClass", + "data-test-subj": "dataGridRowCell", + "onFocus": [Function], + "role": "gridcell", + "style": Object { + "color": "blue", + "width": "100px", + }, + "tabIndex": -1, + }, +] +`); + }); + describe('schema datatype classnames', () => { it('applies classnames from explicit datatypes', () => { const component = mount( diff --git a/src/components/datagrid/data_grid_cell.tsx b/src/components/datagrid/data_grid_cell.tsx index 52c8633d277..f15e3e07029 100644 --- a/src/components/datagrid/data_grid_cell.tsx +++ b/src/components/datagrid/data_grid_cell.tsx @@ -17,7 +17,7 @@ import { EuiMutationObserver } from '../observer/mutation_observer'; export interface CellValueElementProps { rowIndex: number; columnId: string; - setCellProps: (props: HTMLAttributes) => void; + setCellProps: (props: CommonProps & HTMLAttributes) => void; } export interface EuiDataGridCellProps { @@ -207,7 +207,7 @@ export class EuiDataGridCell extends Component< [`euiDataGridRowCell--${columnType}`]: columnType, }); - const cellProps: CommonProps & HTMLAttributes = { + const cellProps = { ...this.state.cellProps, 'data-test-subj': classnames( 'dataGridRowCell', From e79e15019159c6816b569f2bd90b919ebfa24cae Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 23 Sep 2019 11:49:22 -0600 Subject: [PATCH 14/20] Performance cleanups --- src-docs/src/views/datagrid/datagrid.js | 194 ++++++++++++------ src-docs/src/views/datagrid/in_memory.js | 7 +- .../__snapshots__/data_grid.test.tsx.snap | 186 ++++------------- src/components/datagrid/column_selector.tsx | 13 +- src/components/datagrid/data_grid.test.tsx | 14 +- src/components/datagrid/data_grid.tsx | 40 ++-- src/components/datagrid/data_grid_body.tsx | 7 +- src/components/datagrid/data_grid_cell.tsx | 67 +++--- .../datagrid/data_grid_inmemory_renderer.tsx | 2 +- src/components/datagrid/data_grid_types.ts | 3 +- src/components/datagrid/style_selector.tsx | 8 +- src/components/icon/icon.tsx | 1 + 12 files changed, 257 insertions(+), 285 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 070ac54aa45..910af6c9609 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -1,4 +1,10 @@ -import React, { Component, Fragment, useEffect } from 'react'; +import React, { + Fragment, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { fake } from 'faker'; import { @@ -7,6 +13,7 @@ import { EuiLink, } from '../../../../src/components/'; import { iconTypes } from '../../../../src-docs/src/views/icon/icons'; +import { EuiRadioGroup } from '../../../../src/components/form/radio'; const columns = [ { @@ -38,10 +45,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 +79,37 @@ 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 () => { + // ** 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('false'); + + // 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 +121,100 @@ 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]); - render() { - const { data, pagination, sortingColumns } = this.state; + const renderCellValue = useMemo(() => { + return ({ rowIndex, columnId, setCellProps }) => { + let adjustedRowIndex = rowIndex; - return ( + // 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]); + + return ( +
+ setInMemoryLevel(value)} + /> { - 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} + inMemory={ + inMemoryLevel === 'true' || inMemoryLevel === 'false' + ? inMemoryLevel === 'true' + : inMemoryLevel + } + 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..576e2a515db 100644 --- a/src-docs/src/views/datagrid/in_memory.js +++ b/src-docs/src/views/datagrid/in_memory.js @@ -87,11 +87,8 @@ export default class InMemoryDataGrid extends Component { aria-label="Top EUI contributors" columns={columns} rowCount={data.length} - renderCellValue={({ rowIndex, columnId }) => { - const value = data[rowIndex][columnId]; - return value; - }} - inMemory="sorting" + renderCellValue={({ rowIndex, columnId }) => data[rowIndex][columnId]} + inMemory="pagination" sorting={{ columns: sortingColumns, onSort: this.setSorting }} pagination={{ ...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 735f6c7ccd4..259f8ada41f 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..79864621295 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}`; }} diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index e7ead4bfea6..659719681ce 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -270,7 +270,9 @@ function useOnResize( ); } -function useInMemoryValues(): [ +function useInMemoryValues( + inMemory: EuiDataGridInMemory +): [ EuiDataGridInMemoryValues, (rowIndex: number, column: EuiDataGridColumn, value: string) => void ] { @@ -287,9 +289,15 @@ function useInMemoryValues(): [ return nextInMemoryVaues; }); }, - [inMemoryValues, setInMemoryValues] + [setInMemoryValues] ); + useEffect(() => { + if (inMemory === false) { + setInMemoryValues({}); + } + }, [inMemory]); + return [inMemoryValues, onCellRender]; } @@ -324,27 +332,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; @@ -396,8 +404,8 @@ export const EuiDataGrid: FunctionComponent = 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,7 +437,7 @@ export const EuiDataGrid: FunctionComponent = props => { className ); - const [inMemoryValues, onCellRender] = useInMemoryValues(); + const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory); const detectedSchema = useDetectSchema( inMemoryValues, @@ -441,8 +449,8 @@ export const EuiDataGrid: FunctionComponent = props => { // These grid controls will only show when there is room. Check the resize observer callback const gridControls = ( - - + {columnSelector} + {styleSelector} ); @@ -515,7 +523,13 @@ export const EuiDataGrid: FunctionComponent = props => { ) : null} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index 6d5a1c04af0..1aa2301c62d 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -123,9 +123,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..882c9cd2813 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -66,7 +66,7 @@ export const EuiDataGridInMemoryRenderer: FunctionComponent< } return rows; - }, [columns, rowCount, renderCellValue]); + }, [columns, rowCount, renderCellValue, onCellRender]); return createPortal( {rows}, diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 5691e1bf472..9065067b539 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -48,7 +48,8 @@ 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 +* false - all service calls, not all data is available +* true - all service calls, enable additional functionality based on rendered content * "pagination" - only pagination is performed in-memory * "sorting" - sorting & pagination is performed in-memory * "filtering" - all operations are performed in-memory, no service calls 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]; }; diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 1c3dc0ce8a2..bafc577585b 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -446,6 +446,7 @@ export class EuiIcon extends Component { isMounted = true; constructor(props: Props) { super(props); + // console.log('constructing'); const { type } = props; const initialIcon = getInitialIcon(type); From 6a2f174a2ce6198382a5d642d1358ee87f3241ac Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Mon, 23 Sep 2019 11:58:11 -0600 Subject: [PATCH 15/20] Clean up datagrid doc's inMemory selection --- src-docs/src/views/datagrid/datagrid.js | 71 +++++++++++++++---------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 910af6c9609..61f64d64284 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -8,9 +8,11 @@ import 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'; @@ -80,6 +82,8 @@ for (let i = 1; i < 1000; i++) { } export default () => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + // ** Pagination config const [pagination, setPagination] = useState({ pageIndex: 0, pageSize: 25 }); const onChangeItemsPerPage = useCallback( @@ -170,33 +174,46 @@ export default () => { return (
- setInMemoryLevel(value)} - /> + setIsPopoverOpen(state => !state)}> + inMemory options + + } + closePopover={() => setIsPopoverOpen(false)}> + { + setInMemoryLevel(value); + setIsPopoverOpen(false); + }} + /> + + Date: Mon, 23 Sep 2019 15:29:29 -0600 Subject: [PATCH 16/20] Merged in feature branch --- src-docs/src/views/datagrid/datagrid.js | 2 +- src/components/datagrid/data_grid.tsx | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index b09c7e6a01a..61f64d64284 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -138,7 +138,7 @@ export default () => { 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; diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index cf34e5b5d3c..659719681ce 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -446,13 +446,6 @@ export const EuiDataGrid: FunctionComponent = props => { ); const mergedSchema = getMergedSchema(detectedSchema, columns); - const detectedSchema = useDetectSchema( - inMemoryValues, - schemaDetectors, - inMemory !== false - ); - const mergedSchema = getMergedSchema(detectedSchema, columns); - // These grid controls will only show when there is room. Check the resize observer callback const gridControls = ( From 04562b347adcfaed0dc7a2bdf92f21b4e9cb361d Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Tue, 24 Sep 2019 12:26:08 -0600 Subject: [PATCH 17/20] EuiDataGrid in-memory options --- src-docs/src/views/datagrid/datagrid.js | 26 +++++---- src-docs/src/views/datagrid/in_memory.js | 2 +- src/components/datagrid/data_grid.test.tsx | 14 ++--- src/components/datagrid/data_grid.tsx | 13 ++--- src/components/datagrid/data_grid_body.tsx | 5 +- .../datagrid/data_grid_inmemory_renderer.tsx | 54 ++++++++++--------- src/components/datagrid/data_grid_types.ts | 35 ++++++------ 7 files changed, 81 insertions(+), 68 deletions(-) diff --git a/src-docs/src/views/datagrid/datagrid.js b/src-docs/src/views/datagrid/datagrid.js index 61f64d64284..fca7e084203 100644 --- a/src-docs/src/views/datagrid/datagrid.js +++ b/src-docs/src/views/datagrid/datagrid.js @@ -104,7 +104,7 @@ export default () => { [setSortingColumns] ); - const [inMemoryLevel, setInMemoryLevel] = useState('false'); + const [inMemoryLevel, setInMemoryLevel] = useState(''); // Sort data let data = useMemo(() => { @@ -172,6 +172,14 @@ export default () => { }; }, [data, inMemoryLevel]); + const inMemoryProps = {}; + if (inMemoryLevel !== '') { + inMemoryProps.inMemory = { + level: inMemoryLevel, + skipColumns: ['actions'], + }; + } + return (
{ compressed={true} options={[ { - id: 'false', + id: '', label: 'off', - value: 'false', + value: '', }, { - id: 'true', + id: 'enhancements', label: 'only enhancements', - value: 'true', + value: 'enhancements', }, { id: 'pagination', @@ -208,7 +216,7 @@ export default () => { ]} idSelected={inMemoryLevel} onChange={(id, value) => { - setInMemoryLevel(value); + setInMemoryLevel(value === '' ? undefined : value); setIsPopoverOpen(false); }} /> @@ -219,11 +227,7 @@ export default () => { columns={columns} rowCount={raw_data.length} renderCellValue={renderCellValue} - inMemory={ - inMemoryLevel === 'true' || inMemoryLevel === 'false' - ? inMemoryLevel === 'true' - : inMemoryLevel - } + {...inMemoryProps} sorting={{ columns: sortingColumns, onSort }} pagination={{ ...pagination, diff --git a/src-docs/src/views/datagrid/in_memory.js b/src-docs/src/views/datagrid/in_memory.js index 1833b13f744..e97b8a6c3b8 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="pagination" + inMemory={{ level: 'pagination' }} sorting={{ columns: sortingColumns, onSort: this.setSorting }} pagination={{ ...pagination, diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index 79864621295..59b57f921a3 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -420,7 +420,7 @@ Array [ { if (columnId === 'A') { @@ -454,7 +454,7 @@ Array [ columnId === 'A' ? 5.5 : 'true' @@ -489,7 +489,7 @@ Array [ ({ id }))} - inMemory="pagination" + inMemory={{ level: 'pagination' }} rowCount={1} renderCellValue={({ columnId }) => values[columnId]} /> @@ -530,7 +530,7 @@ Array [ }, }, ]} - inMemory="pagination" + inMemory={{ level: 'pagination' }} rowCount={1} renderCellValue={({ columnId }) => values[columnId]} /> @@ -939,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: () => {}, @@ -967,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' }, @@ -1003,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 659719681ce..740ad8f679b 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -271,7 +271,7 @@ function useOnResize( } function useInMemoryValues( - inMemory: EuiDataGridInMemory + inMemory?: EuiDataGridInMemory ): [ EuiDataGridInMemoryValues, (rowIndex: number, column: EuiDataGridColumn, value: string) => void @@ -293,7 +293,7 @@ function useInMemoryValues( ); useEffect(() => { - if (inMemory === false) { + if (inMemory == null) { setInMemoryValues({}); } }, [inMemory]); @@ -397,7 +397,7 @@ export const EuiDataGrid: FunctionComponent = props => { gridStyle, pagination, sorting, - inMemory = false, + inMemory, ...rest } = props; @@ -442,7 +442,7 @@ export const EuiDataGrid: FunctionComponent = props => { const detectedSchema = useDetectSchema( inMemoryValues, schemaDetectors, - inMemory !== false + inMemory != null ); const mergedSchema = getMergedSchema(detectedSchema, columns); @@ -521,11 +521,12 @@ export const EuiDataGrid: FunctionComponent = props => {
{inMemory ? ( 0 ) { diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index 882c9cd2813..d3393f5b6c3 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -8,10 +8,11 @@ import React, { } from 'react'; import { createPortal } from 'react-dom'; import { CellValueElementProps, EuiDataGridCellProps } from './data_grid_cell'; -import { EuiDataGridColumn } from './data_grid_types'; +import { EuiDataGridColumn, EuiDataGridInMemory } from './data_grid_types'; import { EuiInnerText } from '../inner_text'; interface EuiDataGridInMemoryRendererProps { + inMemory: EuiDataGridInMemory; columns: EuiDataGridColumn[]; rowCount: number; renderCellValue: EuiDataGridCellProps['renderCellValue']; @@ -26,7 +27,7 @@ function noop() {} export const EuiDataGridInMemoryRenderer: FunctionComponent< EuiDataGridInMemoryRendererProps -> = ({ columns, rowCount, renderCellValue, onCellRender }) => { +> = ({ inMemory, columns, rowCount, renderCellValue, onCellRender }) => { const [documentFragment] = useState(() => document.createDocumentFragment()); const rows = useMemo(() => { @@ -39,28 +40,33 @@ export const EuiDataGridInMemoryRenderer: FunctionComponent< 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 => + inMemory.skipColumns && + inMemory.skipColumns.indexOf(column.id) !== -1 ? null : ( + + + {(ref, text) => { + useEffect(() => { + if (text != null) { + onCellRender(i, column, text); + } + }, [text]); + return ( +
+ +
+ ); + }} +
+
+ ) + ) + .filter(cell => cell != null)}
); } diff --git a/src/components/datagrid/data_grid_types.ts b/src/components/datagrid/data_grid_types.ts index 9065067b539..9fd29ce20fe 100644 --- a/src/components/datagrid/data_grid_types.ts +++ b/src/components/datagrid/data_grid_types.ts @@ -41,24 +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, not all data is available -* true - all service calls, enable additional functionality based on rendered content -* "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 }; From 800ea28ff79dc4dc8ff11023255b8deefcf660a0 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 25 Sep 2019 11:04:42 -0600 Subject: [PATCH 18/20] Performance refactor for in-memory values --- src/components/datagrid/data_grid.tsx | 21 ++-- .../datagrid/data_grid_inmemory_renderer.tsx | 117 +++++++++++++----- src/components/datagrid/data_grid_schema.ts | 1 + 3 files changed, 98 insertions(+), 41 deletions(-) diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index 740ad8f679b..6b685472eb0 100644 --- a/src/components/datagrid/data_grid.tsx +++ b/src/components/datagrid/data_grid.tsx @@ -271,7 +271,8 @@ function useOnResize( } function useInMemoryValues( - inMemory?: EuiDataGridInMemory + inMemory: EuiDataGridInMemory | undefined, + rowCount: number ): [ EuiDataGridInMemoryValues, (rowIndex: number, column: EuiDataGridColumn, value: string) => void @@ -283,20 +284,20 @@ 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; }); }, [setInMemoryValues] ); + // if `inMemory.level` or `rowCount` changes reset the values + const inMemoryLevel = inMemory && inMemory.level; useEffect(() => { - if (inMemory == null) { - setInMemoryValues({}); - } - }, [inMemory]); + setInMemoryValues({}); + }, [inMemoryLevel, rowCount]); return [inMemoryValues, onCellRender]; } @@ -437,7 +438,7 @@ export const EuiDataGrid: FunctionComponent = props => { className ); - const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory); + const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory, rowCount); const detectedSchema = useDetectSchema( inMemoryValues, diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index d3393f5b6c3..f1945e02f79 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -6,10 +6,9 @@ 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, EuiDataGridInMemory } from './data_grid_types'; -import { EuiInnerText } from '../inner_text'; interface EuiDataGridInMemoryRendererProps { inMemory: EuiDataGridInMemory; @@ -25,6 +24,72 @@ interface EuiDataGridInMemoryRendererProps { function noop() {} +const _queue: Function[] = []; + +function processQueue() { + 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 > = ({ inMemory, columns, rowCount, renderCellValue, onCellRender }) => { @@ -33,39 +98,29 @@ export const EuiDataGridInMemoryRenderer: FunctionComponent< const rows = useMemo(() => { const rows = []; - const CellElement = renderCellValue as JSXElementConstructor< - CellValueElementProps - >; - for (let i = 0; i < rowCount; i++) { rows.push( {columns - .map(column => - inMemory.skipColumns && - inMemory.skipColumns.indexOf(column.id) !== -1 ? null : ( - - - {(ref, text) => { - useEffect(() => { - if (text != null) { - onCellRender(i, column, text); - } - }, [text]); - return ( -
- -
- ); - }} -
-
- ) - ) + .map(column => { + const skipThisColumn = + inMemory.skipColumns && + inMemory.skipColumns.indexOf(column.id) !== -1; + + if (skipThisColumn) { + return null; + } + + return ( + + ); + }) .filter(cell => cell != null)}
); 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]; From c23a0b90bb7a77ef22f77dac2d8138f42d568611 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Wed, 25 Sep 2019 12:02:48 -0600 Subject: [PATCH 19/20] added a comment --- src/components/datagrid/data_grid_inmemory_renderer.tsx | 3 +++ src/components/icon/icon.tsx | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/datagrid/data_grid_inmemory_renderer.tsx b/src/components/datagrid/data_grid_inmemory_renderer.tsx index f1945e02f79..fa320900585 100644 --- a/src/components/datagrid/data_grid_inmemory_renderer.tsx +++ b/src/components/datagrid/data_grid_inmemory_renderer.tsx @@ -27,6 +27,9 @@ 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](); diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index bafc577585b..1c3dc0ce8a2 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -446,7 +446,6 @@ export class EuiIcon extends Component { isMounted = true; constructor(props: Props) { super(props); - // console.log('constructing'); const { type } = props; const initialIcon = getInitialIcon(type); From ea09f89d3ff0aab52eb7f1c73cab98a14d252794 Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Thu, 26 Sep 2019 12:05:05 -0600 Subject: [PATCH 20/20] Fix sorting on in-memory and schema datagrid docs --- src-docs/src/views/datagrid/in_memory.js | 2 +- src-docs/src/views/datagrid/schema.js | 17 ++++++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src-docs/src/views/datagrid/in_memory.js b/src-docs/src/views/datagrid/in_memory.js index e97b8a6c3b8..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={{ level: 'pagination' }} + 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 }) => ({