Expressions service (#36885)

This commit is contained in:
Joe Reuter 2019-05-24 14:07:26 +02:00 committed by GitHub
parent 5e59de262b
commit 3232fc80d1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 546 additions and 4 deletions

View file

@ -18,3 +18,5 @@
*/
export { Registry } from './lib/registry';
export { fromExpression, Ast } from './lib/ast';

View file

@ -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;

View file

@ -0,0 +1,59 @@
/*
* 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';
import { Ast } from '@kbn/interpreter/common';
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<keyof ExpressionRunnerOptions, 'element'>
> & {
expression: string | Ast;
};
export type ExpressionRenderer = React.FC<ExpressionRendererProps>;
export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({
expression,
...options
}: ExpressionRendererProps) => {
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
useEffect(
() => {
if (mountpoint.current) {
run(expression, { ...options, element: mountpoint.current });
}
},
[expression, mountpoint.current]
);
return (
<div
ref={el => {
mountpoint.current = el;
}}
/>
);
};

View file

@ -0,0 +1,72 @@
/*
* 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 interface ExpressionRunnerOptions {
// TODO use the real types here once they are ready
context?: object;
getInitialContext?: () => object;
element?: Element;
}
export type ExpressionRunner = (
expression: string | Ast,
options: ExpressionRunnerOptions
) => Promise<Result>;
export const createRunFn = (
renderersRegistry: RenderFunctionsRegistry,
interpreterPromise: Promise<Interpreter>
): 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;
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;
};

View file

@ -0,0 +1,217 @@
/*
* 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 {
ExpressionsService,
RenderFunctionsRegistry,
RenderFunction,
Interpreter,
ExpressionsServiceDependencies,
Result,
ExpressionsSetup,
} from './expressions_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('expressions_service', () => {
let interpreterMock: jest.Mocked<Interpreter>;
let renderFunctionMock: jest.Mocked<RenderFunction>;
let setupPluginsMock: ExpressionsServiceDependencies;
const expressionResult: Result = { type: 'render', as: 'abc', value: {} };
let api: ExpressionsSetup;
let testExpression: string;
let testAst: Ast;
beforeEach(() => {
interpreterMock = { interpretAst: jest.fn(_ => Promise.resolve(expressionResult)) };
renderFunctionMock = ({
render: jest.fn(),
} as unknown) as jest.Mocked<RenderFunction>;
setupPluginsMock = {
interpreter: {
getInterpreter: () => Promise.resolve({ interpreter: interpreterMock }),
renderersRegistry: ({
get: () => renderFunctionMock,
} as unknown) as RenderFunctionsRegistry,
},
};
api = new ExpressionsService().setup(setupPluginsMock);
testExpression = 'test | expression';
testAst = fromExpression(testExpression);
});
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()
);
});
});
describe('expression_renderer', () => {
it('should call interpreter and render function when called through react component', async () => {
const ExpressionRenderer = api.ExpressionRenderer;
mount(<ExpressionRenderer expression={testExpression} />);
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 the interpreter with given context and getInitialContext functions', async () => {
const getInitialContext = () => ({});
const context = {};
const ExpressionRenderer = api.ExpressionRenderer;
mount(
<ExpressionRenderer
expression={testExpression}
getInitialContext={getInitialContext}
context={context}
/>
);
await waitForInterpreterRun();
const interpretCall = interpreterMock.interpretAst.mock.calls[0];
expect(interpretCall[1]).toBe(context);
expect(interpretCall[2].getInitialContext).toBe(getInitialContext);
});
it('should call interpreter and render function again if expression changes', async () => {
const ExpressionRenderer = api.ExpressionRenderer;
const instance = mount(<ExpressionRenderer expression={testExpression} />);
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(<ExpressionRenderer expression={testExpression} />);
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);
});
});
});

View file

@ -0,0 +1,126 @@
/*
* 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 { 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;
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<unknown, RenderFunction>;
export interface Interpreter {
interpretAst(ast: Ast, context: Context, handlers: Handlers): Promise<Result>;
}
type InterpreterGetter = () => Promise<{ interpreter: Interpreter }>;
export interface ExpressionsServiceDependencies {
interpreter: {
renderersRegistry: RenderFunctionsRegistry;
getInterpreter: InterpreterGetter;
};
}
/**
* Expressions 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<ExpressionsService['setup']>;

View file

@ -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 { ExpressionsService, ExpressionsSetup } from './expressions_service';
export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer';
export { ExpressionRunner } from './expression_runner';

View file

@ -17,6 +17,15 @@
* 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 { ExpressionsService, ExpressionsSetup } from './expressions';
import { SearchService, SearchSetup } from './search';
import { QueryService, QuerySetup } from './query';
import { IndexPatternsService, IndexPatternsSetup } from './index_patterns';
@ -25,18 +34,26 @@ class DataPlugin {
private readonly indexPatterns: IndexPatternsService;
private readonly search: SearchService;
private readonly query: QueryService;
private readonly expressions: ExpressionsService;
constructor() {
this.indexPatterns = new IndexPatternsService();
this.query = new QueryService();
this.search = new SearchService();
this.expressions = new ExpressionsService();
}
public setup() {
public setup(): DataSetup {
return {
indexPatterns: this.indexPatterns.setup(),
search: this.search.setup(),
query: this.query.setup(),
expressions: this.expressions.setup({
interpreter: {
getInterpreter,
renderersRegistry,
},
}),
};
}
@ -44,6 +61,7 @@ class DataPlugin {
this.indexPatterns.stop();
this.search.stop();
this.query.stop();
this.expressions.stop();
}
}
@ -57,9 +75,13 @@ export const data = new DataPlugin().setup();
/** @public */
export interface DataSetup {
indexPatterns: IndexPatternsSetup;
expressions: ExpressionsSetup;
search: SearchSetup;
query: QuerySetup;
}
/** @public types */
export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from './expressions';
/** @public types */
export { IndexPattern, StaticIndexPattern, StaticIndexPatternField, Field } from './index_patterns';

View file

@ -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;
}