## 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:
parent
e028409a98
commit
9a2ce68c9e
|
@ -63,6 +63,7 @@ export type ColumnHeaderOptions = Pick<
|
|||
| 'id'
|
||||
| 'initialWidth'
|
||||
| 'isSortable'
|
||||
| 'schema'
|
||||
> & {
|
||||
aggregatable?: boolean;
|
||||
tGridCellActions?: TGridCellAction[];
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue