[Expressions] Update expressions public API to expose partial results support (#102403) (#104210)

* Add partial result flag to the execution result
* Update expressions plugin run method to return observable
* Update data getter in the execution contract to return observable
* Update the expression loader to take into account the partial results flag
This commit is contained in:
Michael Dokolin 2021-07-02 00:57:17 +02:00 committed by GitHub
parent e601457bba
commit c73e13d744
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 370 additions and 281 deletions

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown>;
interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>>;
```
## Parameters
@ -19,5 +19,5 @@ interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown>;
<b>Returns:</b>
`Observable<unknown>`
`Observable<ExecutionResult<unknown>>`

View file

@ -26,8 +26,8 @@ export declare class Execution<Input = unknown, Output = unknown, InspectorAdapt
| [expression](./kibana-plugin-plugins-expressions-public.execution.expression.md) | | <code>string</code> | |
| [input](./kibana-plugin-plugins-expressions-public.execution.input.md) | | <code>Input</code> | Initial input of the execution.<!-- -->N.B. It is initialized to <code>null</code> rather than <code>undefined</code> for legacy reasons, because in legacy interpreter it was set to <code>null</code> by default. |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.execution.inspectoradapters.md) | | <code>InspectorAdapters</code> | |
| [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | <code>Observable&lt;Output &#124; ExpressionValueError&gt;</code> | Future that tracks result or error of this execution. |
| [state](./kibana-plugin-plugins-expressions-public.execution.state.md) | | <code>ExecutionContainer&lt;Output &#124; ExpressionValueError&gt;</code> | Dynamic state of the execution. |
| [result](./kibana-plugin-plugins-expressions-public.execution.result.md) | | <code>Observable&lt;ExecutionResult&lt;Output &#124; ExpressionValueError&gt;&gt;</code> | Future that tracks result or error of this execution. |
| [state](./kibana-plugin-plugins-expressions-public.execution.state.md) | | <code>ExecutionContainer&lt;ExecutionResult&lt;Output &#124; ExpressionValueError&gt;&gt;</code> | Dynamic state of the execution. |
## Methods

View file

@ -9,5 +9,5 @@ Future that tracks result or error of this execution.
<b>Signature:</b>
```typescript
readonly result: Observable<Output | ExpressionValueError>;
readonly result: Observable<ExecutionResult<Output | ExpressionValueError>>;
```

View file

@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons
<b>Signature:</b>
```typescript
start(input?: Input): Observable<Output | ExpressionValueError>;
start(input?: Input): Observable<ExecutionResult<Output | ExpressionValueError>>;
```
## Parameters
@ -22,5 +22,5 @@ start(input?: Input): Observable<Output | ExpressionValueError>;
<b>Returns:</b>
`Observable<Output | ExpressionValueError>`
`Observable<ExecutionResult<Output | ExpressionValueError>>`

View file

@ -9,5 +9,5 @@ Dynamic state of the execution.
<b>Signature:</b>
```typescript
readonly state: ExecutionContainer<Output | ExpressionValueError>;
readonly state: ExecutionContainer<ExecutionResult<Output | ExpressionValueError>>;
```

View file

@ -9,5 +9,5 @@ Returns the final output of expression, if any error happens still wraps that er
<b>Signature:</b>
```typescript
getData: () => Promise<Output | ExpressionValueError>;
getData: () => Observable<ExecutionResult<Output | ExpressionValueError>>;
```

View file

@ -25,7 +25,7 @@ export declare class ExecutionContract<Input = unknown, Output = unknown, Inspec
| [cancel](./kibana-plugin-plugins-expressions-public.executioncontract.cancel.md) | | <code>() =&gt; void</code> | Cancel the execution of the expression. This will set abort signal (available in execution context) to aborted state, letting expression functions to stop their execution. |
| [execution](./kibana-plugin-plugins-expressions-public.executioncontract.execution.md) | | <code>Execution&lt;Input, Output, InspectorAdapters&gt;</code> | |
| [getAst](./kibana-plugin-plugins-expressions-public.executioncontract.getast.md) | | <code>() =&gt; ExpressionAstExpression</code> | Get AST used to execute the expression. |
| [getData](./kibana-plugin-plugins-expressions-public.executioncontract.getdata.md) | | <code>() =&gt; Promise&lt;Output &#124; ExpressionValueError&gt;</code> | Returns the final output of expression, if any error happens still wraps that error into <code>ExpressionValueError</code> type and returns that. This function never throws. |
| [getData](./kibana-plugin-plugins-expressions-public.executioncontract.getdata.md) | | <code>() =&gt; Observable&lt;ExecutionResult&lt;Output &#124; ExpressionValueError&gt;&gt;</code> | Returns the final output of expression, if any error happens still wraps that error into <code>ExpressionValueError</code> type and returns that. This function never throws. |
| [getExpression](./kibana-plugin-plugins-expressions-public.executioncontract.getexpression.md) | | <code>() =&gt; string</code> | Get string representation of the expression. Returns the original string if execution was started from a string. If execution was started from an AST this method returns a string generated from AST. |
| [inspect](./kibana-plugin-plugins-expressions-public.executioncontract.inspect.md) | | <code>() =&gt; InspectorAdapters</code> | Get Inspector adapters provided to all functions of expression through execution context. |
| [isPending](./kibana-plugin-plugins-expressions-public.executioncontract.ispending.md) | | <code>boolean</code> | |

View file

@ -9,7 +9,7 @@ Execute expression and return result.
<b>Signature:</b>
```typescript
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<Output | ExpressionValueError>;
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<ExecutionResult<Output | ExpressionValueError>>;
```
## Parameters
@ -22,5 +22,5 @@ run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?:
<b>Returns:</b>
`Observable<Output | ExpressionValueError>`
`Observable<ExecutionResult<Output | ExpressionValueError>>`

View file

@ -21,7 +21,7 @@ export interface ExpressionsServiceStart
| [getFunction](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getfunction.md) | <code>(name: string) =&gt; ReturnType&lt;Executor['getFunction']&gt;</code> | Get a registered <code>ExpressionFunction</code> by its name, which was registered using the <code>registerFunction</code> method. The returned <code>ExpressionFunction</code> instance is an internal representation of the function in Expressions service - do not mutate that object. |
| [getRenderer](./kibana-plugin-plugins-expressions-public.expressionsservicestart.getrenderer.md) | <code>(name: string) =&gt; ReturnType&lt;ExpressionRendererRegistry['get']&gt;</code> | Get a registered <code>ExpressionRenderer</code> by its name, which was registered using the <code>registerRenderer</code> method. The returned <code>ExpressionRenderer</code> instance is an internal representation of the renderer in Expressions service - do not mutate that object. |
| [getType](./kibana-plugin-plugins-expressions-public.expressionsservicestart.gettype.md) | <code>(name: string) =&gt; ReturnType&lt;Executor['getType']&gt;</code> | Get a registered <code>ExpressionType</code> by its name, which was registered using the <code>registerType</code> method. The returned <code>ExpressionType</code> instance is an internal representation of the type in Expressions service - do not mutate that object. |
| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <code>&lt;Input, Output&gt;(ast: string &#124; ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) =&gt; Promise&lt;Output&gt;</code> | Executes expression string or a parsed expression AST and immediately returns the result.<!-- -->Below example will execute <code>sleep 100 &#124; clog</code> expression with <code>123</code> initial input to the first function.
| [run](./kibana-plugin-plugins-expressions-public.expressionsservicestart.run.md) | <code>&lt;Input, Output&gt;(ast: string &#124; ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) =&gt; Observable&lt;ExecutionResult&lt;Output &#124; ExpressionValueError&gt;&gt;</code> | Executes expression string or a parsed expression AST and immediately returns the result.<!-- -->Below example will execute <code>sleep 100 &#124; clog</code> expression with <code>123</code> initial input to the first function.
```ts
expressions.run('sleep 100 | clog', 123);

View file

@ -24,5 +24,5 @@ expressions.run('...', null, { elasticsearchClient });
<b>Signature:</b>
```typescript
run: <Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise<Output>;
run: <Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Observable<ExecutionResult<Output | ExpressionValueError>>;
```

View file

@ -22,6 +22,7 @@ export interface IExpressionLoaderParams
| [hasCompatibleActions](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.hascompatibleactions.md) | <code>ExpressionRenderHandlerParams['hasCompatibleActions']</code> | |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.inspectoradapters.md) | <code>Adapters</code> | |
| [onRenderError](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.onrendererror.md) | <code>RenderErrorHandlerFnType</code> | |
| [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md) | <code>boolean</code> | |
| [renderMode](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.rendermode.md) | <code>RenderMode</code> | |
| [searchContext](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchcontext.md) | <code>SerializableState</code> | |
| [searchSessionId](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.searchsessionid.md) | <code>string</code> | |

View file

@ -0,0 +1,11 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
[Home](./index.md) &gt; [kibana-plugin-plugins-expressions-public](./kibana-plugin-plugins-expressions-public.md) &gt; [IExpressionLoaderParams](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) &gt; [partial](./kibana-plugin-plugins-expressions-public.iexpressionloaderparams.partial.md)
## IExpressionLoaderParams.partial property
<b>Signature:</b>
```typescript
partial?: boolean;
```

View file

@ -18,7 +18,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams
| [dataAttrs](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.dataattrs.md) | <code>string[]</code> | |
| [debounce](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.debounce.md) | <code>number</code> | |
| [expression](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.expression.md) | <code>string &#124; ExpressionAstExpression</code> | |
| [onData$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md) | <code>&lt;TData, TInspectorAdapters&gt;(data: TData, adapters?: TInspectorAdapters) =&gt; void</code> | |
| [onData$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.ondata_.md) | <code>&lt;TData, TInspectorAdapters&gt;(data: TData, adapters?: TInspectorAdapters, partial?: boolean) =&gt; void</code> | |
| [onEvent](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.onevent.md) | <code>(event: ExpressionRendererEvent) =&gt; void</code> | |
| [padding](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.padding.md) | <code>'xs' &#124; 's' &#124; 'm' &#124; 'l' &#124; 'xl'</code> | |
| [reload$](./kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.reload_.md) | <code>Observable&lt;unknown&gt;</code> | An observable which can be used to re-run the expression without destroying the component |

View file

@ -7,5 +7,5 @@
<b>Signature:</b>
```typescript
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void;
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters, partial?: boolean) => void;
```

View file

@ -7,7 +7,7 @@
<b>Signature:</b>
```typescript
interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown>;
interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>>;
```
## Parameters
@ -19,5 +19,5 @@ interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown>;
<b>Returns:</b>
`Observable<unknown>`
`Observable<ExecutionResult<unknown>>`

View file

@ -26,8 +26,8 @@ export declare class Execution<Input = unknown, Output = unknown, InspectorAdapt
| [expression](./kibana-plugin-plugins-expressions-server.execution.expression.md) | | <code>string</code> | |
| [input](./kibana-plugin-plugins-expressions-server.execution.input.md) | | <code>Input</code> | Initial input of the execution.<!-- -->N.B. It is initialized to <code>null</code> rather than <code>undefined</code> for legacy reasons, because in legacy interpreter it was set to <code>null</code> by default. |
| [inspectorAdapters](./kibana-plugin-plugins-expressions-server.execution.inspectoradapters.md) | | <code>InspectorAdapters</code> | |
| [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | <code>Observable&lt;Output &#124; ExpressionValueError&gt;</code> | Future that tracks result or error of this execution. |
| [state](./kibana-plugin-plugins-expressions-server.execution.state.md) | | <code>ExecutionContainer&lt;Output &#124; ExpressionValueError&gt;</code> | Dynamic state of the execution. |
| [result](./kibana-plugin-plugins-expressions-server.execution.result.md) | | <code>Observable&lt;ExecutionResult&lt;Output &#124; ExpressionValueError&gt;&gt;</code> | Future that tracks result or error of this execution. |
| [state](./kibana-plugin-plugins-expressions-server.execution.state.md) | | <code>ExecutionContainer&lt;ExecutionResult&lt;Output &#124; ExpressionValueError&gt;&gt;</code> | Dynamic state of the execution. |
## Methods

View file

@ -9,5 +9,5 @@ Future that tracks result or error of this execution.
<b>Signature:</b>
```typescript
readonly result: Observable<Output | ExpressionValueError>;
readonly result: Observable<ExecutionResult<Output | ExpressionValueError>>;
```

View file

@ -11,7 +11,7 @@ N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons
<b>Signature:</b>
```typescript
start(input?: Input): Observable<Output | ExpressionValueError>;
start(input?: Input): Observable<ExecutionResult<Output | ExpressionValueError>>;
```
## Parameters
@ -22,5 +22,5 @@ start(input?: Input): Observable<Output | ExpressionValueError>;
<b>Returns:</b>
`Observable<Output | ExpressionValueError>`
`Observable<ExecutionResult<Output | ExpressionValueError>>`

View file

@ -9,5 +9,5 @@ Dynamic state of the execution.
<b>Signature:</b>
```typescript
readonly state: ExecutionContainer<Output | ExpressionValueError>;
readonly state: ExecutionContainer<ExecutionResult<Output | ExpressionValueError>>;
```

View file

@ -9,7 +9,7 @@ Execute expression and return result.
<b>Signature:</b>
```typescript
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<Output | ExpressionValueError>;
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<ExecutionResult<Output | ExpressionValueError>>;
```
## Parameters
@ -22,5 +22,5 @@ run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?:
<b>Returns:</b>
`Observable<Output | ExpressionValueError>`
`Observable<ExecutionResult<Output | ExpressionValueError>>`

View file

@ -7,6 +7,7 @@
*/
import React, { useState, useEffect, useMemo } from 'react';
import { pluck } from 'rxjs/operators';
import {
EuiCodeBlock,
EuiFlexItem,
@ -35,7 +36,7 @@ interface Props {
export function RunExpressionsExample({ expressions, inspector }: Props) {
const [expression, updateExpression] = useState('markdown "## expressions explorer"');
const [result, updateResult] = useState({});
const [result, updateResult] = useState<unknown>({});
const expressionChanged = (value: string) => {
updateExpression(value);
@ -49,17 +50,13 @@ export function RunExpressionsExample({ expressions, inspector }: Props) {
);
useEffect(() => {
const runExpression = async () => {
const execution = expressions.execute(expression, null, {
debug: true,
inspectorAdapters,
});
const execution = expressions.execute(expression, null, {
debug: true,
inspectorAdapters,
});
const subscription = execution.getData().pipe(pluck('result')).subscribe(updateResult);
const data: any = await execution.getData();
updateResult(data);
};
runExpression();
return () => subscription.unsubscribe();
}, [expression, expressions, inspectorAdapters]);
return (

View file

@ -6,7 +6,6 @@
* Side Public License, v 1.
*/
import { first } from 'rxjs/operators';
import { waitFor } from '@testing-library/react';
import { Execution } from './execution';
import { parseExpression } from '../ast';
@ -40,9 +39,9 @@ describe('Execution abortion tests', () => {
execution.start();
execution.cancel();
const result = await execution.result.pipe(first()).toPromise();
const result = await execution.result.toPromise();
expect(result).toMatchObject({
expect(result).toHaveProperty('result', {
type: 'error',
error: {
message: 'The expression was aborted.',
@ -58,9 +57,9 @@ describe('Execution abortion tests', () => {
jest.advanceTimersByTime(100);
execution.cancel();
const result = await execution.result.pipe(first()).toPromise();
const result = await execution.result.toPromise();
expect(result).toMatchObject({
expect(result).toHaveProperty('result', {
type: 'error',
error: {
message: 'The expression was aborted.',
@ -76,7 +75,7 @@ describe('Execution abortion tests', () => {
execution.start();
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
execution.cancel();
@ -136,7 +135,7 @@ describe('Execution abortion tests', () => {
await waitFor(() => expect(started).toHaveBeenCalledTimes(1));
execution.cancel();
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toMatchObject({
type: 'error',
error: {

View file

@ -7,7 +7,7 @@
*/
import { of } from 'rxjs';
import { first, scan } from 'rxjs/operators';
import { scan } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/testing';
import { Execution } from './execution';
import { parseExpression, ExpressionAstExpression } from '../ast';
@ -45,7 +45,7 @@ const run = async (
) => {
const execution = createExecution(expression, context);
execution.start(input);
return await execution.result.pipe(first()).toPromise();
return await execution.result.toPromise();
};
let testScheduler: TestScheduler;
@ -84,7 +84,7 @@ describe('Execution', () => {
/* eslint-enable no-console */
execution.start(123);
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toBe(123);
expect(spy).toHaveBeenCalledTimes(1);
@ -102,7 +102,7 @@ describe('Execution', () => {
value: -1,
});
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -117,7 +117,7 @@ describe('Execution', () => {
value: 0,
});
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -131,7 +131,7 @@ describe('Execution', () => {
// Below 1 is cast to { type: 'num', value: 1 }.
execution.start(1);
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -143,7 +143,7 @@ describe('Execution', () => {
const execution = createExecution('add val=1');
execution.start(Promise.resolve(1));
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -155,7 +155,7 @@ describe('Execution', () => {
const execution = createExecution('add val=1');
execution.start(of(1));
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -167,14 +167,14 @@ describe('Execution', () => {
const execution = createExecution('add val=1');
testScheduler.run(({ cold, expectObservable }) => {
const input = cold(' -a--b-c-', { a: 1, b: 2, c: 3 });
const input = cold(' -a--b-c|', { a: 1, b: 2, c: 3 });
const subscription = ' ---^---!';
const expected = ' ---ab-c-';
const expected = ' ---ab-c|';
expectObservable(execution.start(input), subscription).toBe(expected, {
a: { type: 'num', value: 2 },
b: { type: 'num', value: 3 },
c: { type: 'num', value: 4 },
a: { partial: false, result: { type: 'num', value: 2 } },
b: { partial: false, result: { type: 'num', value: 3 } },
c: { partial: false, result: { type: 'num', value: 4 } },
});
});
});
@ -187,21 +187,21 @@ describe('Execution', () => {
const expected = ' -a-#';
expectObservable(execution.start(input)).toBe(expected, {
a: { type: 'num', value: 2 },
a: { partial: false, result: { type: 'num', value: 2 } },
});
});
});
test('does not complete when input completes', () => {
test('completes when input completes', () => {
const execution = createExecution('add val=1');
testScheduler.run(({ cold, expectObservable }) => {
const input = cold('-a-b|', { a: 1, b: 2 });
const expected = ' -a-b-';
const expected = ' -a-b|';
expectObservable(execution.start(input)).toBe(expected, {
a: { type: 'num', value: 2 },
b: { type: 'num', value: 3 },
a: expect.objectContaining({ result: { type: 'num', value: 2 } }),
b: expect.objectContaining({ result: { type: 'num', value: 3 } }),
});
});
});
@ -216,9 +216,9 @@ describe('Execution', () => {
const input = items.pipe(scan((result, value) => [...result, value], new Array<number>()));
expectObservable(execution.start(input), subscription).toBe(expected, {
a: { type: 'num', value: 1 },
b: { type: 'num', value: 3 },
c: { type: 'num', value: 6 },
a: { partial: false, result: { type: 'num', value: 1 } },
b: { partial: false, result: { type: 'num', value: 3 } },
c: { partial: false, result: { type: 'num', value: 6 } },
});
});
});
@ -263,44 +263,51 @@ describe('Execution', () => {
describe('execution context', () => {
test('context.variables is an object', async () => {
const { result } = (await run('introspectContext key="variables"')) as any;
expect(typeof result).toBe('object');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.types is an object', async () => {
const { result } = (await run('introspectContext key="types"')) as any;
expect(typeof result).toBe('object');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.abortSignal is an object', async () => {
const { result } = (await run('introspectContext key="abortSignal"')) as any;
expect(typeof result).toBe('object');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.inspectorAdapters is an object', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
expect(typeof result).toBe('object');
expect(result).toHaveProperty('result', expect.any(Object));
});
test('context.getKibanaRequest is a function if provided', async () => {
const { result } = (await run('introspectContext key="getKibanaRequest"', {
kibanaRequest: {},
})) as any;
expect(typeof result).toBe('function');
expect(result).toHaveProperty('result', expect.any(Function));
});
test('context.getKibanaRequest is undefined if not provided', async () => {
const { result } = (await run('introspectContext key="getKibanaRequest"')) as any;
expect(typeof result).toBe('undefined');
expect(result).toHaveProperty('result', undefined);
});
test('unknown context key is undefined', async () => {
const { result } = (await run('introspectContext key="foo"')) as any;
expect(typeof result).toBe('undefined');
expect(result).toHaveProperty('result', undefined);
});
test('can set context variables', async () => {
const variables = { foo: 'bar' };
const result = await run('var name="foo"', { variables });
const { result } = await run('var name="foo"', { variables });
expect(result).toBe('bar');
});
});
@ -308,10 +315,13 @@ describe('Execution', () => {
describe('inspector adapters', () => {
test('by default, "tables" and "requests" inspector adapters are available', async () => {
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
expect(result).toMatchObject({
tables: expect.any(Object),
requests: expect.any(Object),
});
expect(result).toHaveProperty(
'result',
expect.objectContaining({
tables: expect.any(Object),
requests: expect.any(Object),
})
);
});
test('can set custom inspector adapters', async () => {
@ -319,7 +329,7 @@ describe('Execution', () => {
const { result } = (await run('introspectContext key="inspectorAdapters"', {
inspectorAdapters,
})) as any;
expect(result).toBe(inspectorAdapters);
expect(result).toHaveProperty('result', inspectorAdapters);
});
test('can access custom inspector adapters on Execution object', async () => {
@ -335,8 +345,7 @@ describe('Execution', () => {
test('context has abortSignal object', async () => {
const { result } = (await run('introspectContext key="abortSignal"')) as any;
expect(typeof result).toBe('object');
expect((result as AbortSignal).aborted).toBe(false);
expect(result).toHaveProperty('result.aborted', false);
});
});
@ -348,7 +357,7 @@ describe('Execution', () => {
value: 0,
});
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -357,8 +366,8 @@ describe('Execution', () => {
});
test('can execute async functions', async () => {
const res = await run('sleep 10 | sleep 10');
expect(res).toBe(null);
const { result } = await run('sleep 10 | sleep 10');
expect(result).toBe(null);
});
test('result is undefined until execution completes', async () => {
@ -374,7 +383,7 @@ describe('Execution', () => {
jest.advanceTimersByTime(10);
await new Promise(process.nextTick);
expect(execution.state.get().result).toBe(null);
expect(execution.state.get().result).toHaveProperty('result', null);
jest.useRealTimers();
});
@ -382,7 +391,7 @@ describe('Execution', () => {
test('handles functions returning observables', () => {
testScheduler.run(({ cold, expectObservable }) => {
const arg = cold(' -a-b-c|', { a: 1, b: 2, c: 3 });
const expected = ' -a-b-c-';
const expected = ' -a-b-c|';
const observable: ExpressionFunctionDefinition<'observable', any, {}, any> = {
name: 'observable',
args: {},
@ -394,14 +403,18 @@ describe('Execution', () => {
const result = executor.run('observable', null, {});
expectObservable(result).toBe(expected, { a: 1, b: 2, c: 3 });
expectObservable(result).toBe(expected, {
a: { result: 1, partial: true },
b: { result: 2, partial: true },
c: { result: 3, partial: false },
});
});
});
});
describe('when function throws', () => {
test('error is reported in output object', async () => {
const result = await run('error "foobar"');
const { result } = await run('error "foobar"');
expect(result).toMatchObject({
type: 'error',
@ -409,7 +422,7 @@ describe('Execution', () => {
});
test('error message is prefixed with function name', async () => {
const result = await run('error "foobar"');
const { result } = await run('error "foobar"');
expect(result).toMatchObject({
error: {
@ -419,7 +432,7 @@ describe('Execution', () => {
});
test('returns error of the first function that throws', async () => {
const result = await run('error "foo" | error "bar"');
const { result } = await run('error "foo" | error "bar"');
expect(result).toMatchObject({
error: {
@ -432,15 +445,18 @@ describe('Execution', () => {
const execution = await createExecution('error "foo"');
execution.start(null);
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toMatchObject({
type: 'error',
});
expect(execution.state.get().state).toBe('result');
expect(execution.state.get().result).toMatchObject({
type: 'error',
});
expect(execution.state.get().result).toHaveProperty(
'result',
expect.objectContaining({
type: 'error',
})
);
});
test('does not execute remaining functions in pipeline', async () => {
@ -453,7 +469,7 @@ describe('Execution', () => {
const executor = createUnitTestExecutor();
executor.registerFunction(spy);
await executor.run('error "..." | spy', null).pipe(first()).toPromise();
await executor.run('error "..." | spy', null).toPromise();
expect(spy.fn).toHaveBeenCalledTimes(0);
});
@ -483,21 +499,21 @@ describe('Execution', () => {
test('execution state is "result" when execution successfully completes', async () => {
const execution = createExecution('sleep 1');
execution.start(null);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
expect(execution.state.get().state).toBe('result');
});
test('execution state is "result" when execution successfully completes - 2', async () => {
const execution = createExecution('var foo');
execution.start(null);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
expect(execution.state.get().state).toBe('result');
});
});
describe('sub-expressions', () => {
test('executes sub-expressions', async () => {
const result = await run('add val={add 5 | access "value"}', {}, null);
const { result } = await run('add val={add 5 | access "value"}', {}, null);
expect(result).toMatchObject({
type: 'num',
@ -506,7 +522,7 @@ describe('Execution', () => {
});
test('can use global variables', async () => {
const result = await run(
const { result } = await run(
'add val={var foo}',
{
variables: {
@ -523,7 +539,7 @@ describe('Execution', () => {
});
test('can modify global variables', async () => {
const result = await run(
const { result } = await run(
'add val={var_set name=foo value=66 | var bar} | var foo',
{
variables: {
@ -547,18 +563,20 @@ describe('Execution', () => {
const executor = createUnitTestExecutor();
executor.registerFunction(observable);
expect(
executor.run('add val={observable}', 1, {}).pipe(first()).toPromise()
).resolves.toEqual({
type: 'num',
value: 2,
});
expect(executor.run('add val={observable}', 1, {}).toPromise()).resolves.toEqual(
expect.objectContaining({
result: {
type: 'num',
value: 2,
},
})
);
});
test('supports observables in arguments emitting multiple values', () => {
testScheduler.run(({ cold, expectObservable }) => {
const arg = cold('-a-b-c-', { a: 1, b: 2, c: 3 });
const expected = '-a-b-c-';
const arg = cold('-a-b-c|', { a: 1, b: 2, c: 3 });
const expected = '-a-b-c|';
const observable = {
name: 'observable',
args: {},
@ -571,18 +589,18 @@ describe('Execution', () => {
const result = executor.run('add val={observable}', 1, {});
expectObservable(result).toBe(expected, {
a: { type: 'num', value: 2 },
b: { type: 'num', value: 3 },
c: { type: 'num', value: 4 },
a: { partial: true, result: { type: 'num', value: 2 } },
b: { partial: true, result: { type: 'num', value: 3 } },
c: { partial: false, result: { type: 'num', value: 4 } },
});
});
});
test('combines multiple observables in arguments', () => {
testScheduler.run(({ cold, expectObservable }) => {
const arg1 = cold('--ab-c-', { a: 0, b: 2, c: 4 });
const arg2 = cold('-a--bc-', { a: 1, b: 3, c: 5 });
const expected = ' --abc(de)-';
const arg1 = cold('--ab-c---|', { a: 0, b: 2, c: 4 });
const arg2 = cold('-a--bc---|', { a: 1, b: 3, c: 5 });
const expected = ' --abc(de)|';
const observable1 = {
name: 'observable1',
args: {},
@ -612,32 +630,11 @@ describe('Execution', () => {
const result = executor.run('max val1={observable1} val2={observable2}', {});
expectObservable(result).toBe(expected, {
a: { type: 'num', value: 1 },
b: { type: 'num', value: 2 },
c: { type: 'num', value: 3 },
d: { type: 'num', value: 4 },
e: { type: 'num', value: 5 },
});
});
});
test('does not complete when an argument completes', () => {
testScheduler.run(({ cold, expectObservable }) => {
const arg = cold('-a|', { a: 1 });
const expected = '-a-';
const observable = {
name: 'observable',
args: {},
help: '',
fn: () => arg,
};
const executor = createUnitTestExecutor();
executor.registerFunction(observable);
const result = executor.run('add val={observable}', 1, {});
expectObservable(result).toBe(expected, {
a: { type: 'num', value: 2 },
a: { partial: true, result: { type: 'num', value: 1 } },
b: { partial: true, result: { type: 'num', value: 2 } },
c: { partial: true, result: { type: 'num', value: 3 } },
d: { partial: true, result: { type: 'num', value: 4 } },
e: { partial: false, result: { type: 'num', value: 5 } },
});
});
});
@ -645,7 +642,7 @@ describe('Execution', () => {
test('handles error in observable arguments', () => {
testScheduler.run(({ cold, expectObservable }) => {
const arg = cold('-a-#', { a: 1 }, new Error('some error'));
const expected = '-a-b';
const expected = '-a-(b|)';
const observable = {
name: 'observable',
args: {},
@ -658,13 +655,15 @@ describe('Execution', () => {
const result = executor.run('add val={observable}', 1, {});
expectObservable(result).toBe(expected, {
a: { type: 'num', value: 2 },
b: {
error: expect.objectContaining({
message: '[add] > [observable] > some error',
}),
type: 'error',
},
a: expect.objectContaining({ result: { type: 'num', value: 2 } }),
b: expect.objectContaining({
result: {
error: expect.objectContaining({
message: '[add] > [observable] > some error',
}),
type: 'error',
},
}),
});
});
});
@ -685,7 +684,7 @@ describe('Execution', () => {
};
const executor = createUnitTestExecutor();
executor.registerFunction(requiredArg);
const result = await executor.run('requiredArg', null, {}).pipe(first()).toPromise();
const { result } = await executor.run('requiredArg', null, {}).toPromise();
expect(result).toMatchObject({
type: 'error',
@ -696,7 +695,7 @@ describe('Execution', () => {
});
test('when required argument is missing and has alias, returns error', async () => {
const result = await run('var_set', {});
const { result } = await run('var_set', {});
expect(result).toMatchObject({
type: 'error',
@ -711,7 +710,7 @@ describe('Execution', () => {
test('can execute expression in debug mode', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -726,7 +725,7 @@ describe('Execution', () => {
true
);
execution.start(0);
const result = await execution.result.pipe(first()).toPromise();
const { result } = await execution.result.toPromise();
expect(result).toEqual({
type: 'num',
@ -738,7 +737,7 @@ describe('Execution', () => {
test('sets "success" flag on all functions to true', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
for (const node of execution.state.get().ast.chain) {
expect(node.debug?.success).toBe(true);
@ -748,7 +747,7 @@ describe('Execution', () => {
test('stores "fn" reference to the function', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
for (const node of execution.state.get().ast.chain) {
expect(node.debug?.fn).toBe('add');
@ -758,7 +757,7 @@ describe('Execution', () => {
test('saves duration it took to execute each function', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
for (const node of execution.state.get().ast.chain) {
expect(typeof node.debug?.duration).toBe('number');
@ -770,7 +769,7 @@ describe('Execution', () => {
test('adds .debug field in expression AST on each executed function', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
for (const node of execution.state.get().ast.chain) {
expect(typeof node.debug).toBe('object');
@ -781,7 +780,7 @@ describe('Execution', () => {
test('stores input of each function', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const { chain } = execution.state.get().ast;
@ -799,7 +798,7 @@ describe('Execution', () => {
test('stores output of each function', async () => {
const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const { chain } = execution.state.get().ast;
@ -824,7 +823,7 @@ describe('Execution', () => {
true
);
execution.start(-1);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const { chain } = execution.state.get().ast;
@ -847,7 +846,7 @@ describe('Execution', () => {
true
);
execution.start(0);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const { chain } = execution.state.get().ast.chain[0].arguments
.val[0] as ExpressionAstExpression;
@ -882,7 +881,7 @@ describe('Execution', () => {
params: { debug: true },
});
execution.start(0);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const node1 = execution.state.get().ast.chain[0];
const node2 = execution.state.get().ast.chain[1];
@ -900,7 +899,7 @@ describe('Execution', () => {
params: { debug: true },
});
execution.start(0);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const node2 = execution.state.get().ast.chain[1];
@ -921,7 +920,7 @@ describe('Execution', () => {
params: { debug: true },
});
execution.start(0);
await execution.result.pipe(first()).toPromise();
await execution.result.toPromise();
const node2 = execution.state.get().ast.chain[1];

View file

@ -20,7 +20,7 @@ import {
Observable,
ReplaySubject,
} from 'rxjs';
import { catchError, finalize, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { catchError, finalize, map, pluck, shareReplay, switchMap, tap } from 'rxjs/operators';
import { Executor } from '../executor';
import { createExecutionContainer, ExecutionContainer } from './container';
import { createError } from '../util';
@ -45,6 +45,21 @@ import { ExpressionExecutionParams } from '../service';
import { TablesAdapter } from '../util/tables_adapter';
import { ExpressionsInspectorAdapter } from '../util/expressions_inspector_adapter';
/**
* The result returned after an expression function execution.
*/
export interface ExecutionResult<Output> {
/**
* Partial result flag.
*/
partial: boolean;
/**
* The expression function result.
*/
result: Output;
}
/**
* AbortController is not available in Node until v15, so we
* need to temporarily mock it for plugins using expressions
@ -91,7 +106,7 @@ export class Execution<
/**
* Dynamic state of the execution.
*/
public readonly state: ExecutionContainer<Output | ExpressionValueError>;
public readonly state: ExecutionContainer<ExecutionResult<Output | ExpressionValueError>>;
/**
* Initial input of the execution.
@ -137,7 +152,7 @@ export class Execution<
/**
* Future that tracks result or error of this execution.
*/
public readonly result: Observable<Output | ExpressionValueError>;
public readonly result: Observable<ExecutionResult<Output | ExpressionValueError>>;
/**
* Keeping track of any child executions
@ -174,7 +189,7 @@ export class Execution<
this.expression = execution.expression || formatExpression(execution.ast!);
const ast = execution.ast || parseExpression(this.expression);
this.state = createExecutionContainer<Output | ExpressionValueError>({
this.state = createExecutionContainer({
...executor.state.get(),
state: 'not-started',
ast,
@ -201,12 +216,40 @@ export class Execution<
};
this.result = this.input$.pipe(
switchMap((input) => this.race(this.invokeChain(this.state.get().ast.chain, input))),
switchMap((input) =>
this.race(this.invokeChain(this.state.get().ast.chain, input)).pipe(
(source) =>
new Observable<ExecutionResult<Output>>((subscriber) => {
let latest: ExecutionResult<Output> | undefined;
subscriber.add(
source.subscribe({
next: (result) => {
latest = { result, partial: true };
subscriber.next(latest);
},
error: (error) => subscriber.error(error),
complete: () => {
if (latest) {
latest.partial = false;
}
subscriber.complete();
},
})
);
subscriber.add(() => {
latest = undefined;
});
})
)
),
catchError((error) => {
if (this.abortController.signal.aborted) {
this.childExecutions.forEach((childExecution) => childExecution.cancel());
return of(createAbortErrorValue());
return of({ result: createAbortErrorValue(), partial: false });
}
return throwError(error);
@ -236,25 +279,20 @@ export class Execution<
* N.B. `input` is initialized to `null` rather than `undefined` for legacy reasons,
* because in legacy interpreter it was set to `null` by default.
*/
public start(input: Input = null as any): Observable<Output | ExpressionValueError> {
public start(
input: Input = null as any
): Observable<ExecutionResult<Output | ExpressionValueError>> {
if (this.hasStarted) throw new Error('Execution already started.');
this.hasStarted = true;
this.input = input;
this.state.transitions.start();
if (isObservable<Input>(input)) {
// `input$` should never complete
input.subscribe(
(value) => this.input$.next(value),
(error) => this.input$.error(error)
);
input.subscribe(this.input$);
} else if (isPromise(input)) {
input.then(
(value) => this.input$.next(value),
(error) => this.input$.error(error)
);
from(input).subscribe(this.input$);
} else {
this.input$.next(input);
of(input).subscribe(this.input$);
}
return this.result;
@ -439,6 +477,7 @@ export class Execution<
const resolveArgFns = mapValues(dealiasedArgAsts, (asts, argName) =>
asts.map((item) => (subInput = input) =>
this.interpret(item, subInput).pipe(
pluck('result'),
map((output) => {
if (isExpressionValueError(output)) {
throw output.error;
@ -486,7 +525,7 @@ export class Execution<
});
}
public interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown> {
public interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>> {
switch (getType(ast)) {
case 'expression':
const execution = this.execution.executor.createExecution(
@ -494,12 +533,13 @@ export class Execution<
this.execution.params
);
this.childExecutions.push(execution);
return execution.start(input);
case 'string':
case 'number':
case 'null':
case 'boolean':
return of(ast);
return of({ result: ast, partial: false });
default:
return throwError(new Error(`Unknown AST object: ${JSON.stringify(ast)}`));
}

View file

@ -66,26 +66,23 @@ describe('ExecutionContract', () => {
});
});
test('can get error result of the expression execution', async () => {
test('can get error result of the expression execution', () => {
const execution = createExecution('foo bar=123');
const contract = new ExecutionContract(execution);
execution.start();
const result = await contract.getData();
expect(result).toMatchObject({
type: 'error',
});
expect(contract.getData().toPromise()).resolves.toHaveProperty(
'result',
expect.objectContaining({ type: 'error' })
);
});
test('can get result of the expression execution', async () => {
test('can get result of the expression execution', () => {
const execution = createExecution('var_set name="foo" value="bar" | var name="foo"');
const contract = new ExecutionContract(execution);
execution.start();
const result = await contract.getData();
expect(result).toBe('bar');
expect(contract.getData().toPromise()).resolves.toHaveProperty('result', 'bar');
});
describe('isPending', () => {

View file

@ -6,9 +6,9 @@
* Side Public License, v 1.
*/
import { of } from 'rxjs';
import { catchError, take } from 'rxjs/operators';
import { Execution } from './execution';
import { of, Observable } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { Execution, ExecutionResult } from './execution';
import { ExpressionValueError } from '../expression_types/specs';
import { ExpressionAstExpression } from '../ast';
@ -39,22 +39,22 @@ export class ExecutionContract<Input = unknown, Output = unknown, InspectorAdapt
* wraps that error into `ExpressionValueError` type and returns that.
* This function never throws.
*/
getData = async (): Promise<Output | ExpressionValueError> => {
return this.execution.result
.pipe(
take(1),
catchError(({ name, message, stack }) =>
of({
getData = (): Observable<ExecutionResult<Output | ExpressionValueError>> => {
return this.execution.result.pipe(
catchError(({ name, message, stack }) =>
of({
partial: false,
result: {
type: 'error',
error: {
name,
message,
stack,
},
} as ExpressionValueError)
)
} as ExpressionValueError,
})
)
.toPromise();
);
};
/**

View file

@ -13,7 +13,7 @@ import { Observable } from 'rxjs';
import { ExecutorState, ExecutorContainer } from './container';
import { createExecutorContainer } from './container';
import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions';
import { Execution, ExecutionParams } from '../execution/execution';
import { Execution, ExecutionParams, ExecutionResult } from '../execution/execution';
import { IRegistry } from '../types';
import { ExpressionType } from '../expression_types/expression_type';
import { AnyExpressionTypeDefinition } from '../expression_types/types';
@ -160,7 +160,7 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
ast: string | ExpressionAstExpression,
input: Input,
params: ExpressionExecutionParams = {}
): Observable<Output | ExpressionValueError> {
): Observable<ExecutionResult<Output | ExpressionValueError>> {
return this.createExecution<Input, Output>(ast, params).start(input);
}

View file

@ -65,7 +65,7 @@ describe('expression_functions', () => {
it('sets the variables', async () => {
const vars = {};
const result = await executor
const { result } = await executor
.run('var_set name=test1 name=test2 value=1', 2, { variables: vars })
.pipe(first())
.toPromise();

View file

@ -114,7 +114,7 @@ describe('ExpressionsService', () => {
},
});
const result = await fork.run('__test__', null);
const { result } = await fork.run('__test__', null).toPromise();
expect(result).toBe('123');
});

View file

@ -6,15 +6,15 @@
* Side Public License, v 1.
*/
import { take } from 'rxjs/operators';
import { Observable } from 'rxjs';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import type { KibanaRequest } from 'src/core/server';
import { Executor } from '../executor';
import { AnyExpressionRenderDefinition, ExpressionRendererRegistry } from '../expression_renderers';
import { ExpressionAstExpression } from '../ast';
import { ExecutionContract } from '../execution/execution_contract';
import { AnyExpressionTypeDefinition } from '../expression_types';
import { ExecutionContract, ExecutionResult } from '../execution';
import { AnyExpressionTypeDefinition, ExpressionValueError } from '../expression_types';
import { AnyExpressionFunctionDefinition } from '../expression_functions';
import { SavedObjectReference } from '../../../../core/types';
import { PersistableStateService, SerializableState } from '../../../kibana_utils/common';
@ -136,7 +136,7 @@ export interface ExpressionsServiceStart {
ast: string | ExpressionAstExpression,
input: Input,
params?: ExpressionExecutionParams
) => Promise<Output>;
) => Observable<ExecutionResult<Output | ExpressionValueError>>;
/**
* Starts expression execution and immediately returns `ExecutionContract`
@ -243,7 +243,7 @@ export class ExpressionsService implements PersistableStateService<ExpressionAst
): void => this.renderers.register(definition);
public readonly run: ExpressionsServiceStart['run'] = (ast, input, params) =>
this.executor.run(ast, input, params).pipe(take(1)).toPromise<any>();
this.executor.run(ast, input, params);
public readonly getFunction: ExpressionsServiceStart['getFunction'] = (name) =>
this.executor.getFunction(name);

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import { of } from 'rxjs';
import { first, skip, toArray } from 'rxjs/operators';
import { loader, ExpressionLoader } from './loader';
import { Observable } from 'rxjs';
@ -111,8 +112,29 @@ describe('ExpressionLoader', () => {
it('emits on $data when data is available', async () => {
const expressionLoader = new ExpressionLoader(element, 'var foo', { variables: { foo: 123 } });
const response = await expressionLoader.data$.pipe(first()).toPromise();
expect(response).toBe(123);
const { result } = await expressionLoader.data$.pipe(first()).toPromise();
expect(result).toBe(123);
});
it('ignores partial results by default', async () => {
const expressionLoader = new ExpressionLoader(element, 'var foo', {
variables: { foo: of(1, 2) },
});
const { result, partial } = await expressionLoader.data$.pipe(first()).toPromise();
expect(partial).toBe(false);
expect(result).toBe(2);
});
it('emits partial results if enabled', async () => {
const expressionLoader = new ExpressionLoader(element, 'var foo', {
variables: { foo: of(1, 2) },
partial: true,
});
const { result, partial } = await expressionLoader.data$.pipe(first()).toPromise();
expect(partial).toBe(true);
expect(result).toBe(1);
});
it('emits on loading$ on initial load and on updates', async () => {

View file

@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { filter, map, delay } from 'rxjs/operators';
import { defaults } from 'lodash';
import { UnwrapObservable } from '@kbn/utility-types';
import { Adapters } from '../../inspector/public';
import { IExpressionLoaderParams } from './types';
import { ExpressionAstExpression } from '../common';
@ -20,7 +21,7 @@ import { getExpressionsService } from './services';
type Data = any;
export class ExpressionLoader {
data$: Observable<Data>;
data$: ReturnType<ExecutionContract['getData']>;
update$: ExpressionRenderHandler['update$'];
render$: ExpressionRenderHandler['render$'];
events$: ExpressionRenderHandler['events$'];
@ -28,10 +29,11 @@ export class ExpressionLoader {
private execution: ExecutionContract | undefined;
private renderHandler: ExpressionRenderHandler;
private dataSubject: Subject<Data>;
private dataSubject: Subject<UnwrapObservable<ExpressionLoader['data$']>>;
private loadingSubject: Subject<boolean>;
private data: Data;
private params: IExpressionLoaderParams = {};
private subscription?: Subscription;
constructor(
element: HTMLElement,
@ -67,8 +69,8 @@ export class ExpressionLoader {
}
});
this.data$.subscribe((data) => {
this.render(data);
this.data$.subscribe(({ result }) => {
this.render(result);
});
this.render$.subscribe(() => {
@ -87,27 +89,20 @@ export class ExpressionLoader {
this.dataSubject.complete();
this.loadingSubject.complete();
this.renderHandler.destroy();
if (this.execution) {
this.execution.cancel();
}
this.cancel();
this.subscription?.unsubscribe();
}
cancel() {
if (this.execution) {
this.execution.cancel();
}
this.execution?.cancel();
}
getExpression(): string | undefined {
if (this.execution) {
return this.execution.getExpression();
}
return this.execution?.getExpression();
}
getAst(): ExpressionAstExpression | undefined {
if (this.execution) {
return this.execution.getAst();
}
return this.execution?.getAst();
}
getElement(): HTMLElement {
@ -115,27 +110,25 @@ export class ExpressionLoader {
}
inspect(): Adapters | undefined {
return this.execution ? (this.execution.inspect() as Adapters) : undefined;
return this.execution?.inspect() as Adapters;
}
async update(
expression?: string | ExpressionAstExpression,
params?: IExpressionLoaderParams
): Promise<void> {
update(expression?: string | ExpressionAstExpression, params?: IExpressionLoaderParams): void {
this.setParams(params);
this.loadingSubject.next(true);
if (expression) {
await this.loadData(expression, this.params);
this.loadData(expression, this.params);
} else if (this.data) {
this.render(this.data);
}
}
private loadData = async (
private loadData = (
expression: string | ExpressionAstExpression,
params: IExpressionLoaderParams
): Promise<void> => {
) => {
this.subscription?.unsubscribe();
if (this.execution && this.execution.isPending) {
this.execution.cancel();
}
@ -148,13 +141,13 @@ export class ExpressionLoader {
debug: params.debug,
syncColors: params.syncColors,
});
const prevDataHandler = this.execution;
const data = await prevDataHandler.getData();
if (this.execution !== prevDataHandler) {
return;
}
this.dataSubject.next(data);
this.subscription = this.execution
.getData()
.pipe(
delay(0), // delaying until the next tick since we execute the expression in the constructor
filter(({ partial }) => params.partial || !partial)
)
.subscribe((value) => this.dataSubject.next(value));
};
private render(data: Data): void {
@ -184,6 +177,7 @@ export class ExpressionLoader {
}
this.params.syncColors = params.syncColors;
this.params.debug = Boolean(params.debug);
this.params.partial = Boolean(params.partial);
this.params.inspectorAdapters = (params.inspectorAdapters ||
this.execution?.inspect()) as Adapters;

View file

@ -36,8 +36,10 @@ describe('ExpressionsPublicPlugin', () => {
describe('.run()', () => {
test('can execute simple expression', async () => {
const { setup } = await expressionsPluginMock.createPlugin();
const bar = await setup.run('var_set name="foo" value="bar" | var name="foo"', null);
expect(bar).toBe('bar');
const { result } = await setup
.run('var_set name="foo" value="bar" | var name="foo"', null)
.toPromise();
expect(result).toBe('bar');
});
});
});

View file

@ -112,16 +112,17 @@ export class Execution<Input = unknown, Output = unknown, InspectorAdapters exte
// (undocumented)
get inspectorAdapters(): InspectorAdapters;
// (undocumented)
interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown>;
interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>>;
// (undocumented)
invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable<any>;
// (undocumented)
invokeFunction(fn: ExpressionFunction, input: unknown, args: Record<string, unknown>): Observable<any>;
// (undocumented)
resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable<any>;
readonly result: Observable<Output | ExpressionValueError>;
start(input?: Input): Observable<Output | ExpressionValueError>;
readonly state: ExecutionContainer<Output | ExpressionValueError>;
readonly result: Observable<ExecutionResult<Output | ExpressionValueError>>;
start(input?: Input): Observable<ExecutionResult<Output | ExpressionValueError>>;
// Warning: (ae-forgotten-export) The symbol "ExecutionResult" needs to be exported by the entry point index.d.ts
readonly state: ExecutionContainer<ExecutionResult<Output | ExpressionValueError>>;
}
// Warning: (ae-forgotten-export) The symbol "StateContainer" needs to be exported by the entry point index.d.ts
@ -155,7 +156,7 @@ export class ExecutionContract<Input = unknown, Output = unknown, InspectorAdapt
// (undocumented)
protected readonly execution: Execution<Input, Output, InspectorAdapters>;
getAst: () => ExpressionAstExpression;
getData: () => Promise<Output | ExpressionValueError>;
getData: () => Observable<ExecutionResult<Output | ExpressionValueError>>;
getExpression: () => string;
inspect: () => InspectorAdapters;
// (undocumented)
@ -230,7 +231,7 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
registerFunction(functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)): void;
// (undocumented)
registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void;
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<Output | ExpressionValueError>;
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<ExecutionResult<Output | ExpressionValueError>>;
// (undocumented)
readonly state: ExecutorContainer<Context>;
// (undocumented)
@ -637,7 +638,7 @@ export interface ExpressionsServiceStart {
getFunction: (name: string) => ReturnType<Executor['getFunction']>;
getRenderer: (name: string) => ReturnType<ExpressionRendererRegistry['get']>;
getType: (name: string) => ReturnType<Executor['getType']>;
run: <Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Promise<Output>;
run: <Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams) => Observable<ExecutionResult<Output | ExpressionValueError>>;
}
// Warning: (ae-missing-release-tag) "ExpressionsSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
@ -907,6 +908,8 @@ export interface IExpressionLoaderParams {
//
// (undocumented)
onRenderError?: RenderErrorHandlerFnType;
// (undocumented)
partial?: boolean;
// Warning: (ae-forgotten-export) The symbol "RenderMode" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@ -1073,7 +1076,7 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
// (undocumented)
expression: string | ExpressionAstExpression;
// (undocumented)
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void;
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters, partial?: boolean) => void;
// (undocumented)
onEvent?: (event: ExpressionRendererEvent) => void;
// (undocumented)

View file

@ -255,7 +255,7 @@ describe('ExpressionRenderer', () => {
const dataSubject = new Subject();
const data$ = dataSubject.asObservable().pipe(share());
const newData = {};
const result = {};
const inspectData = {};
const onData$ = jest.fn();
@ -275,11 +275,11 @@ describe('ExpressionRenderer', () => {
expect(onData$).toHaveBeenCalledTimes(0);
act(() => {
dataSubject.next(newData);
dataSubject.next({ result });
});
expect(onData$).toHaveBeenCalledTimes(1);
expect(onData$.mock.calls[0][0]).toBe(newData);
expect(onData$.mock.calls[0][0]).toBe(result);
expect(onData$.mock.calls[0][1]).toBe(inspectData);
});

View file

@ -30,7 +30,11 @@ export interface ReactExpressionRendererProps extends IExpressionLoaderParams {
) => React.ReactElement | React.ReactElement[];
padding?: 'xs' | 's' | 'm' | 'l' | 'xl';
onEvent?: (event: ExpressionRendererEvent) => void;
onData$?: <TData, TInspectorAdapters>(data: TData, adapters?: TInspectorAdapters) => void;
onData$?: <TData, TInspectorAdapters>(
data: TData,
adapters?: TInspectorAdapters,
partial?: boolean
) => void;
/**
* An observable which can be used to re-run the expression without destroying the component
*/
@ -135,8 +139,8 @@ export const ReactExpressionRenderer = ({
}
if (onData$) {
subs.push(
expressionLoaderRef.current.data$.subscribe((newData) => {
onData$(newData, expressionLoaderRef.current?.inspect());
expressionLoaderRef.current.data$.subscribe(({ partial, result }) => {
onData$(result, expressionLoaderRef.current?.inspect(), partial);
})
);
}

View file

@ -48,6 +48,7 @@ export interface IExpressionLoaderParams {
renderMode?: RenderMode;
syncColors?: boolean;
hasCompatibleActions?: ExpressionRenderHandlerParams['hasCompatibleActions'];
partial?: boolean;
}
export interface ExpressionRenderError extends Error {

View file

@ -28,8 +28,10 @@ describe('ExpressionsServerPlugin', () => {
describe('.run()', () => {
test('can execute simple expression', async () => {
const { setup } = await expressionsPluginMock.createPlugin();
const bar = await setup.run('var_set name="foo" value="bar" | var name="foo"', null);
expect(bar).toBe('bar');
const { result } = await setup
.run('var_set name="foo" value="bar" | var name="foo"', null)
.toPromise();
expect(result).toBe('bar');
});
});
});

View file

@ -110,16 +110,17 @@ export class Execution<Input = unknown, Output = unknown, InspectorAdapters exte
// (undocumented)
get inspectorAdapters(): InspectorAdapters;
// (undocumented)
interpret<T>(ast: ExpressionAstNode, input: T): Observable<unknown>;
interpret<T>(ast: ExpressionAstNode, input: T): Observable<ExecutionResult<unknown>>;
// (undocumented)
invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Observable<any>;
// (undocumented)
invokeFunction(fn: ExpressionFunction, input: unknown, args: Record<string, unknown>): Observable<any>;
// (undocumented)
resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Observable<any>;
readonly result: Observable<Output | ExpressionValueError>;
start(input?: Input): Observable<Output | ExpressionValueError>;
readonly state: ExecutionContainer<Output | ExpressionValueError>;
readonly result: Observable<ExecutionResult<Output | ExpressionValueError>>;
start(input?: Input): Observable<ExecutionResult<Output | ExpressionValueError>>;
// Warning: (ae-forgotten-export) The symbol "ExecutionResult" needs to be exported by the entry point index.d.ts
readonly state: ExecutionContainer<ExecutionResult<Output | ExpressionValueError>>;
}
// Warning: (ae-forgotten-export) The symbol "StateContainer" needs to be exported by the entry point index.d.ts
@ -212,7 +213,7 @@ export class Executor<Context extends Record<string, unknown> = Record<string, u
registerFunction(functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)): void;
// (undocumented)
registerType(typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)): void;
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<Output | ExpressionValueError>;
run<Input, Output>(ast: string | ExpressionAstExpression, input: Input, params?: ExpressionExecutionParams): Observable<ExecutionResult<Output | ExpressionValueError>>;
// (undocumented)
readonly state: ExecutorContainer<Context>;
// (undocumented)

View file

@ -10,6 +10,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { keys, EuiFlexGroup, EuiFlexItem, EuiButton, EuiText, EuiSwitch } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { pluck } from 'rxjs/operators';
const MIN_CHART_HEIGHT = 300;
@ -55,7 +56,9 @@ class VisEditorVisualizationUI extends Component {
await this._handler.render(this._visEl.current);
this.props.eventEmitter.emit('embeddableRendered');
this._subscription = this._handler.handler.data$.subscribe((data) => onDataChange(data.value));
this._subscription = this._handler.handler.data$
.pipe(pluck('result'))
.subscribe((data) => onDataChange(data.value));
}
/**

View file

@ -9,7 +9,7 @@ import './main.scss';
import React from 'react';
import { EuiPage, EuiPageBody, EuiPageContent, EuiPageContentHeader } from '@elastic/eui';
import { first } from 'rxjs/operators';
import { first, pluck } from 'rxjs/operators';
import {
IInterpreterRenderHandlers,
ExpressionValue,
@ -59,7 +59,9 @@ class Main extends React.Component<{}, State> {
inspectorAdapters: adapters,
searchContext: initialContext as any,
})
.getData();
.getData()
.pipe(pluck('result'))
.toPromise();
};
let lastRenderHandler: ExpressionRenderHandler;

View file

@ -7,6 +7,7 @@
*/
import { schema } from '@kbn/config-schema';
import { pluck } from 'rxjs/operators';
import { CoreSetup, Plugin, HttpResponsePayload } from '../../../../../src/core/server';
import { PluginStart as DataPluginStart } from '../../../../../src/plugins/data/server';
import { ExpressionsServerStart } from '../../../../../src/plugins/expressions/server';
@ -32,13 +33,12 @@ export class TestPlugin implements Plugin<TestPluginSetup, TestPluginStart, {},
},
async (context, req, res) => {
const [, { expressions }] = await core.getStartServices();
const output = await expressions.run<unknown, HttpResponsePayload>(
req.body.expression,
req.body.input,
{
const output = await expressions
.run<unknown, HttpResponsePayload>(req.body.expression, req.body.input, {
kibanaRequest: req,
}
);
})
.pipe(pluck('result'))
.toPromise();
return res.ok({ body: output });
}
);

View file

@ -6,6 +6,7 @@
*/
import { fromExpression, getType } from '@kbn/interpreter/common';
import { pluck } from 'rxjs/operators';
import { ExpressionValue, ExpressionAstExpression } from 'src/plugins/expressions/public';
import { pluginServices, expressionsService } from '../services';
@ -21,7 +22,12 @@ export async function interpretAst(
variables: Record<string, any>
): Promise<ExpressionValue> {
const context = { variables };
return await expressionsService.getService().execute(ast, null, context).getData();
return await expressionsService
.getService()
.execute(ast, null, context)
.getData()
.pipe(pluck('result'))
.toPromise();
}
/**
@ -43,7 +49,12 @@ export async function runInterpreter(
const context = { variables };
try {
const renderable = await expressionsService.getService().execute(ast, input, context).getData();
const renderable = await expressionsService
.getService()
.execute(ast, input, context)
.getData()
.pipe(pluck('result'))
.toPromise();
if (getType(renderable) === 'render') {
return renderable;