diff --git a/src/plugins/data/common/es_query/__fixtures__/index_pattern_response.ts b/src/plugins/data/common/es_query/__fixtures__/index_pattern_response.ts new file mode 100644 index 0000000000000..1784a2650a95a --- /dev/null +++ b/src/plugins/data/common/es_query/__fixtures__/index_pattern_response.ts @@ -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' } }, + }, + ], +}; diff --git a/src/plugins/data/common/es_query/es_query/from_filters.test.ts b/src/plugins/data/common/es_query/es_query/from_filters.test.ts index 8c1d990c389b8..a93a91a42dbf3 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.test.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.test.ts @@ -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); + }); }); }); diff --git a/src/plugins/data/common/es_query/es_query/from_filters.ts b/src/plugins/data/common/es_query/es_query/from_filters.ts index e33040485bf47..ed91d391fc1fd 100644 --- a/src/plugins/data/common/es_query/es_query/from_filters.ts +++ b/src/plugins/data/common/es_query/es_query/from_filters.ts @@ -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)); - return { - must: [], - filter: filters - .filter(filterNegate(false)) + 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) - .map(filter => migrateFilter(filter, indexPattern)), + .map(cleanFilter); + }; + + return { + must: [], + 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), }; }; diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts new file mode 100644 index 0000000000000..594b2641c39be --- /dev/null +++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.test.ts @@ -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); + } +}); diff --git a/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts new file mode 100644 index 0000000000000..27be7925fe00c --- /dev/null +++ b/src/plugins/data/common/es_query/es_query/handle_nested_filter.ts @@ -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, + }, + }; +}; diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts index e01240da87543..698d7bb48e685 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.test.ts @@ -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() { diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index fdc40768ebe41..22fbfe0e1ab08 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -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 = get(filter, ['match', fieldName]); + if (isDeprecatedMatchPhraseFilter(filter)) { + const fieldName = Object.keys(filter.query.match)[0]; + const params: Record = 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'), + }, }, }; } diff --git a/src/plugins/data/common/es_query/filters/exists_filter.test.ts b/src/plugins/data/common/es_query/filters/exists_filter.test.ts new file mode 100644 index 0000000000000..af52192dd85e4 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/exists_filter.test.ts @@ -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'); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/exists_filter.ts b/src/plugins/data/common/es_query/filters/exists_filter.ts index a20a4f0634766..035983dc446dc 100644 --- a/src/plugins/data/common/es_query/filters/exists_filter.ts +++ b/src/plugins/data/common/es_query/filters/exists_filter.ts @@ -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: { diff --git a/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.test.ts b/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.test.ts new file mode 100644 index 0000000000000..63c3a59044c1f --- /dev/null +++ b/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.test.ts @@ -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'); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts b/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts index f4673af96b2cd..619903954ff55 100644 --- a/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts +++ b/src/plugins/data/common/es_query/filters/geo_bounding_box_filter.ts @@ -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') + ); +}; diff --git a/src/plugins/data/common/es_query/filters/geo_polygon_filter.test.ts b/src/plugins/data/common/es_query/filters/geo_polygon_filter.test.ts new file mode 100644 index 0000000000000..ba8e43b0cea85 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/geo_polygon_filter.test.ts @@ -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'); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/geo_polygon_filter.ts b/src/plugins/data/common/es_query/filters/geo_polygon_filter.ts index 4cf82a92d2cef..03367feb83ee4 100644 --- a/src/plugins/data/common/es_query/filters/geo_polygon_filter.ts +++ b/src/plugins/data/common/es_query/filters/geo_polygon_filter.ts @@ -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') + ); +}; diff --git a/src/plugins/data/common/es_query/filters/get_filter_field.test.ts b/src/plugins/data/common/es_query/filters/get_filter_field.test.ts new file mode 100644 index 0000000000000..2fc8ffef9713b --- /dev/null +++ b/src/plugins/data/common/es_query/filters/get_filter_field.test.ts @@ -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); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/get_filter_field.ts b/src/plugins/data/common/es_query/filters/get_filter_field.ts new file mode 100644 index 0000000000000..dfb575157d362 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/get_filter_field.ts @@ -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; +}; diff --git a/src/plugins/data/common/es_query/filters/index.ts b/src/plugins/data/common/es_query/filters/index.ts index 1bd534bf74ff7..403ff2b79b55f 100644 --- a/src/plugins/data/common/es_query/filters/index.ts +++ b/src/plugins/data/common/es_query/filters/index.ts @@ -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'; diff --git a/src/plugins/data/common/es_query/filters/missing_filter.test.ts b/src/plugins/data/common/es_query/filters/missing_filter.test.ts new file mode 100644 index 0000000000000..240d8fb26f3e0 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/missing_filter.test.ts @@ -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'); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/missing_filter.ts b/src/plugins/data/common/es_query/filters/missing_filter.ts index 5411187cbcfd7..c8e1194a8f3cc 100644 --- a/src/plugins/data/common/es_query/filters/missing_filter.ts +++ b/src/plugins/data/common/es_query/filters/missing_filter.ts @@ -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; +}; diff --git a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts b/src/plugins/data/common/es_query/filters/phrase_filter.test.ts index 3c7d00a80fecf..9f90097e55475 100644 --- a/src/plugins/data/common/es_query/filters/phrase_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/phrase_filter.test.ts @@ -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'); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.test.ts b/src/plugins/data/common/es_query/filters/phrases_filter.test.ts new file mode 100644 index 0000000000000..3a121eb9da034 --- /dev/null +++ b/src/plugins/data/common/es_query/filters/phrases_filter.test.ts @@ -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'); + }); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/phrases_filter.ts b/src/plugins/data/common/es_query/filters/phrases_filter.ts index f7164f0ad3c83..006e0623be913 100644 --- a/src/plugins/data/common/es_query/filters/phrases_filter.ts +++ b/src/plugins/data/common/es_query/filters/phrases_filter.ts @@ -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 diff --git a/src/plugins/data/common/es_query/filters/query_string_filter.test.ts b/src/plugins/data/common/es_query/filters/query_string_filter.test.ts index 4fcb15ccac44a..18285194c6054 100644 --- a/src/plugins/data/common/es_query/filters/query_string_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/query_string_filter.test.ts @@ -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'); }); diff --git a/src/plugins/data/common/es_query/filters/range_filter.test.ts b/src/plugins/data/common/es_query/filters/range_filter.test.ts index 56b63018b5153..45d59c97941b3 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.test.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.test.ts @@ -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'); + }); +}); diff --git a/src/plugins/data/common/es_query/filters/range_filter.ts b/src/plugins/data/common/es_query/filters/range_filter.ts index 3d819bd145fa6..b300539f4280a 100644 --- a/src/plugins/data/common/es_query/filters/range_filter.ts +++ b/src/plugins/data/common/es_query/filters/range_filter.ts @@ -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(' '); diff --git a/test/functional/apps/discover/_discover.js b/test/functional/apps/discover/_discover.js index 94b2941ecd3d1..58eef9fbf6b87 100644 --- a/test/functional/apps/discover/_discover.js +++ b/test/functional/apps/discover/_discover.js @@ -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 = { diff --git a/test/functional/apps/discover/_filter_editor.js b/test/functional/apps/discover/_filter_editor.js new file mode 100644 index 0000000000000..cab16252add4a --- /dev/null +++ b/test/functional/apps/discover/_filter_editor.js @@ -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' + ); + }); + }); + }); + }); +} diff --git a/test/functional/apps/discover/index.js b/test/functional/apps/discover/index.js index 902490bebd1ac..28df897b67c09 100644 --- a/test/functional/apps/discover/index.js +++ b/test/functional/apps/discover/index.js @@ -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')); diff --git a/test/functional/services/filter_bar.ts b/test/functional/services/filter_bar.ts index adf0f2266ba17..9d494b1e6d950 100644 --- a/test/functional/services/filter_bar.ts +++ b/test/functional/services/filter_bar.ts @@ -166,6 +166,7 @@ export function FilterBarProvider({ getService, getPageObjects }: FtrProviderCon if (cancelSaveFilterModalButtonExists) { await testSubjects.click('cancelSaveFilter'); } + await testSubjects.waitForDeleted('cancelSaveFilter'); } /**