[SIEM] Fix duplicate in the droppable filter in the timeline (#37537) (#37593)

* Fix duplicate in the droppable filter in the timeline

* able to query empty string and null value in the timeline
This commit is contained in:
Xavier Mouligneau 2019-05-30 23:24:10 -04:00 committed by GitHub
parent 400d661b92
commit b80e0eceb2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 168 additions and 38 deletions

View file

@ -7,13 +7,14 @@ exports[`empty_column_renderer renders correctly against snapshot 1`] = `
Object {
"and": Array [],
"enabled": true,
"excluded": false,
"excluded": true,
"id": "id-timeline-column-source_ip-for-event-1-source_ip",
"kqlQuery": "",
"name": "source.ip: ",
"queryMatch": Object {
"displayValue": "--",
"field": "source.ip",
"operator": ":",
"operator": ":*",
"value": "",
},
}

View file

@ -11,7 +11,7 @@ import { ColumnRenderer } from './column_renderer';
import { DraggableWrapper, DragEffects } from '../../../drag_and_drop/draggable_wrapper';
import { escapeDataProviderId } from '../../../drag_and_drop/helpers';
import { parseQueryValue } from './parse_query_value';
import { IS_OPERATOR } from '../../data_providers/data_provider';
import { EXISTS_OPERATOR } from '../../data_providers/data_provider';
import { Provider } from '../../data_providers/provider';
import { TimelineNonEcsData } from '../../../../graphql/types';
import { getEmptyValue } from '../../../empty_value';
@ -44,9 +44,10 @@ export const emptyColumnRenderer: ColumnRenderer = {
queryMatch: {
field: field.id,
value: parseQueryValue(null),
operator: IS_OPERATOR,
displayValue: getEmptyValue(),
operator: EXISTS_OPERATOR,
},
excluded: false,
excluded: true,
kqlQuery: '',
and: [],
}}

View file

@ -15,6 +15,14 @@ export const EXISTS_OPERATOR = ':*';
/** The operator applied to a field */
export type QueryOperator = ':' | ':*';
export interface QueryMatch {
field: string;
displayField?: string;
value: string | number;
displayValue?: string | number;
operator: QueryOperator;
}
export interface DataProvider {
/** Uniquely identifies a data provider */
id: string;
@ -37,13 +45,7 @@ export interface DataProvider {
/**
* Returns a query properties that, when executed, returns the data for this provider
*/
queryMatch: {
field: string;
displayField?: string;
value: string | number;
displayValue?: string | number;
operator: QueryOperator;
};
queryMatch: QueryMatch;
/**
* Additional query clauses that are ANDed with this query to narrow results
*/

View file

@ -6,10 +6,12 @@
import { EuiBadge } from '@elastic/eui';
import classNames from 'classnames';
import { isString } from 'lodash/fp';
import React from 'react';
import { pure } from 'recompose';
import styled from 'styled-components';
import { getEmptyString } from '../../empty_value';
import { EXISTS_OPERATOR, QueryOperator } from './data_provider';
import * as i18n from './translations';
@ -62,9 +64,9 @@ export const ProviderBadge = pure<ProviderBadgeProps>(
'globalFilterItem-isDisabled': !isEnabled,
'globalFilterItem-isExcluded': isExcluded,
});
const formattedValue = isString(val) && val === '' ? getEmptyString() : val;
const prefix = isExcluded ? <span>{i18n.NOT} </span> : null;
const title = `${field}: "${val}"`;
const title = `${field}: "${formattedValue}"`;
return (
<ProviderBadgeStyled
@ -77,7 +79,7 @@ export const ProviderBadge = pure<ProviderBadgeProps>(
iconType="cross"
iconSide="right"
onClick={togglePopover}
onClickAriaLabel={`${i18n.SHOW_OPTIONS_DATA_PROVIDER} ${val}`}
onClickAriaLabel={`${i18n.SHOW_OPTIONS_DATA_PROVIDER} ${formattedValue}`}
closeButtonProps={{
// Removing tab focus on close button because the same option can be obtained through the context menu
// TODO: add a `DEL` keyboard press functionality
@ -89,7 +91,7 @@ export const ProviderBadge = pure<ProviderBadgeProps>(
{operator !== EXISTS_OPERATOR ? (
<>
<span className="field-value">{`${field}: `}</span>
<span className="field-value">{`"${val}"`}</span>
<span className="field-value">{`"${formattedValue}"`}</span>
</>
) : (
<span className="field-value">

View file

@ -5,7 +5,7 @@
*/
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { isString, flow } from 'lodash/fp';
import { isEmpty, isString, flow } from 'lodash/fp';
import { StaticIndexPattern } from 'ui/index_patterns';
import { KueryFilterQuery } from '../../store';
@ -25,6 +25,9 @@ export const convertKueryToElasticSearchQuery = (
export const escapeQueryValue = (val: number | string = ''): string | number => {
if (isString(val)) {
if (isEmpty(val)) {
return '""';
}
return escapeKuery(val);
}

View file

@ -3,7 +3,7 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { getOr, omit, uniq } from 'lodash/fp';
import { getOr, omit, uniq, isEmpty, isEqualWith } from 'lodash/fp';
import { ColumnHeader } from '../../components/timeline/body/column_headers/column_header';
import { getColumnWidthFromType } from '../../components/timeline/body/helpers';
@ -11,6 +11,7 @@ import { Sort } from '../../components/timeline/body/sort';
import {
DataProvider,
QueryOperator,
QueryMatch,
} from '../../components/timeline/data_providers/data_provider';
import { KueryFilterQuery, SerializedFilterQuery } from '../model';
@ -207,6 +208,13 @@ export const applyDeltaToCurrentWidth = ({
};
};
const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => {
if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) {
return true;
}
return false;
};
const addAndToProviderInTimeline = (
id: string,
provider: DataProvider,
@ -220,6 +228,16 @@ const addAndToProviderInTimeline = (
const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id);
const { and, ...andProvider } = provider;
if (
isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) ||
(alreadyExistsAndProviderIndex === -1 &&
newProvider.and.filter(itemAndProvider =>
isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch)
).length > 0)
) {
return timelineById;
}
const dataProviders = [
...timeline.dataProviders.slice(0, alreadyExistsProviderIndex),
{
@ -252,8 +270,15 @@ const addProviderToTimeline = (
timelineById: TimelineById
): TimelineById => {
const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id);
if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) {
provider.id = `${provider.id}-${
timeline.dataProviders.filter(p => p.id === provider.id).length
}`;
}
const dataProviders =
alreadyExistsAtIndex > -1
alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)
? [
...timeline.dataProviders.slice(0, alreadyExistsAtIndex),
provider,

View file

@ -387,7 +387,7 @@ describe('Timeline', () => {
expect(update).toEqual(set('foo.dataProviders', addedDataProvider, timelineByIdMock));
});
test('should NOT add a new timeline provider if it already exists', () => {
test('should NOT add a new timeline provider if it already exists and the attributes "and" is empty', () => {
const providerToAdd: DataProvider = {
and: [],
id: '123',
@ -410,6 +410,44 @@ describe('Timeline', () => {
expect(update).toEqual(timelineByIdMock);
});
test('should add a new timeline provider if it already exists and the attributes "and" is NOT empty', () => {
const myMockTimelineByIdMock = cloneDeep(timelineByIdMock);
myMockTimelineByIdMock.foo.dataProviders[0].and = [
{
id: '456',
name: 'and data provider 1',
enabled: true,
excluded: false,
kqlQuery: '',
queryMatch: {
field: '',
value: '',
operator: IS_OPERATOR,
},
},
];
const providerToAdd: DataProvider = {
and: [],
id: '123',
name: 'data provider 1',
enabled: true,
queryMatch: {
field: '',
value: '',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
provider: providerToAdd,
timelineById: myMockTimelineByIdMock,
});
expect(update).toEqual(set('foo.dataProviders[1]', providerToAdd, myMockTimelineByIdMock));
});
test('should UPSERT an existing timeline provider if it already exists', () => {
const providerToAdd: DataProvider = {
and: [],
@ -621,8 +659,8 @@ describe('Timeline', () => {
name: 'data provider 2',
enabled: true,
queryMatch: {
field: '',
value: '',
field: 'handsome',
value: 'garrett',
operator: IS_OPERATOR,
},
excluded: false,
@ -643,8 +681,8 @@ describe('Timeline', () => {
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
field: 'smart',
value: 'frank',
operator: IS_OPERATOR,
},
@ -664,7 +702,7 @@ describe('Timeline', () => {
newTimeline.foo.highlightedDropAndProviderId = '';
});
test('should NOT add a new timeline and provider if it already exists', () => {
test('should add another and provider because it is not a duplicate', () => {
const providerToAdd: DataProvider = {
and: [
{
@ -672,11 +710,10 @@ describe('Timeline', () => {
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
field: 'smart',
value: 'garrett',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
},
@ -685,11 +722,10 @@ describe('Timeline', () => {
name: 'data provider 1',
enabled: true,
queryMatch: {
field: '',
value: '',
field: 'handsome',
value: 'frank',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
};
@ -704,26 +740,86 @@ describe('Timeline', () => {
const andProviderToAdd: DataProvider = {
and: [],
id: '568',
id: '569',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: '',
value: '',
field: 'happy',
value: 'andrewG',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
};
// temporary, we will have to decouple DataProvider & DataProvidersAnd
// that's bigger a refactor than just fixing a bug
delete andProviderToAdd.and;
const update = addTimelineProvider({
id: 'foo',
provider: andProviderToAdd,
timelineById: newTimeline,
});
const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567');
expect(update.foo.dataProviders[indexProvider].and.length).toEqual(1);
expect(update).toEqual(set('foo.dataProviders[1].and[1]', andProviderToAdd, newTimeline));
newTimeline.foo.highlightedDropAndProviderId = '';
});
test('should NOT add another and provider because it is a duplicate', () => {
const providerToAdd: DataProvider = {
and: [
{
id: '568',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: 'smart',
value: 'garrett',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
},
],
id: '567',
name: 'data provider 1',
enabled: true,
queryMatch: {
field: 'handsome',
value: 'frank',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
};
const newTimeline = addTimelineProvider({
id: 'foo',
provider: providerToAdd,
timelineById: timelineByIdMock,
});
newTimeline.foo.highlightedDropAndProviderId = '567';
const andProviderToAdd: DataProvider = {
and: [],
id: '569',
name: 'And Data Provider',
enabled: true,
queryMatch: {
field: 'smart',
value: 'garrett',
operator: IS_OPERATOR,
},
excluded: false,
kqlQuery: '',
};
const update = addTimelineProvider({
id: 'foo',
provider: andProviderToAdd,
timelineById: newTimeline,
});
expect(update).toEqual(newTimeline);
newTimeline.foo.highlightedDropAndProviderId = '';
});
});