mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Security Solution] Fixes sorting and tooltips on columns for non-ECS fields that are only one level deep (#132570)
## [Security Solution] Fixes sorting and tooltips on columns for non-ECS fields that are only one level deep This PR fixes <https://github.com/elastic/kibana/issues/132490>, an issue where Timeline columns for non-ECS fields that are only one level deep couldn't be sorted, and displayed incomplete metadata in the column's tooltip. ### Before  _Before: The column is **not** sortable, and the tooltip displays incomplete metadata_ ### After  _After: The column is sortable, and the tooltip displays the expected metadata_ ### Desk testing See the _Steps to reproduce_ section of <https://github.com/elastic/kibana/issues/132490> for testing details.
This commit is contained in:
parent
51ae0208dc
commit
788dd2e718
3 changed files with 251 additions and 10 deletions
|
@ -6,11 +6,12 @@
|
|||
*/
|
||||
|
||||
import { mockBrowserFields } from '../../../../../common/containers/source/mock';
|
||||
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import { getColumnWidthFromType, getColumnHeaders } from './helpers';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import '../../../../../common/mock/match_media';
|
||||
import { BrowserFields } from '../../../../../../common/search_strategy';
|
||||
import { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
import { defaultHeaders } from './default_headers';
|
||||
import { getColumnWidthFromType, getColumnHeaders, getRootCategory } from './helpers';
|
||||
|
||||
describe('helpers', () => {
|
||||
describe('getColumnWidthFromType', () => {
|
||||
|
@ -23,6 +24,32 @@ describe('helpers', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getRootCategory', () => {
|
||||
const baseFields = ['@timestamp', '_id', 'message'];
|
||||
|
||||
baseFields.forEach((field) => {
|
||||
test(`it returns the 'base' category for the ${field} field`, () => {
|
||||
expect(
|
||||
getRootCategory({
|
||||
field,
|
||||
browserFields: mockBrowserFields,
|
||||
})
|
||||
).toEqual('base');
|
||||
});
|
||||
});
|
||||
|
||||
test(`it echos the field name for a field that's NOT in the base category`, () => {
|
||||
const field = 'test_field_1';
|
||||
|
||||
expect(
|
||||
getRootCategory({
|
||||
field,
|
||||
browserFields: mockBrowserFields,
|
||||
})
|
||||
).toEqual(field);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getColumnHeaders', () => {
|
||||
test('should return a full object of ColumnHeader from the default header', () => {
|
||||
const expectedData = [
|
||||
|
@ -80,5 +107,202 @@ describe('helpers', () => {
|
|||
);
|
||||
expect(getColumnHeaders(mockHeader, mockBrowserFields)).toEqual(expectedData);
|
||||
});
|
||||
|
||||
test('it should return the expected metadata for the `_id` field, which is one level deep, and belongs to the `base` category', () => {
|
||||
const headers: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: '_id',
|
||||
initialWidth: 180,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([
|
||||
{
|
||||
aggregatable: false,
|
||||
category: 'base',
|
||||
columnHeaderType: 'not-filtered',
|
||||
description: 'Each document has an _id that uniquely identifies it',
|
||||
esTypes: [],
|
||||
example: 'Y-6TfmcB0WOhS6qyMv3s',
|
||||
id: '_id',
|
||||
indexes: ['auditbeat', 'filebeat', 'packetbeat'],
|
||||
initialWidth: 180,
|
||||
name: '_id',
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should return the expected metadata for a field one level deep that does NOT belong to the `base` category', () => {
|
||||
const headers: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'test_field_1', // one level deep, but does NOT belong to the `base` category
|
||||
initialWidth: 180,
|
||||
},
|
||||
];
|
||||
|
||||
const oneLevelDeep: BrowserFields = {
|
||||
test_field_1: {
|
||||
fields: {
|
||||
test_field_1: {
|
||||
aggregatable: true,
|
||||
category: 'test_field_1',
|
||||
esTypes: ['keyword'],
|
||||
format: 'string',
|
||||
indexes: [
|
||||
'-*elastic-cloud-logs-*',
|
||||
'.alerts-security.alerts-default',
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'traces-apm*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
name: 'test_field_1',
|
||||
readFromDocValues: true,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getColumnHeaders(headers, oneLevelDeep)).toEqual([
|
||||
{
|
||||
aggregatable: true,
|
||||
category: 'test_field_1',
|
||||
columnHeaderType: 'not-filtered',
|
||||
esTypes: ['keyword'],
|
||||
format: 'string',
|
||||
id: 'test_field_1',
|
||||
indexes: [
|
||||
'-*elastic-cloud-logs-*',
|
||||
'.alerts-security.alerts-default',
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'traces-apm*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
initialWidth: 180,
|
||||
name: 'test_field_1',
|
||||
readFromDocValues: true,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should return the expected metadata for a field that is more than one level deep', () => {
|
||||
const headers: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'foo.bar', // two levels deep
|
||||
initialWidth: 180,
|
||||
},
|
||||
];
|
||||
|
||||
const twoLevelsDeep: BrowserFields = {
|
||||
foo: {
|
||||
fields: {
|
||||
'foo.bar': {
|
||||
aggregatable: true,
|
||||
category: 'foo',
|
||||
esTypes: ['keyword'],
|
||||
format: 'string',
|
||||
indexes: [
|
||||
'-*elastic-cloud-logs-*',
|
||||
'.alerts-security.alerts-default',
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'traces-apm*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
name: 'foo.bar',
|
||||
readFromDocValues: true,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
expect(getColumnHeaders(headers, twoLevelsDeep)).toEqual([
|
||||
{
|
||||
aggregatable: true,
|
||||
category: 'foo',
|
||||
columnHeaderType: 'not-filtered',
|
||||
esTypes: ['keyword'],
|
||||
format: 'string',
|
||||
id: 'foo.bar',
|
||||
indexes: [
|
||||
'-*elastic-cloud-logs-*',
|
||||
'.alerts-security.alerts-default',
|
||||
'apm-*-transaction*',
|
||||
'auditbeat-*',
|
||||
'endgame-*',
|
||||
'filebeat-*',
|
||||
'logs-*',
|
||||
'packetbeat-*',
|
||||
'traces-apm*',
|
||||
'winlogbeat-*',
|
||||
],
|
||||
initialWidth: 180,
|
||||
name: 'foo.bar',
|
||||
readFromDocValues: true,
|
||||
searchable: true,
|
||||
type: 'string',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should return the expected metadata for an UNKNOWN field one level deep', () => {
|
||||
const headers: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'unknown', // one level deep, but not contained in the `BrowserFields`
|
||||
initialWidth: 180,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'unknown',
|
||||
initialWidth: 180,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('it should return the expected metadata for an UNKNOWN field that is more than one level deep', () => {
|
||||
const headers: ColumnHeaderOptions[] = [
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'unknown.more.than.one.level', // more than one level deep, and not contained in the `BrowserFields`
|
||||
initialWidth: 180,
|
||||
},
|
||||
];
|
||||
|
||||
expect(getColumnHeaders(headers, mockBrowserFields)).toEqual([
|
||||
{
|
||||
columnHeaderType: 'not-filtered',
|
||||
id: 'unknown.more.than.one.level',
|
||||
initialWidth: 180,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,12 +5,28 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { get } from 'lodash/fp';
|
||||
import { has, get } from 'lodash/fp';
|
||||
import { ColumnHeaderOptions } from '../../../../../../common/types';
|
||||
|
||||
import { BrowserFields } from '../../../../../common/containers/source';
|
||||
import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../constants';
|
||||
|
||||
/**
|
||||
* Returns the root category for fields that are only one level, e.g. `_id` or `test_field_1`
|
||||
*
|
||||
* The `base` category will be returned for fields that are members of `base`,
|
||||
* e.g. the `@timestamp`, `_id`, and `message` fields.
|
||||
*
|
||||
* The field name will be echoed-back for all other fields, e.g. `test_field_1`
|
||||
*/
|
||||
export const getRootCategory = ({
|
||||
browserFields,
|
||||
field,
|
||||
}: {
|
||||
browserFields: BrowserFields;
|
||||
field: string;
|
||||
}): string => (has(`base.fields.${field}`, browserFields) ? 'base' : field);
|
||||
|
||||
/** Enriches the column headers with field details from the specified browserFields */
|
||||
export const getColumnHeaders = (
|
||||
headers: ColumnHeaderOptions[],
|
||||
|
@ -19,13 +35,14 @@ export const getColumnHeaders = (
|
|||
return headers
|
||||
? headers.map((header) => {
|
||||
const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name]
|
||||
const category =
|
||||
splitHeader.length > 1
|
||||
? splitHeader[0]
|
||||
: getRootCategory({ field: header.id, browserFields });
|
||||
|
||||
return {
|
||||
...header,
|
||||
...get(
|
||||
[splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id],
|
||||
browserFields
|
||||
),
|
||||
...get([category, 'fields', header.id], browserFields),
|
||||
};
|
||||
})
|
||||
: [];
|
||||
|
|
|
@ -138,7 +138,7 @@ describe('helpers', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('it defaults to a `columnType` of empty string when a column does NOT has a corresponding entry in `columnHeaders`', () => {
|
||||
test('it defaults to a `columnType` of empty string when a column does NOT have a corresponding entry in `columnHeaders`', () => {
|
||||
const withUnknownColumn: Array<{
|
||||
id: string;
|
||||
direction: 'asc' | 'desc';
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue