Skip to content

Commit

Permalink
[Security Solution] Fixes sorting and tooltips on columns for non-ECS…
Browse files Browse the repository at this point in the history
… fields that are only one level deep (elastic#132570)

## [Security Solution] Fixes sorting and tooltips on columns for non-ECS fields that are only one level deep

This PR fixes <elastic#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

![test_field_1_actual_tooltip](https://user-images.githubusercontent.com/4459398/169208299-51d9296a-15e1-4eb0-bc31-a0df6a63f0c5.png)

_Before: The column is **not** sortable, and the tooltip displays incomplete metadata_

### After

![after](https://user-images.githubusercontent.com/4459398/169414767-7274a795-015f-4805-8c3f-b233ead994ea.png)

_After: The column is sortable, and the tooltip displays the expected metadata_

### Desk testing

See the _Steps to reproduce_ section of <elastic#132490> for testing details.
  • Loading branch information
andrew-goldstein authored and emilioalvap committed May 23, 2022
1 parent 62be263 commit e80cf84
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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 = [
Expand Down Expand Up @@ -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,
},
]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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[],
Expand All @@ -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),
};
})
: [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down

0 comments on commit e80cf84

Please sign in to comment.