mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
expression service (#42337)
This commit is contained in:
parent
1dfa972b64
commit
2f5306ea42
18 changed files with 867 additions and 474 deletions
|
@ -19,4 +19,4 @@
|
|||
|
||||
export { Registry } from './lib/registry';
|
||||
|
||||
export { fromExpression, Ast } from './lib/ast';
|
||||
export { fromExpression, toExpression, Ast } from './lib/ast';
|
||||
|
|
|
@ -20,3 +20,4 @@
|
|||
export type Ast = unknown;
|
||||
|
||||
export declare function fromExpression(expression: string): Ast;
|
||||
export declare function toExpression(astObj: Ast, type?: string): string;
|
||||
|
|
|
@ -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;
|
||||
}}
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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']>;
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>;
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
115
src/legacy/core_plugins/data/public/expressions/lib/execute.ts
Normal file
115
src/legacy/core_plugins/data/public/expressions/lib/execute.ts
Normal 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);
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
130
src/legacy/core_plugins/data/public/expressions/lib/loader.ts
Normal file
130
src/legacy/core_plugins/data/public/expressions/lib/loader.ts
Normal 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);
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
}
|
41
src/legacy/core_plugins/data/public/expressions/services.ts
Normal file
41
src/legacy/core_plugins/data/public/expressions/services.ts
Normal 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;
|
||||
};
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue