mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* 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:
parent
400d661b92
commit
b80e0eceb2
7 changed files with 168 additions and 38 deletions
|
@ -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": "",
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
}}
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 = '';
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue