diff --git a/src-docs/src/views/datagrid/schema.js b/src-docs/src/views/datagrid/schema.js index 91df66542b9..ac6e4997f14 100644 --- a/src-docs/src/views/datagrid/schema.js +++ b/src-docs/src/views/datagrid/schema.js @@ -43,21 +43,9 @@ const columns = [ const data = []; for (let i = 1; i < 5; 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.amount}}'), - json: JSON.stringify([ + let json; + if (i < 3) { + json = JSON.stringify([ { name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), email: fake('{{internet.email}}'), @@ -76,7 +64,30 @@ for (let i = 1; i < 5; i++) { }, ], }, - ]), + ]); + } else { + json = JSON.stringify([ + { + name: fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'), + }, + ]); + } + + 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.amount}}'), + json: json, version: fake('{{system.semver}}'), }); } diff --git a/src/components/datagrid/column_sorting.tsx b/src/components/datagrid/column_sorting.tsx index eab6b2bb639..898d00d0128 100644 --- a/src/components/datagrid/column_sorting.tsx +++ b/src/components/datagrid/column_sorting.tsx @@ -149,104 +149,110 @@ export const useColumnSorting = (

)} - - - - setAvailableColumnsIsOpen(false)} - anchorPosition="downLeft" - ownFocus - panelPaddingSize="s" - button={ + {(inactiveColumns.length > 0 || sorting.columns.length > 0) && ( + + + + {inactiveColumns.length > 0 && ( + setAvailableColumnsIsOpen(false)} + anchorPosition="downLeft" + ownFocus + panelPaddingSize="s" + button={ + + setAvailableColumnsIsOpen(!avilableColumnsisOpen) + }> + + + }> + + {(sortFieldAriaLabel: string) => ( +
+ {inactiveColumns.map(({ id }) => ( + + ))} +
+ )} +
+
+ )} +
+ {sorting.columns.length > 0 ? ( + - setAvailableColumnsIsOpen(!avilableColumnsisOpen) - }> + flush="right" + onClick={() => sorting.onSort([])}> - }> - - {(sortFieldAriaLabel: string) => ( -
- {inactiveColumns.map(({ id }) => ( - - ))} -
- )} -
-
-
- {sorting.columns.length > 0 ? ( - - sorting.onSort([])}> - - - - ) : null} -
-
+ + ) : null} + + + )} ); diff --git a/src/components/datagrid/data_grid.test.tsx b/src/components/datagrid/data_grid.test.tsx index aaa5ae5f487..ad6b41027f3 100644 --- a/src/components/datagrid/data_grid.test.tsx +++ b/src/components/datagrid/data_grid.test.tsx @@ -116,21 +116,24 @@ function getColumnSortDirection( columnSelectionPopover = datagrid.find( 'EuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' ); - expect(columnSelectionPopover).euiPopoverToBeOpen(); + // popover will go away if all of the columns are selected + if (columnSelectionPopover.length > 0) { + expect(columnSelectionPopover).euiPopoverToBeOpen(); + + popoverButton = columnSelectionPopover + .find('div[className="euiPopover__anchor"]') + .find('[onClick]') + .first(); + // @ts-ignore-next-line + act(() => popoverButton.props().onClick()); - popoverButton = columnSelectionPopover - .find('div[className="euiPopover__anchor"]') - .find('[onClick]') - .first(); - // @ts-ignore-next-line - act(() => popoverButton.props().onClick()); + datagrid.update(); - datagrid.update(); - - columnSelectionPopover = datagrid.find( - 'EuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' - ); - expect(columnSelectionPopover).not.euiPopoverToBeOpen(); + columnSelectionPopover = datagrid.find( + 'EuiPopover[data-test-subj="dataGridColumnSortingPopoverColumnSelection"]' + ); + expect(columnSelectionPopover).not.euiPopoverToBeOpen(); + } // find the column sorter columnSelectionPopover = datagrid.find( @@ -1234,6 +1237,34 @@ Array [ ]); }); }); + + it('uses schema information to sort', () => { + const component = mount( + + // render A 0->4 and B 12->8 + columnId === 'A' ? rowIndex : 12 - rowIndex + } + inMemory={{ level: 'sorting' }} + sorting={{ + columns: [{ id: 'B', direction: 'asc' }], + onSort: () => {}, + }} + /> + ); + + expect(extractGridData(component)).toEqual([ + ['A', 'B'], + ['4', '8'], + ['3', '9'], + ['2', '10'], + ['1', '11'], + ['0', '12'], + ]); + }); }); describe('keyboard controls', () => { diff --git a/src/components/datagrid/data_grid.tsx b/src/components/datagrid/data_grid.tsx index a41283c2dde..b90d15368a0 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'; @@ -44,6 +45,7 @@ import { getMergedSchema, SchemaDetector, useDetectSchema, + schemaDetectors as providedSchemaDetectors, } from './data_grid_schema'; import { useColumnSorting } from './column_sorting'; @@ -317,9 +319,13 @@ export const EuiDataGrid: FunctionComponent = props => { const [inMemoryValues, onCellRender] = useInMemoryValues(inMemory, rowCount); + const allSchemaDetetors = useMemo( + () => [...providedSchemaDetectors, ...(schemaDetectors || [])], + [schemaDetectors] + ); const detectedSchema = useDetectSchema( inMemoryValues, - schemaDetectors, + allSchemaDetetors, inMemory != null ); const mergedSchema = getMergedSchema(detectedSchema, columns); @@ -465,6 +471,7 @@ export const EuiDataGrid: FunctionComponent = props => { inMemory={inMemory} columns={visibleColumns} schema={mergedSchema} + schemaDetectors={allSchemaDetetors} expansionFormatters={expansionFormatters} focusedCell={focusedCell} onCellFocus={setFocusedCell} diff --git a/src/components/datagrid/data_grid_body.tsx b/src/components/datagrid/data_grid_body.tsx index c081658dbc0..9881a09bf3d 100644 --- a/src/components/datagrid/data_grid_body.tsx +++ b/src/components/datagrid/data_grid_body.tsx @@ -23,7 +23,7 @@ import { EuiDataGridDataRow, EuiDataGridDataRowProps, } from './data_grid_data_row'; -import { EuiDataGridSchema } from './data_grid_schema'; +import { EuiDataGridSchema, SchemaDetector } from './data_grid_schema'; import { useInnerText } from '../inner_text'; interface EuiDataGridBodyProps { @@ -31,6 +31,7 @@ interface EuiDataGridBodyProps { defaultColumnWidth?: number | null; columns: EuiDataGridColumn[]; schema: EuiDataGridSchema; + schemaDetectors: SchemaDetector[]; expansionFormatters?: EuiDataGridExpansionFormatters; focusedCell: EuiDataGridDataRowProps['focusedCell']; onCellFocus: EuiDataGridDataRowProps['onCellFocus']; @@ -43,6 +44,16 @@ interface EuiDataGridBodyProps { sorting?: EuiDataGridSorting; } +const defaultComparator: NonNullable = ( + a, + b, + direction +) => { + if (a < b) return direction === 'asc' ? -1 : 1; + if (a > b) return direction === 'asc' ? 1 : -1; + return 0; +}; + const providedExpansionFormatters: EuiDataGridExpansionFormatters = { json: ({ children }) => { const invisibleRef = useRef(null); @@ -102,6 +113,7 @@ export const EuiDataGridBody: FunctionComponent< defaultColumnWidth, columns, schema, + schemaDetectors, expansionFormatters, focusedCell, onCellFocus, @@ -153,8 +165,24 @@ export const EuiDataGridBody: FunctionComponent< const aValue = a.values[column.id]; const bValue = b.values[column.id]; - if (aValue < bValue) return column.direction === 'asc' ? -1 : 1; - if (aValue > bValue) return column.direction === 'asc' ? 1 : -1; + // get the comparator, based on schema + let comparator = defaultComparator; + if (schema.hasOwnProperty(column.id)) { + const columnType = schema[column.id].columnType; + for (let i = 0; i < schemaDetectors.length; i++) { + const detector = schemaDetectors[i]; + if ( + detector.type === columnType && + detector.hasOwnProperty('comparator') + ) { + comparator = detector.comparator!; + } + } + } + + const result = comparator(aValue, bValue, column.direction); + // only return if the columns are inequal, otherwise allow the next sort-by column to run + if (result !== 0) return result; } return 0; @@ -166,7 +194,7 @@ export const EuiDataGridBody: FunctionComponent< } return rowMap; - }, [sorting, inMemory, inMemoryValues]); + }, [sorting, inMemory, inMemoryValues, schema, schemaDetectors]); const setCellFocus = useCallback( ([colIndex, rowIndex]) => { diff --git a/src/components/datagrid/data_grid_schema.tsx b/src/components/datagrid/data_grid_schema.tsx index a9ad43f2e15..43d0673475e 100644 --- a/src/components/datagrid/data_grid_schema.tsx +++ b/src/components/datagrid/data_grid_schema.tsx @@ -11,40 +11,64 @@ import { palettes } from '../../services/color/eui_palettes'; export interface SchemaDetector { type: string; detector: (value: string) => number; + comparator?: (a: string, b: string, direction: 'asc' | 'desc') => -1 | 0 | 1; icon: string; color: string; sortTextAsc: ReactNode; sortTextDesc: ReactNode; } -const schemaDetectors: SchemaDetector[] = [ +const numericChars = new Set([ + '0', + '1', + '2', + '3', + '4', + '5', + '6', + '7', + '8', + '9', + '.', + '-', +]); +export const schemaDetectors: SchemaDetector[] = [ { type: 'boolean', - detector(value: string) { - return value === 'true' || value === 'false' ? 1 : 0; + detector(value) { + return value.toLowerCase() === 'true' || value.toLowerCase() === 'false' + ? 1 + : 0; + }, + comparator(a, b, direction) { + const aValue = a.toLowerCase() === 'true'; + const bValue = b.toLowerCase() === 'true'; + if (aValue < bValue) return direction === 'asc' ? 1 : -1; + if (aValue > bValue) return direction === 'asc' ? -1 : 1; + return 0; }, icon: 'invert', color: palettes.euiPaletteColorBlind.colors[5], sortTextAsc: ( ), sortTextDesc: ( ), }, { type: 'currency', - detector(value: string) { + detector(value) { 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*)?[$)]*/ + /(^[^-(.]{1,3})?[$-(]*[\d,]+(\.\d*)?[$)]*/ ) || [''])[0].length; // if there is no currency symbol then reduce the score @@ -53,6 +77,17 @@ const schemaDetectors: SchemaDetector[] = [ return (matchLength / value.length) * confidenceAdjustment || 0; }, + comparator: (a, b, direction) => { + const aChars = a.split('').filter(char => numericChars.has(char)); + const aValue = parseFloat(aChars.join('')); + + const bChars = b.split('').filter(char => numericChars.has(char)); + const bValue = parseFloat(bChars.join('')); + + if (aValue < bValue) return direction === 'asc' ? -1 : 1; + if (aValue > bValue) return direction === 'asc' ? 1 : -1; + return 0; + }, icon: 'currency', color: palettes.euiPaletteColorBlind.colors[0], sortTextAsc: ( @@ -70,7 +105,7 @@ const schemaDetectors: SchemaDetector[] = [ }, { type: 'datetime', - detector(value: string) { + detector(value) { // matches the most common forms of ISO-8601 const isoTimestampMatch = value.match( // 2019 - 09 - 17 T 12 : 18 : 32 .853 Z or -0600 @@ -103,11 +138,22 @@ const schemaDetectors: SchemaDetector[] = [ }, { type: 'numeric', - detector(value: string) { + detector(value) { const matchLength = (value.match(/[%-(]*[\d,]+(\.\d*)?[%)]*/) || [''])[0] .length; return matchLength / value.length || 0; }, + comparator: (a, b, direction) => { + const aChars = a.split('').filter(char => numericChars.has(char)); + const aValue = parseFloat(aChars.join('')); + + const bChars = b.split('').filter(char => numericChars.has(char)); + const bValue = parseFloat(bChars.join('')); + + if (aValue < bValue) return direction === 'asc' ? -1 : 1; + if (aValue > bValue) return direction === 'asc' ? 1 : -1; + return 0; + }, icon: 'number', color: palettes.euiPaletteColorBlind.colors[0], sortTextAsc: ( @@ -135,6 +181,11 @@ const schemaDetectors: SchemaDetector[] = [ return 0; } }, + comparator: (a, b, direction) => { + if (a.length > b.length) return direction === 'asc' ? 1 : -1; + if (a.length < b.length) return direction === 'asc' ? 1 : -1; + return 0; + }, icon: 'visVega', color: palettes.euiPaletteColorBlind.colors[3], sortTextAsc: ( @@ -163,13 +214,12 @@ interface SchemaTypeScore { function scoreValueBySchemaType( value: string, - extraSchemaDetectors: SchemaDetector[] = [] + schemaDetectors: SchemaDetector[] = [] ) { const scores: SchemaTypeScore[] = []; - const detectors = [...schemaDetectors, ...extraSchemaDetectors]; - for (let i = 0; i < detectors.length; i++) { - const { type, detector } = detectors[i]; + for (let i = 0; i < schemaDetectors.length; i++) { + const { type, detector } = schemaDetectors[i]; const score = detector(value); scores.push({ type, score }); } @@ -286,7 +336,7 @@ export function useDetectSchema( }, {} ); - }, [inMemoryValues]); + }, [inMemoryValues, schemaDetectors]); return schema; } @@ -294,20 +344,22 @@ 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 useMemo(() => { + 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; + return mergedSchema; + }, [detectedSchema, columns]); } // Given a provided schema, return the details for the schema