diff --git a/packages/kbn-es-query/src/filters/build_filters/types.ts b/packages/kbn-es-query/src/filters/build_filters/types.ts index 12830e4b6eeac..3f6d586feed98 100644 --- a/packages/kbn-es-query/src/filters/build_filters/types.ts +++ b/packages/kbn-es-query/src/filters/build_filters/types.ts @@ -56,6 +56,8 @@ export type FilterMeta = { negate?: boolean; // controlledBy is there to identify who owns the filter controlledBy?: string; + // allows grouping of filters + group?: string; // index and type are optional only because when you create a new filter, there are no defaults index?: string; isMultiIndex?: boolean; diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index fbf3ba7f32c5c..05496b4d8c885 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -36,6 +36,8 @@ export * from './field'; export * from './phrase_filter'; export * from './exists_filter'; export * from './range_filter'; +export * from './remove_filter'; +export * from './select_filter'; export * from './kibana_filter'; export * from './filters_to_ast'; export * from './timerange'; diff --git a/src/plugins/data/common/search/expressions/remove_filter.test.ts b/src/plugins/data/common/search/expressions/remove_filter.test.ts new file mode 100644 index 0000000000000..f5a924769df70 --- /dev/null +++ b/src/plugins/data/common/search/expressions/remove_filter.test.ts @@ -0,0 +1,206 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createMockContext } from '../../../../expressions/common'; +import { functionWrapper } from './utils'; +import { removeFilterFunction } from './remove_filter'; +import { KibanaContext } from './kibana_context_type'; + +describe('interpreter/functions#removeFilter', () => { + const fn = functionWrapper(removeFilterFunction); + const kibanaContext: KibanaContext = { + type: 'kibana_context', + filters: [ + { + meta: { + group: 'g1', + }, + query: {}, + }, + { + meta: { + group: 'g2', + }, + query: {}, + }, + { + meta: { + group: 'g1', + controlledBy: 'i1', + }, + query: {}, + }, + { + meta: { + group: 'g1', + controlledBy: 'i2', + }, + query: {}, + }, + { + meta: { + controlledBy: 'i1', + }, + query: {}, + }, + ], + }; + + it('removes all filters when called without arguments', () => { + const actual = fn(kibanaContext, {}, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [], + "type": "kibana_context", + } + `); + }); + + it('removes filters belonging to certain group', () => { + const actual = fn(kibanaContext, { group: 'g1' }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g2", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('removes ungrouped filters', () => { + const actual = fn(kibanaContext, { ungrouped: true }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "group": "g2", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i2", + "group": "g1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('removes ungrouped filters and filters matching a group', () => { + const actual = fn(kibanaContext, { group: 'g1', ungrouped: true }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g2", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('removes filters controlled by specified id', () => { + const actual = fn(kibanaContext, { from: 'i1' }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "group": "g2", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i2", + "group": "g1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('removes filters controlled by specified id and matching a group', () => { + const actual = fn(kibanaContext, { group: 'g1', from: 'i1' }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "group": "g2", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i2", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/remove_filter.ts b/src/plugins/data/common/search/expressions/remove_filter.ts new file mode 100644 index 0000000000000..f45cc4c557a73 --- /dev/null +++ b/src/plugins/data/common/search/expressions/remove_filter.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaContext } from './kibana_context_type'; + +interface Arguments { + group?: string; + from?: string; + ungrouped?: boolean; +} + +export type ExpressionFunctionRemoveFilter = ExpressionFunctionDefinition< + 'removeFilter', + KibanaContext, + Arguments, + KibanaContext +>; + +export const removeFilterFunction: ExpressionFunctionRemoveFilter = { + name: 'removeFilter', + type: 'kibana_context', + inputTypes: ['kibana_context'], + help: i18n.translate('data.search.functions.removeFilter.help', { + defaultMessage: 'Removes filters from context', + }), + args: { + group: { + types: ['string'], + aliases: ['_'], + help: i18n.translate('data.search.functions.removeFilter.group.help', { + defaultMessage: 'Removes only filters belonging to the provided group', + }), + }, + from: { + types: ['string'], + help: i18n.translate('data.search.functions.removeFilter.from.help', { + defaultMessage: 'Removes only filters owned by the provided id', + }), + }, + ungrouped: { + types: ['boolean'], + aliases: ['nogroup', 'nogroups'], + default: false, + help: i18n.translate('data.search.functions.removeFilter.ungrouped.help', { + defaultMessage: 'Should filters without group be removed', + }), + }, + }, + + fn(input, { group, from, ungrouped }) { + return { + ...input, + filters: + input.filters?.filter(({ meta }) => { + const isGroupMatching = + (!group && !ungrouped) || group === meta.group || (ungrouped && !meta.group); + const isOriginMatching = !from || from === meta.controlledBy; + return !isGroupMatching || !isOriginMatching; + }) || [], + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/select_filter.test.ts b/src/plugins/data/common/search/expressions/select_filter.test.ts new file mode 100644 index 0000000000000..a2515dbcb171d --- /dev/null +++ b/src/plugins/data/common/search/expressions/select_filter.test.ts @@ -0,0 +1,223 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { createMockContext } from '../../../../expressions/common'; +import { functionWrapper } from './utils'; +import { selectFilterFunction } from './select_filter'; +import { KibanaContext } from './kibana_context_type'; + +describe('interpreter/functions#selectFilter', () => { + const fn = functionWrapper(selectFilterFunction); + const kibanaContext: KibanaContext = { + type: 'kibana_context', + filters: [ + { + meta: { + group: 'g1', + }, + query: {}, + }, + { + meta: { + group: 'g2', + }, + query: {}, + }, + { + meta: { + group: 'g1', + controlledBy: 'i1', + }, + query: {}, + }, + { + meta: { + group: 'g1', + controlledBy: 'i2', + }, + query: {}, + }, + { + meta: { + controlledBy: 'i1', + }, + query: {}, + }, + ], + }; + + it('selects all filters when called without arguments', () => { + const actual = fn(kibanaContext, {}, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "group": "g2", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i2", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('selects filters belonging to certain group', () => { + const actual = fn(kibanaContext, { group: 'g1' }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i2", + "group": "g1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('selects ungrouped filters', () => { + const actual = fn(kibanaContext, { ungrouped: true }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "controlledBy": "i1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('selects ungrouped filters and filters matching a group', () => { + const actual = fn(kibanaContext, { group: 'g1', ungrouped: true }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i2", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('selects filters controlled by specified id', () => { + const actual = fn(kibanaContext, { from: 'i1' }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "controlledBy": "i1", + "group": "g1", + }, + "query": Object {}, + }, + Object { + "meta": Object { + "controlledBy": "i1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); + + it('selects filters controlled by specified id and matching a group', () => { + const actual = fn(kibanaContext, { group: 'g1', from: 'i1' }, createMockContext()); + expect(actual).toMatchInlineSnapshot(` + Object { + "filters": Array [ + Object { + "meta": Object { + "controlledBy": "i1", + "group": "g1", + }, + "query": Object {}, + }, + ], + "type": "kibana_context", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/select_filter.ts b/src/plugins/data/common/search/expressions/select_filter.ts new file mode 100644 index 0000000000000..3e76f3a6426c2 --- /dev/null +++ b/src/plugins/data/common/search/expressions/select_filter.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { KibanaContext } from './kibana_context_type'; + +interface Arguments { + group?: string; + from?: string; + ungrouped?: boolean; +} + +export type ExpressionFunctionSelectFilter = ExpressionFunctionDefinition< + 'selectFilter', + KibanaContext, + Arguments, + KibanaContext +>; + +export const selectFilterFunction: ExpressionFunctionSelectFilter = { + name: 'selectFilter', + type: 'kibana_context', + inputTypes: ['kibana_context'], + help: i18n.translate('data.search.functions.selectFilter.help', { + defaultMessage: 'Selects filters from context', + }), + args: { + group: { + types: ['string'], + aliases: ['_'], + help: i18n.translate('data.search.functions.selectFilter.group.help', { + defaultMessage: 'Select only filters belonging to the provided group', + }), + }, + from: { + types: ['string'], + help: i18n.translate('data.search.functions.selectFilter.from.help', { + defaultMessage: 'Select only filters owned by the provided id', + }), + }, + ungrouped: { + types: ['boolean'], + aliases: ['nogroup', 'nogroups'], + default: false, + help: i18n.translate('data.search.functions.selectFilter.ungrouped.help', { + defaultMessage: 'Should filters without group be included', + }), + }, + }, + + fn(input, { group, ungrouped, from }) { + return { + ...input, + filters: + input.filters?.filter(({ meta }) => { + const isGroupMatching = + (!group && !ungrouped) || group === meta.group || (ungrouped && !meta.group); + const isOriginMatching = !from || from === meta.controlledBy; + return isGroupMatching && isOriginMatching; + }) || [], + }; + }, +}; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 7cba1ec17c322..ecc0e84917251 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -39,6 +39,8 @@ import { geoPointFunction, queryFilterFunction, rangeFilterFunction, + removeFilterFunction, + selectFilterFunction, kibanaFilterFunction, phraseFilterFunction, esRawResponse, @@ -139,6 +141,8 @@ export class SearchService implements Plugin { expressions.registerFunction(existsFilterFunction); expressions.registerFunction(queryFilterFunction); expressions.registerFunction(rangeFilterFunction); + expressions.registerFunction(removeFilterFunction); + expressions.registerFunction(selectFilterFunction); expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index fa7296c822467..04db51fdce7fb 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -66,6 +66,8 @@ import { numericalRangeFunction, queryFilterFunction, rangeFilterFunction, + removeFilterFunction, + selectFilterFunction, rangeFunction, SearchSourceDependencies, searchSourceRequiredUiSettings, @@ -205,6 +207,8 @@ export class SearchService implements Plugin { expressions.registerFunction(existsFilterFunction); expressions.registerFunction(queryFilterFunction); expressions.registerFunction(rangeFilterFunction); + expressions.registerFunction(removeFilterFunction); + expressions.registerFunction(selectFilterFunction); expressions.registerFunction(phraseFilterFunction); expressions.registerType(kibanaContext); expressions.registerType(esRawResponse);