expression service (#42337)

This commit is contained in:
Peter Pisljar 2019-09-04 14:09:38 +02:00 committed by GitHub
parent 1dfa972b64
commit 2f5306ea42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 867 additions and 474 deletions

View file

@ -19,4 +19,4 @@
export { Registry } from './lib/registry';
export { fromExpression, Ast } from './lib/ast';
export { fromExpression, toExpression, Ast } from './lib/ast';

View file

@ -20,3 +20,4 @@
export type Ast = unknown;
export declare function fromExpression(expression: string): Ast;
export declare function toExpression(astObj: Ast, type?: string): string;

View file

@ -19,47 +19,60 @@
import { useRef, useEffect } from 'react';
import React from 'react';
import { Ast } from '@kbn/interpreter/common';
import { ExpressionRunnerOptions, ExpressionRunner } from './expression_runner';
import { Result } from './expressions_service';
import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './lib/_types';
import { IExpressionLoader, ExpressionLoader } from './lib/loader';
// 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 interface ExpressionRendererProps extends IExpressionLoaderParams {
className: 'string';
expression: string | ExpressionAST;
/**
* If an element is specified, but the response of the expression run can't be rendered
* because it isn't a valid response or the specified renderer isn't available,
* this callback is called with the given result.
*/
onRenderFailure?: (result: Result) => void;
};
onRenderFailure?: (result: IInterpreterResult) => void;
}
export type ExpressionRenderer = React.FC<ExpressionRendererProps>;
export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({
export const createRenderer = (loader: IExpressionLoader): ExpressionRenderer => ({
className,
expression,
onRenderFailure,
...options
}: ExpressionRendererProps) => {
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
const handlerRef: React.MutableRefObject<null | ExpressionLoader> = useRef(null);
useEffect(() => {
if (mountpoint.current) {
run(expression, { ...options, element: mountpoint.current }).catch(result => {
if (!handlerRef.current) {
handlerRef.current = loader(mountpoint.current, expression, options);
} else {
handlerRef.current.update(expression, options);
}
handlerRef.current.data$.toPromise().catch(result => {
if (onRenderFailure) {
onRenderFailure(result);
}
});
}
}, [expression, mountpoint.current]);
}, [
expression,
options.searchContext,
options.context,
options.variables,
options.disableCaching,
mountpoint.current,
]);
return (
<div
className={className}
ref={el => {
mountpoint.current = el;
}}

View file

@ -1,75 +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 { Ast, fromExpression } from '@kbn/interpreter/common';
import { RequestAdapter, DataAdapter } from '../../../../../plugins/inspector/public';
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 (response.type === 'error') {
throw response;
}
if (element) {
if (response.type === 'render' && response.as && renderersRegistry.get(response.as) !== null) {
renderersRegistry.get(response.as).render(element, response.value, {
onDestroy: fn => {
// TODO implement
},
done: () => {
// TODO implement
},
});
} else {
throw response;
}
}
return response;
};

View file

@ -1,276 +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 { 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));
};
const RENDERER_ID = 'mockId';
describe('expressions_service', () => {
let interpretAstMock: jest.Mocked<Interpreter>['interpretAst'];
let interpreterMock: jest.Mocked<Interpreter>;
let renderFunctionMock: jest.Mocked<RenderFunction>;
let setupPluginsMock: ExpressionsServiceDependencies;
const expressionResult: Result = { type: 'render', as: RENDERER_ID, value: {} };
let api: ExpressionsSetup;
let testExpression: string;
let testAst: Ast;
beforeEach(() => {
interpretAstMock = jest.fn((..._) => Promise.resolve(expressionResult));
interpreterMock = { interpretAst: interpretAstMock };
renderFunctionMock = ({
render: jest.fn(),
} as unknown) as jest.Mocked<RenderFunction>;
setupPluginsMock = {
interpreter: {
getInterpreter: () => Promise.resolve({ interpreter: interpreterMock }),
renderersRegistry: ({
get: (id: string) => (id === RENDERER_ID ? renderFunctionMock : null),
} 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 return the result of the interpreter run', async () => {
const response = await api.run(testAst, {});
expect(response).toBe(expressionResult);
});
it('should reject the promise if the response is not renderable but an element is passed', async () => {
const unexpectedResult = { type: 'datatable', value: {} };
interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult));
expect(
api.run(testAst, {
element: document.createElement('div'),
})
).rejects.toBe(unexpectedResult);
});
it('should reject the promise if the renderer is not known', async () => {
const unexpectedResult = { type: 'render', as: 'unknown_id' };
interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult));
expect(
api.run(testAst, {
element: document.createElement('div'),
})
).rejects.toBe(unexpectedResult);
});
it('should not reject the promise on unknown renderer if the runner is not rendering', async () => {
const unexpectedResult = { type: 'render', as: 'unknown_id' };
interpretAstMock.mockReturnValue(Promise.resolve(unexpectedResult));
expect(api.run(testAst, {})).resolves.toBe(unexpectedResult);
});
it('should reject the promise if the response is an error', async () => {
const errorResult = { type: 'error', error: {} };
interpretAstMock.mockReturnValue(Promise.resolve(errorResult));
expect(api.run(testAst, {})).rejects.toBe(errorResult);
});
it('should reject the promise if there are syntax errors', async () => {
expect(api.run('|||', {})).rejects.toBeInstanceOf(Error);
});
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);
});
it('should call onRenderFailure if the result can not be rendered', async () => {
const errorResult = { type: 'error', error: {} };
interpretAstMock.mockReturnValue(Promise.resolve(errorResult));
const renderFailureSpy = jest.fn();
const ExpressionRenderer = api.ExpressionRenderer;
mount(<ExpressionRenderer expression={testExpression} onRenderFailure={renderFailureSpy} />);
await waitForInterpreterRun();
expect(renderFailureSpy).toHaveBeenCalledWith(errorResult);
});
});
});

View file

@ -17,107 +17,51 @@
* under the License.
*/
import { Ast } from '@kbn/interpreter/common';
import { npSetup } from 'ui/new_platform';
// @ts-ignore
// 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 'src/plugins/inspector/public';
import { Filter } from '@kbn/es-query';
import { TimeRange } from 'src/plugins/data/public';
import { setInspector, setInterpreter } from './services';
import { execute } from './lib/execute';
import { loader } from './lib/loader';
import { render } from './lib/render';
import { createRenderer } from './expression_renderer';
import { createRunFn } from './expression_runner';
import { Query } from '../query';
export interface InitialContextObject {
timeRange?: TimeRange;
filters?: Filter[];
query?: Query;
import { Start as IInspector } from '../../../../../plugins/inspector/public';
export interface ExpressionsServiceStartDependencies {
inspector: IInspector;
}
export type getInitialContextFunction = () => InitialContextObject;
export interface Handlers {
getInitialContext: getInitialContextFunction;
inspectorAdapters?: Adapters;
abortSignal?: AbortSignal;
}
type Context = object;
export interface Result {
type: string;
as?: string;
value?: unknown;
error?: 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)
);
public setup() {
// eslint-disable-next-line
const { getInterpreter } = require('../../../interpreter/public/interpreter');
getInterpreter()
.then(setInterpreter)
.catch((e: Error) => {
throw new Error('interpreter is not initialized');
});
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),
registerType: npSetup.plugins.data.expressions.registerType,
registerFunction: npSetup.plugins.data.expressions.registerFunction,
registerRenderer: npSetup.plugins.data.expressions.registerRenderer,
};
}
public start({ inspector }: ExpressionsServiceStartDependencies) {
const ExpressionRenderer = createRenderer(loader);
setInspector(inspector);
return {
execute,
render,
loader,
ExpressionRenderer,
};
}
@ -128,3 +72,4 @@ export class ExpressionsService {
/** @public */
export type ExpressionsSetup = ReturnType<ExpressionsService['setup']>;
export type ExpressionsStart = ReturnType<ExpressionsService['start']>;

View file

@ -17,6 +17,5 @@
* under the License.
*/
export { ExpressionsService, ExpressionsSetup } from './expressions_service';
export { ExpressionsService, ExpressionsSetup, ExpressionsStart } from './expressions_service';
export { ExpressionRenderer, ExpressionRendererProps } from './expression_renderer';
export { ExpressionRunner } from './expression_runner';

View file

@ -0,0 +1,86 @@
/*
* 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 { TimeRange } from 'src/plugins/data/public';
import { Filter } from '@kbn/es-query';
import { Adapters } from '../../../../../ui/public/inspector';
import { Query } from '../../query';
import { ExpressionAST } from '../../../../../../plugins/data/common/expressions/types';
export { ExpressionAST, TimeRange, Adapters, Filter, Query };
export type RenderId = number;
export type Data = any;
export type event = any;
export type Context = object;
export interface SearchContext {
type: 'kibana_context';
filters?: Filter[];
query?: Query;
timeRange?: TimeRange;
}
export type IGetInitialContext = () => SearchContext | Context;
export interface IExpressionLoaderParams {
searchContext?: SearchContext;
context?: Context;
variables?: Record<string, any>;
disableCaching?: boolean;
customFunctions?: [];
customRenderers?: [];
}
export interface IInterpreterHandlers {
getInitialContext: IGetInitialContext;
inspectorAdapters?: Adapters;
}
export interface IInterpreterResult {
type: string;
as?: string;
value?: unknown;
error?: unknown;
}
export interface IInterpreterRenderHandlers {
done: () => void;
onDestroy: (fn: () => void) => void;
reload: () => void;
update: (params: any) => void;
event: (event: event) => void;
}
export interface IInterpreterRenderFunction {
name: string;
displayName: string;
help: string;
validate: () => void;
reuseDomNode: boolean;
render: (domNode: Element, data: unknown, handlers: IInterpreterRenderHandlers) => void;
}
export interface IInterpreter {
interpretAst(
ast: ExpressionAST,
context: Context,
handlers: IInterpreterHandlers
): Promise<IInterpreterResult>;
}

View file

@ -0,0 +1,94 @@
/*
* 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 { execute, ExpressionDataHandler } from './execute';
import { fromExpression } from '@kbn/interpreter/common';
import { ExpressionAST } from '../../../../../../plugins/data/common/expressions/types';
jest.mock('../services', () => ({
getInterpreter: () => {
return {
interpretAst: async (expression: ExpressionAST) => {
return {};
},
};
},
}));
describe('execute helper function', () => {
it('returns ExpressionDataHandler instance', () => {
const response = execute('');
expect(response).toBeInstanceOf(ExpressionDataHandler);
});
});
describe('ExpressionDataHandler', () => {
const expressionString = '';
describe('constructor', () => {
it('accepts expression string', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
it('accepts expression AST', () => {
const expressionAST = fromExpression(expressionString) as ExpressionAST;
const expressionDataHandler = new ExpressionDataHandler(expressionAST, {});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
expect(expressionDataHandler.getAst()).toEqual(expressionAST);
});
it('allows passing in context', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {
context: { test: 'hello' },
});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
it('allows passing in search context', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {
searchContext: { type: 'kibana_context', filters: [] },
});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
});
describe('getData()', () => {
it('returns a promise', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(expressionDataHandler.getData()).toBeInstanceOf(Promise);
});
it('promise resolves with data', async () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(await expressionDataHandler.getData()).toEqual({});
});
});
it('cancel() aborts request', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expressionDataHandler.cancel();
});
it('inspect() returns correct inspector adapters', () => {
const expressionDataHandler = new ExpressionDataHandler(expressionString, {});
expect(expressionDataHandler.inspect()).toHaveProperty('requests');
expect(expressionDataHandler.inspect()).toHaveProperty('data');
});
});

View file

@ -0,0 +1,115 @@
/*
* 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 } from '@kbn/interpreter/target/common';
import { DataAdapter, RequestAdapter, Adapters } from '../../../../../../plugins/inspector/public';
import { getInterpreter } from '../services';
import { ExpressionAST, IExpressionLoaderParams, IInterpreterResult } from './_types';
/**
* The search context describes a specific context (filters, time range and query)
* that will be applied to the expression for execution. Not every expression will
* be effected by that. You have to use special functions
* that will pick up this search context and forward it to following functions that
* understand it.
*/
export class ExpressionDataHandler {
private abortController: AbortController;
private expression: string;
private ast: ExpressionAST;
private inspectorAdapters: Adapters;
private promise: Promise<IInterpreterResult>;
constructor(expression: string | ExpressionAST, params: IExpressionLoaderParams) {
if (typeof expression === 'string') {
this.expression = expression;
this.ast = fromExpression(expression) as ExpressionAST;
} else {
this.ast = expression;
this.expression = '';
}
this.abortController = new AbortController();
this.inspectorAdapters = this.getActiveInspectorAdapters();
const getInitialContext = () => ({
type: 'kibana_context',
...params.searchContext,
});
const defaultContext = { type: 'null' };
const interpreter = getInterpreter();
this.promise = interpreter.interpretAst(this.ast, params.context || defaultContext, {
getInitialContext,
inspectorAdapters: this.inspectorAdapters,
});
}
cancel = () => {
this.abortController.abort();
};
getData = async () => {
return await this.promise;
};
getExpression = () => {
return this.expression;
};
getAst = () => {
return this.ast;
};
inspect = () => {
return this.inspectorAdapters;
};
/**
* Returns an object of all inspectors for this vis object.
* This must only be called after this.type has properly be initialized,
* since we need to read out data from the the vis type to check which
* inspectors are available.
*/
private getActiveInspectorAdapters = (): Adapters => {
const adapters: Adapters = {};
// Add the requests inspector adapters if the vis type explicitly requested it via
// inspectorAdapters.requests: true in its definition or if it's using the courier
// request handler, since that will automatically log its requests.
adapters.requests = new RequestAdapter();
// Add the data inspector adapter if the vis type requested it or if the
// vis is using courier, since we know that courier supports logging
// its data.
adapters.data = new DataAdapter();
return adapters;
};
}
export function execute(
expression: string | ExpressionAST,
params: IExpressionLoaderParams = {}
): ExpressionDataHandler {
return new ExpressionDataHandler(expression, params);
}

View file

@ -0,0 +1,118 @@
/*
* 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 { first } from 'rxjs/operators';
import { loader, ExpressionLoader } from './loader';
import { fromExpression } from '@kbn/interpreter/common';
import { IInterpreterRenderHandlers } from './_types';
import { Observable } from 'rxjs';
import { ExpressionAST } from '../../../../../../plugins/data/common/expressions/types';
const element: HTMLElement = null as any;
jest.mock('../services', () => ({
getInterpreter: () => {
return {
interpretAst: async (expression: ExpressionAST) => {
return { type: 'render', as: 'test' };
},
};
},
}));
jest.mock('../../../../interpreter/public/registries', () => {
const _registry: Record<string, any> = {};
_registry.test = {
render: (el: HTMLElement, value: any, handlers: IInterpreterRenderHandlers) => {
handlers.done();
},
};
return {
renderersRegistry: {
get: (id: string) => {
return _registry[id];
},
},
};
});
describe('execute helper function', () => {
it('returns ExpressionDataHandler instance', () => {
const response = loader(element, '', {});
expect(response).toBeInstanceOf(ExpressionLoader);
});
});
describe('ExpressionDataHandler', () => {
const expressionString = '';
describe('constructor', () => {
it('accepts expression string', () => {
const expressionDataHandler = new ExpressionLoader(element, expressionString, {});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
});
it('accepts expression AST', () => {
const expressionAST = fromExpression(expressionString) as ExpressionAST;
const expressionDataHandler = new ExpressionLoader(element, expressionAST, {});
expect(expressionDataHandler.getExpression()).toEqual(expressionString);
expect(expressionDataHandler.getAst()).toEqual(expressionAST);
});
it('creates observables', () => {
const expressionLoader = new ExpressionLoader(element, expressionString, {});
expect(expressionLoader.events$).toBeInstanceOf(Observable);
expect(expressionLoader.render$).toBeInstanceOf(Observable);
expect(expressionLoader.update$).toBeInstanceOf(Observable);
expect(expressionLoader.data$).toBeInstanceOf(Observable);
});
});
it('emits on $data when data is available', async () => {
const expressionLoader = new ExpressionLoader(element, expressionString, {});
const response = await expressionLoader.data$.pipe(first()).toPromise();
expect(response).toEqual({ type: 'render', as: 'test' });
});
it('emits on render$ when rendering is done', async () => {
const expressionLoader = new ExpressionLoader(element, expressionString, {});
const response = await expressionLoader.render$.pipe(first()).toPromise();
expect(response).toBe(1);
});
it('allows updating configuration', async () => {
const expressionLoader = new ExpressionLoader(element, expressionString, {});
let response = await expressionLoader.render$.pipe(first()).toPromise();
expect(response).toBe(1);
expressionLoader.update('', {});
response = await expressionLoader.render$.pipe(first()).toPromise();
expect(response).toBe(2);
});
it('cancel() aborts request', () => {
const expressionDataHandler = new ExpressionLoader(element, expressionString, {});
expressionDataHandler.cancel();
});
it('inspect() returns correct inspector adapters', () => {
const expressionDataHandler = new ExpressionLoader(element, expressionString, {});
expect(expressionDataHandler.inspect()).toHaveProperty('data');
expect(expressionDataHandler.inspect()).toHaveProperty('requests');
});
});

View file

@ -0,0 +1,130 @@
/*
* 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 { Observable, Subject } from 'rxjs';
import { first, share } from 'rxjs/operators';
import { Adapters, InspectorSession } from '../../../../../../plugins/inspector/public';
import { execute, ExpressionDataHandler } from './execute';
import { ExpressionRenderHandler } from './render';
import { RenderId, Data, IExpressionLoaderParams, ExpressionAST } from './_types';
import { getInspector } from '../services';
export class ExpressionLoader {
data$: Observable<Data>;
update$: Observable<any>;
render$: Observable<RenderId>;
events$: Observable<any>;
private dataHandler: ExpressionDataHandler;
private renderHandler: ExpressionRenderHandler;
private dataSubject: Subject<Data>;
private data: Data;
constructor(
element: HTMLElement,
expression: string | ExpressionAST,
params: IExpressionLoaderParams
) {
this.dataSubject = new Subject();
this.data$ = this.dataSubject.asObservable().pipe(share());
this.renderHandler = new ExpressionRenderHandler(element);
this.render$ = this.renderHandler.render$;
this.update$ = this.renderHandler.update$;
this.events$ = this.renderHandler.events$;
this.update$.subscribe(({ newExpression, newParams }) => {
this.update(newExpression, newParams);
});
this.data$.subscribe(data => {
this.render(data);
});
this.execute(expression, params);
// @ts-ignore
this.dataHandler = this.dataHandler;
}
destroy() {}
cancel() {
this.dataHandler.cancel();
}
getExpression(): string {
return this.dataHandler.getExpression();
}
getAst(): ExpressionAST {
return this.dataHandler.getAst();
}
getElement(): HTMLElement {
return this.renderHandler.getElement();
}
openInspector(title: string): InspectorSession {
return getInspector().open(this.inspect(), {
title,
});
}
inspect(): Adapters {
return this.dataHandler.inspect();
}
update(expression: string | ExpressionAST, params: IExpressionLoaderParams): Promise<RenderId> {
const promise = this.render$.pipe(first()).toPromise();
if (expression !== null) {
this.execute(expression, params);
} else {
this.render(this.data);
}
return promise;
}
private execute = async (
expression: string | ExpressionAST,
params: IExpressionLoaderParams
): Promise<Data> => {
if (this.dataHandler) {
this.dataHandler.cancel();
}
this.dataHandler = execute(expression, params);
const data = await this.dataHandler.getData();
this.dataSubject.next(data);
return data;
};
private async render(data: Data): Promise<RenderId> {
return this.renderHandler.render(data);
}
}
export type IExpressionLoader = (
element: HTMLElement,
expression: string | ExpressionAST,
params: IExpressionLoaderParams
) => ExpressionLoader;
export const loader: IExpressionLoader = (element, expression, params) => {
return new ExpressionLoader(element, expression, params);
};

View file

@ -0,0 +1,88 @@
/*
* 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 { render, ExpressionRenderHandler } from './render';
import { Observable } from 'rxjs';
import { IInterpreterRenderHandlers } from './_types';
const element: HTMLElement = null as any;
jest.mock('../../../../interpreter/public/registries', () => {
const _registry: Record<string, any> = {};
_registry.test = {
render: (el: HTMLElement, value: any, handlers: IInterpreterRenderHandlers) => {
handlers.done();
},
};
return {
renderersRegistry: {
get: (id: string) => {
return _registry[id];
},
},
};
});
describe('render helper function', () => {
it('returns ExpressionRenderHandler instance', () => {
const response = render(element, {});
expect(response).toBeInstanceOf(ExpressionRenderHandler);
});
});
describe('ExpressionRenderHandler', () => {
const data = { type: 'render', as: 'test' };
it('constructor creates observers', () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
expect(expressionRenderHandler.events$).toBeInstanceOf(Observable);
expect(expressionRenderHandler.render$).toBeInstanceOf(Observable);
expect(expressionRenderHandler.update$).toBeInstanceOf(Observable);
});
it('getElement returns the element', () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
expect(expressionRenderHandler.getElement()).toBe(element);
});
describe('render()', () => {
it('throws if invalid data is provided', async () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
await expect(expressionRenderHandler.render({})).rejects.toThrow();
});
it('throws if renderer does not exist', async () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
await expect(
expressionRenderHandler.render({ type: 'render', as: 'something' })
).rejects.toThrow();
});
it('returns a promise', () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
expect(expressionRenderHandler.render(data)).toBeInstanceOf(Promise);
});
it('resolves a promise once rendering is complete', async () => {
const expressionRenderHandler = new ExpressionRenderHandler(element);
const response = await expressionRenderHandler.render(data);
expect(response).toBe(1);
});
});
});

View file

@ -0,0 +1,99 @@
/*
* 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 { Observable } from 'rxjs';
import * as Rx from 'rxjs';
import { share, first } from 'rxjs/operators';
import { renderersRegistry } from '../../../../interpreter/public/registries';
import { event, RenderId, Data, IInterpreterRenderHandlers } from './_types';
export class ExpressionRenderHandler {
render$: Observable<RenderId>;
update$: Observable<any>;
events$: Observable<event>;
private element: HTMLElement;
private destroyFn?: any;
private renderCount: number = 0;
private handlers: IInterpreterRenderHandlers;
constructor(element: HTMLElement) {
this.element = element;
const eventsSubject = new Rx.Subject();
this.events$ = eventsSubject.asObservable().pipe(share());
const renderSubject = new Rx.Subject();
this.render$ = renderSubject.asObservable().pipe(share());
const updateSubject = new Rx.Subject();
this.update$ = updateSubject.asObservable().pipe(share());
this.handlers = {
onDestroy: (fn: any) => {
this.destroyFn = fn;
},
done: () => {
this.renderCount++;
renderSubject.next(this.renderCount);
},
reload: () => {
updateSubject.next(null);
},
update: params => {
updateSubject.next(params);
},
event: data => {
eventsSubject.next(data);
},
};
}
render = async (data: Data) => {
if (data.type !== 'render' || !data.as) {
throw new Error('invalid data provided to expression renderer');
}
if (!renderersRegistry.get(data.as)) {
throw new Error(`invalid renderer id '${data.as}'`);
}
const promise = this.render$.pipe(first()).toPromise();
renderersRegistry.get(data.as).render(this.element, data.value, this.handlers);
return promise;
};
destroy = () => {
if (this.destroyFn) {
this.destroyFn();
}
};
getElement = () => {
return this.element;
};
}
export function render(element: HTMLElement, data: Data): ExpressionRenderHandler {
const handler = new ExpressionRenderHandler(element);
handler.render(data);
return handler;
}

View file

@ -0,0 +1,41 @@
/*
* 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 { IInterpreter } from './lib/_types';
import { Start as IInspector } from '../../../../../plugins/inspector/public';
let interpreter: IInterpreter | undefined;
let inspector: IInspector;
export const getInterpreter = (): IInterpreter => {
if (!interpreter) throw new Error('interpreter was not set');
return interpreter;
};
export const setInterpreter = (inspectorInstance: IInterpreter) => {
interpreter = inspectorInstance;
};
export const getInspector = (): IInspector => {
return inspector;
};
export const setInspector = (inspectorInstance: IInspector) => {
inspector = inspectorInstance;
};

View file

@ -18,7 +18,7 @@
*/
// /// Define plugin function
import { DataPlugin as Plugin, DataSetup } from './plugin';
import { DataPlugin as Plugin, DataSetup, DataStart } from './plugin';
export function plugin() {
return new Plugin();
@ -28,7 +28,9 @@ export function plugin() {
/** @public types */
export type DataSetup = DataSetup;
export { ExpressionRenderer, ExpressionRendererProps, ExpressionRunner } from './expressions';
export type DataStart = DataStart;
export { ExpressionRenderer, ExpressionRendererProps } from './expressions';
export { FilterBar, ApplyFiltersPopover } from './filter';
export {
Field,

View file

@ -34,9 +34,7 @@
* data that will eventually be injected by the new platform.
*/
import { npSetup } from 'ui/new_platform';
// @ts-ignore
import { renderersRegistry } from 'plugins/interpreter/registries';
import { npSetup, npStart } from 'ui/new_platform';
// @ts-ignore
import { getInterpreter } from 'plugins/interpreter/interpreter';
import { LegacyDependenciesPlugin } from './shim/legacy_dependencies_plugin';
@ -47,8 +45,9 @@ const legacyPlugin = new LegacyDependenciesPlugin();
export const setup = dataPlugin.setup(npSetup.core, {
__LEGACY: legacyPlugin.setup(),
interpreter: {
renderersRegistry,
getInterpreter,
},
inspector: npSetup.plugins.inspector,
});
export const start = dataPlugin.start(npStart.core, {
inspector: npStart.plugins.inspector,
});

View file

@ -18,12 +18,16 @@
*/
import { CoreSetup, CoreStart, Plugin } from '../../../../core/public';
import { ExpressionsService, ExpressionsSetup } from './expressions';
import { ExpressionsService, ExpressionsSetup, ExpressionsStart } from './expressions';
import { SearchService, SearchSetup } from './search';
import { QueryService, QuerySetup } from './query';
import { FilterService, FilterSetup } from './filter';
import { IndexPatternsService, IndexPatternsSetup } from './index_patterns';
import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin';
import {
Start as InspectorStart,
Setup as InspectorSetup,
} from '../../../../plugins/inspector/public';
/**
* Interface for any dependencies on other plugins' `setup` contracts.
@ -32,7 +36,11 @@ import { LegacyDependenciesPluginSetup } from './shim/legacy_dependencies_plugin
*/
export interface DataPluginSetupDependencies {
__LEGACY: LegacyDependenciesPluginSetup;
interpreter: any;
inspector: InspectorSetup;
}
export interface DataPluginStartDependencies {
inspector: InspectorStart;
}
/**
@ -48,6 +56,10 @@ export interface DataSetup {
search: SearchSetup;
}
export interface DataStart {
expressions: ExpressionsStart;
}
/**
* Data Plugin - public
*
@ -59,7 +71,7 @@ export interface DataSetup {
* in the setup/start interfaces. The remaining items exported here are either types,
* or static code.
*/
export class DataPlugin implements Plugin<DataSetup, void, DataPluginSetupDependencies> {
export class DataPlugin implements Plugin<DataSetup, DataStart, DataPluginSetupDependencies> {
// Exposed services, sorted alphabetically
private readonly expressions: ExpressionsService = new ExpressionsService();
private readonly filter: FilterService = new FilterService();
@ -67,7 +79,7 @@ export class DataPlugin implements Plugin<DataSetup, void, DataPluginSetupDepend
private readonly query: QueryService = new QueryService();
private readonly search: SearchService = new SearchService();
public setup(core: CoreSetup, { __LEGACY, interpreter }: DataPluginSetupDependencies): DataSetup {
public setup(core: CoreSetup, { __LEGACY }: DataPluginSetupDependencies): DataSetup {
const { uiSettings } = core;
const savedObjectsClient = __LEGACY.savedObjectsClient;
@ -76,9 +88,7 @@ export class DataPlugin implements Plugin<DataSetup, void, DataPluginSetupDepend
savedObjectsClient,
});
return {
expressions: this.expressions.setup({
interpreter,
}),
expressions: this.expressions.setup(),
indexPatterns: indexPatternsService,
filter: this.filter.setup({
uiSettings,
@ -89,7 +99,11 @@ export class DataPlugin implements Plugin<DataSetup, void, DataPluginSetupDepend
};
}
public start(core: CoreStart) {}
public start(core: CoreStart, plugins: DataPluginStartDependencies) {
return {
expressions: this.expressions.start({ inspector: plugins.inspector }),
};
}
public stop() {
this.expressions.stop();