forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[RAC] [TGrid] Implements sorting in the
TGrid
(elastic#107495)
## Summary This PR implements sorting in the `TGrid`, per the animated gifs below: ![observability-sorting](https://user-images.githubusercontent.com/4459398/127960825-5be21a92-81c1-487d-9c62-1335495f4561.gif) _Above: Sorting in Observability, via `EuiDataGrid`'s sort popover_ ![security-solution-sorting](https://user-images.githubusercontent.com/4459398/128050301-0ea9ccbc-7896-46ef-96da-17b5b6d2e34b.gif) _Above: Sorting and hiding columns in the Security Solution via `EuiDataGrid`'s column header actions_ ## Details * Sorting is disabled for non-aggregatble fields * This PR resolves the `Sort [Object Object]` TODO described [here](elastic#106199 (comment)) * ~This PR restores the column header tooltips where the TGrid is used in the Security Solution~ ## Desk testing To desk test this PR, you must enable feature flags in the Observability and Security Solution: - To desk test the `Observability > Alerts` page, add the following settings to `config/kibana.dev.yml`: ``` xpack.observability.unsafe.cases.enabled: true xpack.observability.unsafe.alertingExperience.enabled: true xpack.ruleRegistry.write.enabled: true ``` - To desk test the TGrid in the following Security Solution, edit `x-pack/plugins/security_solution/common/experimental_features.ts` and in the `allowedExperimentalValues` section set: ```typescript tGridEnabled: true, ``` cc @mdefazio
- Loading branch information
1 parent
2db04e6
commit f99f33d
Showing
14 changed files
with
920 additions
and
314 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
116 changes: 0 additions & 116 deletions
116
x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.ts
This file was deleted.
Oops, something went wrong.
258 changes: 258 additions & 0 deletions
258
x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License | ||
* 2.0; you may not use this file except in compliance with the Elastic License | ||
* 2.0. | ||
*/ | ||
import { mount } from 'enzyme'; | ||
import { omit, set } from 'lodash/fp'; | ||
import React from 'react'; | ||
|
||
import { defaultHeaders } from './default_headers'; | ||
import { getActionsColumnWidth, getColumnWidthFromType, getColumnHeaders } from './helpers'; | ||
import { | ||
DEFAULT_COLUMN_MIN_WIDTH, | ||
DEFAULT_DATE_COLUMN_MIN_WIDTH, | ||
DEFAULT_ACTIONS_COLUMN_WIDTH, | ||
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, | ||
SHOW_CHECK_BOXES_COLUMN_WIDTH, | ||
} from '../constants'; | ||
import { mockBrowserFields } from '../../../../mock/browser_fields'; | ||
|
||
window.matchMedia = jest.fn().mockImplementation((query) => { | ||
return { | ||
matches: false, | ||
media: query, | ||
onchange: null, | ||
addListener: jest.fn(), | ||
removeListener: jest.fn(), | ||
}; | ||
}); | ||
|
||
describe('helpers', () => { | ||
describe('getColumnWidthFromType', () => { | ||
test('it returns the expected width for a non-date column', () => { | ||
expect(getColumnWidthFromType('keyword')).toEqual(DEFAULT_COLUMN_MIN_WIDTH); | ||
}); | ||
|
||
test('it returns the expected width for a date column', () => { | ||
expect(getColumnWidthFromType('date')).toEqual(DEFAULT_DATE_COLUMN_MIN_WIDTH); | ||
}); | ||
}); | ||
|
||
describe('getActionsColumnWidth', () => { | ||
test('returns the default actions column width when isEventViewer is false', () => { | ||
expect(getActionsColumnWidth(false)).toEqual(DEFAULT_ACTIONS_COLUMN_WIDTH); | ||
}); | ||
|
||
test('returns the default actions column width + checkbox width when isEventViewer is false and showCheckboxes is true', () => { | ||
expect(getActionsColumnWidth(false, true)).toEqual( | ||
DEFAULT_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH | ||
); | ||
}); | ||
|
||
test('returns the events viewer actions column width when isEventViewer is true', () => { | ||
expect(getActionsColumnWidth(true)).toEqual(EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH); | ||
}); | ||
|
||
test('returns the events viewer actions column width + checkbox width when isEventViewer is true and showCheckboxes is true', () => { | ||
expect(getActionsColumnWidth(true, true)).toEqual( | ||
EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH + SHOW_CHECK_BOXES_COLUMN_WIDTH | ||
); | ||
}); | ||
}); | ||
|
||
describe('getColumnHeaders', () => { | ||
// additional properties used by `EuiDataGrid`: | ||
const actions = { | ||
showSortAsc: { | ||
label: 'Sort A-Z', | ||
}, | ||
showSortDesc: { | ||
label: 'Sort Z-A', | ||
}, | ||
}; | ||
const defaultSortDirection = 'desc'; | ||
const isSortable = true; | ||
|
||
const mockHeader = defaultHeaders.filter((h) => | ||
['@timestamp', 'source.ip', 'destination.ip'].includes(h.id) | ||
); | ||
|
||
describe('display', () => { | ||
const renderedByDisplay = 'I am rendered via a React component: header.display'; | ||
const renderedByDisplayAsText = 'I am rendered by header.displayAsText'; | ||
|
||
test('it renders via `display` when the header has JUST a `display` property (`displayAsText` is undefined)', () => { | ||
const headerWithJustDisplay = mockHeader.map((x) => | ||
x.id === '@timestamp' | ||
? { | ||
...x, | ||
display: <span>{renderedByDisplay}</span>, | ||
} | ||
: x | ||
); | ||
|
||
const wrapper = mount( | ||
<>{getColumnHeaders(headerWithJustDisplay, mockBrowserFields)[0].display}</> | ||
); | ||
|
||
expect(wrapper.text()).toEqual(renderedByDisplay); | ||
}); | ||
|
||
test('it (also) renders via `display` when the header has BOTH a `display` property AND a `displayAsText`', () => { | ||
const headerWithBoth = mockHeader.map((x) => | ||
x.id === '@timestamp' | ||
? { | ||
...x, | ||
display: <span>{renderedByDisplay}</span>, // this has a higher priority... | ||
displayAsText: renderedByDisplayAsText, // ...so this text won't be rendered | ||
} | ||
: x | ||
); | ||
|
||
const wrapper = mount( | ||
<>{getColumnHeaders(headerWithBoth, mockBrowserFields)[0].display}</> | ||
); | ||
|
||
expect(wrapper.text()).toEqual(renderedByDisplay); | ||
}); | ||
|
||
test('it renders via `displayAsText` when the header does NOT have a `display`, BUT it has `displayAsText`', () => { | ||
const headerWithJustDisplayAsText = mockHeader.map((x) => | ||
x.id === '@timestamp' | ||
? { | ||
...x, | ||
displayAsText: renderedByDisplayAsText, // fallback to rendering via displayAsText | ||
} | ||
: x | ||
); | ||
|
||
const wrapper = mount( | ||
<>{getColumnHeaders(headerWithJustDisplayAsText, mockBrowserFields)[0].display}</> | ||
); | ||
|
||
expect(wrapper.text()).toEqual(renderedByDisplayAsText); | ||
}); | ||
|
||
test('it renders `header.id` when the header does NOT have a `display`, AND it does NOT have a `displayAsText`', () => { | ||
const wrapper = mount(<>{getColumnHeaders(mockHeader, mockBrowserFields)[0].display}</>); | ||
|
||
expect(wrapper.text()).toEqual('@timestamp'); // fallback to rendering by header.id | ||
}); | ||
}); | ||
|
||
test('it renders the default actions when the header does NOT have custom actions', () => { | ||
expect(getColumnHeaders(mockHeader, mockBrowserFields)[0].actions).toEqual(actions); | ||
}); | ||
|
||
test('it renders custom actions when `actions` is defined in the header', () => { | ||
const customActions = { | ||
showSortAsc: { | ||
label: 'A custom sort ascending', | ||
}, | ||
showSortDesc: { | ||
label: 'A custom sort descending', | ||
}, | ||
}; | ||
|
||
const headerWithCustomActions = mockHeader.map((x) => | ||
x.id === '@timestamp' | ||
? { | ||
...x, | ||
actions: customActions, | ||
} | ||
: x | ||
); | ||
|
||
expect(getColumnHeaders(headerWithCustomActions, mockBrowserFields)[0].actions).toEqual( | ||
customActions | ||
); | ||
}); | ||
|
||
describe('isSortable', () => { | ||
test("it is sortable, because `@timestamp`'s `aggregatable` BrowserFields property is `true`", () => { | ||
expect(getColumnHeaders(mockHeader, mockBrowserFields)[0].isSortable).toEqual(true); | ||
}); | ||
|
||
test("it is NOT sortable, when `@timestamp`'s `aggregatable` BrowserFields property is `false`", () => { | ||
const withAggregatableOverride = set( | ||
'[email protected]', | ||
false, // override `aggregatable` for `@timestamp`, a date field that is normally aggregatable | ||
mockBrowserFields | ||
); | ||
|
||
expect(getColumnHeaders(mockHeader, withAggregatableOverride)[0].isSortable).toEqual(false); | ||
}); | ||
|
||
test('it is NOT sortable when BrowserFields does not have metadata for the field', () => { | ||
const noBrowserFieldEntry = omit('base', mockBrowserFields); // omit the 'base` category, which contains `@timestamp` | ||
|
||
expect(getColumnHeaders(mockHeader, noBrowserFieldEntry)[0].isSortable).toEqual(false); | ||
}); | ||
}); | ||
|
||
test('should return a full object of ColumnHeader from the default header', () => { | ||
const expectedData = [ | ||
{ | ||
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', | ||
searchable: true, | ||
type: 'date', | ||
initialWidth: 190, | ||
}, | ||
{ | ||
actions, | ||
aggregatable: true, | ||
category: 'source', | ||
columnHeaderType: 'not-filtered', | ||
defaultSortDirection, | ||
description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', | ||
example: '', | ||
format: '', | ||
id: 'source.ip', | ||
indexes: ['auditbeat', 'filebeat', 'packetbeat'], | ||
isSortable, | ||
name: 'source.ip', | ||
searchable: true, | ||
type: 'ip', | ||
initialWidth: 180, | ||
}, | ||
{ | ||
actions, | ||
aggregatable: true, | ||
category: 'destination', | ||
columnHeaderType: 'not-filtered', | ||
defaultSortDirection, | ||
description: | ||
'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', | ||
example: '', | ||
format: '', | ||
id: 'destination.ip', | ||
indexes: ['auditbeat', 'filebeat', 'packetbeat'], | ||
isSortable, | ||
name: 'destination.ip', | ||
searchable: true, | ||
type: 'ip', | ||
initialWidth: 180, | ||
}, | ||
]; | ||
|
||
// NOTE: the omitted `display` (`React.ReactNode`) property is tested separately above | ||
expect(getColumnHeaders(mockHeader, mockBrowserFields).map(omit('display'))).toEqual( | ||
expectedData | ||
); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.