[RAC] [TGrid] Use EuiDataGridColumn schemas (for sorting) (#109983) (#109986)

## Summary

Updates the `TGrid` to use `EuiDataGrid` [schemas](https://eui.elastic.co/#/tabular-content/data-grid-schemas-and-popovers/) as suggested by @snide in the following issue: <https://github.com/elastic/kibana/issues/108894>

## Desk testing

1) In the `Security Solution`, navigate to `Security > Rules` and enable multiple detection rules that have different `Risk Score`s

**Expected result**

- The Detection Engine generates alerts (when the rule's criteria is met) that have different risk scores

2) Navigate to the `Security > Alerts` page

**Expected results**

As shown in the screenshot below:

- The alerts table is sorted by `@timestamp` in descending (Z-A) order, "newest first"
- The `@timestamp` field in every row is newer than, or the same time as the row below it
- The alerts table shows a non-zero count of alerts, e.g. `20,600 alerts`

![alerts-table-at-page-load](https://user-images.githubusercontent.com/4459398/130700525-343d51af-7a3a-475c-b3b4-b429bc212adf.png)

_Above: At page load, the alerts table is sorted by `@timestamp` in descending (Z-A) order, "newest first"_

3) Observe the count of alerts shown in the header of the alerts table, e.g. `20,600 alerts`, and then change the global date picker in the KQL bar from `Today` to `Last 1 year`

**Expected results**

- The golbal date picker now reads `Last 1 year`
- The count of the alerts displayed in the alerts table has increased, e.g. from `20,600 alerts` to `118,709 alerts`
- The `@timestamp` field in every row is (still) newer than, or the same time as the row below it

4) Click on the `@timestamp` column, and choose `Sort A-Z` from the popover, to change the sorting to ascending, "oldest first", as shown in the screenshot below:

![click-sort-ascending](https://user-images.githubusercontent.com/4459398/130701250-3f229644-2a78-409e-80ff-f88588562190.png)

_Above: Click `Sort A-Z` to sort ascending, "oldest first"_

**Expected results**

As shown in the screenshot below:

- The alerts table is sorted by `@timestamp` in ascending (A-Z) order, "oldest first"
- The `@timestamp` field in every row is older than, or the same time as the row below it
- `@timestamp` is older than the previously shown value, e.g. `Aug 3` instead of `Aug 24`

![timestamp-ascending-oldest-first](https://user-images.githubusercontent.com/4459398/130702221-cc8cf84f-c044-4574-8a93-b9d35c14c890.png)

_Above: The alerts table is now sorted by `@timestamp` in ascending (A-Z) order, "oldest first"_

5) Click on the `Risk Score` column, and choose `Sort A-Z` from the popover, to add `Risk Score` as a secondary sort in descending (Z-A) "highest first" order, as shown in the screenshot below:

![sort-risk-score](https://user-images.githubusercontent.com/4459398/130702599-e4c0d74a-8775-435b-a263-5b6b278f6dfd.png)

_Above: Click `Sort A-Z` to add `Risk Score` as a secondary sort in descending (Z-A) "highest first" order_

**Expected results**

- The alerts table re-fetches data
- The alerts table shows `2 fields sorted`

6) Hover over the alerts table and click the `Inspect` magnifiing glass icon

**Expected result**

- The `Inspect` modal appaers, as shown in the screenshot below:

![inspect](https://user-images.githubusercontent.com/4459398/130702849-1189f32e-eb03-4d9d-b248-6c6f0b5665fa.png)

_Above: the `Inspect` modal_

7) Click the `Request` tab, and scroll to the `sort` section of the request

**Expected result**

Per the JSON shown below:

- The request is sorted first by `@timestamp` in ascending (A-Z) order, "oldest first"
- The request is sorted second by `signal.rule.risk_score` descending (Z-A) "highest first" order

```json
  "sort": [
    {
      "@timestamp": {
        "order": "asc",
        "unmapped_type": "date"
      }
    },
    {
      "signal.rule.risk_score": {
        "order": "desc",
        "unmapped_type": "number"
      }
    }
  ],
```

8) Click `Close` to close the `Inspect` modal

9) Click `2 fields sorted` to display the sort popover

10) Use the drag handles to, via drag-and-drop, update the sorting such that `Risk Score` is sorted **before** `@timestamp`, as shown in the screenshot below:

![sort-by-risk-score-first](https://user-images.githubusercontent.com/4459398/130704159-523effa2-21ef-4599-a939-964fc523f9ec.png)

_Above: Use the drag handles to, via drag-and-drop, update the sorting such that `Risk Score` is sorted **before** `@timestamp`_

**Expected results**

As shown in the screenshot below:

- The table is updated to be sorted first by the higest risk score, e.g. previously `47`, now `73`
- The alerts table is sorted second by `@timestamp` in ascending (A-Z) order, "oldest first", and *may* have changed, e.g. from `Aug 3` to `Aug 12`, depending on the sample data in your environment

![highest-risk-score](https://user-images.githubusercontent.com/4459398/130704878-163a2427-fc7a-4755-9adc-a06b0d7b8e43.png)

_Above: The alerts table is now sorted first by highest risk score_

11) Once again, hover over the alerts table and click the `Inspect` magnifiing glass icon

12) Once again, click the `Request` tab, and scroll to the `sort` section of the request

**Expected result**

Per the JSON shown below:

- The request is sorted first by `signal.rule.risk_score` in descending (Z-A) "highest first" order
- The request is sorted second by `@timestamp` in ascending (A-Z) order, "oldest first"

```json
  "sort": [
    {
      "signal.rule.risk_score": {
        "order": "desc",
        "unmapped_type": "number"
      }
    },
    {
      "@timestamp": {
        "order": "asc",
        "unmapped_type": "date"
      }
    }
  ],
```

Co-authored-by: Andrew Goldstein <andrew-goldstein@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2021-08-25 01:30:13 -04:00 committed by GitHub
parent e028409a98
commit 9a2ce68c9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 2 deletions

View file

@ -63,6 +63,7 @@ export type ColumnHeaderOptions = Pick<
| 'id'
| 'initialWidth'
| 'isSortable'
| 'schema'
> & {
aggregatable?: boolean;
tGridCellActions?: TGridCellAction[];

View file

@ -9,7 +9,13 @@ import { omit, set } from 'lodash/fp';
import React from 'react';
import { defaultHeaders } from './default_headers';
import { getActionsColumnWidth, getColumnWidthFromType, getColumnHeaders } from './helpers';
import {
BUILT_IN_SCHEMA,
getActionsColumnWidth,
getColumnWidthFromType,
getColumnHeaders,
getSchema,
} from './helpers';
import {
DEFAULT_COLUMN_MIN_WIDTH,
DEFAULT_DATE_COLUMN_MIN_WIDTH,
@ -18,6 +24,7 @@ import {
SHOW_CHECK_BOXES_COLUMN_WIDTH,
} from '../constants';
import { mockBrowserFields } from '../../../../mock/browser_fields';
import { ColumnHeaderOptions } from '../../../../../common';
window.matchMedia = jest.fn().mockImplementation((query) => {
return {
@ -62,6 +69,32 @@ describe('helpers', () => {
});
});
describe('getSchema', () => {
const expected: Record<string, BUILT_IN_SCHEMA> = {
date: 'datetime',
date_nanos: 'datetime',
double: 'numeric',
long: 'numeric',
number: 'numeric',
object: 'json',
boolean: 'boolean',
};
Object.keys(expected).forEach((type) =>
test(`it returns the expected schema for type '${type}'`, () => {
expect(getSchema(type)).toEqual(expected[type]);
})
);
test('it returns `undefined` when `type` does NOT match a built-in schema type', () => {
expect(getSchema('string')).toBeUndefined(); // 'keyword` doesn't have a schema
});
test('it returns `undefined` when `type` is undefined', () => {
expect(getSchema(undefined)).toBeUndefined();
});
});
describe('getColumnHeaders', () => {
// additional properties used by `EuiDataGrid`:
const actions = {
@ -208,6 +241,7 @@ describe('helpers', () => {
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
isSortable,
name: '@timestamp',
schema: 'datetime',
searchable: true,
type: 'date',
initialWidth: 190,
@ -254,5 +288,62 @@ describe('helpers', () => {
expectedData
);
});
test('it should NOT override a custom `schema` when the `header` provides it', () => {
const expected = [
{
actions,
aggregatable: true,
category: 'base',
columnHeaderType: 'not-filtered',
defaultSortDirection,
description:
'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.',
example: '2016-05-23T08:05:34.853Z',
format: '',
id: '@timestamp',
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
isSortable,
name: '@timestamp',
schema: 'custom', // <-- we expect our custom schema will NOT be overridden by a built-in schema
searchable: true,
type: 'date', // <-- the built-in schema for `type: 'date'` is 'datetime', but the custom schema overrides it
initialWidth: 190,
},
];
const headerWithCustomSchema: ColumnHeaderOptions = {
columnHeaderType: 'not-filtered',
id: '@timestamp',
initialWidth: 190,
schema: 'custom', // <-- overrides the default of 'datetime'
};
expect(
getColumnHeaders([headerWithCustomSchema], mockBrowserFields).map(omit('display'))
).toEqual(expected);
});
test('it should return an `undefined` `schema` when a `header` does NOT have an entry in `BrowserFields`', () => {
const expected = [
{
actions,
columnHeaderType: 'not-filtered',
defaultSortDirection,
id: 'no_matching_browser_field',
isSortable: false,
schema: undefined, // <-- no `BrowserFields` entry for this field
},
];
const headerDoesNotMatchBrowserField: ColumnHeaderOptions = {
columnHeaderType: 'not-filtered',
id: 'no_matching_browser_field',
};
expect(
getColumnHeaders([headerDoesNotMatchBrowserField], mockBrowserFields).map(omit('display'))
).toEqual(expected);
});
});
});

View file

@ -44,15 +44,59 @@ const getAllFieldsByName = (
): { [fieldName: string]: Partial<BrowserField> } =>
keyBy('name', getAllBrowserFields(browserFields));
/**
* Valid built-in schema types for the `schema` property of `EuiDataGridColumn`
* are enumerated in the following comment in the EUI repository (permalink):
* https://github.com/elastic/eui/blob/edc71160223c8d74e1293501f7199fba8fa57c6c/src/components/datagrid/data_grid_types.ts#L417
*/
export type BUILT_IN_SCHEMA = 'boolean' | 'currency' | 'datetime' | 'numeric' | 'json';
/**
* Returns a valid value for the `EuiDataGridColumn` `schema` property, or
* `undefined` when the specified `BrowserFields` `type` doesn't match a
* built-in schema type
*
* Notes:
*
* - At the time of this writing, the type definition of the
* `EuiDataGridColumn` `schema` property is:
*
* ```ts
* schema?: string;
* ```
* - At the time of this writing, Elasticsearch Field data types are documented here:
* https://www.elastic.co/guide/en/elasticsearch/reference/7.14/mapping-types.html
*/
export const getSchema = (type: string | undefined): BUILT_IN_SCHEMA | undefined => {
switch (type) {
case 'date': // fall through
case 'date_nanos':
return 'datetime';
case 'double': // fall through
case 'long': // fall through
case 'number':
return 'numeric';
case 'object':
return 'json';
case 'boolean':
return 'boolean';
default:
return undefined;
}
};
/** Enriches the column headers with field details from the specified browserFields */
export const getColumnHeaders = (
headers: ColumnHeaderOptions[],
browserFields: BrowserFields
): ColumnHeaderOptions[] => {
const browserFieldByName = getAllFieldsByName(browserFields);
return headers
? headers.map((header) => {
const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name]
const browserField: Partial<BrowserField> | undefined = browserFieldByName[header.id];
// augment the header with metadata from browserFields:
const augmentedHeader = {
...header,
@ -60,6 +104,7 @@ export const getColumnHeaders = (
[splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id],
browserFields
),
schema: header.schema ?? getSchema(browserField?.type),
};
const content = <>{header.display ?? header.displayAsText ?? header.id}</>;
@ -71,7 +116,7 @@ export const getColumnHeaders = (
defaultSortDirection: 'desc', // the default action when a user selects a field via `EuiDataGrid`'s `Pick fields to sort by` UI
display: <>{content}</>,
isSortable: allowSorting({
browserField: getAllFieldsByName(browserFields)[header.id],
browserField,
fieldName: header.id,
}),
};