refactor: 💡 separate server-side function reg from executor (#44531) (#46775)

* refactor: 💡 separate server-side function reg from executor

* refactor: 💡 separate Canvas functionality of Interpreter

* refactor: 💡 simplify, move server fn init into Canavas

* fix: 🐛 adjust Expressions service usage after master merge
This commit is contained in:
Vadim Dalecky 2019-09-27 15:26:31 +02:00 committed by GitHub
parent e7e0723f05
commit 4bda107558
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 114 additions and 171 deletions

View file

@ -0,0 +1,93 @@
/*
* 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.
*/
/**
* This file needs to be deleted by 8.0 release. It is here to load available
* server side functions and create a wrappers around them on client side, to
* execute them from client side. This functionality is used only by Canvas
* and all server side functions are in Canvas plugin.
*
* In 8.0 there will be no server-side functions, plugins will register only
* client side functions and if they need those to execute something on the
* server side, it should be respective function's internal implementation detail.
*/
import { get, identity } from 'lodash';
// @ts-ignore
import { npSetup } from 'ui/new_platform';
import { FUNCTIONS_URL } from './consts';
import { ajaxStream } from './ajax_stream';
import { batchedFetch } from './batched_fetch';
export function getType(node: any) {
if (node == null) return 'null';
if (typeof node === 'object') {
if (!node.type) throw new Error('Objects must have a type property');
return node.type;
}
return typeof node;
}
export function serializeProvider(types: any) {
return {
serialize: provider('serialize'),
deserialize: provider('deserialize'),
};
function provider(key: any) {
return (context: any) => {
const type = getType(context);
const typeDef = types[type];
const fn: any = get(typeDef, key) || identity;
return fn(context);
};
}
}
let cached: Promise<void> | null = null;
export const loadLegacyServerFunctionWrappers = async () => {
if (!cached) {
cached = (async () => {
const serverFunctionList = await npSetup.core.http.get(FUNCTIONS_URL);
const types = npSetup.plugins.expressions.__LEGACY.types.toJS();
const { serialize } = serializeProvider(types);
const batch = batchedFetch({
ajaxStream: ajaxStream(
npSetup.core.injectedMetadata.getKibanaVersion(),
npSetup.core.injectedMetadata.getBasePath()
),
serialize,
});
// For every sever-side function, register a client-side
// function that matches its definition, but which simply
// calls the server-side function endpoint.
Object.keys(serverFunctionList).forEach(functionName => {
const fn = () => ({
...serverFunctionList[functionName],
fn: (context: any, args: any) => batch({ functionName, args, context }),
});
npSetup.plugins.expressions.registerFunction(fn);
});
})();
}
return cached;
};

View file

@ -19,19 +19,12 @@
import 'uiExports/interpreter';
import { register, registryFactory } from '@kbn/interpreter/common';
import { npSetup } from 'ui/new_platform';
import { initializeInterpreter } from './lib/interpreter';
import { registries } from './registries';
import { ajaxStream } from './lib/ajax_stream';
import { functions } from './functions';
import { visualization } from './renderers/visualization';
import { typeSpecs } from '../../../../plugins/expressions/common';
const { http } = npSetup.core;
const KIBANA_VERSION = npSetup.core.injectedMetadata.getKibanaVersion();
const KIBANA_BASE_PATH = npSetup.core.injectedMetadata.getBasePath();
// Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins
// can register without a transpile step.
global.kbnInterpreter = Object.assign(global.kbnInterpreter || {}, registryFactory(registries));
@ -42,26 +35,13 @@ register(registries, {
renderers: [visualization],
});
let _resolve;
let _interpreterPromise;
const initialize = async () => {
initializeInterpreter({
http,
ajaxStream: ajaxStream(KIBANA_VERSION, KIBANA_BASE_PATH),
typesRegistry: registries.types,
functionsRegistry: registries.browserFunctions,
}).then(interpreter => {
_resolve({ interpreter });
});
};
let interpreterPromise;
export const getInterpreter = async () => {
if (!_interpreterPromise) {
_interpreterPromise = new Promise(resolve => _resolve = resolve);
initialize();
if (!interpreterPromise) {
interpreterPromise = initializeInterpreter();
}
return await _interpreterPromise;
return await interpreterPromise;
};
export const interpretAst = async (...params) => {

View file

@ -36,7 +36,9 @@ jest.mock('@kbn/interpreter/common', () => ({
}));
const mockInterpreter = {
interpretAst: jest.fn(),
interpreter: {
interpretAst: jest.fn(),
}
};
jest.mock('./lib/interpreter', () => ({
initializeInterpreter: jest.fn().mockReturnValue(Promise.resolve(mockInterpreter)),
@ -71,12 +73,6 @@ describe('interpreter/interpreter', () => {
it('initializes interpreter', async () => {
await getInterpreter();
expect(initializeInterpreter).toHaveBeenCalledTimes(1);
expect(initializeInterpreter.mock.calls[0][0]).toMatchObject({
ajaxStream: expect.any(Function),
http: expect.any(Object),
typesRegistry: expect.any(Function),
functionsRegistry: expect.any(Function),
});
});
it('only initializes interpreter once', async () => {
@ -110,15 +106,15 @@ describe('interpreter/interpreter', () => {
it('calls interpreter.interpretAst with the provided params', async () => {
const params = [{}];
await interpretAst(...params);
expect(mockInterpreter.interpretAst).toHaveBeenCalledTimes(1);
expect(mockInterpreter.interpretAst).toHaveBeenCalledWith(...params);
expect(mockInterpreter.interpreter.interpretAst).toHaveBeenCalledTimes(1);
expect(mockInterpreter.interpreter.interpretAst).toHaveBeenCalledWith(...params);
});
it('calls interpreter.interpretAst each time', async () => {
const params = [{}];
await interpretAst(...params);
await interpretAst(...params);
expect(mockInterpreter.interpretAst).toHaveBeenCalledTimes(2);
expect(mockInterpreter.interpreter.interpretAst).toHaveBeenCalledTimes(2);
});
});

View file

@ -1,105 +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 { FUNCTIONS_URL } from './consts';
import { initializeInterpreter } from './interpreter';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { httpServiceMock } from '../../../../../core/public/http/http_service.mock';
jest.mock('../../common', () => ({
serializeProvider: () => ({ serialize: () => ({}) }),
}));
jest.mock('./create_handlers', () => ({
createHandlers: () => ({}),
}));
it('loads server-side functions', async () => {
const http = httpServiceMock.createStartContract();
http.get.mockImplementation(async () => {
return {
hello: { name: 'hello' },
world: { name: 'world' },
};
});
const ajaxStream = jest.fn(async () => ({}));
await initializeInterpreter({
http,
ajaxStream,
typesRegistry: { toJS: () => ({}) },
functionsRegistry: { register: () => {} },
});
expect(http.get).toHaveBeenCalledTimes(1);
expect(http.get).toHaveBeenCalledWith(FUNCTIONS_URL);
});
it('registers client-side functions that pass through to the server', async () => {
const http = httpServiceMock.createStartContract();
http.get.mockImplementation(async () => {
return {
hello: { name: 'hello' },
world: { name: 'world' },
};
});
const register = jest.fn();
const ajaxStream = jest.fn(async ({ onResponse }) => {
onResponse({ id: 1, result: { hello: 'world' } });
});
await initializeInterpreter({
http,
ajaxStream,
typesRegistry: { toJS: () => ({}) },
functionsRegistry: { register },
});
expect(register).toHaveBeenCalledTimes(2);
const [hello, world] = register.mock.calls.map(([fn]) => fn());
expect(hello.name).toEqual('hello');
expect(typeof hello.fn).toEqual('function');
expect(world.name).toEqual('world');
expect(typeof world.fn).toEqual('function');
const context = {};
const args = { quote: 'All we have to decide is what to do with the time that is given us.' };
const result = await hello.fn(context, args);
expect(result).toEqual({ hello: 'world' });
expect(ajaxStream).toHaveBeenCalledWith({
url: FUNCTIONS_URL,
onResponse: expect.any(Function),
body: JSON.stringify({
functions: [
{
functionName: 'hello',
args,
context,
id: 1,
},
],
}),
});
});

View file

@ -17,44 +17,19 @@
* under the License.
*/
import { interpreterProvider, serializeProvider } from '../../common';
import { interpreterProvider } from '../../common';
import { createHandlers } from './create_handlers';
import { batchedFetch } from './batched_fetch';
import { FUNCTIONS_URL } from './consts';
import { CoreStart } from '../../../../../core/public';
interface Config {
http: CoreStart['http'];
ajaxStream: any; // TODO: Import this from kibana_utils/ajax_stream
typesRegistry: any;
functionsRegistry: any;
}
export async function initializeInterpreter(config: Config) {
const { http, ajaxStream, typesRegistry, functionsRegistry } = config;
const serverFunctionList = await http.get(FUNCTIONS_URL);
const types = typesRegistry.toJS();
const { serialize } = serializeProvider(types);
const batch = batchedFetch({ ajaxStream, serialize });
// For every sever-side function, register a client-side
// function that matches its definition, but which simply
// calls the server-side function endpoint.
Object.keys(serverFunctionList).forEach(functionName => {
functionsRegistry.register(() => ({
...serverFunctionList[functionName],
fn: (context: any, args: any) => batch({ functionName, args, context }),
}));
});
import { registries } from '../registries';
export async function initializeInterpreter() {
const interpretAst = async (ast: any, context: any, handlers: any) => {
const interpretFn = await interpreterProvider({
types: typesRegistry.toJS(),
types: registries.types.toJS(),
handlers: { ...handlers, ...createHandlers() },
functions: functionsRegistry.toJS(),
functions: registries.browserFunctions.toJS(),
});
return interpretFn(ast, context);
};
return { interpretAst };
return { interpreter: { interpretAst } };
}

View file

@ -9,6 +9,7 @@ import { connect } from 'react-redux';
import { compose, withProps } from 'recompose';
import { registries } from 'plugins/interpreter/registries';
import { getInterpreter } from 'plugins/interpreter/interpreter';
import { loadLegacyServerFunctionWrappers } from 'plugins/interpreter/canvas/load_legacy_server_function_wrappers';
import { getAppReady, getBasePath } from '../../state/selectors/app';
import { appReady, appError } from '../../state/actions/app';
import { elementsRegistry } from '../../lib/elements_registry';
@ -71,6 +72,7 @@ register(registries, {
const mapDispatchToProps = dispatch => ({
setAppReady: () => async () => {
try {
await loadLegacyServerFunctionWrappers();
await getInterpreter();
// Register the expression language with the Monaco Editor

View file

@ -6,6 +6,7 @@
import { fromExpression, getType } from '@kbn/interpreter/common';
import { interpretAst } from 'plugins/interpreter/interpreter';
import { loadLegacyServerFunctionWrappers } from 'plugins/interpreter/canvas/load_legacy_server_function_wrappers';
import { notify } from './notify';
/**
@ -19,7 +20,8 @@ import { notify } from './notify';
* @returns {promise}
*/
export function runInterpreter(ast, context = null, options = {}) {
return interpretAst(ast, context)
return loadLegacyServerFunctionWrappers()
.then(() => interpretAst(ast, context))
.then(renderable => {
if (getType(renderable) === 'render') {
return renderable;