mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Expressions refactor (#54342) * feat: 🎸 add UiComponent interface * feat: 🎸 add adapter for react => ui components and back * refactor: 💡 move registries to shared /common folder * feat: 🎸 create expressions service state contaienr * chore: 🤖 export some symbols * feat: 🎸 add Executor class * test: 💍 add simple integration test * feat: 🎸 move registries into executor * feat: 🎸 add initial implementation of Execution * feat: 🎸 move Executor's state container into a signle file * refactor: 💡 move createError() to /common folder * feat: 🎸 use Executor in plugin definition * refactor: 💡 rename handlers to FunctionHandlers * feat: 🎸 improve function typings * feat: 🎸 move types and func in sep folder, improve Execution * refactor: 💡 cleanup expression_types folder * refactor: 💡 improve typing names of expression types * refactor: 💡 remove lodash from ExpressionType and improve types * test: 💍 add ExpressionType tests * refactor: 💡 remove function wrappers around types * refactor: 💡 move functions to /common * test: 💍 improve expression function tests * feat: 🎸 create /parser folder * refactor: 💡 move function types into /expression_functions dir * refactor: 💡 improve parser setup * refactor: 💡 fix export structure and move args into expr_func * test: 💍 add ExpressionFunctionParameter tests * fix: 🐛 fix executor types and imports * refactor: 💡 move getByAlias to plugin, fix Execution types * feat: 🎸 add function for argument parsing * test: 💍 add Executor type tests * test: 💍 add executor function and context tests * test: 💍 check that Executor returns Execution * test: 💍 add basic tests for Execution * test: 💍 add basic test for execution of a chain of functions * test: 💍 add "mult" function tot tests * feat: 🎸 create separate expression_renderer folder * feat: 🎸 use new executor in public plugin * feat: 🎸 remove renderers from executor, add result to execution * fix: 🐛 fix Kibana TypeScript errors * test: 💍 add file to write integration tests for expr plugin * refactor: 💡 move state_containers to /common * refactor: 💡 move /parser to /ast and inline format() function * refactor: 💡 remove remaining @kbn/interpreter imports * feat: 🎸 better handling and typing for Executor context * feat: 🎸 use Executor.run function in plugin * fix: 🐛 fix TypeScript type errors * test: 💍 move integration tests into one file * feat: 🎸 create ExpressionsService * chore: 🤖 clean up legacy code * feat: 🎸 use ExpressionsService in /public * refactor: 💡 move inspector adapters to /common * feat: 🎸 improve execution * feat: 🎸 add state to execution state and don't clone AST * test: 💍 add tests for Execution object * test: 💍 improve expression test helpers * test: 💍 add Execution tests * refactor: 💡 improve required argument checking * fix: 🐛 fix Kibana TypeScript errors * test: 💍 add ExpressionsService unit tests * fix: 🐛 fix Expression plugin TypeScript types * refactor: 💡 prefix React component with React* * fix: 🐛 fix X-Pack TypeScript errors * fix: 🐛 fix test TypeScript errors * fix: 🐛 fix issues preventing loading * feat: 🎸 remove getInitialContext() handler * fix: 🐛 fix TypeScript errors * chore: 🤖 remove uicomponent interface * chore: 🤖 remove missing import * fix: 🐛 correctly handle .query in "kibana" expression function * refactor: 💡 call first arg in expression functions "input" * fix: 🐛 do not free Execution state container * test: 💍 fix tests after refactor * test: 💍 fix more tests after refactor * fix: 🐛 remove redundant export * test: 💍 update intepreter_functional test shapshots * fix: 🐛 relax "kibana" function throwing on missin gsearch ctx * refactor: 💡 don't use ExpressionAST interface in Canvas * docs: ✏️ improve ExpressionRenderer JSDocs * refactor: 💡 rename context.types to inputTypes in internal fn * refactor: 💡 replace context.types by unknown in ExprFuncDef * refactor: 💡 improve expression function definitions in OSS * fix: 🐛 correctly set name on metric_vis_fn * refactor: 💡 improve Lens definitions of expression functions * refactor: 💡 improve Canvas expression function definitions * test: 💍 add createMockExecutionContext() helper * refactor: 💡 add some type to events$ observable in expr handler * feat: 🎸 add types to observables in data handler * refactor: 💡 use inputTypes in canvas * fix: 🐛 fix interpreter grammer generation script * feat: 🎸 allow array in getByAlias * test: 💍 simplify test function specs * test: 💍 fix autocomplete tests * fix: 🐛 use correct expression types and NP getFunctions() API * refactor: 💡 use NP expressions to get renderer * fix: 🐛 use context.types on server-side Canvas function defs * refactor: 💡 use NP API to register Canvas renderers * feat: 🎸 use NP API to get types * style: 💄 minor formatting changes * feat: 🎸 use NP API to get expression functions * fix: 🐛 fix Canvas workpads * test: 💍 add missing mock functions * refactor: 💡 improve Lens func definition argument types * fix: 🐛 fix Lens type error * feat: 🎸 make lens datatable work again * feat: 🎸 bootstrap ExpressionsService on server-side * feat: 🎸 expose more registry related functions in contract * feat: 🎸 add environment: server to server-side expressions * docs: ✏️ add documentation * test: 💍 add missing Jest mocks * fix: 🐛 correct TypeScript type * docs: ✏️ improve documentation * fix: 🐛 make FunctionHelpDict type contain only Canvas functions * fix: 🐛 fix merge conflict * test: 💍 fix expression mocks * fix: fix TypeScript disabled help type Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
c34027098c
commit
b9f4eec008
304 changed files with 5275 additions and 3033 deletions
|
@ -3,7 +3,7 @@
|
|||
"version": "1.0.0",
|
||||
"license": "Apache-2.0",
|
||||
"scripts": {
|
||||
"interpreter:peg": "pegjs common/lib/grammar.peg",
|
||||
"interpreter:peg": "pegjs src/common/lib/grammar.peg",
|
||||
"build": "node scripts/build",
|
||||
"kbn:bootstrap": "node scripts/build --dev",
|
||||
"kbn:watch": "node scripts/build --dev --watch"
|
||||
|
|
|
@ -31,9 +31,7 @@ export class Registry {
|
|||
}
|
||||
|
||||
register(fn) {
|
||||
if (typeof fn !== 'function') throw new Error(`Register requires an function`);
|
||||
|
||||
const obj = fn();
|
||||
const obj = typeof fn === 'function' ? fn() : fn;
|
||||
|
||||
if (typeof obj !== 'object' || !obj[this._prop]) {
|
||||
throw new Error(`Registered functions must return an object with a ${this._prop} property`);
|
||||
|
|
|
@ -24,3 +24,4 @@ type B = UnwrapPromise<A>; // string
|
|||
- `ShallowPromise<T>` — Same as `Promise` type, but it flat maps the wrapped type.
|
||||
- `UnwrapObservable<T>` — Returns wrapped type of an observable.
|
||||
- `UnwrapPromise<T>` — Returns wrapped type of a promise.
|
||||
- `UnwrapPromiseOrReturn<T>` — Returns wrapped type of a promise or the type itself, if it isn't a promise.
|
||||
|
|
|
@ -35,6 +35,11 @@ export type ShallowPromise<T> = T extends Promise<infer U> ? Promise<U> : Promis
|
|||
*/
|
||||
export type UnwrapPromise<T extends Promise<any>> = PromiseType<T>;
|
||||
|
||||
/**
|
||||
* Returns wrapped type of a promise, or returns type as is, if it is not a promise.
|
||||
*/
|
||||
export type UnwrapPromiseOrReturn<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
/**
|
||||
* Minimal interface for an object resembling an `Observable`.
|
||||
*/
|
||||
|
|
|
@ -24,7 +24,7 @@ import { createFormat } from 'ui/visualize/loader/pipeline_helpers/utilities';
|
|||
import {
|
||||
KibanaContext,
|
||||
KibanaDatatable,
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatableColumn,
|
||||
} from 'src/plugins/expressions/public';
|
||||
import {
|
||||
|
@ -66,7 +66,8 @@ export interface RequestHandlerParams {
|
|||
|
||||
const name = 'esaggs';
|
||||
|
||||
type Context = KibanaContext | null;
|
||||
type Input = KibanaContext | null;
|
||||
type Output = Promise<KibanaDatatable>;
|
||||
|
||||
interface Arguments {
|
||||
index: string;
|
||||
|
@ -76,8 +77,6 @@ interface Arguments {
|
|||
aggConfigs: string;
|
||||
}
|
||||
|
||||
type Return = Promise<KibanaDatatable>;
|
||||
|
||||
const handleCourierRequest = async ({
|
||||
searchSource,
|
||||
aggs,
|
||||
|
@ -221,12 +220,10 @@ const handleCourierRequest = async ({
|
|||
return (searchSource as any).tabifiedResponse;
|
||||
};
|
||||
|
||||
export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
|
||||
export const esaggs = (): ExpressionFunctionDefinition<typeof name, Input, Arguments, Output> => ({
|
||||
name,
|
||||
type: 'kibana_datatable',
|
||||
context: {
|
||||
types: ['kibana_context', 'null'],
|
||||
},
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
help: i18n.translate('data.functions.esaggs.help', {
|
||||
defaultMessage: 'Run AggConfig aggregation',
|
||||
}),
|
||||
|
@ -256,7 +253,7 @@ export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Re
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
async fn(context, args, { inspectorAdapters, abortSignal }) {
|
||||
async fn(input, args, { inspectorAdapters, abortSignal }) {
|
||||
const indexPatterns = getIndexPatterns();
|
||||
const { filterManager } = getQueryService();
|
||||
|
||||
|
@ -272,13 +269,13 @@ export const esaggs = (): ExpressionFunction<typeof name, Context, Arguments, Re
|
|||
const response = await handleCourierRequest({
|
||||
searchSource,
|
||||
aggs,
|
||||
timeRange: get(context, 'timeRange', undefined),
|
||||
query: get(context, 'query', undefined),
|
||||
filters: get(context, 'filters', undefined),
|
||||
timeRange: get(input, 'timeRange', undefined),
|
||||
query: get(input, 'query', undefined),
|
||||
filters: get(input, 'filters', undefined),
|
||||
forceFetch: true,
|
||||
metricsAtAllLevels: args.metricsAtAllLevels,
|
||||
partialRows: args.partialRows,
|
||||
inspectorAdapters,
|
||||
inspectorAdapters: inspectorAdapters as Adapters,
|
||||
filterManager,
|
||||
abortSignal: (abortSignal as unknown) as AbortSignal,
|
||||
});
|
||||
|
|
|
@ -20,12 +20,12 @@
|
|||
import { createInputControlVisFn } from './input_control_fn';
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
|
||||
jest.mock('./legacy_imports.ts');
|
||||
|
||||
describe('interpreter/functions#input_control_vis', () => {
|
||||
const fn = functionWrapper(createInputControlVisFn);
|
||||
const fn = functionWrapper(createInputControlVisFn());
|
||||
const visConfig = {
|
||||
controls: [
|
||||
{
|
||||
|
|
|
@ -20,15 +20,11 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatable,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
|
||||
const name = 'input_control_vis';
|
||||
|
||||
type Context = KibanaDatatable;
|
||||
|
||||
interface Arguments {
|
||||
visConfig: string;
|
||||
}
|
||||
|
@ -40,19 +36,15 @@ interface RenderValue {
|
|||
visConfig: VisParams;
|
||||
}
|
||||
|
||||
type Return = Promise<Render<RenderValue>>;
|
||||
|
||||
export const createInputControlVisFn = (): ExpressionFunction<
|
||||
typeof name,
|
||||
Context,
|
||||
export const createInputControlVisFn = (): ExpressionFunctionDefinition<
|
||||
'input_control_vis',
|
||||
KibanaDatatable,
|
||||
Arguments,
|
||||
Return
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name: 'input_control_vis',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: [],
|
||||
},
|
||||
inputTypes: [],
|
||||
help: i18n.translate('inputControl.function.help', {
|
||||
defaultMessage: 'Input control visualization',
|
||||
}),
|
||||
|
@ -63,7 +55,7 @@ export const createInputControlVisFn = (): ExpressionFunction<
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
async fn(context, args) {
|
||||
fn(input, args) {
|
||||
const params = JSON.parse(args.visConfig);
|
||||
return {
|
||||
type: 'render',
|
||||
|
|
|
@ -1,22 +1,2 @@
|
|||
Interpreter legacy plugin has been migrated to the New Platform. Use
|
||||
`expressions` New Platform plugin instead.
|
||||
|
||||
In the New Platform:
|
||||
|
||||
```ts
|
||||
class MyPlugin {
|
||||
setup(core, { expressions }) {
|
||||
expressions.registerFunction(myFunction);
|
||||
}
|
||||
start(core, { expressions }) {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the Legacy Platform:
|
||||
|
||||
```ts
|
||||
import { npSetup, npStart } from 'ui/new_platform';
|
||||
|
||||
npSetup.plugins.expressions.registerFunction(myFunction);
|
||||
```
|
||||
|
|
|
@ -22,10 +22,7 @@ import 'uiExports/interpreter';
|
|||
import { register, registryFactory } from '@kbn/interpreter/common';
|
||||
import { npSetup } from 'ui/new_platform';
|
||||
import { registries } from './registries';
|
||||
import {
|
||||
ExpressionInterpretWithHandlers,
|
||||
ExpressionExecutor,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
import { Executor, ExpressionExecutor } from '../../../../plugins/expressions/public';
|
||||
|
||||
// Expose kbnInterpreter.register(specs) and kbnInterpreter.registries() globally so that plugins
|
||||
// can register without a transpile step.
|
||||
|
@ -46,7 +43,7 @@ export const getInterpreter = async () => {
|
|||
};
|
||||
|
||||
// TODO: This function will be left behind in the legacy platform.
|
||||
export const interpretAst: ExpressionInterpretWithHandlers = async (ast, context, handlers) => {
|
||||
export const interpretAst: Executor['run'] = async (ast, context, handlers) => {
|
||||
const { interpreter } = await getInterpreter();
|
||||
return await interpreter.interpretAst(ast, context, handlers);
|
||||
};
|
||||
|
|
|
@ -18,13 +18,13 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
import { createRegionMapFn } from './region_map_fn';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
describe('interpreter/functions#regionmap', () => {
|
||||
const fn = functionWrapper(createRegionMapFn);
|
||||
const fn = functionWrapper(createRegionMapFn());
|
||||
const context = {
|
||||
type: 'kibana_datatable',
|
||||
rows: [{ 'col-0-1': 0 }],
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
import { createTileMapFn } from './tile_map_fn';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
@ -41,7 +41,7 @@ jest.mock('ui/vis/map/convert_to_geojson', () => ({
|
|||
import { convertToGeoJson } from 'ui/vis/map/convert_to_geojson';
|
||||
|
||||
describe('interpreter/functions#tilemap', () => {
|
||||
const fn = functionWrapper(createTileMapFn);
|
||||
const fn = functionWrapper(createTileMapFn());
|
||||
const context = {
|
||||
type: 'kibana_datatable',
|
||||
rows: [{ 'col-0-1': 0 }],
|
||||
|
|
|
@ -18,11 +18,11 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
import { createMarkdownVisFn } from './markdown_fn';
|
||||
|
||||
describe('interpreter/functions#markdown', () => {
|
||||
const fn = functionWrapper(createMarkdownVisFn);
|
||||
const fn = functionWrapper(createMarkdownVisFn());
|
||||
const args = {
|
||||
font: { spec: { fontSize: 12 } },
|
||||
openLinksInNewTab: true,
|
||||
|
|
|
@ -18,31 +18,23 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunction, Render } from '../../../../plugins/expressions/public';
|
||||
import { ExpressionFunctionDefinition, Render } from '../../../../plugins/expressions/public';
|
||||
import { Arguments, MarkdownVisParams } from './types';
|
||||
|
||||
const name = 'markdownVis';
|
||||
|
||||
type Context = undefined;
|
||||
|
||||
interface RenderValue {
|
||||
visType: 'markdown';
|
||||
visConfig: MarkdownVisParams;
|
||||
}
|
||||
|
||||
type Return = Promise<Render<RenderValue>>;
|
||||
|
||||
export const createMarkdownVisFn = (): ExpressionFunction<
|
||||
typeof name,
|
||||
Context,
|
||||
export const createMarkdownVisFn = (): ExpressionFunctionDefinition<
|
||||
'markdownVis',
|
||||
unknown,
|
||||
Arguments,
|
||||
Return
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name,
|
||||
name: 'markdownVis',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: [],
|
||||
},
|
||||
inputTypes: [],
|
||||
help: i18n.translate('visTypeMarkdown.function.help', {
|
||||
defaultMessage: 'Markdown visualization',
|
||||
}),
|
||||
|
@ -70,7 +62,7 @@ export const createMarkdownVisFn = (): ExpressionFunction<
|
|||
}),
|
||||
},
|
||||
},
|
||||
async fn(context, args) {
|
||||
fn(input, args) {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'visualization',
|
||||
|
|
|
@ -19,13 +19,11 @@
|
|||
|
||||
import { last, findIndex, isNaN } from 'lodash';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { isColorDark } from '@elastic/eui';
|
||||
|
||||
import { getFormat } from '../legacy_imports';
|
||||
import { MetricVisValue } from './metric_vis_value';
|
||||
import { Input } from '../metric_vis_fn';
|
||||
import { FieldFormatsContentType, IFieldFormat } from '../../../../../plugins/data/public';
|
||||
import { Context } from '../metric_vis_fn';
|
||||
import { KibanaDatatable } from '../../../../../plugins/expressions/public';
|
||||
import { getHeatmapColors } from '../../../../../plugins/charts/public';
|
||||
import { VisParams, MetricVisMetric } from '../types';
|
||||
|
@ -33,7 +31,7 @@ import { SchemaConfig, Vis } from '../../../visualizations/public';
|
|||
|
||||
export interface MetricVisComponentProps {
|
||||
visParams: VisParams;
|
||||
visData: Context;
|
||||
visData: Input;
|
||||
vis: Vis;
|
||||
renderComplete: () => void;
|
||||
}
|
||||
|
|
|
@ -19,12 +19,12 @@
|
|||
|
||||
import { createMetricVisFn } from './metric_vis_fn';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
|
||||
jest.mock('ui/new_platform');
|
||||
|
||||
describe('interpreter/functions#metric', () => {
|
||||
const fn = functionWrapper(createMetricVisFn);
|
||||
const fn = functionWrapper(createMetricVisFn());
|
||||
const context = {
|
||||
type: 'kibana_datatable',
|
||||
rows: [{ 'col-0-1': 0 }],
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatable,
|
||||
Range,
|
||||
Render,
|
||||
|
@ -30,9 +30,7 @@ import { ColorModes } from '../../vis_type_vislib/public';
|
|||
import { visType, DimensionsVisParam, VisParams } from './types';
|
||||
import { ColorSchemas, vislibColorMaps } from '../../../../plugins/charts/public';
|
||||
|
||||
export type Context = KibanaDatatable;
|
||||
|
||||
const name = 'metricVis';
|
||||
export type Input = KibanaDatatable;
|
||||
|
||||
interface Arguments {
|
||||
percentageMode: boolean;
|
||||
|
@ -51,24 +49,20 @@ interface Arguments {
|
|||
|
||||
interface RenderValue {
|
||||
visType: typeof visType;
|
||||
visData: Context;
|
||||
visData: Input;
|
||||
visConfig: Pick<VisParams, 'metric' | 'dimensions'>;
|
||||
params: any;
|
||||
}
|
||||
|
||||
type Return = Render<RenderValue>;
|
||||
|
||||
export const createMetricVisFn = (): ExpressionFunction<
|
||||
typeof name,
|
||||
Context,
|
||||
export const createMetricVisFn = (): ExpressionFunctionDefinition<
|
||||
'metricVis',
|
||||
Input,
|
||||
Arguments,
|
||||
Return
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name,
|
||||
name: 'metricVis',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_datatable'],
|
||||
},
|
||||
inputTypes: ['kibana_datatable'],
|
||||
help: i18n.translate('visTypeMetric.function.help', {
|
||||
defaultMessage: 'Metric visualization',
|
||||
}),
|
||||
|
@ -165,7 +159,7 @@ export const createMetricVisFn = (): ExpressionFunction<
|
|||
}),
|
||||
},
|
||||
},
|
||||
fn(context: Context, args: Arguments) {
|
||||
fn(input, args) {
|
||||
const dimensions: DimensionsVisParam = {
|
||||
metrics: args.metric,
|
||||
};
|
||||
|
@ -184,7 +178,7 @@ export const createMetricVisFn = (): ExpressionFunction<
|
|||
type: 'render',
|
||||
as: 'visualization',
|
||||
value: {
|
||||
visData: context,
|
||||
visData: input,
|
||||
visType,
|
||||
visConfig: {
|
||||
metric: {
|
||||
|
|
|
@ -21,7 +21,7 @@ import { createTableVisFn } from './table_vis_fn';
|
|||
import { tableVisResponseHandler } from './table_vis_response_handler';
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
|
||||
jest.mock('./table_vis_response_handler', () => ({
|
||||
tableVisResponseHandler: jest.fn().mockReturnValue({
|
||||
|
@ -30,7 +30,7 @@ jest.mock('./table_vis_response_handler', () => ({
|
|||
}));
|
||||
|
||||
describe('interpreter/functions#table', () => {
|
||||
const fn = functionWrapper(createTableVisFn);
|
||||
const fn = functionWrapper(createTableVisFn());
|
||||
const context = {
|
||||
type: 'kibana_datatable',
|
||||
rows: [{ 'col-0-1': 0 }],
|
||||
|
|
|
@ -19,16 +19,13 @@
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { tableVisResponseHandler, TableContext } from './table_vis_response_handler';
|
||||
|
||||
import {
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatable,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
|
||||
const name = 'kibana_table';
|
||||
|
||||
export type Context = KibanaDatatable;
|
||||
export type Input = KibanaDatatable;
|
||||
|
||||
interface Arguments {
|
||||
visConfig: string | null;
|
||||
|
@ -45,19 +42,15 @@ interface RenderValue {
|
|||
};
|
||||
}
|
||||
|
||||
type Return = Render<RenderValue>;
|
||||
|
||||
export const createTableVisFn = (): ExpressionFunction<
|
||||
typeof name,
|
||||
Context,
|
||||
export const createTableVisFn = (): ExpressionFunctionDefinition<
|
||||
'kibana_table',
|
||||
Input,
|
||||
Arguments,
|
||||
Return
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name,
|
||||
name: 'kibana_table',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_datatable'],
|
||||
},
|
||||
inputTypes: ['kibana_datatable'],
|
||||
help: i18n.translate('visTypeTable.function.help', {
|
||||
defaultMessage: 'Table visualization',
|
||||
}),
|
||||
|
@ -68,9 +61,9 @@ export const createTableVisFn = (): ExpressionFunction<
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
fn(context, args) {
|
||||
fn(input, args) {
|
||||
const visConfig = args.visConfig && JSON.parse(args.visConfig);
|
||||
const convertedData = tableVisResponseHandler(context, visConfig.dimensions);
|
||||
const convertedData = tableVisResponseHandler(input, visConfig.dimensions);
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import { Required } from '@kbn/utility-types';
|
||||
|
||||
import { getFormat } from './legacy_imports';
|
||||
import { Context } from './table_vis_fn';
|
||||
import { Input } from './table_vis_fn';
|
||||
|
||||
export interface TableContext {
|
||||
tables: Array<TableGroup | Table>;
|
||||
|
@ -29,7 +29,7 @@ export interface TableContext {
|
|||
|
||||
export interface TableGroup {
|
||||
$parent: TableContext;
|
||||
table: Context;
|
||||
table: Input;
|
||||
tables: Table[];
|
||||
title: string;
|
||||
name: string;
|
||||
|
@ -40,11 +40,11 @@ export interface TableGroup {
|
|||
|
||||
export interface Table {
|
||||
$parent?: TableGroup;
|
||||
columns: Context['columns'];
|
||||
rows: Context['rows'];
|
||||
columns: Input['columns'];
|
||||
rows: Input['rows'];
|
||||
}
|
||||
|
||||
export function tableVisResponseHandler(table: Context, dimensions: any): TableContext {
|
||||
export function tableVisResponseHandler(table: Input, dimensions: any): TableContext {
|
||||
const converted: TableContext = {
|
||||
tables: [],
|
||||
};
|
||||
|
@ -63,8 +63,7 @@ export function tableVisResponseHandler(table: Context, dimensions: any): TableC
|
|||
const splitValue: any = row[splitColumn.id];
|
||||
|
||||
if (!splitMap.hasOwnProperty(splitValue as any)) {
|
||||
// @ts-ignore
|
||||
splitMap[splitValue] = splitIndex++;
|
||||
(splitMap as any)[splitValue] = splitIndex++;
|
||||
const tableGroup: Required<TableGroup, 'tables'> = {
|
||||
$parent: converted,
|
||||
title: `${splitColumnFormatter.convert(splitValue)}: ${splitColumn.name}`,
|
||||
|
@ -85,10 +84,8 @@ export function tableVisResponseHandler(table: Context, dimensions: any): TableC
|
|||
converted.tables.push(tableGroup);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
const tableIndex = splitMap[splitValue];
|
||||
// @ts-ignore
|
||||
converted.tables[tableIndex].tables[0].rows.push(row);
|
||||
const tableIndex = (splitMap as any)[splitValue];
|
||||
(converted.tables[tableIndex] as any).tables[0].rows.push(row);
|
||||
});
|
||||
} else {
|
||||
converted.tables.push({
|
||||
|
|
|
@ -20,10 +20,10 @@
|
|||
import { createTagCloudFn } from './tag_cloud_fn';
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
|
||||
describe('interpreter/functions#tagcloud', () => {
|
||||
const fn = functionWrapper(createTagCloudFn);
|
||||
const fn = functionWrapper(createTagCloudFn());
|
||||
const context = {
|
||||
type: 'kibana_datatable',
|
||||
rows: [{ 'col-0-1': 0 }],
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatable,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
|
@ -28,8 +28,6 @@ import { TagCloudVisParams } from './types';
|
|||
|
||||
const name = 'tagcloud';
|
||||
|
||||
type Context = KibanaDatatable;
|
||||
|
||||
interface Arguments extends TagCloudVisParams {
|
||||
metric: any; // these aren't typed yet
|
||||
bucket: any; // these aren't typed yet
|
||||
|
@ -37,24 +35,20 @@ interface Arguments extends TagCloudVisParams {
|
|||
|
||||
interface RenderValue {
|
||||
visType: typeof name;
|
||||
visData: Context;
|
||||
visData: KibanaDatatable;
|
||||
visConfig: Arguments;
|
||||
params: any;
|
||||
}
|
||||
|
||||
type Return = Render<RenderValue>;
|
||||
|
||||
export const createTagCloudFn = (): ExpressionFunction<
|
||||
export const createTagCloudFn = (): ExpressionFunctionDefinition<
|
||||
typeof name,
|
||||
Context,
|
||||
KibanaDatatable,
|
||||
Arguments,
|
||||
Return
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name,
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_datatable'],
|
||||
},
|
||||
inputTypes: ['kibana_datatable'],
|
||||
help: i18n.translate('visTypeTagCloud.function.help', {
|
||||
defaultMessage: 'Tagcloud visualization',
|
||||
}),
|
||||
|
@ -104,7 +98,7 @@ export const createTagCloudFn = (): ExpressionFunction<
|
|||
}),
|
||||
},
|
||||
},
|
||||
fn(context, args) {
|
||||
fn(input, args) {
|
||||
const visConfig = {
|
||||
scale: args.scale,
|
||||
orientation: args.orientation,
|
||||
|
@ -122,7 +116,7 @@ export const createTagCloudFn = (): ExpressionFunction<
|
|||
type: 'render',
|
||||
as: 'visualization',
|
||||
value: {
|
||||
visData: context,
|
||||
visData: input,
|
||||
visType: name,
|
||||
visConfig,
|
||||
params: {
|
||||
|
|
|
@ -19,36 +19,36 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunction, KibanaContext, Render } from 'src/plugins/expressions/public';
|
||||
import {
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaContext,
|
||||
Render,
|
||||
} from 'src/plugins/expressions/public';
|
||||
import { getTimelionRequestHandler } from './helpers/timelion_request_handler';
|
||||
import { TIMELION_VIS_NAME } from './timelion_vis_type';
|
||||
import { TimelionVisDependencies } from './plugin';
|
||||
|
||||
const name = 'timelion_vis';
|
||||
|
||||
type Input = KibanaContext | null;
|
||||
type Output = Promise<Render<RenderValue>>;
|
||||
interface Arguments {
|
||||
expression: string;
|
||||
interval: string;
|
||||
}
|
||||
|
||||
interface RenderValue {
|
||||
visData: Context;
|
||||
visData: Input;
|
||||
visType: 'timelion';
|
||||
visParams: VisParams;
|
||||
}
|
||||
|
||||
type Context = KibanaContext | null;
|
||||
export type VisParams = Arguments;
|
||||
type Return = Promise<Render<RenderValue>>;
|
||||
|
||||
export const getTimelionVisualizationConfig = (
|
||||
dependencies: TimelionVisDependencies
|
||||
): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
|
||||
name,
|
||||
): ExpressionFunctionDefinition<'timelion_vis', Input, Arguments, Output> => ({
|
||||
name: 'timelion_vis',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_context', 'null'],
|
||||
},
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
help: i18n.translate('timelion.function.help', {
|
||||
defaultMessage: 'Timelion visualization',
|
||||
}),
|
||||
|
@ -65,15 +65,15 @@ export const getTimelionVisualizationConfig = (
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
async fn(context, args) {
|
||||
async fn(input, args) {
|
||||
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
|
||||
|
||||
const visParams = { expression: args.expression, interval: args.interval };
|
||||
|
||||
const response = await timelionRequestHandler({
|
||||
timeRange: get(context, 'timeRange'),
|
||||
query: get(context, 'query'),
|
||||
filters: get(context, 'filters'),
|
||||
timeRange: get(input, 'timeRange'),
|
||||
query: get(input, 'query'),
|
||||
filters: get(input, 'filters'),
|
||||
visParams,
|
||||
forceFetch: true,
|
||||
});
|
||||
|
|
|
@ -19,14 +19,18 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public';
|
||||
import {
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaContext,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
|
||||
// @ts-ignore
|
||||
import { metricsRequestHandler } from './request_handler';
|
||||
import { PersistedState } from './legacy_imports';
|
||||
|
||||
const name = 'tsvb';
|
||||
type Context = KibanaContext | null;
|
||||
type Input = KibanaContext | null;
|
||||
type Output = Promise<Render<RenderValue>>;
|
||||
|
||||
interface Arguments {
|
||||
params: string;
|
||||
|
@ -38,19 +42,20 @@ type VisParams = Required<Arguments>;
|
|||
|
||||
interface RenderValue {
|
||||
visType: 'metrics';
|
||||
visData: Context;
|
||||
visData: Input;
|
||||
visConfig: VisParams;
|
||||
uiState: any;
|
||||
}
|
||||
|
||||
type Return = Promise<Render<RenderValue>>;
|
||||
|
||||
export const createMetricsFn = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
|
||||
name,
|
||||
export const createMetricsFn = (): ExpressionFunctionDefinition<
|
||||
'tsvb',
|
||||
Input,
|
||||
Arguments,
|
||||
Output
|
||||
> => ({
|
||||
name: 'tsvb',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_context', 'null'],
|
||||
},
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
help: i18n.translate('visTypeTimeseries.function.help', {
|
||||
defaultMessage: 'TSVB visualization',
|
||||
}),
|
||||
|
@ -71,16 +76,16 @@ export const createMetricsFn = (): ExpressionFunction<typeof name, Context, Argu
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
async fn(context: Context, args: Arguments) {
|
||||
async fn(input, args) {
|
||||
const params = JSON.parse(args.params);
|
||||
const uiStateParams = JSON.parse(args.uiState);
|
||||
const savedObjectId = args.savedObjectId;
|
||||
const uiState = new PersistedState(uiStateParams);
|
||||
|
||||
const response = await metricsRequestHandler({
|
||||
timeRange: get(context, 'timeRange', null),
|
||||
query: get(context, 'query', null),
|
||||
filters: get(context, 'filters', null),
|
||||
timeRange: get(input, 'timeRange', null),
|
||||
query: get(input, 'query', null),
|
||||
filters: get(input, 'filters', null),
|
||||
visParams: params,
|
||||
uiState,
|
||||
savedObjectId,
|
||||
|
|
|
@ -19,13 +19,16 @@
|
|||
|
||||
import { get } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { ExpressionFunction, KibanaContext, Render } from '../../../../plugins/expressions/public';
|
||||
import {
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaContext,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
import { VegaVisualizationDependencies } from './plugin';
|
||||
import { createVegaRequestHandler } from './vega_request_handler';
|
||||
|
||||
const name = 'vega';
|
||||
type Context = KibanaContext | null;
|
||||
type Input = KibanaContext | null;
|
||||
type Output = Promise<Render<RenderValue>>;
|
||||
|
||||
interface Arguments {
|
||||
spec: string;
|
||||
|
@ -34,21 +37,17 @@ interface Arguments {
|
|||
export type VisParams = Required<Arguments>;
|
||||
|
||||
interface RenderValue {
|
||||
visData: Context;
|
||||
visType: typeof name;
|
||||
visData: Input;
|
||||
visType: 'vega';
|
||||
visConfig: VisParams;
|
||||
}
|
||||
|
||||
type Return = Promise<Render<RenderValue>>;
|
||||
|
||||
export const createVegaFn = (
|
||||
dependencies: VegaVisualizationDependencies
|
||||
): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
|
||||
name,
|
||||
): ExpressionFunctionDefinition<'vega', Input, Arguments, Output> => ({
|
||||
name: 'vega',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_context', 'null'],
|
||||
},
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
help: i18n.translate('visTypeVega.function.help', {
|
||||
defaultMessage: 'Vega visualization',
|
||||
}),
|
||||
|
@ -59,13 +58,13 @@ export const createVegaFn = (
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
async fn(context, args) {
|
||||
async fn(input, args) {
|
||||
const vegaRequestHandler = createVegaRequestHandler(dependencies);
|
||||
|
||||
const response = await vegaRequestHandler({
|
||||
timeRange: get(context, 'timeRange'),
|
||||
query: get(context, 'query'),
|
||||
filters: get(context, 'filters'),
|
||||
timeRange: get(input, 'timeRange'),
|
||||
query: get(input, 'query'),
|
||||
filters: get(input, 'filters'),
|
||||
visParams: { spec: args.spec },
|
||||
});
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
// eslint-disable-next-line
|
||||
import { functionWrapper } from '../../../../plugins/expressions/public/functions/tests/utils';
|
||||
import { functionWrapper } from '../../../../plugins/expressions/common/expression_functions/specs/tests/utils';
|
||||
import { createPieVisFn } from './pie_fn';
|
||||
// @ts-ignore
|
||||
import { vislibSlicesResponseHandler } from './vislib/response_handler';
|
||||
|
@ -42,7 +42,7 @@ jest.mock('./vislib/response_handler', () => ({
|
|||
}));
|
||||
|
||||
describe('interpreter/functions#pie', () => {
|
||||
const fn = functionWrapper(createPieVisFn);
|
||||
const fn = functionWrapper(createPieVisFn());
|
||||
const context = {
|
||||
type: 'kibana_datatable',
|
||||
rows: [{ 'col-0-1': 0 }],
|
||||
|
|
|
@ -18,19 +18,14 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatable,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
// @ts-ignore
|
||||
import { vislibSlicesResponseHandler } from './vislib/response_handler';
|
||||
|
||||
const name = 'kibana_pie';
|
||||
|
||||
type Context = KibanaDatatable;
|
||||
|
||||
interface Arguments {
|
||||
visConfig: string;
|
||||
}
|
||||
|
@ -41,14 +36,15 @@ interface RenderValue {
|
|||
visConfig: VisParams;
|
||||
}
|
||||
|
||||
type Return = Render<RenderValue>;
|
||||
|
||||
export const createPieVisFn = (): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
|
||||
export const createPieVisFn = (): ExpressionFunctionDefinition<
|
||||
'kibana_pie',
|
||||
KibanaDatatable,
|
||||
Arguments,
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name: 'kibana_pie',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_datatable'],
|
||||
},
|
||||
inputTypes: ['kibana_datatable'],
|
||||
help: i18n.translate('visTypeVislib.functions.pie.help', {
|
||||
defaultMessage: 'Pie visualization',
|
||||
}),
|
||||
|
@ -59,9 +55,9 @@ export const createPieVisFn = (): ExpressionFunction<typeof name, Context, Argum
|
|||
help: '',
|
||||
},
|
||||
},
|
||||
fn(context, args) {
|
||||
fn(input, args) {
|
||||
const visConfig = JSON.parse(args.visConfig);
|
||||
const convertedData = vislibSlicesResponseHandler(context, visConfig.dimensions);
|
||||
const convertedData = vislibSlicesResponseHandler(input, visConfig.dimensions);
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
|
|
|
@ -83,7 +83,7 @@ export class VisTypeVislibPlugin implements Plugin<Promise<void>, void> {
|
|||
createGaugeVisTypeDefinition,
|
||||
createGoalVisTypeDefinition,
|
||||
];
|
||||
const vislibFns = [createVisTypeVislibVisFn, createPieVisFn];
|
||||
const vislibFns = [createVisTypeVislibVisFn(), createPieVisFn()];
|
||||
|
||||
const visTypeXy = core.injectedMetadata.getInjectedVar('visTypeXy') as
|
||||
| VisTypeXyConfigSchema['visTypeXy']
|
||||
|
|
|
@ -18,19 +18,14 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
ExpressionFunction,
|
||||
ExpressionFunctionDefinition,
|
||||
KibanaDatatable,
|
||||
Render,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
// @ts-ignore
|
||||
import { vislibSeriesResponseHandler } from './vislib/response_handler';
|
||||
|
||||
const name = 'vislib';
|
||||
|
||||
type Context = KibanaDatatable;
|
||||
|
||||
interface Arguments {
|
||||
type: string;
|
||||
visConfig: string;
|
||||
|
@ -43,19 +38,15 @@ interface RenderValue {
|
|||
visConfig: VisParams;
|
||||
}
|
||||
|
||||
type Return = Render<RenderValue>;
|
||||
|
||||
export const createVisTypeVislibVisFn = (): ExpressionFunction<
|
||||
typeof name,
|
||||
Context,
|
||||
export const createVisTypeVislibVisFn = (): ExpressionFunctionDefinition<
|
||||
'vislib',
|
||||
KibanaDatatable,
|
||||
Arguments,
|
||||
Return
|
||||
Render<RenderValue>
|
||||
> => ({
|
||||
name: 'vislib',
|
||||
type: 'render',
|
||||
context: {
|
||||
types: ['kibana_datatable'],
|
||||
},
|
||||
inputTypes: ['kibana_datatable'],
|
||||
help: i18n.translate('visTypeVislib.functions.vislib.help', {
|
||||
defaultMessage: 'Vislib visualization',
|
||||
}),
|
||||
|
|
|
@ -350,7 +350,6 @@ export class VisualizeEmbeddable extends Embeddable<VisualizeInput, VisualizeOut
|
|||
private async updateHandler() {
|
||||
const expressionParams: IExpressionLoaderParams = {
|
||||
searchContext: {
|
||||
type: 'kibana_context',
|
||||
timeRange: this.timeRange,
|
||||
query: this.input.query,
|
||||
filters: this.input.filters,
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import { get } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { VisResponseValue } from 'src/plugins/visualizations/public';
|
||||
import { ExpressionFunction, Render } from 'src/plugins/expressions/public';
|
||||
import { ExpressionFunctionDefinition, Render } from 'src/plugins/expressions/public';
|
||||
import { PersistedState } from '../../../legacy_imports';
|
||||
import { getTypes, getIndexPatterns, getFilterManager } from '../services';
|
||||
|
||||
|
@ -34,7 +34,7 @@ interface Arguments {
|
|||
uiState?: string;
|
||||
}
|
||||
|
||||
export type ExpressionFunctionVisualization = ExpressionFunction<
|
||||
export type ExpressionFunctionVisualization = ExpressionFunctionDefinition<
|
||||
'visualization',
|
||||
any,
|
||||
Arguments,
|
||||
|
@ -86,7 +86,7 @@ export const visualization = (): ExpressionFunctionVisualization => ({
|
|||
help: 'User interface state',
|
||||
},
|
||||
},
|
||||
async fn(context, args, handlers) {
|
||||
async fn(input, args, { inspectorAdapters }) {
|
||||
const visConfigParams = args.visConfig ? JSON.parse(args.visConfig) : {};
|
||||
const schemas = args.schemas ? JSON.parse(args.schemas) : {};
|
||||
const visType = getTypes().get(args.type || 'histogram') as any;
|
||||
|
@ -96,25 +96,25 @@ export const visualization = (): ExpressionFunctionVisualization => ({
|
|||
const uiState = new PersistedState(uiStateParams);
|
||||
|
||||
if (typeof visType.requestHandler === 'function') {
|
||||
context = await visType.requestHandler({
|
||||
input = await visType.requestHandler({
|
||||
partialRows: args.partialRows,
|
||||
metricsAtAllLevels: args.metricsAtAllLevels,
|
||||
index: indexPattern,
|
||||
visParams: visConfigParams,
|
||||
timeRange: get(context, 'timeRange', null),
|
||||
query: get(context, 'query', null),
|
||||
filters: get(context, 'filters', null),
|
||||
timeRange: get(input, 'timeRange', null),
|
||||
query: get(input, 'query', null),
|
||||
filters: get(input, 'filters', null),
|
||||
uiState,
|
||||
inspectorAdapters: handlers.inspectorAdapters,
|
||||
inspectorAdapters,
|
||||
queryFilter: getFilterManager(),
|
||||
forceFetch: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof visType.responseHandler === 'function') {
|
||||
if (context.columns) {
|
||||
if (input.columns) {
|
||||
// assign schemas to aggConfigs
|
||||
context.columns.forEach((column: any) => {
|
||||
input.columns.forEach((column: any) => {
|
||||
if (column.aggConfig) {
|
||||
column.aggConfig.aggConfigs.schemas = visType.schemas.all;
|
||||
}
|
||||
|
@ -122,21 +122,21 @@ export const visualization = (): ExpressionFunctionVisualization => ({
|
|||
|
||||
Object.keys(schemas).forEach(key => {
|
||||
schemas[key].forEach((i: any) => {
|
||||
if (context.columns[i] && context.columns[i].aggConfig) {
|
||||
context.columns[i].aggConfig.schema = key;
|
||||
if (input.columns[i] && input.columns[i].aggConfig) {
|
||||
input.columns[i].aggConfig.schema = key;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
context = await visType.responseHandler(context, visConfigParams.dimensions);
|
||||
input = await visType.responseHandler(input, visConfigParams.dimensions);
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'visualization',
|
||||
value: {
|
||||
visData: context,
|
||||
visData: input,
|
||||
visType: args.type || '',
|
||||
visConfig: visConfigParams,
|
||||
},
|
||||
|
|
35
src/plugins/expressions/README.md
Normal file
35
src/plugins/expressions/README.md
Normal file
|
@ -0,0 +1,35 @@
|
|||
# `expressions` plugin
|
||||
|
||||
This plugin provides methods which will parse & execute an *expression pipeline*
|
||||
string for you, as well as a series of registries for advanced users who might
|
||||
want to incorporate their own functions, types, and renderers into the service
|
||||
for use in their own application.
|
||||
|
||||
Expression pipeline is a chain of functions that *pipe* its output to the
|
||||
input of the next function. Functions can be configured using arguments provided
|
||||
by the user. The final output of the expression pipeline can be rendered using
|
||||
one of the *renderers* registered in `expressions` plugin.
|
||||
|
||||
Expressions power visualizations in Dashboard and Lens, as well as, every
|
||||
*element* in Canvas is backed by an expression.
|
||||
|
||||
Below is an example of one Canvas element that fetches data using `essql` function,
|
||||
pipes it further to `math` and `metric` functions, and final `render` function
|
||||
renders the result.
|
||||
|
||||
```
|
||||
filters
|
||||
| essql
|
||||
query="SELECT COUNT(timestamp) as total_errors
|
||||
FROM kibana_sample_data_logs
|
||||
WHERE tags LIKE '%warning%' OR tags LIKE '%error%'"
|
||||
| math "total_errors"
|
||||
| metric "TOTAL ISSUES"
|
||||
metricFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=48 align="left" color="#FFFFFF" weight="normal" underline=false italic=false}
|
||||
labelFont={font family="'Open Sans', Helvetica, Arial, sans-serif" size=30 align="left" color="#FFFFFF" weight="lighter" underline=false italic=false}
|
||||
| render
|
||||
```
|
||||
|
||||

|
||||
|
||||
[See Canvas documentation about expressions](https://www.elastic.co/guide/en/kibana/current/canvas-function-arguments.html).
|
39
src/plugins/expressions/common/ast/format.test.ts
Normal file
39
src/plugins/expressions/common/ast/format.test.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { formatExpression } from './format';
|
||||
|
||||
describe('formatExpression()', () => {
|
||||
test('converts expression AST to string', () => {
|
||||
const str = formatExpression({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
arguments: {
|
||||
bar: ['baz'],
|
||||
},
|
||||
function: 'foo',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(str).toMatchInlineSnapshot(`"foo bar=\\"baz\\""`);
|
||||
});
|
||||
});
|
34
src/plugins/expressions/common/ast/format.ts
Normal file
34
src/plugins/expressions/common/ast/format.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { ExpressionAstExpression, ExpressionAstArgument } from './types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { toExpression } = require('@kbn/interpreter/common');
|
||||
|
||||
export function format(
|
||||
ast: ExpressionAstExpression | ExpressionAstArgument,
|
||||
type: 'expression' | 'argument'
|
||||
): string {
|
||||
return toExpression(ast, type);
|
||||
}
|
||||
|
||||
export function formatExpression(ast: ExpressionAstExpression): string {
|
||||
return format(ast, 'expression');
|
||||
}
|
23
src/plugins/expressions/common/ast/index.ts
Normal file
23
src/plugins/expressions/common/ast/index.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './parse';
|
||||
export * from './parse_expression';
|
||||
export * from './format';
|
44
src/plugins/expressions/common/ast/parse.test.ts
Normal file
44
src/plugins/expressions/common/ast/parse.test.ts
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 { parse } from './parse';
|
||||
|
||||
describe('parse()', () => {
|
||||
test('parses an expression', () => {
|
||||
const ast = parse('foo bar="baz"', 'expression');
|
||||
|
||||
expect(ast).toMatchObject({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
arguments: {
|
||||
bar: ['baz'],
|
||||
},
|
||||
function: 'foo',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('parses an argument', () => {
|
||||
const arg = parse('foo', 'argument');
|
||||
expect(arg).toBe('foo');
|
||||
});
|
||||
});
|
34
src/plugins/expressions/common/ast/parse.ts
Normal file
34
src/plugins/expressions/common/ast/parse.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
* 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 { ExpressionAstExpression, ExpressionAstArgument } from './types';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { parse: parseRaw } = require('@kbn/interpreter/common');
|
||||
|
||||
export function parse(
|
||||
expression: string,
|
||||
startRule: 'expression' | 'argument'
|
||||
): ExpressionAstExpression | ExpressionAstArgument {
|
||||
try {
|
||||
return parseRaw(String(expression), { startRule });
|
||||
} catch (e) {
|
||||
throw new Error(`Unable to parse expression: ${e.message}`);
|
||||
}
|
||||
}
|
68
src/plugins/expressions/common/ast/parse_expression.test.ts
Normal file
68
src/plugins/expressions/common/ast/parse_expression.test.ts
Normal file
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { parseExpression } from './parse_expression';
|
||||
|
||||
describe('parseExpression()', () => {
|
||||
test('parses an expression', () => {
|
||||
const ast = parseExpression('foo bar="baz"');
|
||||
|
||||
expect(ast).toMatchObject({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
arguments: {
|
||||
bar: ['baz'],
|
||||
},
|
||||
function: 'foo',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('parses an expression with sub-expression', () => {
|
||||
const ast = parseExpression('foo bar="baz" quux={quix}');
|
||||
|
||||
expect(ast).toMatchObject({
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
arguments: {
|
||||
bar: ['baz'],
|
||||
quux: [
|
||||
{
|
||||
type: 'expression',
|
||||
chain: [
|
||||
{
|
||||
type: 'function',
|
||||
function: 'quix',
|
||||
arguments: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
function: 'foo',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
30
src/plugins/expressions/common/ast/parse_expression.ts
Normal file
30
src/plugins/expressions/common/ast/parse_expression.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* 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 { ExpressionAstExpression } from './types';
|
||||
import { parse } from './parse';
|
||||
|
||||
/**
|
||||
* Given expression pipeline string, returns parsed AST.
|
||||
*
|
||||
* @param expression Expression pipeline string.
|
||||
*/
|
||||
export function parseExpression(expression: string): ExpressionAstExpression {
|
||||
return parse(expression, 'expression') as ExpressionAstExpression;
|
||||
}
|
36
src/plugins/expressions/common/ast/types.ts
Normal file
36
src/plugins/expressions/common/ast/types.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type ExpressionAstNode =
|
||||
| ExpressionAstExpression
|
||||
| ExpressionAstFunction
|
||||
| ExpressionAstArgument;
|
||||
|
||||
export interface ExpressionAstExpression {
|
||||
type: 'expression';
|
||||
chain: ExpressionAstFunction[];
|
||||
}
|
||||
|
||||
export interface ExpressionAstFunction {
|
||||
type: 'function';
|
||||
function: string;
|
||||
arguments: Record<string, ExpressionAstArgument[]>;
|
||||
}
|
||||
|
||||
export type ExpressionAstArgument = string | boolean | number | ExpressionAstExpression;
|
108
src/plugins/expressions/common/execution/container.ts
Normal file
108
src/plugins/expressions/common/execution/container.ts
Normal file
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 {
|
||||
StateContainer,
|
||||
createStateContainer,
|
||||
} from '../../../kibana_utils/common/state_containers';
|
||||
import { ExecutorState, defaultState as executorDefaultState } from '../executor';
|
||||
import { ExpressionAstExpression } from '../ast';
|
||||
import { ExpressionValue } from '../expression_types';
|
||||
|
||||
export interface ExecutionState<Output = ExpressionValue> extends ExecutorState {
|
||||
ast: ExpressionAstExpression;
|
||||
|
||||
/**
|
||||
* Tracks state of execution.
|
||||
*
|
||||
* - `not-started` - before .start() method was called.
|
||||
* - `pending` - immediately after .start() method is called.
|
||||
* - `result` - when expression execution completed.
|
||||
* - `error` - when execution failed with error.
|
||||
*/
|
||||
state: 'not-started' | 'pending' | 'result' | 'error';
|
||||
|
||||
/**
|
||||
* Result of the expression execution.
|
||||
*/
|
||||
result?: Output;
|
||||
|
||||
/**
|
||||
* Error happened during the execution.
|
||||
*/
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
const executionDefaultState: ExecutionState = {
|
||||
...executorDefaultState,
|
||||
state: 'not-started',
|
||||
ast: {
|
||||
type: 'expression',
|
||||
chain: [],
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line
|
||||
export interface ExecutionPureTransitions<Output = ExpressionValue> {
|
||||
start: (state: ExecutionState<Output>) => () => ExecutionState<Output>;
|
||||
setResult: (state: ExecutionState<Output>) => (result: Output) => ExecutionState<Output>;
|
||||
setError: (state: ExecutionState<Output>) => (error: Error) => ExecutionState<Output>;
|
||||
}
|
||||
|
||||
export const executionPureTransitions: ExecutionPureTransitions = {
|
||||
start: state => () => ({
|
||||
...state,
|
||||
state: 'pending',
|
||||
}),
|
||||
setResult: state => result => ({
|
||||
...state,
|
||||
state: 'result',
|
||||
result,
|
||||
}),
|
||||
setError: state => error => ({
|
||||
...state,
|
||||
state: 'error',
|
||||
error,
|
||||
}),
|
||||
};
|
||||
|
||||
export type ExecutionContainer<Output = ExpressionValue> = StateContainer<
|
||||
ExecutionState<Output>,
|
||||
ExecutionPureTransitions<Output>
|
||||
>;
|
||||
|
||||
const freeze = <T>(state: T): T => state;
|
||||
|
||||
export const createExecutionContainer = <Output = ExpressionValue>(
|
||||
state: ExecutionState<Output> = executionDefaultState
|
||||
): ExecutionContainer<Output> => {
|
||||
const container = createStateContainer<
|
||||
ExecutionState<Output>,
|
||||
ExecutionPureTransitions<Output>,
|
||||
object
|
||||
>(
|
||||
state,
|
||||
executionPureTransitions,
|
||||
{},
|
||||
{
|
||||
freeze,
|
||||
}
|
||||
);
|
||||
return container;
|
||||
};
|
372
src/plugins/expressions/common/execution/execution.test.ts
Normal file
372
src/plugins/expressions/common/execution/execution.test.ts
Normal file
|
@ -0,0 +1,372 @@
|
|||
/*
|
||||
* 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 { Execution } from './execution';
|
||||
import { parseExpression } from '../ast';
|
||||
import { createUnitTestExecutor } from '../test_helpers';
|
||||
import { ExpressionFunctionDefinition } from '../../public';
|
||||
|
||||
const createExecution = (
|
||||
expression: string = 'foo bar=123',
|
||||
context: Record<string, unknown> = {}
|
||||
) => {
|
||||
const executor = createUnitTestExecutor();
|
||||
const execution = new Execution({
|
||||
executor,
|
||||
ast: parseExpression(expression),
|
||||
context,
|
||||
});
|
||||
return execution;
|
||||
};
|
||||
|
||||
const run = async (
|
||||
expression: string = 'foo bar=123',
|
||||
context?: Record<string, unknown>,
|
||||
input: any = null
|
||||
) => {
|
||||
const execution = createExecution(expression, context);
|
||||
execution.start(input);
|
||||
return await execution.result;
|
||||
};
|
||||
|
||||
describe('Execution', () => {
|
||||
test('can instantiate', () => {
|
||||
const execution = createExecution('foo bar=123');
|
||||
expect(execution.params.ast.chain[0].arguments.bar).toEqual([123]);
|
||||
});
|
||||
|
||||
test('initial input is null at creation', () => {
|
||||
const execution = createExecution();
|
||||
expect(execution.input).toBe(null);
|
||||
});
|
||||
|
||||
test('creates default ExecutionContext', () => {
|
||||
const execution = createExecution();
|
||||
expect(execution.context).toMatchObject({
|
||||
getInitialInput: expect.any(Function),
|
||||
variables: expect.any(Object),
|
||||
types: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test('executes a single clog function in expression pipeline', async () => {
|
||||
const execution = createExecution('clog');
|
||||
/* eslint-disable no-console */
|
||||
const console$log = console.log;
|
||||
const spy = (console.log = jest.fn());
|
||||
/* eslint-enable no-console */
|
||||
|
||||
execution.start(123);
|
||||
const result = await execution.result;
|
||||
|
||||
expect(result).toBe(123);
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith(123);
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log = console$log;
|
||||
/* eslint-enable no-console */
|
||||
});
|
||||
|
||||
test('executes a chain of multiple "add" functions', async () => {
|
||||
const execution = createExecution('add val=1 | add val=2 | add val=3');
|
||||
execution.start({
|
||||
type: 'num',
|
||||
value: -1,
|
||||
});
|
||||
|
||||
const result = await execution.result;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'num',
|
||||
value: 5,
|
||||
});
|
||||
});
|
||||
|
||||
test('executes a chain of "add" and "mult" functions', async () => {
|
||||
const execution = createExecution('add val=5 | mult val=-1 | add val=-10 | mult val=2');
|
||||
execution.start({
|
||||
type: 'num',
|
||||
value: 0,
|
||||
});
|
||||
|
||||
const result = await execution.result;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'num',
|
||||
value: -30,
|
||||
});
|
||||
});
|
||||
|
||||
test('casts input to correct type', async () => {
|
||||
const execution = createExecution('add val=1');
|
||||
|
||||
// Below 1 is cast to { type: 'num', value: 1 }.
|
||||
execution.start(1);
|
||||
const result = await execution.result;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'num',
|
||||
value: 2,
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution context', () => {
|
||||
test('context.variables is an object', async () => {
|
||||
const { result } = (await run('introspectContext key="variables"')) as any;
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
|
||||
test('context.types is an object', async () => {
|
||||
const { result } = (await run('introspectContext key="types"')) as any;
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
|
||||
test('context.abortSignal is an object', async () => {
|
||||
const { result } = (await run('introspectContext key="abortSignal"')) as any;
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
|
||||
test('context.inspectorAdapters is an object', async () => {
|
||||
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
|
||||
expect(typeof result).toBe('object');
|
||||
});
|
||||
|
||||
test('unknown context key is undefined', async () => {
|
||||
const { result } = (await run('introspectContext key="foo"')) as any;
|
||||
expect(typeof result).toBe('undefined');
|
||||
});
|
||||
|
||||
test('can set context variables', async () => {
|
||||
const variables = { foo: 'bar' };
|
||||
const result = await run('var name="foo"', { variables });
|
||||
expect(result).toBe('bar');
|
||||
});
|
||||
});
|
||||
|
||||
describe('inspector adapters', () => {
|
||||
test('by default, "data" and "requests" inspector adapters are available', async () => {
|
||||
const { result } = (await run('introspectContext key="inspectorAdapters"')) as any;
|
||||
expect(result).toMatchObject({
|
||||
data: expect.any(Object),
|
||||
requests: expect.any(Object),
|
||||
});
|
||||
});
|
||||
|
||||
test('can set custom inspector adapters', async () => {
|
||||
const inspectorAdapters = {};
|
||||
const { result } = (await run('introspectContext key="inspectorAdapters"', {
|
||||
inspectorAdapters,
|
||||
})) as any;
|
||||
expect(result).toBe(inspectorAdapters);
|
||||
});
|
||||
|
||||
test('can access custom inspector adapters on Execution object', async () => {
|
||||
const inspectorAdapters = {};
|
||||
const execution = createExecution('introspectContext key="inspectorAdapters"', {
|
||||
inspectorAdapters,
|
||||
});
|
||||
expect(execution.inspectorAdapters).toBe(inspectorAdapters);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expression abortion', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe('expression execution', () => {
|
||||
test('supports default argument alias _', async () => {
|
||||
const execution = createExecution('add val=1 | add 2');
|
||||
execution.start({
|
||||
type: 'num',
|
||||
value: 0,
|
||||
});
|
||||
|
||||
const result = await execution.result;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: 'num',
|
||||
value: 3,
|
||||
});
|
||||
});
|
||||
|
||||
test('can execute async functions', async () => {
|
||||
const res = await run('sleep 10 | sleep 10');
|
||||
expect(res).toBe(null);
|
||||
});
|
||||
|
||||
test('result is undefined until execution completes', async () => {
|
||||
const execution = createExecution('sleep 10');
|
||||
expect(execution.state.get().result).toBe(undefined);
|
||||
execution.start(null);
|
||||
expect(execution.state.get().result).toBe(undefined);
|
||||
await new Promise(r => setTimeout(r, 1));
|
||||
expect(execution.state.get().result).toBe(undefined);
|
||||
await new Promise(r => setTimeout(r, 11));
|
||||
expect(execution.state.get().result).toBe(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when function throws', () => {
|
||||
test('error is reported in output object', async () => {
|
||||
const result = await run('error "foobar"');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
test('error message is prefixed with function name', async () => {
|
||||
const result = await run('error "foobar"');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
error: {
|
||||
message: `[error] > foobar`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('returns error of the first function that throws', async () => {
|
||||
const result = await run('error "foo" | error "bar"');
|
||||
|
||||
expect(result).toMatchObject({
|
||||
error: {
|
||||
message: `[error] > foo`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('when function throws, execution still succeeds', async () => {
|
||||
const execution = await createExecution('error "foo"');
|
||||
execution.start(null);
|
||||
|
||||
const result = await execution.result;
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: 'error',
|
||||
});
|
||||
expect(execution.state.get().state).toBe('result');
|
||||
expect(execution.state.get().result).toMatchObject({
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
|
||||
test('does not execute remaining functions in pipeline', async () => {
|
||||
const spy: ExpressionFunctionDefinition<'spy', any, {}, any> = {
|
||||
name: 'spy',
|
||||
args: {},
|
||||
help: '',
|
||||
fn: jest.fn(),
|
||||
};
|
||||
const executor = createUnitTestExecutor();
|
||||
executor.registerFunction(spy);
|
||||
|
||||
await executor.run('error "..." | spy', null);
|
||||
|
||||
expect(spy.fn).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('state', () => {
|
||||
test('execution state is "not-started" before .start() is called', async () => {
|
||||
const execution = createExecution('var foo');
|
||||
expect(execution.state.get().state).toBe('not-started');
|
||||
});
|
||||
|
||||
test('execution state is "pending" after .start() was called', async () => {
|
||||
const execution = createExecution('var foo');
|
||||
execution.start(null);
|
||||
expect(execution.state.get().state).toBe('pending');
|
||||
});
|
||||
|
||||
test('execution state is "pending" while execution is in progress', async () => {
|
||||
const execution = createExecution('sleep 20');
|
||||
execution.start(null);
|
||||
await new Promise(r => setTimeout(r, 5));
|
||||
expect(execution.state.get().state).toBe('pending');
|
||||
});
|
||||
|
||||
test('execution state is "result" when execution successfully completes', async () => {
|
||||
const execution = createExecution('sleep 1');
|
||||
execution.start(null);
|
||||
await new Promise(r => setTimeout(r, 30));
|
||||
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;
|
||||
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);
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: 'num',
|
||||
value: 5,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when arguments are missing', () => {
|
||||
test('when required argument is missing and has not alias, returns error', async () => {
|
||||
const requiredArg: ExpressionFunctionDefinition<'requiredArg', any, { arg: any }, any> = {
|
||||
name: 'requiredArg',
|
||||
args: {
|
||||
arg: {
|
||||
help: '',
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
help: '',
|
||||
fn: jest.fn(),
|
||||
};
|
||||
const executor = createUnitTestExecutor();
|
||||
executor.registerFunction(requiredArg);
|
||||
const result = await executor.run('requiredArg', null, {});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: '[requiredArg] > requiredArg requires an argument',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('when required argument is missing and has alias, returns error', async () => {
|
||||
const result = await run('var_set', {});
|
||||
|
||||
expect(result).toMatchObject({
|
||||
type: 'error',
|
||||
error: {
|
||||
message: '[var_set] > var_set requires an "name" argument',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
330
src/plugins/expressions/common/execution/execution.ts
Normal file
330
src/plugins/expressions/common/execution/execution.ts
Normal file
|
@ -0,0 +1,330 @@
|
|||
/*
|
||||
* 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 { keys, last, mapValues, reduce, zipObject } from 'lodash';
|
||||
import { Executor } from '../executor';
|
||||
import { createExecutionContainer, ExecutionContainer } from './container';
|
||||
import { createError } from '../util';
|
||||
import { Defer } from '../../../kibana_utils/common';
|
||||
import { RequestAdapter, DataAdapter } from '../../../inspector/common';
|
||||
import { isExpressionValueError } from '../expression_types/specs/error';
|
||||
import { ExpressionAstExpression, ExpressionAstFunction, parse } from '../ast';
|
||||
import { ExecutionContext, DefaultInspectorAdapters } from './types';
|
||||
import { getType } from '../expression_types';
|
||||
import { ArgumentType, ExpressionFunction } from '../expression_functions';
|
||||
import { getByAlias } from '../util/get_by_alias';
|
||||
|
||||
export interface ExecutionParams<
|
||||
ExtraContext extends Record<string, unknown> = Record<string, unknown>
|
||||
> {
|
||||
executor: Executor<any>;
|
||||
ast: ExpressionAstExpression;
|
||||
context?: ExtraContext;
|
||||
}
|
||||
|
||||
const createDefaultInspectorAdapters = (): DefaultInspectorAdapters => ({
|
||||
requests: new RequestAdapter(),
|
||||
data: new DataAdapter(),
|
||||
});
|
||||
|
||||
export class Execution<
|
||||
ExtraContext extends Record<string, unknown> = Record<string, unknown>,
|
||||
Input = unknown,
|
||||
Output = unknown,
|
||||
InspectorAdapters = ExtraContext['inspectorAdapters'] extends object
|
||||
? ExtraContext['inspectorAdapters']
|
||||
: DefaultInspectorAdapters
|
||||
> {
|
||||
/**
|
||||
* Dynamic state of the execution.
|
||||
*/
|
||||
public readonly state: ExecutionContainer<Output>;
|
||||
|
||||
/**
|
||||
* Initial input of the execution.
|
||||
*
|
||||
* N.B. It is initialized to `null` rather than `undefined` for legacy reasons,
|
||||
* because in legacy interpreter it was set to `null` by default.
|
||||
*/
|
||||
public input: Input = null as any;
|
||||
|
||||
/**
|
||||
* Execution context - object that allows to do side-effects. Context is passed
|
||||
* to every function.
|
||||
*/
|
||||
public readonly context: ExecutionContext<Input, InspectorAdapters> & ExtraContext;
|
||||
|
||||
/**
|
||||
* AbortController to cancel this Execution.
|
||||
*/
|
||||
private readonly abortController = new AbortController();
|
||||
|
||||
/**
|
||||
* Whether .start() method has been called.
|
||||
*/
|
||||
private hasStarted: boolean = false;
|
||||
|
||||
/**
|
||||
* Future that tracks result or error of this execution.
|
||||
*/
|
||||
private readonly firstResultFuture = new Defer<Output>();
|
||||
|
||||
public get result(): Promise<unknown> {
|
||||
return this.firstResultFuture.promise;
|
||||
}
|
||||
|
||||
public get inspectorAdapters(): InspectorAdapters {
|
||||
return this.context.inspectorAdapters;
|
||||
}
|
||||
|
||||
constructor(public readonly params: ExecutionParams<ExtraContext>) {
|
||||
const { executor, ast } = params;
|
||||
this.state = createExecutionContainer<Output>({
|
||||
...executor.state.get(),
|
||||
state: 'not-started',
|
||||
ast,
|
||||
});
|
||||
|
||||
this.context = {
|
||||
getInitialInput: () => this.input,
|
||||
variables: {},
|
||||
types: executor.getTypes(),
|
||||
abortSignal: this.abortController.signal,
|
||||
...(params.context || ({} as ExtraContext)),
|
||||
inspectorAdapters: (params.context && params.context.inspectorAdapters
|
||||
? params.context.inspectorAdapters
|
||||
: createDefaultInspectorAdapters()) as InspectorAdapters,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop execution of expression.
|
||||
*/
|
||||
cancel() {
|
||||
this.abortController.abort();
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this method to start 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) {
|
||||
if (this.hasStarted) throw new Error('Execution already started.');
|
||||
this.hasStarted = true;
|
||||
|
||||
this.input = input;
|
||||
this.state.transitions.start();
|
||||
|
||||
const { resolve, reject } = this.firstResultFuture;
|
||||
this.invokeChain(this.state.get().ast.chain, input).then(resolve, reject);
|
||||
|
||||
this.firstResultFuture.promise.then(
|
||||
result => {
|
||||
this.state.transitions.setResult(result);
|
||||
},
|
||||
error => {
|
||||
this.state.transitions.setError(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async invokeChain(chainArr: ExpressionAstFunction[], input: unknown): Promise<any> {
|
||||
if (!chainArr.length) return input;
|
||||
|
||||
for (const link of chainArr) {
|
||||
// if execution was aborted return error
|
||||
if (this.context.abortSignal && this.context.abortSignal.aborted) {
|
||||
return createError({
|
||||
message: 'The expression was aborted.',
|
||||
name: 'AbortError',
|
||||
});
|
||||
}
|
||||
|
||||
const { function: fnName, arguments: fnArgs } = link;
|
||||
const fnDef = getByAlias(this.state.get().functions, fnName);
|
||||
|
||||
if (!fnDef) {
|
||||
return createError({ message: `Function ${fnName} could not be found.` });
|
||||
}
|
||||
|
||||
try {
|
||||
// Resolve arguments before passing to function
|
||||
// resolveArgs returns an object because the arguments themselves might
|
||||
// actually have a 'then' function which would be treated as a promise
|
||||
const { resolvedArgs } = await this.resolveArgs(fnDef, input, fnArgs);
|
||||
const output = await this.invokeFunction(fnDef, input, resolvedArgs);
|
||||
if (getType(output) === 'error') return output;
|
||||
input = output;
|
||||
} catch (e) {
|
||||
e.message = `[${fnName}] > ${e.message}`;
|
||||
return createError(e);
|
||||
}
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
async invokeFunction(
|
||||
fn: ExpressionFunction,
|
||||
input: unknown,
|
||||
args: Record<string, unknown>
|
||||
): Promise<any> {
|
||||
const normalizedInput = this.cast(input, fn.inputTypes);
|
||||
const output = await fn.fn(normalizedInput, args, this.context);
|
||||
|
||||
// Validate that the function returned the type it said it would.
|
||||
// This isn't required, but it keeps function developers honest.
|
||||
const returnType = getType(output);
|
||||
const expectedType = fn.type;
|
||||
if (expectedType && returnType !== expectedType) {
|
||||
throw new Error(
|
||||
`Function '${fn.name}' should return '${expectedType}',` +
|
||||
` actually returned '${returnType}'`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate the function output against the type definition's validate function.
|
||||
const type = this.context.types[fn.type];
|
||||
if (type && type.validate) {
|
||||
try {
|
||||
type.validate(output);
|
||||
} catch (e) {
|
||||
throw new Error(`Output of '${fn.name}' is not a valid type '${fn.type}': ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
public cast(value: any, toTypeNames?: string[]) {
|
||||
// If you don't give us anything to cast to, you'll get your input back
|
||||
if (!toTypeNames || toTypeNames.length === 0) return value;
|
||||
|
||||
// No need to cast if node is already one of the valid types
|
||||
const fromTypeName = getType(value);
|
||||
if (toTypeNames.includes(fromTypeName)) return value;
|
||||
|
||||
const { types } = this.state.get();
|
||||
const fromTypeDef = types[fromTypeName];
|
||||
|
||||
for (const toTypeName of toTypeNames) {
|
||||
// First check if the current type can cast to this type
|
||||
if (fromTypeDef && fromTypeDef.castsTo(toTypeName)) {
|
||||
return fromTypeDef.to(value, toTypeName, types);
|
||||
}
|
||||
|
||||
// If that isn't possible, check if this type can cast from the current type
|
||||
const toTypeDef = types[toTypeName];
|
||||
if (toTypeDef && toTypeDef.castsFrom(fromTypeName)) return toTypeDef.from(value, types);
|
||||
}
|
||||
|
||||
throw new Error(`Can not cast '${fromTypeName}' to any of '${toTypeNames.join(', ')}'`);
|
||||
}
|
||||
|
||||
// Processes the multi-valued AST argument values into arguments that can be passed to the function
|
||||
async resolveArgs(fnDef: ExpressionFunction, input: unknown, argAsts: any): Promise<any> {
|
||||
const argDefs = fnDef.args;
|
||||
|
||||
// Use the non-alias name from the argument definition
|
||||
const dealiasedArgAsts = reduce(
|
||||
argAsts,
|
||||
(acc, argAst, argName) => {
|
||||
const argDef = getByAlias(argDefs, argName);
|
||||
if (!argDef) {
|
||||
throw new Error(`Unknown argument '${argName}' passed to function '${fnDef.name}'`);
|
||||
}
|
||||
acc[argDef.name] = (acc[argDef.name] || []).concat(argAst);
|
||||
return acc;
|
||||
},
|
||||
{} as any
|
||||
);
|
||||
|
||||
// Check for missing required arguments.
|
||||
for (const argDef of Object.values(argDefs)) {
|
||||
const { aliases, default: argDefault, name: argName, required } = argDef as ArgumentType<
|
||||
any
|
||||
> & { name: string };
|
||||
if (
|
||||
typeof argDefault !== 'undefined' ||
|
||||
!required ||
|
||||
typeof dealiasedArgAsts[argName] !== 'undefined'
|
||||
)
|
||||
continue;
|
||||
|
||||
if (!aliases || aliases.length === 0) {
|
||||
throw new Error(`${fnDef.name} requires an argument`);
|
||||
}
|
||||
|
||||
// use an alias if _ is the missing arg
|
||||
const errorArg = argName === '_' ? aliases[0] : argName;
|
||||
throw new Error(`${fnDef.name} requires an "${errorArg}" argument`);
|
||||
}
|
||||
|
||||
// Fill in default values from argument definition
|
||||
const argAstsWithDefaults = reduce(
|
||||
argDefs,
|
||||
(acc: any, argDef: any, argName: any) => {
|
||||
if (typeof acc[argName] === 'undefined' && typeof argDef.default !== 'undefined') {
|
||||
acc[argName] = [parse(argDef.default, 'argument')];
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
dealiasedArgAsts
|
||||
);
|
||||
|
||||
// Create the functions to resolve the argument ASTs into values
|
||||
// These are what are passed to the actual functions if you opt out of resolving
|
||||
const resolveArgFns = mapValues(argAstsWithDefaults, (asts, argName) => {
|
||||
return asts.map((item: ExpressionAstExpression) => {
|
||||
return async (subInput = input) => {
|
||||
const output = await this.params.executor.interpret(item, subInput);
|
||||
if (isExpressionValueError(output)) throw output.error;
|
||||
const casted = this.cast(output, argDefs[argName as any].types);
|
||||
return casted;
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const argNames = keys(resolveArgFns);
|
||||
|
||||
// Actually resolve unless the argument definition says not to
|
||||
const resolvedArgValues = await Promise.all(
|
||||
argNames.map(argName => {
|
||||
const interpretFns = resolveArgFns[argName];
|
||||
if (!argDefs[argName].resolve) return interpretFns;
|
||||
return Promise.all(interpretFns.map((fn: any) => fn()));
|
||||
})
|
||||
);
|
||||
|
||||
const resolvedMultiArgs = zipObject(argNames, resolvedArgValues);
|
||||
|
||||
// Just return the last unless the argument definition allows multiple
|
||||
const resolvedArgs = mapValues(resolvedMultiArgs, (argValues, argName) => {
|
||||
if (argDefs[argName as any].multi) return argValues;
|
||||
return last(argValues as any);
|
||||
});
|
||||
|
||||
// Return an object here because the arguments themselves might actually have a 'then'
|
||||
// function which would be treated as a promise
|
||||
return { resolvedArgs };
|
||||
}
|
||||
}
|
|
@ -17,6 +17,6 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './type_registry';
|
||||
export * from './function_registry';
|
||||
export * from './render_registry';
|
||||
export * from './types';
|
||||
export * from './container';
|
||||
export * from './execution';
|
72
src/plugins/expressions/common/execution/types.ts
Normal file
72
src/plugins/expressions/common/execution/types.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { ExpressionType } from '../expression_types';
|
||||
import { DataAdapter, RequestAdapter } from '../../../inspector/common';
|
||||
import { TimeRange, Query, esFilters } from '../../../data/common';
|
||||
|
||||
/**
|
||||
* `ExecutionContext` is an object available to all functions during a single execution;
|
||||
* it provides various methods to perform side-effects.
|
||||
*/
|
||||
export interface ExecutionContext<Input = unknown, InspectorAdapters = DefaultInspectorAdapters> {
|
||||
/**
|
||||
* Get initial input with which execution started.
|
||||
*/
|
||||
getInitialInput: () => Input;
|
||||
|
||||
/**
|
||||
* Context variables that can be consumed using `var` and `var_set` functions.
|
||||
*/
|
||||
variables: Record<string, unknown>;
|
||||
|
||||
/**
|
||||
* A map of available expression types.
|
||||
*/
|
||||
types: Record<string, ExpressionType>;
|
||||
|
||||
/**
|
||||
* Adds ability to abort current execution.
|
||||
*/
|
||||
abortSignal: AbortSignal;
|
||||
|
||||
/**
|
||||
* Adapters for `inspector` plugin.
|
||||
*/
|
||||
inspectorAdapters: InspectorAdapters;
|
||||
|
||||
/**
|
||||
* Search context in which expression should operate.
|
||||
*/
|
||||
search?: ExecutionContextSearch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default inspector adapters created if inspector adapters are not set explicitly.
|
||||
*/
|
||||
export interface DefaultInspectorAdapters {
|
||||
requests: RequestAdapter;
|
||||
data: DataAdapter;
|
||||
}
|
||||
|
||||
export interface ExecutionContextSearch {
|
||||
filters?: esFilters.Filter[];
|
||||
query?: Query | Query[];
|
||||
timeRange?: TimeRange;
|
||||
}
|
81
src/plugins/expressions/common/executor/container.ts
Normal file
81
src/plugins/expressions/common/executor/container.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 {
|
||||
StateContainer,
|
||||
createStateContainer,
|
||||
} from '../../../kibana_utils/common/state_containers';
|
||||
import { ExpressionFunction } from '../expression_functions';
|
||||
import { ExpressionType } from '../expression_types';
|
||||
|
||||
export interface ExecutorState<Context extends Record<string, unknown> = Record<string, unknown>> {
|
||||
functions: Record<string, ExpressionFunction>;
|
||||
types: Record<string, ExpressionType>;
|
||||
context: Context;
|
||||
}
|
||||
|
||||
export const defaultState: ExecutorState<any> = {
|
||||
functions: {},
|
||||
types: {},
|
||||
context: {},
|
||||
};
|
||||
|
||||
export interface ExecutorPureTransitions {
|
||||
addFunction: (state: ExecutorState) => (fn: ExpressionFunction) => ExecutorState;
|
||||
addType: (state: ExecutorState) => (type: ExpressionType) => ExecutorState;
|
||||
extendContext: (state: ExecutorState) => (extraContext: Record<string, unknown>) => ExecutorState;
|
||||
}
|
||||
|
||||
export const pureTransitions: ExecutorPureTransitions = {
|
||||
addFunction: state => fn => ({ ...state, functions: { ...state.functions, [fn.name]: fn } }),
|
||||
addType: state => type => ({ ...state, types: { ...state.types, [type.name]: type } }),
|
||||
extendContext: state => extraContext => ({
|
||||
...state,
|
||||
context: { ...state.context, ...extraContext },
|
||||
}),
|
||||
};
|
||||
|
||||
export interface ExecutorPureSelectors {
|
||||
getFunction: (state: ExecutorState) => (id: string) => ExpressionFunction | null;
|
||||
getType: (state: ExecutorState) => (id: string) => ExpressionType | null;
|
||||
getContext: (state: ExecutorState) => () => ExecutorState['context'];
|
||||
}
|
||||
|
||||
export const pureSelectors: ExecutorPureSelectors = {
|
||||
getFunction: state => id => state.functions[id] || null,
|
||||
getType: state => id => state.types[id] || null,
|
||||
getContext: ({ context }) => () => context,
|
||||
};
|
||||
|
||||
export type ExecutorContainer<
|
||||
Context extends Record<string, unknown> = Record<string, unknown>
|
||||
> = StateContainer<ExecutorState<Context>, ExecutorPureTransitions, ExecutorPureSelectors>;
|
||||
|
||||
export const createExecutorContainer = <
|
||||
Context extends Record<string, unknown> = Record<string, unknown>
|
||||
>(
|
||||
state: ExecutorState<Context> = defaultState
|
||||
): ExecutorContainer<Context> => {
|
||||
const container = createStateContainer<
|
||||
ExecutorState<Context>,
|
||||
ExecutorPureTransitions,
|
||||
ExecutorPureSelectors
|
||||
>(state, pureTransitions, pureSelectors);
|
||||
return container;
|
||||
};
|
155
src/plugins/expressions/common/executor/executor.test.ts
Normal file
155
src/plugins/expressions/common/executor/executor.test.ts
Normal file
|
@ -0,0 +1,155 @@
|
|||
/*
|
||||
* 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 { Executor } from './executor';
|
||||
import * as expressionTypes from '../expression_types';
|
||||
import * as expressionFunctions from '../expression_functions';
|
||||
import { Execution } from '../execution';
|
||||
import { parseExpression } from '../ast';
|
||||
|
||||
describe('Executor', () => {
|
||||
test('can instantiate', () => {
|
||||
new Executor();
|
||||
});
|
||||
|
||||
describe('type registry', () => {
|
||||
test('can register a type', () => {
|
||||
const executor = new Executor();
|
||||
executor.registerType(expressionTypes.datatable);
|
||||
});
|
||||
|
||||
test('can register all types', () => {
|
||||
const executor = new Executor();
|
||||
for (const type of expressionTypes.typeSpecs) executor.registerType(type);
|
||||
});
|
||||
|
||||
test('can retrieve all types', () => {
|
||||
const executor = new Executor();
|
||||
executor.registerType(expressionTypes.datatable);
|
||||
const types = executor.getTypes();
|
||||
expect(Object.keys(types)).toEqual(['datatable']);
|
||||
});
|
||||
|
||||
test('can retrieve all types - 2', () => {
|
||||
const executor = new Executor();
|
||||
for (const type of expressionTypes.typeSpecs) executor.registerType(type);
|
||||
const types = executor.getTypes();
|
||||
expect(Object.keys(types).sort()).toEqual(
|
||||
expressionTypes.typeSpecs.map(spec => spec.name).sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('function registry', () => {
|
||||
test('can register a function', () => {
|
||||
const executor = new Executor();
|
||||
executor.registerFunction(expressionFunctions.clog);
|
||||
});
|
||||
|
||||
test('can register all functions', () => {
|
||||
const executor = new Executor();
|
||||
for (const functionDefinition of expressionFunctions.functionSpecs)
|
||||
executor.registerFunction(functionDefinition);
|
||||
});
|
||||
|
||||
test('can retrieve all functions', () => {
|
||||
const executor = new Executor();
|
||||
executor.registerFunction(expressionFunctions.clog);
|
||||
const functions = executor.getFunctions();
|
||||
expect(Object.keys(functions)).toEqual(['clog']);
|
||||
});
|
||||
|
||||
test('can retrieve all functions - 2', () => {
|
||||
const executor = new Executor();
|
||||
for (const functionDefinition of expressionFunctions.functionSpecs)
|
||||
executor.registerFunction(functionDefinition);
|
||||
const functions = executor.getFunctions();
|
||||
expect(Object.keys(functions).sort()).toEqual(
|
||||
expressionFunctions.functionSpecs.map(spec => spec.name).sort()
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context', () => {
|
||||
test('context is empty by default', () => {
|
||||
const executor = new Executor();
|
||||
expect(executor.context).toEqual({});
|
||||
});
|
||||
|
||||
test('can extend context', () => {
|
||||
const executor = new Executor();
|
||||
executor.extendContext({
|
||||
foo: 'bar',
|
||||
});
|
||||
expect(executor.context).toEqual({
|
||||
foo: 'bar',
|
||||
});
|
||||
});
|
||||
|
||||
test('can extend context multiple times with multiple keys', () => {
|
||||
const executor = new Executor();
|
||||
const abortSignal = {};
|
||||
const env = {};
|
||||
|
||||
executor.extendContext({
|
||||
foo: 'bar',
|
||||
});
|
||||
executor.extendContext({
|
||||
abortSignal,
|
||||
env,
|
||||
});
|
||||
|
||||
expect(executor.context).toEqual({
|
||||
foo: 'bar',
|
||||
abortSignal,
|
||||
env,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execution', () => {
|
||||
describe('createExecution()', () => {
|
||||
test('returns Execution object from string', () => {
|
||||
const executor = new Executor();
|
||||
const execution = executor.createExecution('foo bar="baz"');
|
||||
|
||||
expect(execution).toBeInstanceOf(Execution);
|
||||
expect(execution.params.ast.chain[0].function).toBe('foo');
|
||||
});
|
||||
|
||||
test('returns Execution object from AST', () => {
|
||||
const executor = new Executor();
|
||||
const ast = parseExpression('foo bar="baz"');
|
||||
const execution = executor.createExecution(ast);
|
||||
|
||||
expect(execution).toBeInstanceOf(Execution);
|
||||
expect(execution.params.ast.chain[0].function).toBe('foo');
|
||||
});
|
||||
|
||||
test('Execution inherits context from Executor', () => {
|
||||
const executor = new Executor();
|
||||
const foo = {};
|
||||
executor.extendContext({ foo });
|
||||
const execution = executor.createExecution('foo bar="baz"');
|
||||
|
||||
expect((execution.context as any).foo).toBe(foo);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
204
src/plugins/expressions/common/executor/executor.ts
Normal file
204
src/plugins/expressions/common/executor/executor.ts
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { ExecutorState, ExecutorContainer } from './container';
|
||||
import { createExecutorContainer } from './container';
|
||||
import { AnyExpressionFunctionDefinition, ExpressionFunction } from '../expression_functions';
|
||||
import { Execution } from '../execution/execution';
|
||||
import { IRegistry } from '../types';
|
||||
import { ExpressionType } from '../expression_types/expression_type';
|
||||
import { AnyExpressionTypeDefinition } from '../expression_types/types';
|
||||
import { getType } from '../expression_types';
|
||||
import { ExpressionAstExpression, ExpressionAstNode, parseExpression } from '../ast';
|
||||
import { typeSpecs } from '../expression_types/specs';
|
||||
import { functionSpecs } from '../expression_functions/specs';
|
||||
|
||||
export class TypesRegistry implements IRegistry<ExpressionType> {
|
||||
constructor(private readonly executor: Executor<any>) {}
|
||||
|
||||
public register(
|
||||
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
|
||||
) {
|
||||
this.executor.registerType(typeDefinition);
|
||||
}
|
||||
|
||||
public get(id: string): ExpressionType | null {
|
||||
return this.executor.state.selectors.getType(id);
|
||||
}
|
||||
|
||||
public toJS(): Record<string, ExpressionType> {
|
||||
return this.executor.getTypes();
|
||||
}
|
||||
|
||||
public toArray(): ExpressionType[] {
|
||||
return Object.values(this.toJS());
|
||||
}
|
||||
}
|
||||
|
||||
export class FunctionsRegistry implements IRegistry<ExpressionFunction> {
|
||||
constructor(private readonly executor: Executor<any>) {}
|
||||
|
||||
public register(
|
||||
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
|
||||
) {
|
||||
this.executor.registerFunction(functionDefinition);
|
||||
}
|
||||
|
||||
public get(id: string): ExpressionFunction | null {
|
||||
return this.executor.state.selectors.getFunction(id);
|
||||
}
|
||||
|
||||
public toJS(): Record<string, ExpressionFunction> {
|
||||
return this.executor.getFunctions();
|
||||
}
|
||||
|
||||
public toArray(): ExpressionFunction[] {
|
||||
return Object.values(this.toJS());
|
||||
}
|
||||
}
|
||||
|
||||
export class Executor<Context extends Record<string, unknown> = Record<string, unknown>> {
|
||||
static createWithDefaults<Ctx extends Record<string, unknown> = Record<string, unknown>>(
|
||||
state?: ExecutorState<Ctx>
|
||||
): Executor<Ctx> {
|
||||
const executor = new Executor<Ctx>(state);
|
||||
for (const type of typeSpecs) executor.registerType(type);
|
||||
for (const func of functionSpecs) executor.registerFunction(func);
|
||||
return executor;
|
||||
}
|
||||
|
||||
public readonly state: ExecutorContainer<Context>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public readonly functions: FunctionsRegistry;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
public readonly types: TypesRegistry;
|
||||
|
||||
constructor(state?: ExecutorState<Context>) {
|
||||
this.state = createExecutorContainer<Context>(state);
|
||||
this.functions = new FunctionsRegistry(this);
|
||||
this.types = new TypesRegistry(this);
|
||||
}
|
||||
|
||||
public registerFunction(
|
||||
functionDefinition: AnyExpressionFunctionDefinition | (() => AnyExpressionFunctionDefinition)
|
||||
) {
|
||||
const fn = new ExpressionFunction(
|
||||
typeof functionDefinition === 'object' ? functionDefinition : functionDefinition()
|
||||
);
|
||||
this.state.transitions.addFunction(fn);
|
||||
}
|
||||
|
||||
public getFunction(name: string): ExpressionFunction | undefined {
|
||||
return this.state.get().functions[name];
|
||||
}
|
||||
|
||||
public getFunctions(): Record<string, ExpressionFunction> {
|
||||
return { ...this.state.get().functions };
|
||||
}
|
||||
|
||||
public registerType(
|
||||
typeDefinition: AnyExpressionTypeDefinition | (() => AnyExpressionTypeDefinition)
|
||||
) {
|
||||
const type = new ExpressionType(
|
||||
typeof typeDefinition === 'object' ? typeDefinition : typeDefinition()
|
||||
);
|
||||
this.state.transitions.addType(type);
|
||||
}
|
||||
|
||||
public getType(name: string): ExpressionType | undefined {
|
||||
return this.state.get().types[name];
|
||||
}
|
||||
|
||||
public getTypes(): Record<string, ExpressionType> {
|
||||
return { ...this.state.get().types };
|
||||
}
|
||||
|
||||
public extendContext(extraContext: Record<string, unknown>) {
|
||||
this.state.transitions.extendContext(extraContext);
|
||||
}
|
||||
|
||||
public get context(): Record<string, unknown> {
|
||||
return this.state.selectors.getContext();
|
||||
}
|
||||
|
||||
public async interpret<T>(ast: ExpressionAstNode, input: T): Promise<unknown> {
|
||||
switch (getType(ast)) {
|
||||
case 'expression':
|
||||
return await this.interpretExpression(ast as ExpressionAstExpression, input);
|
||||
case 'string':
|
||||
case 'number':
|
||||
case 'null':
|
||||
case 'boolean':
|
||||
return ast;
|
||||
default:
|
||||
throw new Error(`Unknown AST object: ${JSON.stringify(ast)}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async interpretExpression<T>(
|
||||
ast: string | ExpressionAstExpression,
|
||||
input: T
|
||||
): Promise<unknown> {
|
||||
const execution = this.createExecution(ast);
|
||||
execution.start(input);
|
||||
return await execution.result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute expression and return result.
|
||||
*
|
||||
* @param ast Expression AST or a string representing expression.
|
||||
* @param input Initial input to the first expression function.
|
||||
* @param context Extra global context object that will be merged into the
|
||||
* expression global context object that is provided to each function to allow side-effects.
|
||||
*/
|
||||
public async run<
|
||||
Input,
|
||||
Output,
|
||||
ExtraContext extends Record<string, unknown> = Record<string, unknown>
|
||||
>(ast: string | ExpressionAstExpression, input: Input, context?: ExtraContext) {
|
||||
const execution = this.createExecution(ast, context);
|
||||
execution.start(input);
|
||||
return (await execution.result) as Output;
|
||||
}
|
||||
|
||||
public createExecution<ExtraContext extends Record<string, unknown> = Record<string, unknown>>(
|
||||
ast: string | ExpressionAstExpression,
|
||||
context: ExtraContext = {} as ExtraContext
|
||||
): Execution<Context & ExtraContext> {
|
||||
if (typeof ast === 'string') ast = parseExpression(ast);
|
||||
const execution = new Execution<Context & ExtraContext>({
|
||||
ast,
|
||||
executor: this,
|
||||
context: {
|
||||
...this.context,
|
||||
...context,
|
||||
} as Context & ExtraContext,
|
||||
});
|
||||
return execution;
|
||||
}
|
||||
}
|
|
@ -17,8 +17,5 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export function createHandlers() {
|
||||
return {
|
||||
environment: 'client',
|
||||
};
|
||||
}
|
||||
export * from './container';
|
||||
export * from './executor';
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { KnownTypeToString, TypeString, UnmappedTypeStrings } from './common';
|
||||
import { KnownTypeToString, TypeString, UnmappedTypeStrings } from '../types/common';
|
||||
|
||||
/**
|
||||
* This type represents all of the possible combinations of properties of an
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 { AnyExpressionFunctionDefinition } from './types';
|
||||
import { ExpressionFunctionParameter } from './expression_function_parameter';
|
||||
import { ExpressionValue } from '../expression_types/types';
|
||||
import { ExecutionContext } from '../execution';
|
||||
|
||||
export class ExpressionFunction {
|
||||
/**
|
||||
* Name of function
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* Aliases that can be used instead of `name`.
|
||||
*/
|
||||
aliases: string[];
|
||||
|
||||
/**
|
||||
* Return type of function. This SHOULD be supplied. We use it for UI
|
||||
* and autocomplete hinting. We may also use it for optimizations in
|
||||
* the future.
|
||||
*/
|
||||
type: string;
|
||||
|
||||
/**
|
||||
* Function to run function (context, args)
|
||||
*/
|
||||
fn: (input: ExpressionValue, params: Record<string, any>, handlers: object) => ExpressionValue;
|
||||
|
||||
/**
|
||||
* A short help text.
|
||||
*/
|
||||
help: string;
|
||||
|
||||
/**
|
||||
* Specification of expression function parameters.
|
||||
*/
|
||||
args: Record<string, ExpressionFunctionParameter> = {};
|
||||
|
||||
/**
|
||||
* Type of inputs that this function supports.
|
||||
*/
|
||||
inputTypes: string[] | undefined;
|
||||
|
||||
constructor(functionDefinition: AnyExpressionFunctionDefinition) {
|
||||
const { name, type, aliases, fn, help, args, inputTypes, context } = functionDefinition;
|
||||
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.aliases = aliases || [];
|
||||
this.fn = (input, params, handlers) =>
|
||||
Promise.resolve(fn(input, params, handlers as ExecutionContext));
|
||||
this.help = help || '';
|
||||
this.inputTypes = inputTypes || context?.types;
|
||||
|
||||
for (const [key, arg] of Object.entries(args || {})) {
|
||||
this.args[key] = new ExpressionFunctionParameter(key, arg);
|
||||
}
|
||||
}
|
||||
|
||||
accepts = (type: string): boolean => {
|
||||
// If you don't tell us input types, we'll assume you don't care what you get.
|
||||
if (!this.inputTypes) return true;
|
||||
return this.inputTypes.indexOf(type) > -1;
|
||||
};
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 { ArgumentType } from './arguments';
|
||||
|
||||
export class ExpressionFunctionParameter {
|
||||
name: string;
|
||||
required: boolean;
|
||||
help: string;
|
||||
types: string[];
|
||||
default: any;
|
||||
aliases: string[];
|
||||
multi: boolean;
|
||||
resolve: boolean;
|
||||
options: any[];
|
||||
|
||||
constructor(name: string, arg: ArgumentType<any>) {
|
||||
const { required, help, types, aliases, multi, resolve, options } = arg;
|
||||
|
||||
if (name === '_') {
|
||||
throw Error('Arg names must not be _. Use it in aliases instead.');
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.required = !!required;
|
||||
this.help = help || '';
|
||||
this.types = types || [];
|
||||
this.default = arg.default;
|
||||
this.aliases = aliases || [];
|
||||
this.multi = !!multi;
|
||||
this.resolve = resolve == null ? true : resolve;
|
||||
this.options = options || [];
|
||||
}
|
||||
|
||||
accepts(type: string) {
|
||||
if (!this.types.length) return true;
|
||||
return this.types.indexOf(type) > -1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { ExpressionFunctionParameter } from './expression_function_parameter';
|
||||
|
||||
describe('ExpressionFunctionParameter', () => {
|
||||
test('can instantiate', () => {
|
||||
const param = new ExpressionFunctionParameter('foo', {
|
||||
help: 'bar',
|
||||
});
|
||||
|
||||
expect(param.name).toBe('foo');
|
||||
});
|
||||
|
||||
test('checks supported types', () => {
|
||||
const param = new ExpressionFunctionParameter('foo', {
|
||||
help: 'bar',
|
||||
types: ['baz', 'quux'],
|
||||
});
|
||||
|
||||
expect(param.accepts('baz')).toBe(true);
|
||||
expect(param.accepts('quux')).toBe(true);
|
||||
expect(param.accepts('quix')).toBe(false);
|
||||
});
|
||||
|
||||
test('if no types are provided, then accepts any type', () => {
|
||||
const param = new ExpressionFunctionParameter('foo', {
|
||||
help: 'bar',
|
||||
});
|
||||
|
||||
expect(param.accepts('baz')).toBe(true);
|
||||
expect(param.accepts('quux')).toBe(true);
|
||||
expect(param.accepts('quix')).toBe(true);
|
||||
});
|
||||
});
|
24
src/plugins/expressions/common/expression_functions/index.ts
Normal file
24
src/plugins/expressions/common/expression_functions/index.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './arguments';
|
||||
export * from './expression_function_parameter';
|
||||
export * from './expression_function';
|
||||
export * from './specs';
|
|
@ -17,19 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionFunction } from '../../common/types';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
|
||||
const name = 'clog';
|
||||
|
||||
type Context = any;
|
||||
type ClogExpressionFunction = ExpressionFunction<typeof name, Context, {}, Context>;
|
||||
|
||||
export const clog = (): ClogExpressionFunction => ({
|
||||
name,
|
||||
export const clog: ExpressionFunctionDefinition<'clog', unknown, {}, unknown> = {
|
||||
name: 'clog',
|
||||
args: {},
|
||||
help: 'Outputs the context to the console',
|
||||
fn: context => {
|
||||
console.log(context); // eslint-disable-line no-console
|
||||
return context;
|
||||
fn: (input: unknown) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(input);
|
||||
return input;
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,182 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { openSans, FontLabel as FontFamily } from '../../fonts';
|
||||
import { CSSStyle, FontStyle, FontWeight, Style, TextAlignment, TextDecoration } from '../../types';
|
||||
|
||||
const dashify = (str: string) => {
|
||||
return str
|
||||
.trim()
|
||||
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||
.replace(/\W/g, m => (/[À-ž]/.test(m) ? m : '-'))
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.toLowerCase();
|
||||
};
|
||||
|
||||
const inlineStyle = (obj: Record<string, string | number>) => {
|
||||
if (!obj) return '';
|
||||
const styles = Object.keys(obj).map(key => {
|
||||
const prop = dashify(key);
|
||||
const line = prop.concat(':').concat(String(obj[key]));
|
||||
return line;
|
||||
});
|
||||
return styles.join(';');
|
||||
};
|
||||
|
||||
interface Arguments {
|
||||
align?: TextAlignment;
|
||||
color?: string;
|
||||
family?: FontFamily;
|
||||
italic?: boolean;
|
||||
lHeight?: number | null;
|
||||
size?: number;
|
||||
underline?: boolean;
|
||||
weight?: FontWeight;
|
||||
}
|
||||
|
||||
export const font: ExpressionFunctionDefinition<'font', null, Arguments, Style> = {
|
||||
name: 'font',
|
||||
aliases: [],
|
||||
type: 'style',
|
||||
help: i18n.translate('expressions.functions.fontHelpText', {
|
||||
defaultMessage: 'Create a font style.',
|
||||
}),
|
||||
inputTypes: ['null'],
|
||||
args: {
|
||||
align: {
|
||||
default: 'left',
|
||||
help: i18n.translate('expressions.functions.font.args.alignHelpText', {
|
||||
defaultMessage: 'The horizontal text alignment.',
|
||||
}),
|
||||
options: Object.values(TextAlignment),
|
||||
types: ['string'],
|
||||
},
|
||||
color: {
|
||||
help: i18n.translate('expressions.functions.font.args.colorHelpText', {
|
||||
defaultMessage: 'The text color.',
|
||||
}),
|
||||
types: ['string'],
|
||||
},
|
||||
family: {
|
||||
default: `"${openSans.value}"`,
|
||||
help: i18n.translate('expressions.functions.font.args.familyHelpText', {
|
||||
defaultMessage: 'An acceptable {css} web font string',
|
||||
values: {
|
||||
css: 'CSS',
|
||||
},
|
||||
}),
|
||||
types: ['string'],
|
||||
},
|
||||
italic: {
|
||||
default: false,
|
||||
help: i18n.translate('expressions.functions.font.args.italicHelpText', {
|
||||
defaultMessage: 'Italicize the text?',
|
||||
}),
|
||||
options: [true, false],
|
||||
types: ['boolean'],
|
||||
},
|
||||
lHeight: {
|
||||
default: null,
|
||||
aliases: ['lineHeight'],
|
||||
help: i18n.translate('expressions.functions.font.args.lHeightHelpText', {
|
||||
defaultMessage: 'The line height in pixels',
|
||||
}),
|
||||
types: ['number', 'null'],
|
||||
},
|
||||
size: {
|
||||
default: 14,
|
||||
help: i18n.translate('expressions.functions.font.args.sizeHelpText', {
|
||||
defaultMessage: 'The font size in pixels',
|
||||
}),
|
||||
types: ['number'],
|
||||
},
|
||||
underline: {
|
||||
default: false,
|
||||
help: i18n.translate('expressions.functions.font.args.underlineHelpText', {
|
||||
defaultMessage: 'Underline the text?',
|
||||
}),
|
||||
options: [true, false],
|
||||
types: ['boolean'],
|
||||
},
|
||||
weight: {
|
||||
default: 'normal',
|
||||
help: i18n.translate('expressions.functions.font.args.weightHelpText', {
|
||||
defaultMessage: 'The font weight. For example, {list}, or {end}.',
|
||||
values: {
|
||||
list: Object.values(FontWeight)
|
||||
.slice(0, -1)
|
||||
.map(weight => `\`"${weight}"\``)
|
||||
.join(', '),
|
||||
end: `\`"${Object.values(FontWeight).slice(-1)[0]}"\``,
|
||||
},
|
||||
}),
|
||||
options: Object.values(FontWeight),
|
||||
types: ['string'],
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
if (!Object.values(FontWeight).includes(args.weight!)) {
|
||||
throw new Error(
|
||||
i18n.translate('expressions.functions.font.invalidFontWeightErrorMessage', {
|
||||
defaultMessage: "Invalid font weight: '{weight}'",
|
||||
values: {
|
||||
weight: args.weight,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
if (!Object.values(TextAlignment).includes(args.align!)) {
|
||||
throw new Error(
|
||||
i18n.translate('expressions.functions.font.invalidTextAlignmentErrorMessage', {
|
||||
defaultMessage: "Invalid text alignment: '{align}'",
|
||||
values: {
|
||||
align: args.align,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// the line height shouldn't ever be lower than the size, and apply as a
|
||||
// pixel setting
|
||||
const lineHeight = args.lHeight != null ? `${args.lHeight}px` : '1';
|
||||
|
||||
const spec: CSSStyle = {
|
||||
fontFamily: args.family,
|
||||
fontWeight: args.weight,
|
||||
fontStyle: args.italic ? FontStyle.ITALIC : FontStyle.NORMAL,
|
||||
textDecoration: args.underline ? TextDecoration.UNDERLINE : TextDecoration.NONE,
|
||||
textAlign: args.align,
|
||||
fontSize: `${args.size}px`, // apply font size as a pixel setting
|
||||
lineHeight, // apply line height as a pixel setting
|
||||
};
|
||||
|
||||
// conditionally apply styles based on input
|
||||
if (args.color) {
|
||||
spec.color = args.color;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'style',
|
||||
spec,
|
||||
css: inlineStyle(spec as Record<string, string | number>),
|
||||
};
|
||||
},
|
||||
};
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 { clog } from './clog';
|
||||
import { font } from './font';
|
||||
import { kibana } from './kibana';
|
||||
import { variableSet } from './var_set';
|
||||
import { variable } from './var';
|
||||
import { AnyExpressionFunctionDefinition } from '../types';
|
||||
|
||||
export const functionSpecs: AnyExpressionFunctionDefinition[] = [
|
||||
clog,
|
||||
font,
|
||||
kibana,
|
||||
variableSet,
|
||||
variable,
|
||||
];
|
||||
|
||||
export * from './clog';
|
||||
export * from './font';
|
||||
export * from './kibana';
|
||||
export * from './var_set';
|
||||
export * from './var';
|
|
@ -18,47 +18,43 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunction } from '../../common/types';
|
||||
import { KibanaContext } from '../../common/expression_types';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { ExpressionValueSearchContext } from '../../expression_types';
|
||||
|
||||
export type ExpressionFunctionKibana = ExpressionFunction<
|
||||
const toArray = <T>(query: undefined | T | T[]): T[] =>
|
||||
!query ? [] : Array.isArray(query) ? query : [query];
|
||||
|
||||
export type ExpressionFunctionKibana = ExpressionFunctionDefinition<
|
||||
'kibana',
|
||||
KibanaContext | null,
|
||||
// TODO: Get rid of the `null` type below.
|
||||
ExpressionValueSearchContext | null,
|
||||
object,
|
||||
KibanaContext
|
||||
ExpressionValueSearchContext
|
||||
>;
|
||||
|
||||
export const kibana = (): ExpressionFunctionKibana => ({
|
||||
export const kibana: ExpressionFunctionKibana = {
|
||||
name: 'kibana',
|
||||
type: 'kibana_context',
|
||||
|
||||
context: {
|
||||
types: ['kibana_context', 'null'],
|
||||
},
|
||||
inputTypes: ['kibana_context', 'null'],
|
||||
|
||||
help: i18n.translate('expressions.functions.kibana.help', {
|
||||
defaultMessage: 'Gets kibana global context',
|
||||
}),
|
||||
|
||||
args: {},
|
||||
fn(context, args, handlers) {
|
||||
const initialContext = handlers.getInitialContext ? handlers.getInitialContext() : {};
|
||||
|
||||
if (context && context.query) {
|
||||
initialContext.query = initialContext.query.concat(context.query);
|
||||
}
|
||||
|
||||
if (context && context.filters) {
|
||||
initialContext.filters = initialContext.filters.concat(context.filters);
|
||||
}
|
||||
|
||||
const timeRange = initialContext.timeRange || (context ? context.timeRange : undefined);
|
||||
|
||||
return {
|
||||
...context,
|
||||
fn(input, _, { search = {} }) {
|
||||
const output: ExpressionValueSearchContext = {
|
||||
// TODO: This spread is left here for legacy reasons, possibly Lens uses it.
|
||||
// TODO: But it shouldn't be need.
|
||||
...input,
|
||||
type: 'kibana_context',
|
||||
query: initialContext.query,
|
||||
filters: initialContext.filters,
|
||||
timeRange,
|
||||
query: [...toArray(search.query), ...toArray((input || {}).query)],
|
||||
filters: [...(search.filters || []), ...((input || {}).filters || [])],
|
||||
timeRange: search.timeRange || (input ? input.timeRange : undefined),
|
||||
};
|
||||
|
||||
return output;
|
||||
},
|
||||
});
|
||||
};
|
|
@ -14,10 +14,12 @@ Object {
|
|||
},
|
||||
},
|
||||
],
|
||||
"query": Object {
|
||||
"language": "lucene",
|
||||
"query": "geo.src:US",
|
||||
},
|
||||
"query": Array [
|
||||
Object {
|
||||
"language": "lucene",
|
||||
"query": "geo.src:US",
|
||||
},
|
||||
],
|
||||
"timeRange": Object {
|
||||
"from": "2",
|
||||
"to": "3",
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { openSans } from '../../../common/fonts';
|
||||
import { openSans } from '../../../fonts';
|
||||
import { font } from '../font';
|
||||
import { functionWrapper } from './utils';
|
||||
|
|
@ -19,18 +19,18 @@
|
|||
|
||||
import { functionWrapper } from './utils';
|
||||
import { kibana } from '../kibana';
|
||||
import { FunctionHandlers } from '../../../common/types';
|
||||
import { KibanaContext } from '../../../common/expression_types/kibana_context';
|
||||
import { ExecutionContext } from '../../../execution/types';
|
||||
import { KibanaContext, ExpressionValueSearchContext } from '../../../expression_types';
|
||||
|
||||
describe('interpreter/functions#kibana', () => {
|
||||
const fn = functionWrapper(kibana);
|
||||
let context: Partial<KibanaContext>;
|
||||
let initialContext: KibanaContext;
|
||||
let handlers: FunctionHandlers;
|
||||
let input: Partial<KibanaContext>;
|
||||
let search: ExpressionValueSearchContext;
|
||||
let context: ExecutionContext;
|
||||
|
||||
beforeEach(() => {
|
||||
context = { timeRange: { from: '0', to: '1' } };
|
||||
initialContext = {
|
||||
input = { timeRange: { from: '0', to: '1' } };
|
||||
search = {
|
||||
type: 'kibana_context',
|
||||
query: { language: 'lucene', query: 'geo.src:US' },
|
||||
filters: [
|
||||
|
@ -45,31 +45,29 @@ describe('interpreter/functions#kibana', () => {
|
|||
],
|
||||
timeRange: { from: '2', to: '3' },
|
||||
};
|
||||
handlers = {
|
||||
getInitialContext: () => initialContext,
|
||||
context = {
|
||||
search,
|
||||
getInitialInput: () => input,
|
||||
types: {},
|
||||
variables: {},
|
||||
abortSignal: {} as any,
|
||||
inspectorAdapters: {} as any,
|
||||
};
|
||||
});
|
||||
|
||||
it('returns an object with the correct structure', () => {
|
||||
const actual = fn(context, {}, handlers);
|
||||
const actual = fn(input, {}, context);
|
||||
expect(actual).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('uses timeRange from context if not provided in initialContext', () => {
|
||||
initialContext.timeRange = undefined;
|
||||
const actual = fn(context, {}, handlers);
|
||||
it('uses timeRange from input if not provided in search context', () => {
|
||||
search.timeRange = undefined;
|
||||
const actual = fn(input, {}, context);
|
||||
expect(actual.timeRange).toEqual({ from: '0', to: '1' });
|
||||
});
|
||||
|
||||
it.skip('combines query from context with initialContext', () => {
|
||||
context.query = { language: 'kuery', query: 'geo.dest:CN' };
|
||||
// TODO: currently this fails & likely requires a fix in run_pipeline
|
||||
const actual = fn(context, {}, handlers);
|
||||
expect(actual.query).toEqual('TBD');
|
||||
});
|
||||
|
||||
it('combines filters from context with initialContext', () => {
|
||||
context.filters = [
|
||||
it('combines filters from input with search context', () => {
|
||||
input.filters = [
|
||||
{
|
||||
meta: {
|
||||
disabled: true,
|
||||
|
@ -79,7 +77,7 @@ describe('interpreter/functions#kibana', () => {
|
|||
query: { match: {} },
|
||||
},
|
||||
];
|
||||
const actual = fn(context, {}, handlers);
|
||||
const actual = fn(input, {}, context);
|
||||
expect(actual.filters).toEqual([
|
||||
{
|
||||
meta: {
|
|
@ -18,16 +18,18 @@
|
|||
*/
|
||||
|
||||
import { mapValues } from 'lodash';
|
||||
import { AnyExpressionFunction, FunctionHandlers } from '../../../common/types';
|
||||
import { AnyExpressionFunctionDefinition } from '../../types';
|
||||
import { ExecutionContext } from '../../../execution/types';
|
||||
|
||||
// Takes a function spec and passes in default args,
|
||||
// overriding with any provided args.
|
||||
export const functionWrapper = <T extends AnyExpressionFunction>(fnSpec: () => T) => {
|
||||
const spec = fnSpec();
|
||||
/**
|
||||
* Takes a function spec and passes in default args,
|
||||
* overriding with any provided args.
|
||||
*/
|
||||
export const functionWrapper = (spec: AnyExpressionFunctionDefinition) => {
|
||||
const defaultArgs = mapValues(spec.args, argSpec => argSpec.default);
|
||||
return (
|
||||
context: object | null,
|
||||
args: Record<string, any> = {},
|
||||
handlers: FunctionHandlers = {}
|
||||
handlers: ExecutionContext = {} as ExecutionContext
|
||||
) => spec.fn(context, { ...defaultArgs, ...args }, handlers);
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { functionWrapper } from './utils';
|
||||
import { variable } from '../var';
|
||||
import { ExecutionContext } from '../../../execution/types';
|
||||
import { KibanaContext } from '../../../expression_types';
|
||||
|
||||
describe('expression_functions', () => {
|
||||
describe('var', () => {
|
||||
const fn = functionWrapper(variable);
|
||||
let input: Partial<KibanaContext>;
|
||||
let context: ExecutionContext;
|
||||
|
||||
beforeEach(() => {
|
||||
input = { timeRange: { from: '0', to: '1' } };
|
||||
context = {
|
||||
getInitialInput: () => input,
|
||||
types: {},
|
||||
variables: { test: 1 },
|
||||
abortSignal: {} as any,
|
||||
inspectorAdapters: {} as any,
|
||||
};
|
||||
});
|
||||
|
||||
it('returns the selected variable', () => {
|
||||
const actual = fn(input, { name: 'test' }, context);
|
||||
expect(actual).toEqual(1);
|
||||
});
|
||||
|
||||
it('returns undefined if variable does not exist', () => {
|
||||
const actual = fn(input, { name: 'unknown' }, context);
|
||||
expect(actual).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { functionWrapper } from './utils';
|
||||
import { variableSet } from '../var_set';
|
||||
import { ExecutionContext } from '../../../execution/types';
|
||||
import { KibanaContext } from '../../../expression_types';
|
||||
|
||||
describe('expression_functions', () => {
|
||||
describe('var_set', () => {
|
||||
const fn = functionWrapper(variableSet);
|
||||
let input: Partial<KibanaContext>;
|
||||
let context: ExecutionContext;
|
||||
let variables: Record<string, any>;
|
||||
|
||||
beforeEach(() => {
|
||||
input = { timeRange: { from: '0', to: '1' } };
|
||||
context = {
|
||||
getInitialInput: () => input,
|
||||
types: {},
|
||||
variables: { test: 1 },
|
||||
abortSignal: {} as any,
|
||||
inspectorAdapters: {} as any,
|
||||
};
|
||||
|
||||
variables = context.variables;
|
||||
});
|
||||
|
||||
it('updates a variable', () => {
|
||||
const actual = fn(input, { name: 'test', value: 2 }, context);
|
||||
expect(variables.test).toEqual(2);
|
||||
expect(actual).toEqual(input);
|
||||
});
|
||||
|
||||
it('sets a new variable', () => {
|
||||
const actual = fn(input, { name: 'new', value: 3 }, context);
|
||||
expect(variables.new).toEqual(3);
|
||||
expect(actual).toEqual(input);
|
||||
});
|
||||
|
||||
it('stores context if value is not set', () => {
|
||||
const actual = fn(input, { name: 'test' }, context);
|
||||
expect(variables.test).toEqual(input);
|
||||
expect(actual).toEqual(input);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -18,16 +18,15 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunction } from '../../common/types';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
|
||||
interface Arguments {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type Context = any;
|
||||
type ExpressionFunctionVar = ExpressionFunction<'var', Context, Arguments, any>;
|
||||
type ExpressionFunctionVar = ExpressionFunctionDefinition<'var', unknown, Arguments, unknown>;
|
||||
|
||||
export const variable = (): ExpressionFunctionVar => ({
|
||||
export const variable: ExpressionFunctionVar = {
|
||||
name: 'var',
|
||||
help: i18n.translate('expressions.functions.var.help', {
|
||||
defaultMessage: 'Updates kibana global context',
|
||||
|
@ -42,8 +41,8 @@ export const variable = (): ExpressionFunctionVar => ({
|
|||
}),
|
||||
},
|
||||
},
|
||||
fn(context, args, handlers) {
|
||||
const variables: Record<string, any> = handlers.variables;
|
||||
fn(input, args, context) {
|
||||
const variables: Record<string, any> = context.variables;
|
||||
return variables[args.name];
|
||||
},
|
||||
});
|
||||
};
|
|
@ -18,17 +18,14 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunction } from '../../common/types';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
|
||||
interface Arguments {
|
||||
name: string;
|
||||
value?: any;
|
||||
}
|
||||
|
||||
type Context = any;
|
||||
type ExpressionFunctionVarSet = ExpressionFunction<'var_set', Context, Arguments, Context>;
|
||||
|
||||
export const variableSet = (): ExpressionFunctionVarSet => ({
|
||||
export const variableSet: ExpressionFunctionDefinition<'var_set', unknown, Arguments, unknown> = {
|
||||
name: 'var_set',
|
||||
help: i18n.translate('expressions.functions.varset.help', {
|
||||
defaultMessage: 'Updates kibana global context',
|
||||
|
@ -50,9 +47,9 @@ export const variableSet = (): ExpressionFunctionVarSet => ({
|
|||
}),
|
||||
},
|
||||
},
|
||||
fn(context, args, handlers) {
|
||||
const variables: Record<string, any> = handlers.variables;
|
||||
variables[args.name] = args.value === undefined ? context : args.value;
|
||||
return context;
|
||||
fn(input, args, context) {
|
||||
const variables: Record<string, any> = context.variables;
|
||||
variables[args.name] = args.value === undefined ? input : args.value;
|
||||
return input;
|
||||
},
|
||||
});
|
||||
};
|
96
src/plugins/expressions/common/expression_functions/types.ts
Normal file
96
src/plugins/expressions/common/expression_functions/types.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { UnwrapPromiseOrReturn } from '@kbn/utility-types';
|
||||
import { ArgumentType } from './arguments';
|
||||
import { TypeToString } from '../types/common';
|
||||
import { ExecutionContext } from '../execution/types';
|
||||
|
||||
/**
|
||||
* `ExpressionFunctionDefinition` is the interface plugins have to implement to
|
||||
* register a function in `expressions` plugin.
|
||||
*/
|
||||
export interface ExpressionFunctionDefinition<
|
||||
Name extends string,
|
||||
Input,
|
||||
Arguments,
|
||||
Output,
|
||||
Context extends ExecutionContext = ExecutionContext
|
||||
> {
|
||||
/**
|
||||
* The name of the function, as will be used in expression.
|
||||
*/
|
||||
name: Name;
|
||||
|
||||
/**
|
||||
* Name of type of value this function outputs.
|
||||
*/
|
||||
type?: TypeToString<UnwrapPromiseOrReturn<Output>>;
|
||||
|
||||
/**
|
||||
* List of allowed type names for input value of this function. If this
|
||||
* property is set the input of function will be cast to the first possible
|
||||
* type in this list. If this property is missing the input will be provided
|
||||
* to the function as-is.
|
||||
*/
|
||||
inputTypes?: Array<TypeToString<Input>>;
|
||||
|
||||
/**
|
||||
* Specification of arguments that function supports. This list will also be
|
||||
* used for autocomplete functionality when your function is being edited.
|
||||
*/
|
||||
args: { [key in keyof Arguments]: ArgumentType<Arguments[key]> };
|
||||
|
||||
/**
|
||||
* @todo What is this?
|
||||
*/
|
||||
aliases?: string[];
|
||||
|
||||
/**
|
||||
* Help text displayed in the Expression editor. This text should be
|
||||
* internationalized.
|
||||
*/
|
||||
help: string;
|
||||
|
||||
/**
|
||||
* The actual implementation of the function.
|
||||
*
|
||||
* @param input Output of the previous function, or initial input.
|
||||
* @param args Parameters set for this function in expression.
|
||||
* @param context Object with functions to perform side effects. This object
|
||||
* is created for the duration of the execution of expression and is the
|
||||
* same for all functions in expression chain.
|
||||
*/
|
||||
fn(input: Input, args: Arguments, context: Context): Output;
|
||||
|
||||
/**
|
||||
* @deprecated Use `inputTypes` instead.
|
||||
*/
|
||||
context?: {
|
||||
/**
|
||||
* @deprecated This is alias for `inputTypes`, use `inputTypes` instead.
|
||||
*/
|
||||
types: AnyExpressionFunctionDefinition['inputTypes'];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Type to capture every possible expression function definition.
|
||||
*/
|
||||
export type AnyExpressionFunctionDefinition = ExpressionFunctionDefinition<any, any, any, any>;
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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 { ExpressionRenderDefinition } from './types';
|
||||
|
||||
export class ExpressionRenderer<Config = unknown> {
|
||||
public readonly name: string;
|
||||
public readonly displayName: string;
|
||||
public readonly help: string;
|
||||
public readonly validate: () => void | Error;
|
||||
public readonly reuseDomNode: boolean;
|
||||
public readonly render: ExpressionRenderDefinition<Config>['render'];
|
||||
|
||||
constructor(config: ExpressionRenderDefinition<Config>) {
|
||||
const { name, displayName, help, validate, reuseDomNode, render } = config;
|
||||
|
||||
this.name = name;
|
||||
this.displayName = displayName || name;
|
||||
this.help = help || '';
|
||||
this.validate = validate || (() => {});
|
||||
this.reuseDomNode = Boolean(reuseDomNode);
|
||||
this.render = render;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { IRegistry } from '../types';
|
||||
import { ExpressionRenderer } from './expression_renderer';
|
||||
import { AnyExpressionRenderDefinition } from './types';
|
||||
|
||||
export class ExpressionRendererRegistry implements IRegistry<ExpressionRenderer> {
|
||||
private readonly renderers: Map<string, ExpressionRenderer> = new Map<
|
||||
string,
|
||||
ExpressionRenderer
|
||||
>();
|
||||
|
||||
register(definition: AnyExpressionRenderDefinition | (() => AnyExpressionRenderDefinition)) {
|
||||
if (typeof definition === 'function') definition = definition();
|
||||
const renderer = new ExpressionRenderer(definition);
|
||||
this.renderers.set(renderer.name, renderer);
|
||||
}
|
||||
|
||||
public get(id: string): ExpressionRenderer | null {
|
||||
return this.renderers.get(id) || null;
|
||||
}
|
||||
|
||||
public toJS(): Record<string, ExpressionRenderer> {
|
||||
return this.toArray().reduce(
|
||||
(acc, renderer) => ({
|
||||
...acc,
|
||||
[renderer.name]: renderer,
|
||||
}),
|
||||
{} as Record<string, ExpressionRenderer>
|
||||
);
|
||||
}
|
||||
|
||||
public toArray(): ExpressionRenderer[] {
|
||||
return [...this.renderers.values()];
|
||||
}
|
||||
}
|
22
src/plugins/expressions/common/expression_renderers/index.ts
Normal file
22
src/plugins/expressions/common/expression_renderers/index.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './types';
|
||||
export * from './expression_renderer';
|
||||
export * from './expression_renderer_registry';
|
71
src/plugins/expressions/common/expression_renderers/types.ts
Normal file
71
src/plugins/expressions/common/expression_renderers/types.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface ExpressionRenderDefinition<Config = unknown> {
|
||||
/**
|
||||
* Technical name of the renderer, used as ID to identify renderer in
|
||||
* expression renderer registry. This must match the name of the expression
|
||||
* function that is used to create the `type: render` object.
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* A user friendly name of the renderer as will be displayed to user in UI.
|
||||
*/
|
||||
displayName: string;
|
||||
|
||||
/**
|
||||
* Help text as will be displayed to user. A sentence or few about what this
|
||||
* element does.
|
||||
*/
|
||||
help?: string;
|
||||
|
||||
/**
|
||||
* Used to validate the data before calling the render function.
|
||||
*/
|
||||
validate?: () => undefined | Error;
|
||||
|
||||
/**
|
||||
* Tell the renderer if the dom node should be reused, it's recreated each
|
||||
* time by default.
|
||||
*/
|
||||
reuseDomNode: boolean;
|
||||
|
||||
/**
|
||||
* The function called to render the output data of an expression.
|
||||
*/
|
||||
render: (
|
||||
domNode: HTMLElement,
|
||||
config: Config,
|
||||
handlers: IInterpreterRenderHandlers
|
||||
) => void | Promise<void>;
|
||||
}
|
||||
|
||||
export type AnyExpressionRenderDefinition = ExpressionRenderDefinition<any>;
|
||||
|
||||
export interface IInterpreterRenderHandlers {
|
||||
/**
|
||||
* Done increments the number of rendering successes
|
||||
*/
|
||||
done: () => void;
|
||||
onDestroy: (fn: () => void) => void;
|
||||
reload: () => void;
|
||||
update: (params: any) => void;
|
||||
event: (event: any) => void;
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
/*
|
||||
* 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 { ExpressionType } from './expression_type';
|
||||
import { ExpressionTypeDefinition } from './types';
|
||||
import { ExpressionValueRender } from './specs';
|
||||
|
||||
export const boolean: ExpressionTypeDefinition<'boolean', boolean> = {
|
||||
name: 'boolean',
|
||||
from: {
|
||||
null: () => false,
|
||||
number: n => Boolean(n),
|
||||
string: s => Boolean(s),
|
||||
},
|
||||
to: {
|
||||
render: (value): ExpressionValueRender<{ text: string }> => {
|
||||
const text = `${value}`;
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'text',
|
||||
value: { text },
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const render: ExpressionTypeDefinition<'render', ExpressionValueRender<unknown>> = {
|
||||
name: 'render',
|
||||
from: {
|
||||
'*': <T>(v: T): ExpressionValueRender<T> => ({
|
||||
type: name,
|
||||
as: 'debug',
|
||||
value: v,
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const emptyDatatableValue = {
|
||||
type: 'datatable',
|
||||
columns: [],
|
||||
rows: [],
|
||||
};
|
||||
|
||||
describe('ExpressionType', () => {
|
||||
test('can create a boolean type', () => {
|
||||
new ExpressionType(boolean);
|
||||
});
|
||||
|
||||
describe('castsFrom()', () => {
|
||||
describe('when "from" definition specifies "*" as one of its from types', () => {
|
||||
test('returns true for any value', () => {
|
||||
const type = new ExpressionType(render);
|
||||
expect(type.castsFrom(123)).toBe(true);
|
||||
expect(type.castsFrom('foo')).toBe(true);
|
||||
expect(type.castsFrom(true)).toBe(true);
|
||||
expect(
|
||||
type.castsFrom({
|
||||
type: 'datatable',
|
||||
columns: [],
|
||||
rows: [],
|
||||
})
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('castsTo()', () => {
|
||||
describe('when "to" definition is not specified', () => {
|
||||
test('returns false for any value', () => {
|
||||
const type = new ExpressionType(render);
|
||||
expect(type.castsTo(123)).toBe(false);
|
||||
expect(type.castsTo('foo')).toBe(false);
|
||||
expect(type.castsTo(true)).toBe(false);
|
||||
expect(type.castsTo(emptyDatatableValue)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('from()', () => {
|
||||
test('can cast from any type specified in definition', () => {
|
||||
const type = new ExpressionType(boolean);
|
||||
expect(type.from(1, {})).toBe(true);
|
||||
expect(type.from(0, {})).toBe(false);
|
||||
expect(type.from('foo', {})).toBe(true);
|
||||
expect(type.from('', {})).toBe(false);
|
||||
expect(type.from(null, {})).toBe(false);
|
||||
|
||||
// undefined is used like null in legacy interpreter
|
||||
expect(type.from(undefined, {})).toBe(false);
|
||||
});
|
||||
|
||||
test('throws when casting from type that is not supported', async () => {
|
||||
const type = new ExpressionType(boolean);
|
||||
expect(() => type.from(emptyDatatableValue, {})).toThrowError();
|
||||
expect(() => type.from(emptyDatatableValue, {})).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can not cast 'boolean' from datatable"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('to()', () => {
|
||||
test('can cast to type specified in definition', () => {
|
||||
const type = new ExpressionType(boolean);
|
||||
|
||||
expect(type.to(true, 'render', {})).toMatchObject({
|
||||
as: 'text',
|
||||
type: 'render',
|
||||
value: {
|
||||
text: 'true',
|
||||
},
|
||||
});
|
||||
expect(type.to(false, 'render', {})).toMatchObject({
|
||||
as: 'text',
|
||||
type: 'render',
|
||||
value: {
|
||||
text: 'false',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('throws when casting to type that is not supported', async () => {
|
||||
const type = new ExpressionType(boolean);
|
||||
expect(() => type.to(emptyDatatableValue, 'number', {})).toThrowError();
|
||||
expect(() => type.to(emptyDatatableValue, 'number', {})).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can not cast object of type 'datatable' using 'boolean'"`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -17,35 +17,10 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { get, identity } from 'lodash';
|
||||
import { AnyExpressionType, ExpressionValue } from './types';
|
||||
import { AnyExpressionTypeDefinition, ExpressionValue, ExpressionValueConverter } from './types';
|
||||
import { getType } from './get_type';
|
||||
|
||||
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) {
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
serialize: provider('serialize'),
|
||||
deserialize: provider('deserialize'),
|
||||
};
|
||||
}
|
||||
|
||||
export class Type {
|
||||
export class ExpressionType {
|
||||
name: string;
|
||||
|
||||
/**
|
||||
|
@ -66,41 +41,53 @@ export class Type {
|
|||
serialize?: (value: ExpressionValue) => any;
|
||||
deserialize?: (serialized: any) => ExpressionValue;
|
||||
|
||||
constructor(private readonly config: AnyExpressionType) {
|
||||
const { name, help, deserialize, serialize, validate } = config;
|
||||
constructor(private readonly definition: AnyExpressionTypeDefinition) {
|
||||
const { name, help, deserialize, serialize, validate } = definition;
|
||||
|
||||
this.name = name;
|
||||
this.help = help || '';
|
||||
this.validate = validate || (() => {});
|
||||
|
||||
// Optional
|
||||
this.create = (config as any).create;
|
||||
this.create = (definition as any).create;
|
||||
|
||||
this.serialize = serialize;
|
||||
this.deserialize = deserialize;
|
||||
}
|
||||
|
||||
getToFn = (value: any) => get(this.config, ['to', value]) || get(this.config, ['to', '*']);
|
||||
getFromFn = (value: any) => get(this.config, ['from', value]) || get(this.config, ['from', '*']);
|
||||
getToFn = (
|
||||
typeName: string
|
||||
): undefined | ExpressionValueConverter<ExpressionValue, ExpressionValue> =>
|
||||
!this.definition.to ? undefined : this.definition.to[typeName] || this.definition.to['*'];
|
||||
|
||||
castsTo = (value: any) => typeof this.getToFn(value) === 'function';
|
||||
castsFrom = (value: any) => typeof this.getFromFn(value) === 'function';
|
||||
getFromFn = (
|
||||
typeName: string
|
||||
): undefined | ExpressionValueConverter<ExpressionValue, ExpressionValue> =>
|
||||
!this.definition.from ? undefined : this.definition.from[typeName] || this.definition.from['*'];
|
||||
|
||||
castsTo = (value: ExpressionValue) => typeof this.getToFn(value) === 'function';
|
||||
|
||||
castsFrom = (value: ExpressionValue) => typeof this.getFromFn(value) === 'function';
|
||||
|
||||
to = (value: ExpressionValue, toTypeName: string, types: Record<string, ExpressionType>) => {
|
||||
const typeName = getType(value);
|
||||
|
||||
to = (node: any, toTypeName: any, types: any) => {
|
||||
const typeName = getType(node);
|
||||
if (typeName !== this.name) {
|
||||
throw new Error(`Can not cast object of type '${typeName}' using '${this.name}'`);
|
||||
} else if (!this.castsTo(toTypeName)) {
|
||||
throw new Error(`Can not cast '${typeName}' to '${toTypeName}'`);
|
||||
}
|
||||
|
||||
return (this.getToFn(toTypeName) as any)(node, types);
|
||||
return this.getToFn(toTypeName)!(value, types);
|
||||
};
|
||||
|
||||
from = (node: any, types: any) => {
|
||||
const typeName = getType(node);
|
||||
if (!this.castsFrom(typeName)) throw new Error(`Can not cast '${this.name}' from ${typeName}`);
|
||||
from = (value: ExpressionValue, types: Record<string, ExpressionType>) => {
|
||||
const typeName = getType(value);
|
||||
|
||||
return (this.getFromFn(typeName) as any)(node, types);
|
||||
if (!this.castsFrom(typeName)) {
|
||||
throw new Error(`Can not cast '${this.name}' from ${typeName}`);
|
||||
}
|
||||
|
||||
return this.getFromFn(typeName)!(value, types);
|
||||
};
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { getType } from './type';
|
||||
import { getType } from './get_type';
|
||||
|
||||
describe('getType()', () => {
|
||||
test('returns "null" string for null or undefined', () => {
|
|
@ -17,13 +17,11 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { Registry } from './registry';
|
||||
import { Type } from '../../common/type';
|
||||
import { AnyExpressionType } from '../../common/types';
|
||||
|
||||
export class TypesRegistry extends Registry<Type> {
|
||||
register(typeDefinition: AnyExpressionType | (() => AnyExpressionType)) {
|
||||
const type = new Type(typeof typeDefinition === 'object' ? typeDefinition : typeDefinition());
|
||||
this.set(type.name, type);
|
||||
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;
|
||||
}
|
|
@ -17,52 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { boolean } from './boolean';
|
||||
import { datatable } from './datatable';
|
||||
import { error } from './error';
|
||||
import { filter } from './filter';
|
||||
import { image } from './image';
|
||||
import { kibanaContext } from './kibana_context';
|
||||
import { kibanaDatatable } from './kibana_datatable';
|
||||
import { nullType } from './null';
|
||||
import { number } from './number';
|
||||
import { pointseries } from './pointseries';
|
||||
import { range } from './range';
|
||||
import { render } from './render';
|
||||
import { shape } from './shape';
|
||||
import { string } from './string';
|
||||
import { style } from './style';
|
||||
|
||||
export const typeSpecs = [
|
||||
boolean,
|
||||
datatable,
|
||||
error,
|
||||
filter,
|
||||
image,
|
||||
kibanaContext,
|
||||
kibanaDatatable,
|
||||
nullType,
|
||||
number,
|
||||
pointseries,
|
||||
range,
|
||||
render,
|
||||
shape,
|
||||
string,
|
||||
style,
|
||||
];
|
||||
|
||||
export * from './boolean';
|
||||
export * from './datatable';
|
||||
export * from './error';
|
||||
export * from './filter';
|
||||
export * from './image';
|
||||
export * from './kibana_context';
|
||||
export * from './kibana_datatable';
|
||||
export * from './null';
|
||||
export * from './number';
|
||||
export * from './pointseries';
|
||||
export * from './range';
|
||||
export * from './render';
|
||||
export * from './shape';
|
||||
export * from './string';
|
||||
export * from './style';
|
||||
export * from './types';
|
||||
export * from './get_type';
|
||||
export * from './serialize_provider';
|
||||
export * from './expression_type';
|
||||
export * from './specs';
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { ExpressionType } from './expression_type';
|
||||
import { ExpressionValue } from './types';
|
||||
import { getType } from './get_type';
|
||||
|
||||
const identity = <T>(x: T) => x;
|
||||
|
||||
export const serializeProvider = (types: Record<string, ExpressionType>) => ({
|
||||
serialize: (value: ExpressionValue) => (types[getType(value)].serialize || identity)(value),
|
||||
deserialize: (value: ExpressionValue) => (types[getType(value)].deserialize || identity)(value),
|
||||
});
|
|
@ -17,13 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'boolean';
|
||||
|
||||
export const boolean = (): ExpressionType<'boolean', boolean> => ({
|
||||
export const boolean: ExpressionTypeDefinition<'boolean', boolean> = {
|
||||
name,
|
||||
from: {
|
||||
null: () => false,
|
||||
|
@ -31,7 +31,7 @@ export const boolean = (): ExpressionType<'boolean', boolean> => ({
|
|||
string: s => Boolean(s),
|
||||
},
|
||||
to: {
|
||||
render: (value): Render<{ text: string }> => {
|
||||
render: (value): ExpressionValueRender<{ text: string }> => {
|
||||
const text = `${value}`;
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -45,4 +45,4 @@ export const boolean = (): ExpressionType<'boolean', boolean> => ({
|
|||
rows: [{ value }],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
|
@ -19,9 +19,9 @@
|
|||
|
||||
import { map, pick, zipObject } from 'lodash';
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { PointSeries } from './pointseries';
|
||||
import { Render } from './render';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'datatable';
|
||||
|
||||
|
@ -70,7 +70,7 @@ interface RenderedDatatable {
|
|||
showHeader: boolean;
|
||||
}
|
||||
|
||||
export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDatatable> => ({
|
||||
export const datatable: ExpressionTypeDefinition<typeof name, Datatable, SerializedDatatable> = {
|
||||
name,
|
||||
validate: table => {
|
||||
// TODO: Check columns types. Only string, boolean, number, date, allowed for now.
|
||||
|
@ -115,7 +115,7 @@ export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDa
|
|||
}),
|
||||
},
|
||||
to: {
|
||||
render: (table): Render<RenderedDatatable> => ({
|
||||
render: (table): ExpressionValueRender<RenderedDatatable> => ({
|
||||
type: 'render',
|
||||
as: 'table',
|
||||
value: {
|
||||
|
@ -143,4 +143,4 @@ export const datatable = (): ExpressionType<typeof name, Datatable, SerializedDa
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,20 +17,27 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { Render } from './render';
|
||||
import { ExpressionValueBoxed } from '../types/types';
|
||||
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
|
||||
import { ExpressionValueRender } from './render';
|
||||
import { getType } from '../get_type';
|
||||
|
||||
const name = 'error';
|
||||
|
||||
export type ExpressionValueError = ExpressionValueBoxed<
|
||||
'error',
|
||||
{
|
||||
error: unknown;
|
||||
error: {
|
||||
message: string;
|
||||
name?: string;
|
||||
stack?: string;
|
||||
};
|
||||
info: unknown;
|
||||
}
|
||||
>;
|
||||
|
||||
export const isExpressionValueError = (value: any): value is ExpressionValueError =>
|
||||
getType(value) === 'error';
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
|
@ -38,10 +45,10 @@ export type ExpressionValueError = ExpressionValueBoxed<
|
|||
*/
|
||||
export type InterpreterErrorType = ExpressionValueError;
|
||||
|
||||
export const error = (): ExpressionType<'error', ExpressionValueError> => ({
|
||||
export const error: ExpressionTypeDefinition<'error', ExpressionValueError> = {
|
||||
name,
|
||||
to: {
|
||||
render: (input): Render<Pick<InterpreterErrorType, 'error' | 'info'>> => {
|
||||
render: (input): ExpressionValueRender<Pick<InterpreterErrorType, 'error' | 'info'>> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: name,
|
||||
|
@ -52,4 +59,4 @@ export const error = (): ExpressionType<'error', ExpressionValueError> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
|
||||
const name = 'filter';
|
||||
|
||||
|
@ -34,7 +34,7 @@ export interface Filter {
|
|||
query?: string | null;
|
||||
}
|
||||
|
||||
export const filter = (): ExpressionType<typeof name, Filter> => ({
|
||||
export const filter: ExpressionTypeDefinition<typeof name, Filter> = {
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
|
@ -47,4 +47,4 @@ export const filter = (): ExpressionType<typeof name, Filter> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,8 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { Render } from './render';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'image';
|
||||
|
||||
|
@ -28,10 +28,10 @@ export interface ExpressionImage {
|
|||
dataurl: string;
|
||||
}
|
||||
|
||||
export const image = (): ExpressionType<typeof name, ExpressionImage> => ({
|
||||
export const image: ExpressionTypeDefinition<typeof name, ExpressionImage> = {
|
||||
name,
|
||||
to: {
|
||||
render: (input): Render<Pick<ExpressionImage, 'mode' | 'dataurl'>> => {
|
||||
render: (input): ExpressionValueRender<Pick<ExpressionImage, 'mode' | 'dataurl'>> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'image',
|
||||
|
@ -39,4 +39,4 @@ export const image = (): ExpressionType<typeof name, ExpressionImage> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* 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 { boolean } from './boolean';
|
||||
import { datatable } from './datatable';
|
||||
import { error } from './error';
|
||||
import { filter } from './filter';
|
||||
import { image } from './image';
|
||||
import { kibanaContext } from './kibana_context';
|
||||
import { kibanaDatatable } from './kibana_datatable';
|
||||
import { nullType } from './null';
|
||||
import { num } from './num';
|
||||
import { number } from './number';
|
||||
import { pointseries } from './pointseries';
|
||||
import { range } from './range';
|
||||
import { render } from './render';
|
||||
import { shape } from './shape';
|
||||
import { string } from './string';
|
||||
import { style } from './style';
|
||||
import { AnyExpressionTypeDefinition } from '../types';
|
||||
|
||||
export const typeSpecs: AnyExpressionTypeDefinition[] = [
|
||||
boolean,
|
||||
datatable,
|
||||
error,
|
||||
filter,
|
||||
image,
|
||||
kibanaContext,
|
||||
kibanaDatatable,
|
||||
nullType,
|
||||
num,
|
||||
number,
|
||||
pointseries,
|
||||
range,
|
||||
render,
|
||||
shape,
|
||||
string,
|
||||
style,
|
||||
];
|
||||
|
||||
export * from './boolean';
|
||||
export * from './datatable';
|
||||
export * from './error';
|
||||
export * from './filter';
|
||||
export * from './image';
|
||||
export * from './kibana_context';
|
||||
export * from './kibana_datatable';
|
||||
export * from './null';
|
||||
export * from './num';
|
||||
export * from './number';
|
||||
export * from './pointseries';
|
||||
export * from './range';
|
||||
export * from './render';
|
||||
export * from './shape';
|
||||
export * from './string';
|
||||
export * from './style';
|
|
@ -17,24 +17,24 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { TimeRange, Query, esFilters } from 'src/plugins/data/public';
|
||||
import { ExpressionValueBoxed } from '../types';
|
||||
import { ExecutionContextSearch } from '../../execution/types';
|
||||
|
||||
const name = 'kibana_context';
|
||||
export type ExpressionValueSearchContext = ExpressionValueBoxed<
|
||||
'kibana_context',
|
||||
ExecutionContextSearch
|
||||
>;
|
||||
|
||||
// TODO: These two are exported for legacy reasons - remove them eventually.
|
||||
export type KIBANA_CONTEXT_NAME = 'kibana_context';
|
||||
export type KibanaContext = ExpressionValueSearchContext;
|
||||
|
||||
export interface KibanaContext {
|
||||
type: typeof name;
|
||||
query?: Query | Query[];
|
||||
filters?: esFilters.Filter[];
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export const kibanaContext = () => ({
|
||||
name,
|
||||
export const kibanaContext = {
|
||||
name: 'kibana_context',
|
||||
from: {
|
||||
null: () => {
|
||||
return {
|
||||
type: name,
|
||||
type: 'kibana_context',
|
||||
};
|
||||
},
|
||||
},
|
||||
|
@ -45,4 +45,4 @@ export const kibanaContext = () => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -18,7 +18,7 @@
|
|||
*/
|
||||
|
||||
import { map } from 'lodash';
|
||||
import { SerializedFieldFormat } from '../types/common';
|
||||
import { SerializedFieldFormat } from '../../types/common';
|
||||
import { Datatable, PointSeries } from '.';
|
||||
|
||||
const name = 'kibana_datatable';
|
||||
|
@ -46,7 +46,7 @@ export interface KibanaDatatable {
|
|||
rows: KibanaDatatableRow[];
|
||||
}
|
||||
|
||||
export const kibanaDatatable = () => ({
|
||||
export const kibanaDatatable = {
|
||||
name,
|
||||
from: {
|
||||
datatable: (context: Datatable) => {
|
||||
|
@ -72,4 +72,4 @@ export const kibanaDatatable = () => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,13 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
|
||||
const name = 'null';
|
||||
|
||||
export const nullType = (): ExpressionType<typeof name, null> => ({
|
||||
export const nullType: ExpressionTypeDefinition<typeof name, null> = {
|
||||
name,
|
||||
from: {
|
||||
'*': () => null,
|
||||
},
|
||||
});
|
||||
};
|
80
src/plugins/expressions/common/expression_types/specs/num.ts
Normal file
80
src/plugins/expressions/common/expression_types/specs/num.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
export type ExpressionValueNum = ExpressionValueBoxed<
|
||||
'num',
|
||||
{
|
||||
value: number;
|
||||
}
|
||||
>;
|
||||
|
||||
export const num: ExpressionTypeDefinition<'num', ExpressionValueNum> = {
|
||||
name: 'num',
|
||||
from: {
|
||||
null: () => ({
|
||||
type: 'num',
|
||||
value: 0,
|
||||
}),
|
||||
boolean: b => ({
|
||||
type: 'num',
|
||||
value: Number(b),
|
||||
}),
|
||||
string: n => {
|
||||
const value = Number(n);
|
||||
if (Number.isNaN(value)) {
|
||||
throw new Error(
|
||||
i18n.translate('expressions.types.number.fromStringConversionErrorMessage', {
|
||||
defaultMessage: 'Can\'t typecast "{string}" string to number',
|
||||
values: {
|
||||
string: n,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return {
|
||||
type: 'num',
|
||||
value,
|
||||
};
|
||||
},
|
||||
'*': value => ({
|
||||
type: 'num',
|
||||
value: Number(value),
|
||||
}),
|
||||
},
|
||||
to: {
|
||||
render: ({ value }): ExpressionValueRender<{ text: string }> => {
|
||||
const text = `${value}`;
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'text',
|
||||
value: { text },
|
||||
};
|
||||
},
|
||||
datatable: ({ value }): Datatable => ({
|
||||
type: 'datatable',
|
||||
columns: [{ name: 'value', type: 'number' }],
|
||||
rows: [{ value }],
|
||||
}),
|
||||
},
|
||||
};
|
|
@ -18,13 +18,13 @@
|
|||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionType } from '../../common/types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'number';
|
||||
|
||||
export const number = (): ExpressionType<typeof name, number> => ({
|
||||
export const number: ExpressionTypeDefinition<typeof name, number> = {
|
||||
name,
|
||||
from: {
|
||||
null: () => 0,
|
||||
|
@ -45,7 +45,7 @@ export const number = (): ExpressionType<typeof name, number> => ({
|
|||
},
|
||||
},
|
||||
to: {
|
||||
render: (value: number): Render<{ text: string }> => {
|
||||
render: (value: number): ExpressionValueRender<{ text: string }> => {
|
||||
const text = `${value}`;
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -59,4 +59,4 @@ export const number = (): ExpressionType<typeof name, number> => ({
|
|||
rows: [{ value }],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,10 +17,9 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
import { ExpressionValueBoxed } from '../types/types';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'pointseries';
|
||||
|
||||
|
@ -56,7 +55,7 @@ export type PointSeries = ExpressionValueBoxed<
|
|||
}
|
||||
>;
|
||||
|
||||
export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({
|
||||
export const pointseries: ExpressionTypeDefinition<'pointseries', PointSeries> = {
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
|
@ -71,7 +70,7 @@ export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({
|
|||
render: (
|
||||
pseries: PointSeries,
|
||||
types
|
||||
): Render<{ datatable: Datatable; showHeader: boolean }> => {
|
||||
): ExpressionValueRender<{ datatable: Datatable; showHeader: boolean }> => {
|
||||
const datatable: Datatable = types.datatable.from(pseries, types);
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -83,4 +82,4 @@ export const pointseries = (): ExpressionType<'pointseries', PointSeries> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,8 +17,8 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { Render } from '.';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { ExpressionValueRender } from '.';
|
||||
|
||||
const name = 'range';
|
||||
|
||||
|
@ -28,7 +28,7 @@ export interface Range {
|
|||
to: number;
|
||||
}
|
||||
|
||||
export const range = (): ExpressionType<typeof name, Range> => ({
|
||||
export const range: ExpressionTypeDefinition<typeof name, Range> = {
|
||||
name,
|
||||
from: {
|
||||
null: (): Range => {
|
||||
|
@ -40,7 +40,7 @@ export const range = (): ExpressionType<typeof name, Range> => ({
|
|||
},
|
||||
},
|
||||
to: {
|
||||
render: (value: Range): Render<{ text: string }> => {
|
||||
render: (value: Range): ExpressionValueRender<{ text: string }> => {
|
||||
const text = `from ${value.from} to ${value.to}`;
|
||||
return {
|
||||
type: 'render',
|
||||
|
@ -49,4 +49,4 @@ export const range = (): ExpressionType<typeof name, Range> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,15 +17,14 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionValueBoxed } from '../types/types';
|
||||
import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types';
|
||||
|
||||
const name = 'render';
|
||||
|
||||
/**
|
||||
* Represents an object that is intended to be rendered.
|
||||
*/
|
||||
export type Render<T> = ExpressionValueBoxed<
|
||||
export type ExpressionValueRender<T> = ExpressionValueBoxed<
|
||||
typeof name,
|
||||
{
|
||||
as: string;
|
||||
|
@ -33,13 +32,20 @@ export type Render<T> = ExpressionValueBoxed<
|
|||
}
|
||||
>;
|
||||
|
||||
export const render = (): ExpressionType<typeof name, Render<unknown>> => ({
|
||||
/**
|
||||
* @deprecated
|
||||
*
|
||||
* Use `ExpressionValueRender` instead.
|
||||
*/
|
||||
export type Render<T> = ExpressionValueRender<T>;
|
||||
|
||||
export const render: ExpressionTypeDefinition<typeof name, ExpressionValueRender<unknown>> = {
|
||||
name,
|
||||
from: {
|
||||
'*': <T>(v: T): Render<T> => ({
|
||||
'*': <T>(v: T): ExpressionValueRender<T> => ({
|
||||
type: name,
|
||||
as: 'debug',
|
||||
value: v,
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,12 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { Render } from './render';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'shape';
|
||||
|
||||
export const shape = (): ExpressionType<typeof name, Render<any>> => ({
|
||||
export const shape: ExpressionTypeDefinition<typeof name, ExpressionValueRender<any>> = {
|
||||
name: 'shape',
|
||||
to: {
|
||||
render: input => {
|
||||
|
@ -33,4 +33,4 @@ export const shape = (): ExpressionType<typeof name, Render<any>> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,13 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType } from '../types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { Datatable } from './datatable';
|
||||
import { Render } from './render';
|
||||
import { ExpressionValueRender } from './render';
|
||||
|
||||
const name = 'string';
|
||||
|
||||
export const string = (): ExpressionType<typeof name, string> => ({
|
||||
export const string: ExpressionTypeDefinition<typeof name, string> = {
|
||||
name,
|
||||
from: {
|
||||
null: () => '',
|
||||
|
@ -31,7 +31,7 @@ export const string = (): ExpressionType<typeof name, string> => ({
|
|||
number: n => String(n),
|
||||
},
|
||||
to: {
|
||||
render: <T>(text: T): Render<{ text: T }> => {
|
||||
render: <T>(text: T): ExpressionValueRender<{ text: T }> => {
|
||||
return {
|
||||
type: 'render',
|
||||
as: 'text',
|
||||
|
@ -44,4 +44,4 @@ export const string = (): ExpressionType<typeof name, string> => ({
|
|||
rows: [{ value }],
|
||||
}),
|
||||
},
|
||||
});
|
||||
};
|
|
@ -17,11 +17,12 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { ExpressionType, ExpressionTypeStyle } from '../types';
|
||||
import { ExpressionTypeDefinition } from '../types';
|
||||
import { ExpressionTypeStyle } from '../../types/style';
|
||||
|
||||
const name = 'style';
|
||||
|
||||
export const style = (): ExpressionType<typeof name, ExpressionTypeStyle> => ({
|
||||
export const style: ExpressionTypeDefinition<typeof name, ExpressionTypeStyle> = {
|
||||
name,
|
||||
from: {
|
||||
null: () => {
|
||||
|
@ -32,4 +33,4 @@ export const style = (): ExpressionType<typeof name, ExpressionTypeStyle> => ({
|
|||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
|
@ -21,7 +21,7 @@ import { number } from '../number';
|
|||
|
||||
describe('number', () => {
|
||||
it('should fail when typecasting not numeric string to number', () => {
|
||||
expect(() => number().from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot(
|
||||
expect(() => number.from!.string('123test', {})).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Can't typecast \\"123test\\" string to number"`
|
||||
);
|
||||
});
|
|
@ -34,7 +34,7 @@ export type ExpressionValueConverter<I extends ExpressionValue, O extends Expres
|
|||
* A generic type which represents a custom Expression Type Definition that's
|
||||
* registered to the Interpreter.
|
||||
*/
|
||||
export interface ExpressionType<
|
||||
export interface ExpressionTypeDefinition<
|
||||
Name extends string,
|
||||
Value extends ExpressionValueUnboxed | ExpressionValueBoxed,
|
||||
SerializedType = undefined
|
||||
|
@ -54,4 +54,4 @@ export interface ExpressionType<
|
|||
help?: string;
|
||||
}
|
||||
|
||||
export type AnyExpressionType = ExpressionType<string, ExpressionValueBoxed>;
|
||||
export type AnyExpressionTypeDefinition = ExpressionTypeDefinition<any, any, any>;
|
|
@ -17,6 +17,13 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export * from './type';
|
||||
export * from './types';
|
||||
export * from './ast';
|
||||
export * from './fonts';
|
||||
export * from './expression_types';
|
||||
export * from './expression_functions';
|
||||
export * from './expression_renderers';
|
||||
export * from './executor';
|
||||
export * from './execution';
|
||||
export * from './service';
|
||||
export * from './util';
|
||||
|
|
47
src/plugins/expressions/common/mocks.ts
Normal file
47
src/plugins/expressions/common/mocks.ts
Normal file
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { ExecutionContext } from './execution/types';
|
||||
|
||||
export const createMockExecutionContext = <ExtraContext extends object = object>(
|
||||
extraContext: ExtraContext = {} as ExtraContext
|
||||
): ExecutionContext & ExtraContext => {
|
||||
const executionContext: ExecutionContext = {
|
||||
getInitialInput: jest.fn(),
|
||||
variables: {},
|
||||
types: {},
|
||||
abortSignal: {
|
||||
aborted: false,
|
||||
addEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
onabort: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
},
|
||||
inspectorAdapters: {
|
||||
requests: {} as any,
|
||||
data: {} as any,
|
||||
},
|
||||
search: {},
|
||||
};
|
||||
|
||||
return {
|
||||
...executionContext,
|
||||
...extraContext,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* 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 { ExpressionsService } from './expressions_services';
|
||||
|
||||
describe('ExpressionsService', () => {
|
||||
test('can instantiate', () => {
|
||||
new ExpressionsService();
|
||||
});
|
||||
|
||||
test('returns expected setup contract', () => {
|
||||
const expressions = new ExpressionsService();
|
||||
|
||||
expect(expressions.setup()).toMatchObject({
|
||||
getFunctions: expect.any(Function),
|
||||
registerFunction: expect.any(Function),
|
||||
registerType: expect.any(Function),
|
||||
registerRenderer: expect.any(Function),
|
||||
run: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('returns expected start contract', () => {
|
||||
const expressions = new ExpressionsService();
|
||||
expressions.setup();
|
||||
|
||||
expect(expressions.start()).toMatchObject({
|
||||
getFunctions: expect.any(Function),
|
||||
run: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('has pre-installed default functions', () => {
|
||||
const expressions = new ExpressionsService();
|
||||
|
||||
expect(typeof expressions.setup().getFunctions().var_set).toBe('object');
|
||||
});
|
||||
});
|
168
src/plugins/expressions/common/service/expressions_services.ts
Normal file
168
src/plugins/expressions/common/service/expressions_services.ts
Normal file
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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 { Executor } from '../executor';
|
||||
import { ExpressionRendererRegistry } from '../expression_renderers';
|
||||
import { ExpressionAstExpression } from '../ast';
|
||||
|
||||
export type ExpressionsServiceSetup = ReturnType<ExpressionsService['setup']>;
|
||||
export type ExpressionsServiceStart = ReturnType<ExpressionsService['start']>;
|
||||
|
||||
/**
|
||||
* `ExpressionsService` class is used for multiple purposes:
|
||||
*
|
||||
* 1. It implements the same Expressions service that can be used on both:
|
||||
* (1) server-side and (2) browser-side.
|
||||
* 2. It implements the same Expressions service that users can fork/clone,
|
||||
* thus have their own instance of the Expressions plugin.
|
||||
* 3. `ExpressionsService` defines the public contracts of *setup* and *start*
|
||||
* Kibana Platform life-cycles for ease-of-use on server-side and browser-side.
|
||||
* 4. `ExpressionsService` creates a bound version of all exported contract functions.
|
||||
* 5. Functions are bound the way there are:
|
||||
*
|
||||
* ```ts
|
||||
* registerFunction = (...args: Parameters<Executor['registerFunction']>
|
||||
* ): ReturnType<Executor['registerFunction']> => this.executor.registerFunction(...args);
|
||||
* ```
|
||||
*
|
||||
* so that JSDoc appears in developers IDE when they use those `plugins.expressions.registerFunction(`.
|
||||
*/
|
||||
export class ExpressionsService {
|
||||
public readonly executor = Executor.createWithDefaults();
|
||||
public readonly renderers = new ExpressionRendererRegistry();
|
||||
|
||||
/**
|
||||
* Register an expression function, which will be possible to execute as
|
||||
* part of the expression pipeline.
|
||||
*
|
||||
* Below we register a function which simply sleeps for given number of
|
||||
* milliseconds to delay the execution and outputs its input as-is.
|
||||
*
|
||||
* ```ts
|
||||
* expressions.registerFunction({
|
||||
* name: 'sleep',
|
||||
* args: {
|
||||
* time: {
|
||||
* aliases: ['_'],
|
||||
* help: 'Time in milliseconds for how long to sleep',
|
||||
* types: ['number'],
|
||||
* },
|
||||
* },
|
||||
* help: '',
|
||||
* fn: async (input, args, context) => {
|
||||
* await new Promise(r => setTimeout(r, args.time));
|
||||
* return input;
|
||||
* },
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* The actual function is defined in the `fn` key. The function can be *async*.
|
||||
* It receives three arguments: (1) `input` is the output of the previous function
|
||||
* or the initial input of the expression if the function is first in chain;
|
||||
* (2) `args` are function arguments as defined in expression string, that can
|
||||
* be edited by user (e.g in case of Canvas); (3) `context` is a shared object
|
||||
* passed to all functions that can be used for side-effects.
|
||||
*/
|
||||
public readonly registerFunction = (
|
||||
...args: Parameters<Executor['registerFunction']>
|
||||
): ReturnType<Executor['registerFunction']> => this.executor.registerFunction(...args);
|
||||
|
||||
/**
|
||||
* Executes expression string or a parsed expression AST and immediately
|
||||
* returns the result.
|
||||
*
|
||||
* Below example will execute `sleep 100 | clog` expression with `123` initial
|
||||
* input to the first function.
|
||||
*
|
||||
* ```ts
|
||||
* expressions.run('sleep 100 | clog', 123);
|
||||
* ```
|
||||
*
|
||||
* - `sleep 100` will delay execution by 100 milliseconds and pass the `123` input as
|
||||
* its output.
|
||||
* - `clog` will print to console `123` and pass it as its output.
|
||||
* - The final result of the execution will be `123`.
|
||||
*
|
||||
* Optionally, you can pass an object as the third argument which will be used
|
||||
* to extend the `ExecutionContext`—an object passed to each function
|
||||
* as the third argument, that allows functions to perform side-effects.
|
||||
*
|
||||
* ```ts
|
||||
* expressions.run('...', null, { elasticsearchClient });
|
||||
* ```
|
||||
*/
|
||||
public readonly run = <
|
||||
Input,
|
||||
Output,
|
||||
ExtraContext extends Record<string, unknown> = Record<string, unknown>
|
||||
>(
|
||||
ast: string | ExpressionAstExpression,
|
||||
input: Input,
|
||||
context?: ExtraContext
|
||||
): Promise<Output> => this.executor.run<Input, Output, ExtraContext>(ast, input, context);
|
||||
|
||||
public setup() {
|
||||
const { executor, renderers, registerFunction, run } = this;
|
||||
|
||||
const getFunction = executor.getFunction.bind(executor);
|
||||
const getFunctions = executor.getFunctions.bind(executor);
|
||||
const getRenderer = renderers.get.bind(renderers);
|
||||
const getRenderers = renderers.toJS.bind(renderers);
|
||||
const getType = executor.getType.bind(executor);
|
||||
const getTypes = executor.getTypes.bind(executor);
|
||||
const registerRenderer = renderers.register.bind(renderers);
|
||||
const registerType = executor.registerType.bind(executor);
|
||||
|
||||
return {
|
||||
getFunction,
|
||||
getFunctions,
|
||||
getRenderer,
|
||||
getRenderers,
|
||||
getType,
|
||||
getTypes,
|
||||
registerFunction,
|
||||
registerRenderer,
|
||||
registerType,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
public start() {
|
||||
const { executor, renderers, run } = this;
|
||||
|
||||
const getFunction = executor.getFunction.bind(executor);
|
||||
const getFunctions = executor.getFunctions.bind(executor);
|
||||
const getRenderer = renderers.get.bind(renderers);
|
||||
const getRenderers = renderers.toJS.bind(renderers);
|
||||
const getType = executor.getType.bind(executor);
|
||||
const getTypes = executor.getTypes.bind(executor);
|
||||
|
||||
return {
|
||||
getFunction,
|
||||
getFunctions,
|
||||
getRenderer,
|
||||
getRenderers,
|
||||
getType,
|
||||
getTypes,
|
||||
run,
|
||||
};
|
||||
}
|
||||
|
||||
public stop() {}
|
||||
}
|
20
src/plugins/expressions/common/service/index.ts
Normal file
20
src/plugins/expressions/common/service/index.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './expressions_services';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue