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