diff --git a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx index 80b5bacfc5adb..78fb43772504c 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expression_renderer.tsx @@ -22,6 +22,7 @@ import React from 'react'; import { Ast } from '@kbn/interpreter/common'; import { ExpressionRunnerOptions, ExpressionRunner } from './expression_runner'; +import { Result } from './expressions_service'; // Accept all options of the runner as props except for the // dom element which is provided by the component itself @@ -30,12 +31,19 @@ export type ExpressionRendererProps = Pick< Exclude > & { expression: string | Ast; + /** + * 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; }; export type ExpressionRenderer = React.FC; export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ expression, + onRenderFailure, ...options }: ExpressionRendererProps) => { const mountpoint: React.MutableRefObject = useRef(null); @@ -43,7 +51,11 @@ export const createRenderer = (run: ExpressionRunner): ExpressionRenderer => ({ useEffect( () => { if (mountpoint.current) { - run(expression, { ...options, element: mountpoint.current }); + run(expression, { ...options, element: mountpoint.current }).catch(result => { + if (onRenderFailure) { + onRenderFailure(result); + } + }); } }, [expression, mountpoint.current] diff --git a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts index 26951ea605bf2..bfad401ae8620 100644 --- a/src/legacy/core_plugins/data/public/expressions/expression_runner.ts +++ b/src/legacy/core_plugins/data/public/expressions/expression_runner.ts @@ -52,8 +52,12 @@ export const createRunFn = ( }, }); + if (response.type === 'error') { + throw response; + } + if (element) { - if (response.type === 'render' && response.as) { + if (response.type === 'render' && response.as && renderersRegistry.get(response.as) !== null) { renderersRegistry.get(response.as).render(element, response.value, { onDestroy: fn => { // TODO implement @@ -63,8 +67,7 @@ export const createRunFn = ( }, }); } else { - // eslint-disable-next-line no-console - console.log('Unexpected result of expression', response); + throw response; } } diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx index 9a464da2731c8..fdd0d73763681 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.test.tsx @@ -39,18 +39,22 @@ const waitForInterpreterRun = async () => { await new Promise(resolve => setTimeout(resolve)); }; +const RENDERER_ID = 'mockId'; + describe('expressions_service', () => { + let interpretAstMock: jest.Mocked['interpretAst']; let interpreterMock: jest.Mocked; let renderFunctionMock: jest.Mocked; let setupPluginsMock: ExpressionsServiceDependencies; - const expressionResult: Result = { type: 'render', as: 'abc', value: {} }; + const expressionResult: Result = { type: 'render', as: RENDERER_ID, value: {} }; let api: ExpressionsSetup; let testExpression: string; let testAst: Ast; beforeEach(() => { - interpreterMock = { interpretAst: jest.fn(_ => Promise.resolve(expressionResult)) }; + interpretAstMock = jest.fn(_ => Promise.resolve(expressionResult)); + interpreterMock = { interpretAst: interpretAstMock }; renderFunctionMock = ({ render: jest.fn(), } as unknown) as jest.Mocked; @@ -58,7 +62,7 @@ describe('expressions_service', () => { interpreter: { getInterpreter: () => Promise.resolve({ interpreter: interpreterMock }), renderersRegistry: ({ - get: () => renderFunctionMock, + get: (id: string) => (id === RENDERER_ID ? renderFunctionMock : null), } as unknown) as RenderFunctionsRegistry, }, }; @@ -101,6 +105,47 @@ describe('expressions_service', () => { ); }); + 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'); @@ -213,5 +258,19 @@ describe('expressions_service', () => { 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(); + + await waitForInterpreterRun(); + + expect(renderFailureSpy).toHaveBeenCalledWith(errorResult); + }); }); }); diff --git a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts index 308fd44d6bc08..f22caf3d43ece 100644 --- a/src/legacy/core_plugins/data/public/expressions/expressions_service.ts +++ b/src/legacy/core_plugins/data/public/expressions/expressions_service.ts @@ -46,6 +46,7 @@ export interface Result { type: string; as?: string; value?: unknown; + error?: unknown; } interface RenderHandlers {