From 8fe2cd6456d23122ed7af4d61bde409ddace5aff Mon Sep 17 00:00:00 2001 From: Chandler Prall Date: Fri, 13 Sep 2019 09:14:43 -0600 Subject: [PATCH 01/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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',