diff --git a/api_docs/data.json b/api_docs/data.json index 30d8a7190d4f7..42d6dbc6d226f 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -27843,4 +27843,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 8c828ff7ce80a..0214704a88517 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -19470,4 +19470,4 @@ } ] } -} \ No newline at end of file +} diff --git a/api_docs/expressions.json b/api_docs/expressions.json index ff04fcd03f046..ee496cc7c06a3 100644 --- a/api_docs/expressions.json +++ b/api_docs/expressions.json @@ -33883,4 +33883,4 @@ } ] } -} \ No newline at end of file +} diff --git a/src/plugins/data/common/search/expressions/index.ts b/src/plugins/data/common/search/expressions/index.ts index 8ac5ffec850f6..b38dce247261c 100644 --- a/src/plugins/data/common/search/expressions/index.ts +++ b/src/plugins/data/common/search/expressions/index.ts @@ -8,6 +8,11 @@ export * from './kibana'; export * from './kibana_context'; +export * from './kql'; +export * from './lucene'; +export * from './query_to_ast'; +export * from './timerange_to_ast'; export * from './kibana_context_type'; export * from './esaggs'; export * from './utils'; +export * from './timerange'; diff --git a/src/plugins/data/common/search/expressions/kibana_context.ts b/src/plugins/data/common/search/expressions/kibana_context.ts index 982db7505a3cf..5c2e2f418e69c 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.ts @@ -12,11 +12,13 @@ import { ExpressionFunctionDefinition, ExecutionContext } from 'src/plugins/expr import { Adapters } from 'src/plugins/inspector/common'; import { Query, uniqFilters } from '../../query'; import { ExecutionContextSearch, KibanaContext } from './kibana_context_type'; +import { KibanaQueryOutput } from './kibana_context_type'; +import { KibanaTimerangeOutput } from './timerange'; interface Arguments { - q?: string | null; + q?: KibanaQueryOutput | null; filters?: string | null; - timeRange?: string | null; + timeRange?: KibanaTimerangeOutput | null; savedSearchId?: string | null; } @@ -46,7 +48,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }), args: { q: { - types: ['string', 'null'], + types: ['kibana_query', 'null'], aliases: ['query', '_'], default: null, help: i18n.translate('data.search.functions.kibana_context.q.help', { @@ -61,7 +63,7 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }), }, timeRange: { - types: ['string', 'null'], + types: ['timerange', 'null'], default: null, help: i18n.translate('data.search.functions.kibana_context.timeRange.help', { defaultMessage: 'Specify Kibana time range filter', @@ -77,8 +79,8 @@ export const kibanaContextFunction: ExpressionFunctionKibanaContext = { }, async fn(input, args, { getSavedObject }) { - const timeRange = getParsedValue(args.timeRange, input?.timeRange); - let queries = mergeQueries(input?.query, getParsedValue(args?.q, [])); + const timeRange = args.timeRange || input?.timeRange; + let queries = mergeQueries(input?.query, args?.q || []); let filters = [...(input?.filters || []), ...getParsedValue(args?.filters, [])]; if (args.savedSearchId) { diff --git a/src/plugins/data/common/search/expressions/kibana_context_type.ts b/src/plugins/data/common/search/expressions/kibana_context_type.ts index 40adbc65317ad..090f09f7004ca 100644 --- a/src/plugins/data/common/search/expressions/kibana_context_type.ts +++ b/src/plugins/data/common/search/expressions/kibana_context_type.ts @@ -22,6 +22,8 @@ export type ExpressionValueSearchContext = ExpressionValueBoxed< ExecutionContextSearch >; +export type KibanaQueryOutput = ExpressionValueBoxed<'kibana_query', Query>; + // TODO: These two are exported for legacy reasons - remove them eventually. export type KIBANA_CONTEXT_NAME = 'kibana_context'; export type KibanaContext = ExpressionValueSearchContext; diff --git a/src/plugins/data/common/search/expressions/kql.test.ts b/src/plugins/data/common/search/expressions/kql.test.ts new file mode 100644 index 0000000000000..dcf3906e6c2f5 --- /dev/null +++ b/src/plugins/data/common/search/expressions/kql.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { ExpressionValueSearchContext } from './kibana_context_type'; +import { functionWrapper } from './utils'; +import { kqlFunction } from './kql'; + +describe('interpreter/functions#kql', () => { + const fn = functionWrapper(kqlFunction); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(input, { q: 'test' }, context); + expect(actual).toMatchInlineSnapshot( + ` + Object { + "language": "kuery", + "query": "test", + "type": "kibana_query", + } + ` + ); + }); +}); diff --git a/src/plugins/data/common/search/expressions/kql.ts b/src/plugins/data/common/search/expressions/kql.ts new file mode 100644 index 0000000000000..5dd830f92f834 --- /dev/null +++ b/src/plugins/data/common/search/expressions/kql.ts @@ -0,0 +1,49 @@ +/* + * 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 { KibanaQueryOutput } from './kibana_context_type'; + +interface Arguments { + q: string; +} + +export type ExpressionFunctionKql = ExpressionFunctionDefinition< + 'kql', + null, + Arguments, + KibanaQueryOutput +>; + +export const kqlFunction: ExpressionFunctionKql = { + name: 'kql', + type: 'kibana_query', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.kql.help', { + defaultMessage: 'Create kibana kql query', + }), + args: { + q: { + types: ['string'], + required: true, + aliases: ['query', '_'], + help: i18n.translate('data.search.functions.kql.q.help', { + defaultMessage: 'Specify Kibana KQL free form text query', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_query', + language: 'kuery', + query: args.q, + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/lucene.test.ts b/src/plugins/data/common/search/expressions/lucene.test.ts new file mode 100644 index 0000000000000..d0b26aad98ed8 --- /dev/null +++ b/src/plugins/data/common/search/expressions/lucene.test.ts @@ -0,0 +1,43 @@ +/* + * 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 { ExpressionValueSearchContext } from './kibana_context_type'; +import { functionWrapper } from './utils'; +import { luceneFunction } from './lucene'; + +describe('interpreter/functions#lucene', () => { + const fn = functionWrapper(luceneFunction); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(input, { q: '{ "test": 1 }' }, context); + expect(actual).toMatchInlineSnapshot(` + Object { + "language": "lucene", + "query": Object { + "test": 1, + }, + "type": "kibana_query", + } + `); + }); +}); diff --git a/src/plugins/data/common/search/expressions/lucene.ts b/src/plugins/data/common/search/expressions/lucene.ts new file mode 100644 index 0000000000000..a00ff7ed5f447 --- /dev/null +++ b/src/plugins/data/common/search/expressions/lucene.ts @@ -0,0 +1,49 @@ +/* + * 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 { KibanaQueryOutput } from './kibana_context_type'; + +interface Arguments { + q: string; +} + +export type ExpressionFunctionLucene = ExpressionFunctionDefinition< + 'lucene', + null, + Arguments, + KibanaQueryOutput +>; + +export const luceneFunction: ExpressionFunctionLucene = { + name: 'lucene', + type: 'kibana_query', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.lucene.help', { + defaultMessage: 'Create kibana lucene query', + }), + args: { + q: { + types: ['string'], + required: true, + aliases: ['query', '_'], + help: i18n.translate('data.search.functions.lucene.q.help', { + defaultMessage: 'Specify Lucene free form text query', + }), + }, + }, + + fn(input, args) { + return { + type: 'kibana_query', + language: 'lucene', + query: JSON.parse(args.q), + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/query_to_ast.test.ts b/src/plugins/data/common/search/expressions/query_to_ast.test.ts new file mode 100644 index 0000000000000..4b9c97e99e7c7 --- /dev/null +++ b/src/plugins/data/common/search/expressions/query_to_ast.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { queryToAst } from './query_to_ast'; + +describe('queryToAst', () => { + it('returns an object with the correct structure for lucene queies', () => { + const actual = queryToAst({ language: 'lucene', query: { country: 'US' } }); + expect(actual).toHaveProperty('functions'); + expect(actual.functions[0]).toHaveProperty('name', 'lucene'); + expect(actual.functions[0]).toHaveProperty('arguments', { + q: ['{"country":"US"}'], + }); + }); + + it('returns an object with the correct structure for kql queies', () => { + const actual = queryToAst({ language: 'kuery', query: 'country:US' }); + expect(actual).toHaveProperty('functions'); + expect(actual.functions[0]).toHaveProperty('name', 'kql'); + expect(actual.functions[0]).toHaveProperty('arguments', { + q: ['country:US'], + }); + }); +}); diff --git a/src/plugins/data/common/search/expressions/query_to_ast.ts b/src/plugins/data/common/search/expressions/query_to_ast.ts new file mode 100644 index 0000000000000..a9a6583f566c8 --- /dev/null +++ b/src/plugins/data/common/search/expressions/query_to_ast.ts @@ -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 { Query } from '../../query'; +import { ExpressionFunctionKql } from './kql'; +import { ExpressionFunctionLucene } from './lucene'; + +export const queryToAst = (query: Query) => { + if (query.language === 'kuery') { + return buildExpression([ + buildExpressionFunction('kql', { q: query.query as string }), + ]); + } + return buildExpression([ + buildExpressionFunction('lucene', { q: JSON.stringify(query.query) }), + ]); +}; diff --git a/src/plugins/data/common/search/expressions/timerange.test.ts b/src/plugins/data/common/search/expressions/timerange.test.ts new file mode 100644 index 0000000000000..ae461b482e182 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange.test.ts @@ -0,0 +1,44 @@ +/* + * 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 { ExpressionValueSearchContext } from './kibana_context_type'; +import { functionWrapper } from './utils'; +import { kibanaTimerangeFunction } from './timerange'; + +describe('interpreter/functions#timerange', () => { + const fn = functionWrapper(kibanaTimerangeFunction); + let input: Partial; + let context: ExecutionContext; + + beforeEach(() => { + input = { timeRange: { from: '0', to: '1' } }; + context = { + getSearchContext: () => ({}), + getSearchSessionId: () => undefined, + types: {}, + variables: {}, + abortSignal: {} as any, + inspectorAdapters: {} as any, + }; + }); + + it('returns an object with the correct structure', () => { + const actual = fn(input, { from: 'now', to: 'now-7d', mode: 'absolute' }, context); + expect(actual).toMatchInlineSnapshot( + ` + Object { + "from": "now", + "mode": "absolute", + "to": "now-7d", + "type": "timerange", + } + ` + ); + }); +}); diff --git a/src/plugins/data/common/search/expressions/timerange.ts b/src/plugins/data/common/search/expressions/timerange.ts new file mode 100644 index 0000000000000..ed09bab629519 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange.ts @@ -0,0 +1,61 @@ +/* + * 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, ExpressionValueBoxed } from 'src/plugins/expressions/common'; +import { TimeRange } from '../../query'; + +export type KibanaTimerangeOutput = ExpressionValueBoxed<'timerange', TimeRange>; + +export type ExpressionFunctionKibanaTimerange = ExpressionFunctionDefinition< + 'timerange', + null, + TimeRange, + KibanaTimerangeOutput +>; + +export const kibanaTimerangeFunction: ExpressionFunctionKibanaTimerange = { + name: 'timerange', + type: 'timerange', + inputTypes: ['null'], + help: i18n.translate('data.search.functions.timerange.help', { + defaultMessage: 'Create kibana timerange', + }), + args: { + from: { + types: ['string'], + required: true, + help: i18n.translate('data.search.functions.timerange.from.help', { + defaultMessage: 'Specify the start date', + }), + }, + to: { + types: ['string'], + required: true, + help: i18n.translate('data.search.functions.timerange.to.help', { + defaultMessage: 'Specify the end date', + }), + }, + mode: { + types: ['string'], + options: ['absolute', 'relative'], + help: i18n.translate('data.search.functions.timerange.mode.help', { + defaultMessage: 'Specify the mode (absolute or relative)', + }), + }, + }, + + fn(input, args) { + return { + type: 'timerange', + from: args.from, + to: args.to, + mode: args.mode, + }; + }, +}; diff --git a/src/plugins/data/common/search/expressions/timerange_to_ast.test.ts b/src/plugins/data/common/search/expressions/timerange_to_ast.test.ts new file mode 100644 index 0000000000000..12ba1e012bb65 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange_to_ast.test.ts @@ -0,0 +1,21 @@ +/* + * 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 { timerangeToAst } from './timerange_to_ast'; + +describe('timerangeToAst', () => { + it('returns an object with the correct structure', () => { + const actual = timerangeToAst({ from: 'now', to: 'now-7d', mode: 'absolute' }); + expect(actual).toHaveProperty('name', 'timerange'); + expect(actual).toHaveProperty('arguments', { + from: ['now'], + mode: ['absolute'], + to: ['now-7d'], + }); + }); +}); diff --git a/src/plugins/data/common/search/expressions/timerange_to_ast.ts b/src/plugins/data/common/search/expressions/timerange_to_ast.ts new file mode 100644 index 0000000000000..ad66c12e68c83 --- /dev/null +++ b/src/plugins/data/common/search/expressions/timerange_to_ast.ts @@ -0,0 +1,19 @@ +/* + * 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 { buildExpressionFunction } from '../../../../expressions/common'; +import { TimeRange } from '../../query'; +import { ExpressionFunctionKibanaTimerange } from './timerange'; + +export const timerangeToAst = (timerange: TimeRange) => { + return buildExpressionFunction('timerange', { + from: timerange.from, + to: timerange.to, + mode: timerange.mode, + }); +}; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index 6522cae3e044f..8eb73ba62244f 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -25,6 +25,9 @@ import { ISearchGeneric, SearchSourceDependencies, SearchSourceService, + kibanaTimerangeFunction, + luceneFunction, + kqlFunction, } from '../../common/search'; import { getCallMsearch } from './legacy'; import { AggsService, AggsStartDependencies } from './aggs'; @@ -102,6 +105,9 @@ export class SearchService implements Plugin { ); expressions.registerFunction(kibana); expressions.registerFunction(kibanaContextFunction); + expressions.registerFunction(luceneFunction); + expressions.registerFunction(kqlFunction); + expressions.registerFunction(kibanaTimerangeFunction); expressions.registerType(kibanaContext); expressions.registerFunction(esdsl); diff --git a/src/plugins/data/server/search/search_service.ts b/src/plugins/data/server/search/search_service.ts index 6ece8ff945468..c978f40caa281 100644 --- a/src/plugins/data/server/search/search_service.ts +++ b/src/plugins/data/server/search/search_service.ts @@ -52,6 +52,9 @@ import { kibana, kibanaContext, kibanaContextFunction, + kibanaTimerangeFunction, + kqlFunction, + luceneFunction, SearchSourceDependencies, searchSourceRequiredUiSettings, SearchSourceService, @@ -142,6 +145,9 @@ export class SearchService implements Plugin { expressions.registerFunction(getEsaggs({ getStartServices: core.getStartServices })); expressions.registerFunction(kibana); + expressions.registerFunction(luceneFunction); + expressions.registerFunction(kqlFunction); + expressions.registerFunction(kibanaTimerangeFunction); expressions.registerFunction(kibanaContextFunction); expressions.registerType(kibanaContext); diff --git a/src/plugins/visualizations/public/embeddable/to_ast.ts b/src/plugins/visualizations/public/embeddable/to_ast.ts index 5436b78c1b71f..7ccff9394943a 100644 --- a/src/plugins/visualizations/public/embeddable/to_ast.ts +++ b/src/plugins/visualizations/public/embeddable/to_ast.ts @@ -10,6 +10,7 @@ import { ExpressionFunctionKibana, ExpressionFunctionKibanaContext } from '../.. import { buildExpression, buildExpressionFunction } from '../../../expressions/public'; import { VisToExpressionAst } from '../types'; +import { queryToAst } from '../../../data/common'; /** * Creates an ast expression for a visualization based on kibana context (query, filters, timerange) @@ -25,7 +26,7 @@ export const toExpressionAst: VisToExpressionAst = async (vis, params) => { const kibana = buildExpressionFunction('kibana', {}); const kibanaContext = buildExpressionFunction('kibana_context', { - q: query && JSON.stringify(query), + q: query && queryToAst(query), filters: filters && JSON.stringify(filters), savedSearchId, }); diff --git a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts index 7f6e9a6439165..f71fa58cd7cc5 100644 --- a/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts +++ b/test/interpreter_functional/test_suites/run_pipeline/esaggs.ts @@ -49,7 +49,7 @@ export default function ({ to: '2015-09-22T00:00:00Z', }; const expression = ` - kibana_context timeRange='${JSON.stringify(timeRange)}' + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} | esaggs index={indexPatternLoad id='logstash-*'} aggs={aggCount id="1" enabled=true schema="metric"} `; @@ -63,7 +63,7 @@ export default function ({ to: '2015-09-22T00:00:00Z', }; const expression = ` - kibana_context timeRange='${JSON.stringify(timeRange)}' + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} | esaggs index={indexPatternLoad id='logstash-*'} timeFields='relatedContent.article:published_time' aggs={aggCount id="1" enabled=true schema="metric"} @@ -78,7 +78,7 @@ export default function ({ to: '2015-09-22T00:00:00Z', }; const expression = ` - kibana_context timeRange='${JSON.stringify(timeRange)}' + kibana_context timeRange={timerange from='${timeRange.from}' to='${timeRange.to}'} | esaggs index={indexPatternLoad id='logstash-*'} timeFields='relatedContent.article:published_time' timeFields='@timestamp'