Skip to content

Commit

Permalink
adding kibana filter expression functions (#94069) (#95287)
Browse files Browse the repository at this point in the history
  • Loading branch information
ppisljar authored Mar 24, 2021
1 parent 91bb8e7 commit ecd8871
Show file tree
Hide file tree
Showing 26 changed files with 851 additions and 10 deletions.
6 changes: 4 additions & 2 deletions src/plugins/data/common/es_query/filters/build_filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,15 @@ export function buildFilter(
disabled: boolean,
params: any,
alias: string | null,
store: FilterStateStore
store?: FilterStateStore
): Filter {
const filter = buildBaseFilter(indexPattern, field, type, params);
filter.meta.alias = alias;
filter.meta.negate = negate;
filter.meta.disabled = disabled;
filter.$state = { store };
if (store) {
filter.$state = { store };
}
return filter;
}

Expand Down
33 changes: 33 additions & 0 deletions src/plugins/data/common/search/expressions/exists_filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { existsFilterFunction } from './exists_filter';

describe('interpreter/functions#existsFilter', () => {
const fn = functionWrapper(existsFilterFunction);

it('returns an object with the correct structure', () => {
const actual = fn(null, { field: { spec: { name: 'test' } } }, createMockContext());
expect(actual).toMatchInlineSnapshot(`
Object {
"exists": Object {
"field": "test",
},
"meta": Object {
"alias": null,
"disabled": false,
"index": undefined,
"negate": false,
},
"type": "kibana_filter",
}
`);
});
});
65 changes: 65 additions & 0 deletions src/plugins/data/common/search/expressions/exists_filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* 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 { KibanaField, KibanaFilter } from './kibana_context_type';
import { buildFilter, FILTERS } from '../../es_query/filters';
import { IndexPattern } from '../../index_patterns/index_patterns';

interface Arguments {
field: KibanaField;
negate?: boolean;
}

export type ExpressionFunctionExistsFilter = ExpressionFunctionDefinition<
'existsFilter',
null,
Arguments,
KibanaFilter
>;

export const existsFilterFunction: ExpressionFunctionExistsFilter = {
name: 'existsFilter',
type: 'kibana_filter',
inputTypes: ['null'],
help: i18n.translate('data.search.functions.existsFilter.help', {
defaultMessage: 'Create kibana exists filter',
}),
args: {
field: {
types: ['kibana_field'],
required: true,
help: i18n.translate('data.search.functions.existsFilter.field.help', {
defaultMessage: 'Specify the field you want to filter on. Use `field` function.',
}),
},
negate: {
types: ['boolean'],
default: false,
help: i18n.translate('data.search.functions.existsFilter.negate.help', {
defaultMessage: 'Should the filter be negated.',
}),
},
},

fn(input, args) {
return {
type: 'kibana_filter',
...buildFilter(
({} as any) as IndexPattern,
args.field.spec,
FILTERS.EXISTS,
args.negate || false,
false,
{},
null
),
};
},
};
42 changes: 42 additions & 0 deletions src/plugins/data/common/search/expressions/field.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* 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 { ExecutionContext } from 'src/plugins/expressions/common';
import { functionWrapper } from './utils';
import { fieldFunction } from './field';

describe('interpreter/functions#field', () => {
const fn = functionWrapper(fieldFunction);
let context: ExecutionContext;

beforeEach(() => {
context = {
getSearchContext: () => ({}),
getSearchSessionId: () => undefined,
types: {},
variables: {},
abortSignal: {} as any,
inspectorAdapters: {} as any,
};
});

it('returns an object with the correct structure', () => {
const actual = fn(null, { name: 'test', type: 'number' }, context);
expect(actual).toMatchInlineSnapshot(`
Object {
"spec": Object {
"name": "test",
"script": undefined,
"scripted": false,
"type": "number",
},
"type": "kibana_field",
}
`);
});
});
67 changes: 67 additions & 0 deletions src/plugins/data/common/search/expressions/field.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { KibanaField } from './kibana_context_type';

interface Arguments {
name: string;
type: string;
script?: string;
}

export type ExpressionFunctionField = ExpressionFunctionDefinition<
'field',
null,
Arguments,
KibanaField
>;

export const fieldFunction: ExpressionFunctionField = {
name: 'field',
type: 'kibana_field',
inputTypes: ['null'],
help: i18n.translate('data.search.functions.field.help', {
defaultMessage: 'Create a Kibana field.',
}),
args: {
name: {
types: ['string'],
required: true,
help: i18n.translate('data.search.functions.field.name.help', {
defaultMessage: 'Name of the field',
}),
},
type: {
types: ['string'],
required: true,
help: i18n.translate('data.search.functions.field.type.help', {
defaultMessage: 'Type of the field',
}),
},
script: {
types: ['string'],
help: i18n.translate('data.search.functions.field.script.help', {
defaultMessage: 'A field script, in case the field is scripted.',
}),
},
},

fn(input, args) {
return {
type: 'kibana_field',
spec: {
name: args.name,
type: args.type,
scripted: args.script ? true : false,
script: args.script,
},
} as KibanaField;
},
};
47 changes: 47 additions & 0 deletions src/plugins/data/common/search/expressions/filters_to_ast.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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 { filtersToAst } from './filters_to_ast';

describe('interpreter/functions#filtersToAst', () => {
const normalFilter = {
meta: { negate: false, alias: '', disabled: false },
query: { test: 'something' },
};
const negatedFilter = {
meta: { negate: true, alias: '', disabled: false },
query: { test: 'something' },
};

it('converts a list of filters to an expression AST node', () => {
const actual = filtersToAst([normalFilter, negatedFilter]);
expect(actual).toHaveLength(2);
expect(actual[0].functions[0]).toHaveProperty('name', 'kibanaFilter');
expect(actual[0].functions[0].arguments).toMatchInlineSnapshot(`
Object {
"negate": Array [
false,
],
"query": Array [
"{\\"query\\":{\\"test\\":\\"something\\"}}",
],
}
`);
expect(actual[1].functions[0]).toHaveProperty('name', 'kibanaFilter');
expect(actual[1].functions[0].arguments).toMatchInlineSnapshot(`
Object {
"negate": Array [
true,
],
"query": Array [
"{\\"query\\":{\\"test\\":\\"something\\"}}",
],
}
`);
});
});
23 changes: 23 additions & 0 deletions src/plugins/data/common/search/expressions/filters_to_ast.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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 { buildExpression, buildExpressionFunction } from '../../../../expressions/common';
import { Filter } from '../../es_query/filters';
import { ExpressionFunctionKibanaFilter } from './kibana_filter';

export const filtersToAst = (filters: Filter[] | Filter) => {
return (Array.isArray(filters) ? filters : [filters]).map((filter) => {
const { meta, $state, ...restOfFilter } = filter;
return buildExpression([
buildExpressionFunction<ExpressionFunctionKibanaFilter>('kibanaFilter', {
query: JSON.stringify(restOfFilter),
negate: filter.meta.negate,
}),
]);
});
};
7 changes: 7 additions & 0 deletions src/plugins/data/common/search/expressions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,11 @@ export * from './timerange_to_ast';
export * from './kibana_context_type';
export * from './esaggs';
export * from './utils';
export * from './range';
export * from './field';
export * from './phrase_filter';
export * from './exists_filter';
export * from './range_filter';
export * from './kibana_filter';
export * from './filters_to_ast';
export * from './timerange';
11 changes: 6 additions & 5 deletions src/plugins/data/common/search/expressions/kibana_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import { uniqBy } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expressions/common';
import { Adapters } from 'src/plugins/inspector/common';
import { unboxExpressionValue } from '../../../../expressions/common';
import { Query, uniqFilters } from '../../query';
import { ExecutionContextSearch, KibanaContext } from './kibana_context_type';
import { ExecutionContextSearch, KibanaContext, KibanaFilter } from './kibana_context_type';
import { KibanaQueryOutput } from './kibana_context_type';
import { KibanaTimerangeOutput } from './timerange';

interface Arguments {
q?: KibanaQueryOutput | null;
filters?: string | null;
filters?: KibanaFilter[] | null;
timeRange?: KibanaTimerangeOutput | null;
savedSearchId?: string | null;
}
Expand Down Expand Up @@ -56,8 +57,8 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = {
}),
},
filters: {
types: ['string', 'null'],
default: '"[]"',
types: ['kibana_filter', 'null'],
multi: true,
help: i18n.translate('data.search.functions.kibana_context.filters.help', {
defaultMessage: 'Specify Kibana generic filters',
}),
Expand All @@ -81,7 +82,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = {
async fn(input, args, { getSavedObject }) {
const timeRange = args.timeRange || input?.timeRange;
let queries = mergeQueries(input?.query, args?.q || []);
let filters = [...(input?.filters || []), ...getParsedValue(args?.filters, [])];
let filters = [...(input?.filters || []), ...(args?.filters?.map(unboxExpressionValue) || [])];

if (args.savedSearchId) {
if (typeof getSavedObject !== 'function') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { ExpressionValueBoxed } from 'src/plugins/expressions/common';
import { Filter } from '../../es_query';
import { Query, TimeRange } from '../../query';
import { IndexPatternField } from '../../index_patterns/fields';

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type ExecutionContextSearch = {
Expand All @@ -23,6 +24,8 @@ export type ExpressionValueSearchContext = ExpressionValueBoxed<
>;

export type KibanaQueryOutput = ExpressionValueBoxed<'kibana_query', Query>;
export type KibanaFilter = ExpressionValueBoxed<'kibana_filter', Filter>;
export type KibanaField = ExpressionValueBoxed<'kibana_field', IndexPatternField>;

// TODO: These two are exported for legacy reasons - remove them eventually.
export type KIBANA_CONTEXT_NAME = 'kibana_context';
Expand Down
30 changes: 30 additions & 0 deletions src/plugins/data/common/search/expressions/kibana_filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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 { kibanaFilterFunction } from './kibana_filter';

describe('interpreter/functions#kibanaFilter', () => {
const fn = functionWrapper(kibanaFilterFunction);

it('returns an object with the correct structure', () => {
const actual = fn(null, { query: '{ "name": "test" }' }, createMockContext());
expect(actual).toMatchInlineSnapshot(`
Object {
"meta": Object {
"alias": "",
"disabled": false,
"negate": false,
},
"name": "test",
"type": "kibana_filter",
}
`);
});
});
Loading

0 comments on commit ecd8871

Please sign in to comment.