Support nested fields in existing filter types (#49537)

* Add automatic support for nested fields in existing filter types

* Index pattern could be undefined

* add test for handleNestedFilter function

* remove console.log

* add tests for all "getFilterField" functions

* update migrateFilters to work on full filter objects so that it doesn't
have to worry about queries that have been wrapped with `nested`

* add test to ensure fromFilters auto wraps filters on nested fields

* Add smoke test for nested filter and move filter editor tests into
their own suite for easier running and debugging

* fix bad type change

* dedupe filterToQuery logic

* fix helper that wasn't doing what it said it did

* Convert test from pre-merge to jest

* Use new time range style
This commit is contained in:
Matt Bargar 2019-12-09 14:16:15 -05:00 committed by GitHub
parent c7046a080f
commit 77dca06253
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 984 additions and 59 deletions

View file

@ -0,0 +1,322 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const indexPatternResponse = {
id: 'logstash-*',
title: 'logstash-*',
fields: [
{
name: 'bytes',
type: 'number',
esTypes: ['long'],
count: 10,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'ssl',
type: 'boolean',
esTypes: ['boolean'],
count: 20,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: '@timestamp',
type: 'date',
esTypes: ['date'],
count: 30,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'time',
type: 'date',
esTypes: ['date'],
count: 30,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: '@tags',
type: 'string',
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'utc_time',
type: 'date',
esTypes: ['date'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'phpmemory',
type: 'number',
esTypes: ['integer'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'ip',
type: 'ip',
esTypes: ['ip'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'request_body',
type: 'attachment',
esTypes: ['attachment'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'point',
type: 'geo_point',
esTypes: ['geo_point'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'area',
type: 'geo_shape',
esTypes: ['geo_shape'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'hashed',
type: 'murmur3',
esTypes: ['murmur3'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
},
{
name: 'geo.coordinates',
type: 'geo_point',
esTypes: ['geo_point'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'extension',
type: 'string',
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'machine.os',
type: 'string',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'machine.os.raw',
type: 'string',
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
subType: { multi: { parent: 'machine.os' } },
},
{
name: 'geo.src',
type: 'string',
esTypes: ['keyword'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: '_id',
type: 'string',
esTypes: ['_id'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: '_type',
type: 'string',
esTypes: ['_type'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: '_source',
type: '_source',
esTypes: ['_source'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'non-filterable',
type: 'string',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: false,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'non-sortable',
type: 'string',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: false,
aggregatable: false,
readFromDocValues: false,
},
{
name: 'custom_user_field',
type: 'conflict',
esTypes: ['long', 'text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: true,
readFromDocValues: true,
},
{
name: 'script string',
type: 'string',
count: 0,
scripted: true,
script: "'i am a string'",
lang: 'expression',
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'script number',
type: 'number',
count: 0,
scripted: true,
script: '1234',
lang: 'expression',
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'script date',
type: 'date',
count: 0,
scripted: true,
script: '1234',
lang: 'painless',
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'script murmur3',
type: 'murmur3',
count: 0,
scripted: true,
script: '1234',
lang: 'expression',
searchable: true,
aggregatable: true,
readFromDocValues: false,
},
{
name: 'nestedField.child',
type: 'string',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
subType: { nested: { path: 'nestedField' } },
},
{
name: 'nestedField.nestedChild.doublyNestedChild',
type: 'string',
esTypes: ['text'],
count: 0,
scripted: false,
searchable: true,
aggregatable: false,
readFromDocValues: false,
subType: { nested: { path: 'nestedField.nestedChild' } },
},
],
};

View file

@ -144,5 +144,30 @@ describe('build query', () => {
expect(result.filter).toEqual(expectedESQueries);
});
test('should wrap filters targeting nested fields in a nested query', () => {
const filters = [
{
exists: { field: 'nestedField.child' },
meta: { type: 'exists', alias: '', disabled: false, negate: false },
},
];
const expectedESQueries = [
{
nested: {
path: 'nestedField',
query: {
exists: {
field: 'nestedField.child',
},
},
},
},
];
const result = buildQueryFromFilters(filters, indexPattern);
expect(result.filter).toEqual(expectedESQueries);
});
});
});

View file

@ -21,6 +21,7 @@ import { migrateFilter } from './migrate_filter';
import { filterMatchesIndex } from './filter_matches_index';
import { Filter, cleanFilter, isFilterDisabled } from '../filters';
import { IIndexPattern } from '../../index_patterns';
import { handleNestedFilter } from './handle_nested_filter';
/**
* Create a filter that can be reversed for filters with negate set
@ -59,20 +60,22 @@ export const buildQueryFromFilters = (
) => {
filters = filters.filter(filter => filter && !isFilterDisabled(filter));
const filtersToESQueries = (negate: boolean) => {
return filters
.filter(filterNegate(negate))
.filter(filter => !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern))
.map(filter => {
return migrateFilter(filter, indexPattern);
})
.map(filter => handleNestedFilter(filter, indexPattern))
.map(translateToQuery)
.map(cleanFilter);
};
return {
must: [],
filter: filters
.filter(filterNegate(false))
.filter(filter => !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern))
.map(translateToQuery)
.map(cleanFilter)
.map(filter => migrateFilter(filter, indexPattern)),
filter: filtersToESQueries(false),
should: [],
must_not: filters
.filter(filterNegate(true))
.filter(filter => !ignoreFilterIfFieldNotInIndex || filterMatchesIndex(filter, indexPattern))
.map(translateToQuery)
.map(cleanFilter)
.map(filter => migrateFilter(filter, indexPattern)),
must_not: filtersToESQueries(true),
};
};

View file

@ -0,0 +1,91 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { handleNestedFilter } from './handle_nested_filter';
import { fields } from '../../index_patterns/mocks';
import { buildPhraseFilter, buildQueryFilter } from '../filters';
import { IFieldType, IIndexPattern } from '../../index_patterns';
describe('handleNestedFilter', function() {
const indexPattern: IIndexPattern = ({
id: 'logstash-*',
fields,
} as unknown) as IIndexPattern;
it("should return the filter's query wrapped in nested query if the target field is nested", () => {
const field = getField('nestedField.child');
const filter = buildPhraseFilter(field!, 'foo', indexPattern);
const result = handleNestedFilter(filter, indexPattern);
expect(result).toEqual({
meta: {
index: 'logstash-*',
},
nested: {
path: 'nestedField',
query: {
match_phrase: {
'nestedField.child': 'foo',
},
},
},
});
});
it('should return filter untouched if it does not target a nested field', () => {
const field = getField('extension');
const filter = buildPhraseFilter(field!, 'jpg', indexPattern);
const result = handleNestedFilter(filter, indexPattern);
expect(result).toBe(filter);
});
it('should return filter untouched if it does not target a field from the given index pattern', () => {
const field = { ...getField('extension'), name: 'notarealfield' };
const filter = buildPhraseFilter(field as IFieldType, 'jpg', indexPattern);
const result = handleNestedFilter(filter, indexPattern);
expect(result).toBe(filter);
});
it('should return filter untouched if no index pattern is provided', () => {
const field = getField('extension');
const filter = buildPhraseFilter(field!, 'jpg', indexPattern);
const result = handleNestedFilter(filter);
expect(result).toBe(filter);
});
it('should return the filter untouched if a target field cannot be determined', () => {
// for example, we don't support query_string queries
const filter = buildQueryFilter(
{
query: {
query_string: {
query: 'response:200',
},
},
},
'logstash-*',
'foo'
);
const result = handleNestedFilter(filter);
expect(result).toBe(filter);
});
function getField(name: string) {
return indexPattern.fields.find(field => field.name === name);
}
});

View file

@ -0,0 +1,45 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getFilterField, cleanFilter, Filter } from '../filters';
import { IIndexPattern } from '../../index_patterns';
export const handleNestedFilter = (filter: Filter, indexPattern?: IIndexPattern) => {
if (!indexPattern) return filter;
const fieldName = getFilterField(filter);
if (!fieldName) {
return filter;
}
const field = indexPattern.fields.find(indexPatternField => indexPatternField.name === fieldName);
if (!field || !field.subType || !field.subType.nested || !field.subType.nested.path) {
return filter;
}
const query = cleanFilter(filter);
return {
meta: filter.meta,
nested: {
path: field.subType.nested.path,
query: query.query || query,
},
};
};

View file

@ -23,26 +23,32 @@ import { PhraseFilter, MatchAllFilter } from '../filters';
describe('migrateFilter', function() {
const oldMatchPhraseFilter = ({
match: {
fieldFoo: {
query: 'foobar',
type: 'phrase',
query: {
match: {
fieldFoo: {
query: 'foobar',
type: 'phrase',
},
},
},
meta: {},
} as unknown) as DeprecatedMatchPhraseFilter;
const newMatchPhraseFilter = ({
match_phrase: {
fieldFoo: {
query: 'foobar',
query: {
match_phrase: {
fieldFoo: {
query: 'foobar',
},
},
},
meta: {},
} as unknown) as PhraseFilter;
it('should migrate match filters of type phrase', function() {
const migratedFilter = migrateFilter(oldMatchPhraseFilter, undefined);
expect(isEqual(migratedFilter, newMatchPhraseFilter)).toBe(true);
expect(migratedFilter).toEqual(newMatchPhraseFilter);
});
it('should not modify the original filter', function() {

View file

@ -22,31 +22,27 @@ import { getConvertedValueForField } from '../filters';
import { Filter } from '../filters';
import { IIndexPattern } from '../../index_patterns';
/** @deprecated
* see https://github.com/elastic/elasticsearch/pull/17508
* */
export interface DeprecatedMatchPhraseFilter extends Filter {
match: {
[field: string]: {
query: any;
type: 'phrase';
query: {
match: {
[field: string]: {
query: any;
type: 'phrase';
};
};
};
}
/** @deprecated
* see https://github.com/elastic/elasticsearch/pull/17508
* */
function isMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter {
const fieldName = filter.match && Object.keys(filter.match)[0];
function isDeprecatedMatchPhraseFilter(filter: any): filter is DeprecatedMatchPhraseFilter {
const fieldName = filter.query && filter.query.match && Object.keys(filter.query.match)[0];
return Boolean(fieldName && get(filter, ['match', fieldName, 'type']) === 'phrase');
return Boolean(fieldName && get(filter, ['query', 'match', fieldName, 'type']) === 'phrase');
}
export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) {
if (isMatchPhraseFilter(filter)) {
const fieldName = Object.keys(filter.match)[0];
const params: Record<string, any> = get(filter, ['match', fieldName]);
if (isDeprecatedMatchPhraseFilter(filter)) {
const fieldName = Object.keys(filter.query.match)[0];
const params: Record<string, any> = get(filter, ['query', 'match', fieldName]);
if (indexPattern) {
const field = indexPattern.fields.find(f => f.name === fieldName);
@ -55,8 +51,11 @@ export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) {
}
}
return {
match_phrase: {
[fieldName]: omit(params, 'type'),
...filter,
query: {
match_phrase: {
[fieldName]: omit(params, 'type'),
},
},
};
}

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildExistsFilter, getExistsFilterField } from './exists_filter';
import { IIndexPattern } from '../../index_patterns';
import { fields } from '../../index_patterns/fields/fields.mocks.ts';
describe('exists filter', function() {
const indexPattern: IIndexPattern = ({
fields,
} as unknown) as IIndexPattern;
describe('getExistsFilterField', function() {
it('should return the name of the field an exists query is targeting', () => {
const field = indexPattern.fields.find(patternField => patternField.name === 'extension');
const filter = buildExistsFilter(field!, indexPattern);
const result = getExistsFilterField(filter);
expect(result).toBe('extension');
});
});
});

View file

@ -33,6 +33,10 @@ export type ExistsFilter = Filter & {
export const isExistsFilter = (filter: any): filter is ExistsFilter => filter && filter.exists;
export const getExistsFilterField = (filter: ExistsFilter) => {
return filter.exists && filter.exists.field;
};
export const buildExistsFilter = (field: IFieldType, indexPattern: IIndexPattern) => {
return {
meta: {

View file

@ -0,0 +1,47 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getGeoBoundingBoxFilterField } from './geo_bounding_box_filter';
describe('geo_bounding_box filter', function() {
describe('getGeoBoundingBoxFilterField', function() {
it('should return the name of the field a geo_bounding_box query is targeting', () => {
const filter = {
geo_bounding_box: {
geoPointField: {
bottom_right: { lat: 1, lon: 1 },
top_left: { lat: 1, lon: 1 },
},
ignore_unmapped: true,
},
meta: {
disabled: false,
negate: false,
alias: null,
params: {
bottom_right: { lat: 1, lon: 1 },
top_left: { lat: 1, lon: 1 },
},
},
};
const result = getGeoBoundingBoxFilterField(filter);
expect(result).toBe('geoPointField');
});
});
});

View file

@ -33,3 +33,10 @@ export type GeoBoundingBoxFilter = Filter & {
export const isGeoBoundingBoxFilter = (filter: any): filter is GeoBoundingBoxFilter =>
filter && filter.geo_bounding_box;
export const getGeoBoundingBoxFilterField = (filter: GeoBoundingBoxFilter) => {
return (
filter.geo_bounding_box &&
Object.keys(filter.geo_bounding_box).find(key => key !== 'ignore_unmapped')
);
};

View file

@ -0,0 +1,45 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getGeoPolygonFilterField } from './geo_polygon_filter';
describe('geo_polygon filter', function() {
describe('getGeoPolygonFilterField', function() {
it('should return the name of the field a geo_polygon query is targeting', () => {
const filter = {
geo_polygon: {
geoPointField: {
points: [{ lat: 1, lon: 1 }],
},
ignore_unmapped: true,
},
meta: {
disabled: false,
negate: false,
alias: null,
params: {
points: [{ lat: 1, lon: 1 }],
},
},
};
const result = getGeoPolygonFilterField(filter);
expect(result).toBe('geoPointField');
});
});
});

View file

@ -32,3 +32,9 @@ export type GeoPolygonFilter = Filter & {
export const isGeoPolygonFilter = (filter: any): filter is GeoPolygonFilter =>
filter && filter.geo_polygon;
export const getGeoPolygonFilterField = (filter: GeoPolygonFilter) => {
return (
filter.geo_polygon && Object.keys(filter.geo_polygon).find(key => key !== 'ignore_unmapped')
);
};

View file

@ -0,0 +1,54 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildPhraseFilter } from './phrase_filter';
import { buildQueryFilter } from './query_string_filter';
import { getFilterField } from './get_filter_field';
import { IIndexPattern } from '../../index_patterns';
import { fields } from '../../index_patterns/fields/fields.mocks.ts';
describe('getFilterField', function() {
const indexPattern: IIndexPattern = ({
id: 'logstash-*',
fields,
} as unknown) as IIndexPattern;
it('should return the field name from known filter types that target a specific field', () => {
const field = indexPattern.fields.find(patternField => patternField.name === 'extension');
const filter = buildPhraseFilter(field!, 'jpg', indexPattern);
const result = getFilterField(filter);
expect(result).toBe('extension');
});
it('should return undefined for filters that do not target a specific field', () => {
const filter = buildQueryFilter(
{
query: {
query_string: {
query: 'response:200 and extension:jpg',
},
},
},
indexPattern.id!,
''
);
const result = getFilterField(filter);
expect(result).toBe(undefined);
});
});

View file

@ -0,0 +1,53 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { Filter } from './meta_filter';
import { getExistsFilterField, isExistsFilter } from './exists_filter';
import { getGeoBoundingBoxFilterField, isGeoBoundingBoxFilter } from './geo_bounding_box_filter';
import { getGeoPolygonFilterField, isGeoPolygonFilter } from './geo_polygon_filter';
import { getPhraseFilterField, isPhraseFilter } from './phrase_filter';
import { getPhrasesFilterField, isPhrasesFilter } from './phrases_filter';
import { getRangeFilterField, isRangeFilter } from './range_filter';
import { getMissingFilterField, isMissingFilter } from './missing_filter';
export const getFilterField = (filter: Filter) => {
if (isExistsFilter(filter)) {
return getExistsFilterField(filter);
}
if (isGeoBoundingBoxFilter(filter)) {
return getGeoBoundingBoxFilterField(filter);
}
if (isGeoPolygonFilter(filter)) {
return getGeoPolygonFilterField(filter);
}
if (isPhraseFilter(filter)) {
return getPhraseFilterField(filter);
}
if (isPhrasesFilter(filter)) {
return getPhrasesFilterField(filter);
}
if (isRangeFilter(filter)) {
return getRangeFilterField(filter);
}
if (isMissingFilter(filter)) {
return getMissingFilterField(filter);
}
return;
};

View file

@ -22,6 +22,7 @@ import { Filter } from './meta_filter';
export * from './build_filters';
export * from './get_filter_params';
export * from './get_filter_field';
export * from './custom_filter';
export * from './exists_filter';

View file

@ -0,0 +1,39 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { getMissingFilterField } from './missing_filter';
describe('missing filter', function() {
describe('getMissingFilterField', function() {
it('should return the name of the field an missing query is targeting', () => {
const filter = {
missing: {
field: 'extension',
},
meta: {
disabled: false,
negate: false,
alias: null,
},
};
const result = getMissingFilterField(filter);
expect(result).toBe('extension');
});
});
});

View file

@ -27,3 +27,7 @@ export type MissingFilter = Filter & {
};
export const isMissingFilter = (filter: any): filter is MissingFilter => filter && filter.missing;
export const getMissingFilterField = (filter: MissingFilter) => {
return filter.missing && filter.missing.field;
};

View file

@ -17,8 +17,12 @@
* under the License.
*/
import { buildInlineScriptForPhraseFilter, buildPhraseFilter } from './phrase_filter';
import { getField } from '../../index_patterns/mocks';
import {
buildInlineScriptForPhraseFilter,
buildPhraseFilter,
getPhraseFilterField,
} from './phrase_filter';
import { fields, getField } from '../../index_patterns/mocks';
import { IIndexPattern } from '../../index_patterns';
describe('Phrase filter builder', () => {
@ -95,3 +99,16 @@ describe('buildInlineScriptForPhraseFilter', () => {
expect(buildInlineScriptForPhraseFilter(field)).toBe(expected);
});
});
describe('getPhraseFilterField', function() {
const indexPattern: IIndexPattern = ({
fields,
} as unknown) as IIndexPattern;
it('should return the name of the field a phrase query is targeting', () => {
const field = indexPattern.fields.find(patternField => patternField.name === 'extension');
const filter = buildPhraseFilter(field!, 'jpg', indexPattern);
const result = getPhraseFilterField(filter);
expect(result).toBe('extension');
});
});

View file

@ -0,0 +1,37 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { buildPhrasesFilter, getPhrasesFilterField } from './phrases_filter';
import { IIndexPattern } from '../../index_patterns';
import { fields } from '../../index_patterns/fields/fields.mocks.ts';
describe('phrases filter', function() {
const indexPattern: IIndexPattern = ({
fields,
} as unknown) as IIndexPattern;
describe('getPhrasesFilterField', function() {
it('should return the name of the field a phrases query is targeting', () => {
const field = indexPattern.fields.find(patternField => patternField.name === 'extension');
const filter = buildPhrasesFilter(field!, ['jpg', 'png'], indexPattern);
const result = getPhrasesFilterField(filter);
expect(result).toBe('extension');
});
});
});

View file

@ -32,7 +32,13 @@ export type PhrasesFilter = Filter & {
};
export const isPhrasesFilter = (filter: any): filter is PhrasesFilter =>
filter && filter.meta.type === FILTERS.PHRASES;
filter?.meta?.type === FILTERS.PHRASES;
export const getPhrasesFilterField = (filter: PhrasesFilter) => {
// Phrases is a newer filter type that has always been created via a constructor that ensures
// `meta.key` is set to the field name
return filter.meta.key;
};
// Creates a filter where the given field matches one or more of the given values
// params should be an array of values

View file

@ -19,7 +19,7 @@
import { buildQueryFilter } from './query_string_filter';
describe('Phrase filter builder', () => {
describe('Query string filter builder', () => {
it('should be a function', () => {
expect(typeof buildQueryFilter).toBe('function');
});

View file

@ -18,8 +18,8 @@
*/
import { each } from 'lodash';
import { buildRangeFilter, RangeFilter } from './range_filter';
import { getField } from '../../index_patterns/mocks';
import { buildRangeFilter, getRangeFilterField, RangeFilter } from './range_filter';
import { fields, getField } from '../../index_patterns/mocks';
import { IIndexPattern, IFieldType } from '../../index_patterns';
describe('Range filter builder', () => {
@ -172,3 +172,16 @@ describe('Range filter builder', () => {
});
});
});
describe('getRangeFilterField', function() {
const indexPattern: IIndexPattern = ({
fields,
} as unknown) as IIndexPattern;
test('should return the name of the field a range query is targeting', () => {
const field = indexPattern.fields.find(patternField => patternField.name === 'bytes');
const filter = buildRangeFilter(field!, {}, indexPattern);
const result = getRangeFilterField(filter);
expect(result).toBe('bytes');
});
});

View file

@ -88,6 +88,10 @@ export const isScriptedRangeFilter = (filter: any): filter is RangeFilter => {
return hasRangeKeys(params);
};
export const getRangeFilterField = (filter: RangeFilter) => {
return filter.range && Object.keys(filter.range)[0];
};
const formatValue = (field: IFieldType, params: any[]) =>
map(params, (val: any, key: string) => get(operators, key) + format(field, val)).join(' ');

View file

@ -25,7 +25,6 @@ export default function ({ getService, getPageObjects }) {
const esArchiver = getService('esArchiver');
const browser = getService('browser');
const kibanaServer = getService('kibanaServer');
const filterBar = getService('filterBar');
const queryBar = getService('queryBar');
const PageObjects = getPageObjects(['common', 'discover', 'header', 'timePicker']);
const defaultSettings = {
@ -173,20 +172,6 @@ export default function ({ getService, getPageObjects }) {
});
describe('filter editor', function () {
it('should add a phrases filter', async function () {
await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
});
it('should show the phrases if you re-open a phrases filter', async function () {
await filterBar.clickEditFilter('extension.raw', 'jpg');
const phrases = await filterBar.getFilterEditorSelectedPhrases();
expect(phrases.length).to.be(1);
expect(phrases[0]).to.be('jpg');
});
});
describe('data-shared-item', function () {
it('should have correct data-shared-item title and description', async () => {
const expected = {

View file

@ -0,0 +1,73 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from '@kbn/expect';
export default function ({ getService, getPageObjects }) {
const log = getService('log');
const retry = getService('retry');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const filterBar = getService('filterBar');
const PageObjects = getPageObjects(['common', 'discover', 'timePicker']);
const defaultSettings = {
defaultIndex: 'logstash-*',
};
describe('discover filter editor', function describeIndexTests() {
before(async function () {
log.debug('load kibana index with default index pattern');
await esArchiver.loadIfNeeded('discover');
// and load a set of makelogs data
await esArchiver.loadIfNeeded('logstash_functional');
await kibanaServer.uiSettings.replace(defaultSettings);
log.debug('discover filter editor');
await PageObjects.common.navigateToApp('discover');
await PageObjects.timePicker.setDefaultAbsoluteRange();
});
describe('filter editor', function () {
it('should add a phrases filter', async function () {
await filterBar.addFilter('extension.raw', 'is one of', 'jpg');
expect(await filterBar.hasFilter('extension.raw', 'jpg')).to.be(true);
});
it('should show the phrases if you re-open a phrases filter', async function () {
await filterBar.clickEditFilter('extension.raw', 'jpg');
const phrases = await filterBar.getFilterEditorSelectedPhrases();
expect(phrases.length).to.be(1);
expect(phrases[0]).to.be('jpg');
await filterBar.ensureFieldEditorModalIsClosed();
});
it('should support filtering on nested fields', async () => {
await filterBar.addFilter('nestedField.child', 'is', 'nestedValue');
expect(await filterBar.hasFilter('nestedField.child', 'nestedValue')).to.be(true);
await retry.try(async function () {
expect(await PageObjects.discover.getHitCount()).to.be(
'1'
);
});
});
});
});
}

View file

@ -34,6 +34,7 @@ export default function ({ getService, loadTestFile }) {
loadTestFile(require.resolve('./_saved_queries'));
loadTestFile(require.resolve('./_discover'));
loadTestFile(require.resolve('./_filter_editor'));
loadTestFile(require.resolve('./_errors'));
loadTestFile(require.resolve('./_field_data'));
loadTestFile(require.resolve('./_shared_links'));

View file

@ -166,6 +166,7 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon
if (cancelSaveFilterModalButtonExists) {
await testSubjects.click('cancelSaveFilter');
}
await testSubjects.waitForDeleted('cancelSaveFilter');
}
/**