From 3c745149b42a0bc301e5ee6515b9d66b9b9dede0 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 22 May 2019 15:22:03 +0200 Subject: [PATCH 01/10] start work on expression executor service --- .../expression_executor_service.ts | 137 ++++++++++++++++++ .../data/public/expression_executor/index.ts | 20 +++ src/legacy/core_plugins/data/public/index.ts | 21 +++ .../loader/pipeline_helpers/run_pipeline.ts | 6 +- 4 files changed, 181 insertions(+), 3 deletions(-) create mode 100644 src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts create mode 100644 src/legacy/core_plugins/data/public/expression_executor/index.ts diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts new file mode 100644 index 0000000000000..a5fb73af45a47 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts @@ -0,0 +1,137 @@ +/* + * 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 { CoreSetup } from 'kibana/public'; + +// @ts-ignore +import { fromExpression } from '@kbn/interpreter/common'; +const typedFromExpression = fromExpression as (expression: string) => Ast; + +// this type import and the types below them should be switched to the types of +// the interpreter plugin itself once they are ready +import { RunPipelineHandlers } from 'ui/visualize/loader/pipeline_helpers/run_pipeline'; +import { Registry } from '@kbn/interpreter/common'; +import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; + +type Ast = unknown; +type Context = object; +type Handlers = RunPipelineHandlers; +interface Result { + type: string; + as?: string; + value: unknown; +} + +interface RenderHandlers { + done: () => void; + onDestroy: (fn: () => void) => void; +} + +interface RenderFunction { + name: string; + displayName: string; + help: string; + validate: () => void; + reuseDomNode: boolean; + render: (domNode: Element, data: unknown, handlers: RenderHandlers) => void; +} + +type RenderFunctionsRegistry = Registry; + +interface Interpreter { + interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise; +} + +interface ExpressionExecutorSetupPlugins { + // TODO this types will be provided by the interpreter plugin itself + // once it's ready + interpreter: { + renderersRegistry: RenderFunctionsRegistry; + getInterpreter: () => Promise<{ interpreter: Interpreter }>; + }; +} + +async function runFn( + expressionOrAst: string | Ast, + element: Element, + renderersRegistry: RenderFunctionsRegistry, + interpreter: Interpreter +) { + const ast = + typeof expressionOrAst === 'string' ? typedFromExpression(expressionOrAst) : expressionOrAst; + const response = await interpreter.interpretAst( + ast, + { type: 'null' }, + { + getInitialContext: () => ({}), + inspectorAdapters: { + requests: new RequestAdapter(), + data: new DataAdapter(), + }, + } + ); + + if (response.type === 'render' && response.as) { + renderersRegistry.get(response.as).render(element, response.value, { + onDestroy: fn => { + // TODO implement + }, + done: () => { + // TODO implement + }, + }); + } else { + // eslint-disable-next-line no-console + console.log('Unexpected result of expression', response); + } + + return response; +} + +/** + * Expression Executor Service + * @internal + */ +export class ExpressionExecutorService { + private interpreterInstance: Interpreter | null = null; + + // TODO core won't ever be null once this is switched to the new platform + public setup(core: CoreSetup | null, plugins: ExpressionExecutorSetupPlugins) { + return { + run: async (expressionOrAst: string | Ast, element: Element) => { + if (!this.interpreterInstance) { + this.interpreterInstance = (await plugins.interpreter.getInterpreter()).interpreter; + } + return await runFn( + expressionOrAst, + element, + plugins.interpreter.renderersRegistry, + this.interpreterInstance + ); + }, + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type ExpressionExecutorSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/expression_executor/index.ts b/src/legacy/core_plugins/data/public/expression_executor/index.ts new file mode 100644 index 0000000000000..f34fc2168c7b5 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expression_executor/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { ExpressionExecutorService, ExpressionExecutorSetup } from './expression_executor_service'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index dc3092fd286d6..a8fdc468fa010 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -17,6 +17,16 @@ * under the License. */ +// TODO these are imports from the old plugin world. +// Once the new platform is ready, they can get removed +// and handled by the platform itself in the setup method +// of the ExpressionExectorService +// @ts-ignore +import { getInterpreter } from 'plugins/interpreter/interpreter'; +// @ts-ignore +import { renderersRegistry } from 'plugins/interpreter/registries'; +import { ExpressionExecutorService } from './expression_executor'; + import { SearchBarService } from './search_bar'; import { QueryBarService } from './query_bar'; import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; @@ -25,11 +35,13 @@ class DataPlugin { private readonly indexPatterns: IndexPatternsService; private readonly searchBar: SearchBarService; private readonly queryBar: QueryBarService; + private readonly expressionExecutor: ExpressionExecutorService; constructor() { this.indexPatterns = new IndexPatternsService(); this.queryBar = new QueryBarService(); this.searchBar = new SearchBarService(); + this.expressionExecutor = new ExpressionExecutorService(); } public setup() { @@ -37,6 +49,12 @@ class DataPlugin { indexPatterns: this.indexPatterns.setup(), search: this.searchBar.setup(), query: this.queryBar.setup(), + expressionExecutor: this.expressionExecutor.setup(null, { + interpreter: { + getInterpreter, + renderersRegistry, + }, + }), }; } @@ -44,6 +62,7 @@ class DataPlugin { this.indexPatterns.stop(); this.searchBar.stop(); this.queryBar.stop(); + this.expressionExecutor.stop(); } } @@ -59,5 +78,7 @@ export interface DataSetup { indexPatterns: IndexPatternsSetup; } +export { ExpressionExecutorSetup } from './expression_executor'; + /** @public types */ export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns'; diff --git a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts index 459d9dd6a6932..6e08bde332147 100644 --- a/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts +++ b/src/legacy/ui/public/visualize/loader/pipeline_helpers/run_pipeline.ts @@ -25,15 +25,15 @@ import { getInterpreter } from 'plugins/interpreter/interpreter'; import { Adapters } from 'ui/inspector'; import { Filters, Query, TimeRange } from 'ui/visualize'; -interface InitialContextObject { +export interface InitialContextObject { timeRange?: TimeRange; filters?: Filters; query?: Query; } -type getInitialContextFunction = () => InitialContextObject; +export type getInitialContextFunction = () => InitialContextObject; -interface RunPipelineHandlers { +export interface RunPipelineHandlers { getInitialContext: getInitialContextFunction; inspectorAdapters?: Adapters; } From af85958018ef1265536627075374fc5d622a03e7 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Wed, 22 May 2019 18:02:09 +0200 Subject: [PATCH 02/10] add react component encapsulating the same thing --- ...ice.ts => expression_executor_service.tsx} | 41 +++++++++++++++---- 1 file changed, 32 insertions(+), 9 deletions(-) rename src/legacy/core_plugins/data/public/expression_executor/{expression_executor_service.ts => expression_executor_service.tsx} (79%) diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx similarity index 79% rename from src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts rename to src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx index a5fb73af45a47..bd810a4862f15 100644 --- a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.ts +++ b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx @@ -28,6 +28,8 @@ const typedFromExpression = fromExpression as (expression: string) => Ast; import { RunPipelineHandlers } from 'ui/visualize/loader/pipeline_helpers/run_pipeline'; import { Registry } from '@kbn/interpreter/common'; import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; +import { useRef, useEffect } from 'react'; +import React from 'react'; type Ast = unknown; type Context = object; @@ -113,16 +115,37 @@ export class ExpressionExecutorService { // TODO core won't ever be null once this is switched to the new platform public setup(core: CoreSetup | null, plugins: ExpressionExecutorSetupPlugins) { + const run = async (expressionOrAst: string | Ast, element: Element) => { + if (!this.interpreterInstance) { + this.interpreterInstance = (await plugins.interpreter.getInterpreter()).interpreter; + } + return await runFn( + expressionOrAst, + element, + plugins.interpreter.renderersRegistry, + this.interpreterInstance + ); + }; return { - run: async (expressionOrAst: string | Ast, element: Element) => { - if (!this.interpreterInstance) { - this.interpreterInstance = (await plugins.interpreter.getInterpreter()).interpreter; - } - return await runFn( - expressionOrAst, - element, - plugins.interpreter.renderersRegistry, - this.interpreterInstance + run, + ExpressionRenderer({ expression }: { expression: string }) { + const mountpoint: React.MutableRefObject = useRef(null); + + useEffect( + () => { + if (mountpoint.current) { + run(expression, mountpoint.current); + } + }, + [expression, mountpoint.current] + ); + + return ( +
{ + mountpoint.current = el; + }} + /> ); }, }; From 935b00866a92c79c9040a516785c9799a04ef63b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 23 May 2019 12:49:10 +0200 Subject: [PATCH 03/10] add tests and improve typings and docs --- .../kbn-interpreter/src/common/index.d.ts | 2 + .../kbn-interpreter/src/common/lib/ast.d.ts | 22 +++ .../expression_executor_service.test.tsx | 180 ++++++++++++++++++ .../expression_executor_service.tsx | 41 ++-- 4 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 packages/kbn-interpreter/src/common/lib/ast.d.ts create mode 100644 src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx diff --git a/packages/kbn-interpreter/src/common/index.d.ts b/packages/kbn-interpreter/src/common/index.d.ts index b41f9f318a13b..a8917b7a65df1 100644 --- a/packages/kbn-interpreter/src/common/index.d.ts +++ b/packages/kbn-interpreter/src/common/index.d.ts @@ -18,3 +18,5 @@ */ export { Registry } from './lib/registry'; + +export { fromExpression, Ast } from './lib/ast'; diff --git a/packages/kbn-interpreter/src/common/lib/ast.d.ts b/packages/kbn-interpreter/src/common/lib/ast.d.ts new file mode 100644 index 0000000000000..2b0328bda9392 --- /dev/null +++ b/packages/kbn-interpreter/src/common/lib/ast.d.ts @@ -0,0 +1,22 @@ +/* + * 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 type Ast = unknown; + +export declare function fromExpression(expression: string): Ast; diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx new file mode 100644 index 0000000000000..95db032fd41b2 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx @@ -0,0 +1,180 @@ +/* + * 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 { fromExpression, Ast } from '@kbn/interpreter/common'; + +import { + ExpressionExecutorService, + RenderFunctionsRegistry, + RenderFunction, + Interpreter, + ExpressionExecutorSetupPlugins, + Result, + ExpressionExecutorSetup, +} from './expression_executor_service'; +import { mount } from 'enzyme'; +import React from 'react'; + +const waitForInterpreterRun = async () => { + // Wait for two ticks with empty callback queues + // This makes sure the runFn promise and actual interpretAst + // promise have been resolved and processed + await new Promise(resolve => setTimeout(resolve)); + await new Promise(resolve => setTimeout(resolve)); +}; + +describe('expression_executor_service', () => { + let interpreterMock: jest.Mocked; + let renderFunctionMock: jest.Mocked; + let setupPluginsMock: ExpressionExecutorSetupPlugins; + const expressionResult: Result = { type: 'render', as: 'abc', value: {} }; + + let api: ExpressionExecutorSetup; + let testExpression: string; + let testAst: Ast; + + beforeEach(() => { + interpreterMock = { interpretAst: jest.fn(_ => Promise.resolve(expressionResult)) }; + renderFunctionMock = ({ + render: jest.fn(), + } as unknown) as jest.Mocked; + setupPluginsMock = { + interpreter: { + getInterpreter: () => Promise.resolve({ interpreter: interpreterMock }), + renderersRegistry: ({ + get: () => renderFunctionMock, + } as unknown) as RenderFunctionsRegistry, + }, + }; + api = new ExpressionExecutorService().setup(null, setupPluginsMock); + testExpression = 'test | expression'; + testAst = fromExpression(testExpression); + }); + + it('should return run function', () => { + expect(typeof api.run).toBe('function'); + }); + + it('should call the interpreter with parsed expression', async () => { + await api.run(testExpression, document.createElement('div')); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call the interpreter with passed in ast', async () => { + await api.run(testAst, document.createElement('div')); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call the render function with the result and element', async () => { + const element = document.createElement('div'); + + await api.run(testAst, element); + expect(renderFunctionMock.render).toHaveBeenCalledWith( + element, + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call interpreter and render function when called through react component', async () => { + const ExpressionRenderer = api.ExpressionRenderer; + + mount(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call interpreter and render function again if expression changes', async () => { + const ExpressionRenderer = api.ExpressionRenderer; + + const instance = mount(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + + instance.setProps({ expression: 'supertest | expression ' }); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledTimes(2); + expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(2); + }); + + it('should not call interpreter and render function again if expression does not change', async () => { + const ast = fromExpression(testExpression); + + const ExpressionRenderer = api.ExpressionRenderer; + + const instance = mount(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + ast, + expect.anything(), + expect.anything() + ); + + instance.update(); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledTimes(1); + expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx index bd810a4862f15..d970b3b9315bb 100644 --- a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx +++ b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx @@ -18,26 +18,23 @@ */ import { CoreSetup } from 'kibana/public'; +import { useRef, useEffect } from 'react'; +import React from 'react'; -// @ts-ignore -import { fromExpression } from '@kbn/interpreter/common'; -const typedFromExpression = fromExpression as (expression: string) => Ast; +import { fromExpression, Ast } from '@kbn/interpreter/common'; // this type import and the types below them should be switched to the types of // the interpreter plugin itself once they are ready import { RunPipelineHandlers } from 'ui/visualize/loader/pipeline_helpers/run_pipeline'; import { Registry } from '@kbn/interpreter/common'; import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; -import { useRef, useEffect } from 'react'; -import React from 'react'; -type Ast = unknown; type Context = object; type Handlers = RunPipelineHandlers; -interface Result { +export interface Result { type: string; as?: string; - value: unknown; + value?: unknown; } interface RenderHandlers { @@ -45,7 +42,7 @@ interface RenderHandlers { onDestroy: (fn: () => void) => void; } -interface RenderFunction { +export interface RenderFunction { name: string; displayName: string; help: string; @@ -54,15 +51,13 @@ interface RenderFunction { render: (domNode: Element, data: unknown, handlers: RenderHandlers) => void; } -type RenderFunctionsRegistry = Registry; +export type RenderFunctionsRegistry = Registry; -interface Interpreter { +export interface Interpreter { interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise; } -interface ExpressionExecutorSetupPlugins { - // TODO this types will be provided by the interpreter plugin itself - // once it's ready +export interface ExpressionExecutorSetupPlugins { interpreter: { renderersRegistry: RenderFunctionsRegistry; getInterpreter: () => Promise<{ interpreter: Interpreter }>; @@ -76,13 +71,14 @@ async function runFn( interpreter: Interpreter ) { const ast = - typeof expressionOrAst === 'string' ? typedFromExpression(expressionOrAst) : expressionOrAst; + typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; const response = await interpreter.interpretAst( ast, { type: 'null' }, { getInitialContext: () => ({}), inspectorAdapters: { + // TODO connect real adapters requests: new RequestAdapter(), data: new DataAdapter(), }, @@ -114,7 +110,13 @@ export class ExpressionExecutorService { private interpreterInstance: Interpreter | null = null; // TODO core won't ever be null once this is switched to the new platform - public setup(core: CoreSetup | null, plugins: ExpressionExecutorSetupPlugins) { + public setup(_core: CoreSetup | null, plugins: ExpressionExecutorSetupPlugins) { + /** + * Executes the given expression string or ast and renders the result into the + * given DOM element. + * @param expressionOrAst + * @param element + */ const run = async (expressionOrAst: string | Ast, element: Element) => { if (!this.interpreterInstance) { this.interpreterInstance = (await plugins.interpreter.getInterpreter()).interpreter; @@ -128,6 +130,13 @@ export class ExpressionExecutorService { }; return { run, + /** + * Component which executes and renders the given expression in a div element. + * The expression is re-executed on updating the props. + * + * This is a React bridge of the `run` method + * @param props + */ ExpressionRenderer({ expression }: { expression: string }) { const mountpoint: React.MutableRefObject = useRef(null); From 0d99f0a5f08dbf22d957538e05d750598cef7956 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 23 May 2019 13:08:19 +0200 Subject: [PATCH 04/10] add experimental notice --- .../expression_executor/expression_executor_service.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx index d970b3b9315bb..d801b84dd9fdd 100644 --- a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx +++ b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx @@ -112,8 +112,13 @@ export class ExpressionExecutorService { // TODO core won't ever be null once this is switched to the new platform public setup(_core: CoreSetup | null, plugins: ExpressionExecutorSetupPlugins) { /** + * **experimential** This API is experimential and might be removed in the future + * without notice + * * Executes the given expression string or ast and renders the result into the * given DOM element. + * + * * @param expressionOrAst * @param element */ @@ -131,6 +136,9 @@ export class ExpressionExecutorService { return { run, /** + * **experimential** This API is experimential and might be removed in the future + * without notice + * * Component which executes and renders the given expression in a div element. * The expression is re-executed on updating the props. * From 37a6d5510feb9b215c405fa3c1eaaeae54d290ab Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 23 May 2019 13:45:48 +0200 Subject: [PATCH 05/10] add expression executor to data plugin setup type --- src/legacy/core_plugins/data/public/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index a8fdc468fa010..a965f2229b705 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -25,7 +25,7 @@ import { getInterpreter } from 'plugins/interpreter/interpreter'; // @ts-ignore import { renderersRegistry } from 'plugins/interpreter/registries'; -import { ExpressionExecutorService } from './expression_executor'; +import { ExpressionExecutorService, ExpressionExecutorSetup } from './expression_executor'; import { SearchBarService } from './search_bar'; import { QueryBarService } from './query_bar'; @@ -76,6 +76,7 @@ export const data = new DataPlugin().setup(); /** @public */ export interface DataSetup { indexPatterns: IndexPatternsSetup; + expressionExecutor: ExpressionExecutorSetup; } export { ExpressionExecutorSetup } from './expression_executor'; From 802b6f17e1b3adcd1c1b8a562545b0ad16667b61 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Thu, 23 May 2019 18:46:48 +0200 Subject: [PATCH 06/10] Review fixes --- .../expression_executor_service.tsx | 177 ------------------ .../expressions/expression_renderer.tsx | 50 +++++ .../public/expressions/expression_runner.ts | 65 +++++++ .../expressions_service.test.tsx} | 14 +- .../public/expressions/expressions_service.ts | 113 +++++++++++ .../index.ts | 4 +- src/legacy/core_plugins/data/public/index.ts | 15 +- 7 files changed, 246 insertions(+), 192 deletions(-) delete mode 100644 src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx create mode 100644 src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx create mode 100644 src/legacy/core_plugins/data/public/expressions/expression_runner.ts rename src/legacy/core_plugins/data/public/{expression_executor/expression_executor_service.test.tsx => expressions/expressions_service.test.tsx} (94%) create mode 100644 src/legacy/core_plugins/data/public/expressions/expressions_service.ts rename src/legacy/core_plugins/data/public/{expression_executor => expressions}/index.ts (78%) diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx b/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx deleted file mode 100644 index d801b84dd9fdd..0000000000000 --- a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.tsx +++ /dev/null @@ -1,177 +0,0 @@ -/* - * 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 { CoreSetup } from 'kibana/public'; -import { useRef, useEffect } from 'react'; -import React from 'react'; - -import { fromExpression, Ast } from '@kbn/interpreter/common'; - -// this type import and the types below them should be switched to the types of -// the interpreter plugin itself once they are ready -import { RunPipelineHandlers } from 'ui/visualize/loader/pipeline_helpers/run_pipeline'; -import { Registry } from '@kbn/interpreter/common'; -import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; - -type Context = object; -type Handlers = RunPipelineHandlers; -export interface Result { - type: string; - as?: string; - value?: unknown; -} - -interface RenderHandlers { - done: () => void; - onDestroy: (fn: () => void) => void; -} - -export interface RenderFunction { - name: string; - displayName: string; - help: string; - validate: () => void; - reuseDomNode: boolean; - render: (domNode: Element, data: unknown, handlers: RenderHandlers) => void; -} - -export type RenderFunctionsRegistry = Registry; - -export interface Interpreter { - interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise; -} - -export interface ExpressionExecutorSetupPlugins { - interpreter: { - renderersRegistry: RenderFunctionsRegistry; - getInterpreter: () => Promise<{ interpreter: Interpreter }>; - }; -} - -async function runFn( - expressionOrAst: string | Ast, - element: Element, - renderersRegistry: RenderFunctionsRegistry, - interpreter: Interpreter -) { - const ast = - typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; - const response = await interpreter.interpretAst( - ast, - { type: 'null' }, - { - getInitialContext: () => ({}), - inspectorAdapters: { - // TODO connect real adapters - requests: new RequestAdapter(), - data: new DataAdapter(), - }, - } - ); - - if (response.type === 'render' && response.as) { - renderersRegistry.get(response.as).render(element, response.value, { - onDestroy: fn => { - // TODO implement - }, - done: () => { - // TODO implement - }, - }); - } else { - // eslint-disable-next-line no-console - console.log('Unexpected result of expression', response); - } - - return response; -} - -/** - * Expression Executor Service - * @internal - */ -export class ExpressionExecutorService { - private interpreterInstance: Interpreter | null = null; - - // TODO core won't ever be null once this is switched to the new platform - public setup(_core: CoreSetup | null, plugins: ExpressionExecutorSetupPlugins) { - /** - * **experimential** This API is experimential and might be removed in the future - * without notice - * - * Executes the given expression string or ast and renders the result into the - * given DOM element. - * - * - * @param expressionOrAst - * @param element - */ - const run = async (expressionOrAst: string | Ast, element: Element) => { - if (!this.interpreterInstance) { - this.interpreterInstance = (await plugins.interpreter.getInterpreter()).interpreter; - } - return await runFn( - expressionOrAst, - element, - plugins.interpreter.renderersRegistry, - this.interpreterInstance - ); - }; - return { - run, - /** - * **experimential** This API is experimential and might be removed in the future - * without notice - * - * Component which executes and renders the given expression in a div element. - * The expression is re-executed on updating the props. - * - * This is a React bridge of the `run` method - * @param props - */ - ExpressionRenderer({ expression }: { expression: string }) { - const mountpoint: React.MutableRefObject = useRef(null); - - useEffect( - () => { - if (mountpoint.current) { - run(expression, mountpoint.current); - } - }, - [expression, mountpoint.current] - ); - - return ( -
{ - mountpoint.current = el; - }} - /> - ); - }, - }; - } - - public stop() { - // nothing to do here yet - } -} - -/** @public */ -export type ExpressionExecutorSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx new file mode 100644 index 0000000000000..0865a68766318 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -0,0 +1,50 @@ +/* + * 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 { useRef, useEffect } from 'react'; +import React from 'react'; + +export interface ExpressionRendererProps { + expression: string; +} + +export type ExpressionRenderer = React.FC; + +export const createRenderer = ( + run: (expression: string, element: Element) => void +): ExpressionRenderer => ({ expression }: ExpressionRendererProps) => { + const mountpoint: React.MutableRefObject = useRef(null); + + useEffect( + () => { + if (mountpoint.current) { + run(expression, mountpoint.current); + } + }, + [expression, mountpoint.current] + ); + + return ( +
{ + mountpoint.current = el; + }} + /> + ); +}; diff --git a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts new file mode 100644 index 0000000000000..f393950529d61 --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts @@ -0,0 +1,65 @@ +/* + * 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 { Ast, fromExpression } from '@kbn/interpreter/common'; + +import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; +import { RenderFunctionsRegistry, Interpreter, Result } from './expressions_service'; + +export type ExpressionRunner = ( + expressionOrAst: string | Ast, + element?: Element +) => Promise; + +export const createRunFn = ( + renderersRegistry: RenderFunctionsRegistry, + interpreterPromise: Promise +): ExpressionRunner => async (expressionOrAst, element) => { + const interpreter = await interpreterPromise; + const ast = + typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; + const response = await interpreter.interpretAst( + ast, + { type: 'null' }, + { + getInitialContext: () => ({}), + inspectorAdapters: { + // TODO connect real adapters + requests: new RequestAdapter(), + data: new DataAdapter(), + }, + } + ); + + if (element && response.type === 'render' && response.as) { + renderersRegistry.get(response.as).render(element, response.value, { + onDestroy: fn => { + // TODO implement + }, + done: () => { + // TODO implement + }, + }); + } else { + // eslint-disable-next-line no-console + console.log('Unexpected result of expression', response); + } + + return response; +}; diff --git a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx similarity index 94% rename from src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx rename to src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx index 95db032fd41b2..6316c392a7b2c 100644 --- a/src/legacy/core_plugins/data/public/expression_executor/expression_executor_service.test.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx @@ -20,14 +20,14 @@ import { fromExpression, Ast } from '@kbn/interpreter/common'; import { - ExpressionExecutorService, + ExpressionsService, RenderFunctionsRegistry, RenderFunction, Interpreter, - ExpressionExecutorSetupPlugins, + ExpressionsServiceDependencies, Result, - ExpressionExecutorSetup, -} from './expression_executor_service'; + ExpressionsSetup, +} from './expressions_service'; import { mount } from 'enzyme'; import React from 'react'; @@ -42,10 +42,10 @@ const waitForInterpreterRun = async () => { describe('expression_executor_service', () => { let interpreterMock: jest.Mocked; let renderFunctionMock: jest.Mocked; - let setupPluginsMock: ExpressionExecutorSetupPlugins; + let setupPluginsMock: ExpressionsServiceDependencies; const expressionResult: Result = { type: 'render', as: 'abc', value: {} }; - let api: ExpressionExecutorSetup; + let api: ExpressionsSetup; let testExpression: string; let testAst: Ast; @@ -62,7 +62,7 @@ describe('expression_executor_service', () => { } as unknown) as RenderFunctionsRegistry, }, }; - api = new ExpressionExecutorService().setup(null, setupPluginsMock); + api = new ExpressionsService().setup(setupPluginsMock); testExpression = 'test | expression'; testAst = fromExpression(testExpression); }); diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts new file mode 100644 index 0000000000000..2012ce198139a --- /dev/null +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -0,0 +1,113 @@ +/* + * 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 { Ast } from '@kbn/interpreter/common'; + +// TODO: +// this type import and the types below them should be switched to the types of +// the interpreter plugin itself once they are ready +import { RunPipelineHandlers } from 'ui/visualize/loader/pipeline_helpers/run_pipeline'; +import { Registry } from '@kbn/interpreter/common'; +import { createRenderer } from './expression_renderer'; +import { createRunFn } from './expression_runner'; + +type Context = object; +type Handlers = RunPipelineHandlers; +export interface Result { + type: string; + as?: string; + value?: unknown; +} + +interface RenderHandlers { + done: () => void; + onDestroy: (fn: () => void) => void; +} + +export interface RenderFunction { + name: string; + displayName: string; + help: string; + validate: () => void; + reuseDomNode: boolean; + render: (domNode: Element, data: unknown, handlers: RenderHandlers) => void; +} + +export type RenderFunctionsRegistry = Registry; + +export interface Interpreter { + interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise; +} + +type InterpreterGetter = () => Promise<{ interpreter: Interpreter }>; + +export interface ExpressionsServiceDependencies { + interpreter: { + renderersRegistry: RenderFunctionsRegistry; + getInterpreter: InterpreterGetter; + }; +} + +/** + * Expression Executor Service + * @internal + */ +export class ExpressionsService { + public setup({ + interpreter: { renderersRegistry, getInterpreter }, + }: ExpressionsServiceDependencies) { + const run = createRunFn( + renderersRegistry, + getInterpreter().then(({ interpreter }) => interpreter) + ); + + return { + /** + * **experimential** This API is experimential and might be removed in the future + * without notice + * + * Executes the given expression string or ast and renders the result into the + * given DOM element. + * + * + * @param expressionOrAst + * @param element + */ + run, + /** + * **experimential** This API is experimential and might be removed in the future + * without notice + * + * Component which executes and renders the given expression in a div element. + * The expression is re-executed on updating the props. + * + * This is a React bridge of the `run` method + * @param props + */ + ExpressionRenderer: createRenderer(run), + }; + } + + public stop() { + // nothing to do here yet + } +} + +/** @public */ +export type ExpressionsSetup = ReturnType; diff --git a/src/legacy/core_plugins/data/public/expression_executor/index.ts b/src/legacy/core_plugins/data/public/expressions/index.ts similarity index 78% rename from src/legacy/core_plugins/data/public/expression_executor/index.ts rename to src/legacy/core_plugins/data/public/expressions/index.ts index f34fc2168c7b5..fceefce44f81f 100644 --- a/src/legacy/core_plugins/data/public/expression_executor/index.ts +++ b/src/legacy/core_plugins/data/public/expressions/index.ts @@ -17,4 +17,6 @@ * under the License. */ -export { ExpressionExecutorService, ExpressionExecutorSetup } from './expression_executor_service'; +export { ExpressionsService, ExpressionsSetup } from './expressions_service'; +export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer'; +export { ExpressionRunner } from './expression_runner'; diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index b2d285c22cc89..98d9935d8ec97 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -25,7 +25,7 @@ import { getInterpreter } from 'plugins/interpreter/interpreter'; // @ts-ignore import { renderersRegistry } from 'plugins/interpreter/registries'; -import { ExpressionExecutorService, ExpressionExecutorSetup } from './expression_executor'; +import { ExpressionsService, ExpressionsSetup } from './expressions'; import { SearchService, SearchSetup } from './search'; import { QueryService, QuerySetup } from './query'; import { IndexPatternsService, IndexPatternsSetup } from './index_patterns'; @@ -34,13 +34,13 @@ class DataPlugin { private readonly indexPatterns: IndexPatternsService; private readonly search: SearchService; private readonly query: QueryService; - private readonly expressionExecutor: ExpressionExecutorService; + private readonly expressions: ExpressionsService; constructor() { this.indexPatterns = new IndexPatternsService(); this.query = new QueryService(); this.search = new SearchService(); - this.expressionExecutor = new ExpressionExecutorService(); + this.expressions = new ExpressionsService(); } public setup() { @@ -48,7 +48,7 @@ class DataPlugin { indexPatterns: this.indexPatterns.setup(), search: this.search.setup(), query: this.query.setup(), - expressionExecutor: this.expressionExecutor.setup(null, { + expressions: this.expressions.setup({ interpreter: { getInterpreter, renderersRegistry, @@ -61,7 +61,7 @@ class DataPlugin { this.indexPatterns.stop(); this.search.stop(); this.query.stop(); - this.expressionExecutor.stop(); + this.expressions.stop(); } } @@ -75,12 +75,13 @@ export const data = new DataPlugin().setup(); /** @public */ export interface DataSetup { indexPatterns: IndexPatternsSetup; - expressionExecutor: ExpressionExecutorSetup; + expressionExecutor: ExpressionsSetup; search: SearchSetup; query: QuerySetup; } -export { ExpressionExecutorSetup } from './expression_executor'; +/** @public types */ +export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from './expressions'; /** @public types */ export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns'; From ff1d9509088e161b85252c4a64ff25c7715768d4 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 24 May 2019 09:29:53 +0200 Subject: [PATCH 07/10] change old service name --- .../data/public/expressions/expressions_service.test.tsx | 2 +- .../core_plugins/data/public/expressions/expressions_service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx index 6316c392a7b2c..4d53faa2dc976 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx @@ -39,7 +39,7 @@ const waitForInterpreterRun = async () => { await new Promise(resolve => setTimeout(resolve)); }; -describe('expression_executor_service', () => { +describe('expressions_service', () => { let interpreterMock: jest.Mocked; let renderFunctionMock: jest.Mocked; let setupPluginsMock: ExpressionsServiceDependencies; diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts index 2012ce198139a..24d230a93c81a 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -65,7 +65,7 @@ export interface ExpressionsServiceDependencies { } /** - * Expression Executor Service + * Expressions Service * @internal */ export class ExpressionsService { From 0eeceb209205ddb6ce1935ab56d933f937f39d4b Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 24 May 2019 09:32:06 +0200 Subject: [PATCH 08/10] fix types and enforce correctness --- src/legacy/core_plugins/data/public/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/data/public/index.ts b/src/legacy/core_plugins/data/public/index.ts index 98d9935d8ec97..fe695597c4913 100644 --- a/src/legacy/core_plugins/data/public/index.ts +++ b/src/legacy/core_plugins/data/public/index.ts @@ -43,7 +43,7 @@ class DataPlugin { this.expressions = new ExpressionsService(); } - public setup() { + public setup(): DataSetup { return { indexPatterns: this.indexPatterns.setup(), search: this.search.setup(), @@ -75,7 +75,7 @@ export const data = new DataPlugin().setup(); /** @public */ export interface DataSetup { indexPatterns: IndexPatternsSetup; - expressionExecutor: ExpressionsSetup; + expressions: ExpressionsSetup; search: SearchSetup; query: QuerySetup; } From f1a59e0f0d010e5ad8916dea981c9f4592aac18e Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 24 May 2019 11:40:30 +0200 Subject: [PATCH 09/10] make context and getInitialContext configurable --- .../expressions/expression_renderer.tsx | 23 +- .../public/expressions/expression_runner.ts | 60 ++--- .../expressions/expressions_service.test.tsx | 209 +++++++++++------- 3 files changed, 172 insertions(+), 120 deletions(-) diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx index 0865a68766318..80b5bacfc5adb 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -19,22 +19,31 @@ import { useRef, useEffect } from 'react'; import React from 'react'; +import { Ast } from '@kbn/interpreter/common'; -export interface ExpressionRendererProps { - expression: string; -} +import { ExpressionRunnerOptions, ExpressionRunner } from './expression_runner'; + +// Accept all options of the runner as props except for the +// dom element which is provided by the component itself +export type ExpressionRendererProps = Pick< + ExpressionRunnerOptions, + Exclude +> & { + expression: string | Ast; +}; export type ExpressionRenderer = React.FC; -export const createRenderer = ( - run: (expression: string, element: Element) => void -): ExpressionRenderer => ({ expression }: ExpressionRendererProps) => { +export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ + expression, + ...options +}: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); useEffect( () => { if (mountpoint.current) { - run(expression, mountpoint.current); + run(expression, { ...options, element: mountpoint.current }); } }, [expression, mountpoint.current] diff --git a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts index f393950529d61..a5cd4b583b185 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts +++ b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts @@ -22,43 +22,49 @@ import { Ast, fromExpression } from '@kbn/interpreter/common'; import { RequestAdapter, DataAdapter } from 'ui/inspector/adapters'; import { RenderFunctionsRegistry, Interpreter, Result } from './expressions_service'; +export interface ExpressionRunnerOptions { + // TODO use the real types here once they are ready + context?: object; + getInitialContext?: () => object; + element?: Element; +} + export type ExpressionRunner = ( - expressionOrAst: string | Ast, - element?: Element + expression: string | Ast, + options: ExpressionRunnerOptions ) => Promise; export const createRunFn = ( renderersRegistry: RenderFunctionsRegistry, interpreterPromise: Promise -): ExpressionRunner => async (expressionOrAst, element) => { +): ExpressionRunner => async (expressionOrAst, { element, context, getInitialContext }) => { const interpreter = await interpreterPromise; const ast = typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; - const response = await interpreter.interpretAst( - ast, - { type: 'null' }, - { - getInitialContext: () => ({}), - inspectorAdapters: { - // TODO connect real adapters - requests: new RequestAdapter(), - data: new DataAdapter(), - }, - } - ); - if (element && response.type === 'render' && response.as) { - renderersRegistry.get(response.as).render(element, response.value, { - onDestroy: fn => { - // TODO implement - }, - done: () => { - // TODO implement - }, - }); - } else { - // eslint-disable-next-line no-console - console.log('Unexpected result of expression', response); + const response = await interpreter.interpretAst(ast, context || { type: 'null' }, { + getInitialContext: getInitialContext || (() => ({})), + inspectorAdapters: { + // TODO connect real adapters + requests: new RequestAdapter(), + data: new DataAdapter(), + }, + }); + + if (element) { + if (response.type === 'render' && response.as) { + renderersRegistry.get(response.as).render(element, response.value, { + onDestroy: fn => { + // TODO implement + }, + done: () => { + // TODO implement + }, + }); + } else { + // eslint-disable-next-line no-console + console.log('Unexpected result of expression', response); + } } return response; diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx index 4d53faa2dc976..9a464da2731c8 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx @@ -67,114 +67,151 @@ describe('expressions_service', () => { testAst = fromExpression(testExpression); }); - it('should return run function', () => { - expect(typeof api.run).toBe('function'); + describe('expression_runner', () => { + it('should return run function', () => { + expect(typeof api.run).toBe('function'); + }); + + it('should call the interpreter with parsed expression', async () => { + await api.run(testExpression, { element: document.createElement('div') }); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call the interpreter with given context and getInitialContext functions', async () => { + const getInitialContext = () => ({}); + const context = {}; + + await api.run(testExpression, { getInitialContext, context }); + const interpretCall = interpreterMock.interpretAst.mock.calls[0]; + + expect(interpretCall[1]).toBe(context); + expect(interpretCall[2].getInitialContext).toBe(getInitialContext); + }); + + it('should call the interpreter with passed in ast', async () => { + await api.run(testAst, { element: document.createElement('div') }); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); + + it('should call the render function with the result and element', async () => { + const element = document.createElement('div'); + + await api.run(testAst, { element }); + expect(renderFunctionMock.render).toHaveBeenCalledWith( + element, + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); }); - it('should call the interpreter with parsed expression', async () => { - await api.run(testExpression, document.createElement('div')); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); + describe('expression_renderer', () => { + it('should call interpreter and render function when called through react component', async () => { + const ExpressionRenderer = api.ExpressionRenderer; - it('should call the interpreter with passed in ast', async () => { - await api.run(testAst, document.createElement('div')); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); + mount(); - it('should call the render function with the result and element', async () => { - const element = document.createElement('div'); - - await api.run(testAst, element); - expect(renderFunctionMock.render).toHaveBeenCalledWith( - element, - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); + await waitForInterpreterRun(); - it('should call interpreter and render function when called through react component', async () => { - const ExpressionRenderer = api.ExpressionRenderer; + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + }); - mount(); + it('should call the interpreter with given context and getInitialContext functions', async () => { + const getInitialContext = () => ({}); + const context = {}; - await waitForInterpreterRun(); + const ExpressionRenderer = api.ExpressionRenderer; - expect(renderFunctionMock.render).toHaveBeenCalledWith( - expect.any(Element), - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); - }); + mount( + + ); - it('should call interpreter and render function again if expression changes', async () => { - const ExpressionRenderer = api.ExpressionRenderer; + await waitForInterpreterRun(); - const instance = mount(); + const interpretCall = interpreterMock.interpretAst.mock.calls[0]; - await waitForInterpreterRun(); + expect(interpretCall[1]).toBe(context); + expect(interpretCall[2].getInitialContext).toBe(getInitialContext); + }); - expect(renderFunctionMock.render).toHaveBeenCalledWith( - expect.any(Element), - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - testAst, - expect.anything(), - expect.anything() - ); + it('should call interpreter and render function again if expression changes', async () => { + const ExpressionRenderer = api.ExpressionRenderer; - instance.setProps({ expression: 'supertest | expression ' }); + const instance = mount(); - await waitForInterpreterRun(); + await waitForInterpreterRun(); - expect(renderFunctionMock.render).toHaveBeenCalledTimes(2); - expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(2); - }); + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + testAst, + expect.anything(), + expect.anything() + ); + + instance.setProps({ expression: 'supertest | expression ' }); + + await waitForInterpreterRun(); + + expect(renderFunctionMock.render).toHaveBeenCalledTimes(2); + expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(2); + }); - it('should not call interpreter and render function again if expression does not change', async () => { - const ast = fromExpression(testExpression); + it('should not call interpreter and render function again if expression does not change', async () => { + const ast = fromExpression(testExpression); - const ExpressionRenderer = api.ExpressionRenderer; + const ExpressionRenderer = api.ExpressionRenderer; - const instance = mount(); + const instance = mount(); - await waitForInterpreterRun(); + await waitForInterpreterRun(); - expect(renderFunctionMock.render).toHaveBeenCalledWith( - expect.any(Element), - expressionResult.value, - expect.anything() - ); - expect(interpreterMock.interpretAst).toHaveBeenCalledWith( - ast, - expect.anything(), - expect.anything() - ); + expect(renderFunctionMock.render).toHaveBeenCalledWith( + expect.any(Element), + expressionResult.value, + expect.anything() + ); + expect(interpreterMock.interpretAst).toHaveBeenCalledWith( + ast, + expect.anything(), + expect.anything() + ); - instance.update(); + instance.update(); - await waitForInterpreterRun(); + await waitForInterpreterRun(); - expect(renderFunctionMock.render).toHaveBeenCalledTimes(1); - expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(1); + expect(renderFunctionMock.render).toHaveBeenCalledTimes(1); + expect(interpreterMock.interpretAst).toHaveBeenCalledTimes(1); + }); }); }); From a2ac4f6b00b3ac7e947ac7ee347c5474a42f6b8d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Fri, 24 May 2019 11:45:12 +0200 Subject: [PATCH 10/10] import run_pipeline types --- .../public/expressions/expression_runner.ts | 1 + .../public/expressions/expressions_service.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts index a5cd4b583b185..26951ea605bf2 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts +++ b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts @@ -38,6 +38,7 @@ export const createRunFn = ( renderersRegistry: RenderFunctionsRegistry, interpreterPromise: Promise ): ExpressionRunner => async (expressionOrAst, { element, context, getInitialContext }) => { + // TODO: make interpreter initialization synchronous to avoid this const interpreter = await interpreterPromise; const ast = typeof expressionOrAst === 'string' ? fromExpression(expressionOrAst) : expressionOrAst; diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts index 24d230a93c81a..308fd44d6bc08 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -22,13 +22,26 @@ import { Ast } from '@kbn/interpreter/common'; // TODO: // this type import and the types below them should be switched to the types of // the interpreter plugin itself once they are ready -import { RunPipelineHandlers } from 'ui/visualize/loader/pipeline_helpers/run_pipeline'; import { Registry } from '@kbn/interpreter/common'; +import { Adapters } from 'ui/inspector'; +import { Query, Filters, TimeRange } from 'ui/embeddable'; import { createRenderer } from './expression_renderer'; import { createRunFn } from './expression_runner'; +export interface InitialContextObject { + timeRange?: TimeRange; + filters?: Filters; + query?: Query; +} + +export type getInitialContextFunction = () => InitialContextObject; + +export interface Handlers { + getInitialContext: getInitialContextFunction; + inspectorAdapters?: Adapters; +} + type Context = object; -type Handlers = RunPipelineHandlers; export interface Result { type: string; as?: string;