mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[expressions] decrease bundle size (#114229)
This commit is contained in:
parent
109e966a7a
commit
73a0fc0948
39 changed files with 877 additions and 675 deletions
|
@ -19,11 +19,7 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ExpressionsStart,
|
||||
ReactExpressionRenderer,
|
||||
ExpressionsInspectorAdapter,
|
||||
} from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionEditor } from './editor/expression_editor';
|
||||
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
||||
import { NAVIGATE_TRIGGER_ID } from './actions/navigate_trigger';
|
||||
|
@ -42,10 +38,6 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) {
|
|||
updateExpression(value);
|
||||
};
|
||||
|
||||
const inspectorAdapters = {
|
||||
expression: new ExpressionsInspectorAdapter(),
|
||||
};
|
||||
|
||||
const handleEvents = (event: any) => {
|
||||
if (event.name !== 'NAVIGATE') return;
|
||||
// enrich event context with some extra data
|
||||
|
@ -83,10 +75,9 @@ export function ActionsExpressionsExample({ expressions, actions }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel paddingSize="none" role="figure">
|
||||
<ReactExpressionRenderer
|
||||
<expressions.ReactExpressionRenderer
|
||||
expression={expression}
|
||||
debug={true}
|
||||
inspectorAdapters={inspectorAdapters}
|
||||
onEvent={handleEvents}
|
||||
renderError={(message: any) => {
|
||||
return <div>{message}</div>;
|
||||
|
|
|
@ -19,11 +19,7 @@ import {
|
|||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ExpressionsStart,
|
||||
ReactExpressionRenderer,
|
||||
ExpressionsInspectorAdapter,
|
||||
} from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionEditor } from './editor/expression_editor';
|
||||
import { UiActionsStart } from '../../../src/plugins/ui_actions/public';
|
||||
|
||||
|
@ -45,10 +41,6 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) {
|
|||
updateExpression(value);
|
||||
};
|
||||
|
||||
const inspectorAdapters = {
|
||||
expression: new ExpressionsInspectorAdapter(),
|
||||
};
|
||||
|
||||
const handleEvents = (event: any) => {
|
||||
updateVariables({ color: event.data.href === 'http://www.google.com' ? 'red' : 'blue' });
|
||||
};
|
||||
|
@ -81,11 +73,10 @@ export function ActionsExpressionsExample2({ expressions, actions }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel paddingSize="none" role="figure">
|
||||
<ReactExpressionRenderer
|
||||
<expressions.ReactExpressionRenderer
|
||||
data-test-subj="expressionsVariablesTestRenderer"
|
||||
expression={expression}
|
||||
debug={true}
|
||||
inspectorAdapters={inspectorAdapters}
|
||||
variables={variables}
|
||||
onEvent={handleEvents}
|
||||
renderError={(message: any) => {
|
||||
|
|
|
@ -20,11 +20,7 @@ import {
|
|||
EuiTitle,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ExpressionsStart,
|
||||
ReactExpressionRenderer,
|
||||
ExpressionsInspectorAdapter,
|
||||
} from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionEditor } from './editor/expression_editor';
|
||||
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
||||
|
||||
|
@ -42,9 +38,7 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) {
|
|||
updateExpression(value);
|
||||
};
|
||||
|
||||
const inspectorAdapters = {
|
||||
expression: new ExpressionsInspectorAdapter(),
|
||||
};
|
||||
const inspectorAdapters = {};
|
||||
|
||||
return (
|
||||
<EuiPageBody>
|
||||
|
@ -83,10 +77,12 @@ export function RenderExpressionsExample({ expressions, inspector }: Props) {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel data-test-subj="expressionRender" paddingSize="none" role="figure">
|
||||
<ReactExpressionRenderer
|
||||
<expressions.ReactExpressionRenderer
|
||||
expression={expression}
|
||||
debug={true}
|
||||
inspectorAdapters={inspectorAdapters}
|
||||
onData$={(result, panels) => {
|
||||
Object.assign(inspectorAdapters, panels);
|
||||
}}
|
||||
renderError={(message: any) => {
|
||||
return <div>{message}</div>;
|
||||
}}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { pluck } from 'rxjs/operators';
|
||||
import {
|
||||
EuiCodeBlock,
|
||||
|
@ -22,12 +22,9 @@ import {
|
|||
EuiTitle,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
import {
|
||||
ExpressionsStart,
|
||||
ExpressionsInspectorAdapter,
|
||||
} from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionsStart } from '../../../src/plugins/expressions/public';
|
||||
import { ExpressionEditor } from './editor/expression_editor';
|
||||
import { Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
||||
import { Adapters, Start as InspectorStart } from '../../../src/plugins/inspector/public';
|
||||
|
||||
interface Props {
|
||||
expressions: ExpressionsStart;
|
||||
|
@ -37,25 +34,24 @@ interface Props {
|
|||
export function RunExpressionsExample({ expressions, inspector }: Props) {
|
||||
const [expression, updateExpression] = useState('markdownVis "## expressions explorer"');
|
||||
const [result, updateResult] = useState<unknown>({});
|
||||
const [inspectorAdapters, updateInspectorAdapters] = useState<Adapters>({});
|
||||
|
||||
const expressionChanged = (value: string) => {
|
||||
updateExpression(value);
|
||||
};
|
||||
|
||||
const inspectorAdapters = useMemo(
|
||||
() => ({
|
||||
expression: new ExpressionsInspectorAdapter(),
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const execution = expressions.execute(expression, null, {
|
||||
debug: true,
|
||||
inspectorAdapters,
|
||||
});
|
||||
const subscription = execution.getData().pipe(pluck('result')).subscribe(updateResult);
|
||||
|
||||
const subscription = execution
|
||||
.getData()
|
||||
.pipe(pluck('result'))
|
||||
.subscribe((data) => {
|
||||
updateResult(data);
|
||||
updateInspectorAdapters(execution.inspect());
|
||||
});
|
||||
execution.inspect();
|
||||
return () => subscription.unsubscribe();
|
||||
}, [expression, expressions, inspectorAdapters]);
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ import {
|
|||
EsaggsExpressionFunctionDefinition,
|
||||
EsaggsStartDependencies,
|
||||
getEsaggsMeta,
|
||||
handleEsaggsRequest,
|
||||
} from '../../../common/search/expressions';
|
||||
import { DataPublicPluginStart, DataStartDependencies } from '../../types';
|
||||
|
||||
|
@ -48,10 +47,12 @@ export function getFunctionDefinition({
|
|||
);
|
||||
aggConfigs.hierarchical = args.metricsAtAllLevels;
|
||||
|
||||
return { aggConfigs, indexPattern, searchSource, getNow };
|
||||
const { handleEsaggsRequest } = await import('../../../common/search/expressions');
|
||||
|
||||
return { aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest };
|
||||
}).pipe(
|
||||
switchMap(({ aggConfigs, indexPattern, searchSource, getNow }) =>
|
||||
handleEsaggsRequest({
|
||||
switchMap(({ aggConfigs, indexPattern, searchSource, getNow, handleEsaggsRequest }) => {
|
||||
return handleEsaggsRequest({
|
||||
abortSignal,
|
||||
aggs: aggConfigs,
|
||||
filters: get(input, 'filters', undefined),
|
||||
|
@ -65,8 +66,8 @@ export function getFunctionDefinition({
|
|||
timeRange: get(input, 'timeRange', undefined),
|
||||
getNow,
|
||||
executionContext: getExecutionContext(),
|
||||
})
|
||||
)
|
||||
});
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import { catchError } from 'rxjs/operators';
|
|||
import { Execution, ExecutionResult } from './execution';
|
||||
import { ExpressionValueError } from '../expression_types/specs';
|
||||
import { ExpressionAstExpression } from '../ast';
|
||||
import { Adapters } from '../../../inspector/common/adapters';
|
||||
|
||||
/**
|
||||
* `ExecutionContract` is a wrapper around `Execution` class. It provides the
|
||||
|
@ -75,5 +76,5 @@ export class ExecutionContract<Input = unknown, Output = unknown, InspectorAdapt
|
|||
* Get Inspector adapters provided to all functions of expression through
|
||||
* execution context.
|
||||
*/
|
||||
inspect = () => this.execution.inspectorAdapters;
|
||||
inspect = (): Adapters => this.execution.inspectorAdapters;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import type { KibanaExecutionContext } from 'src/core/public';
|
|||
import { Datatable, ExpressionType } from '../expression_types';
|
||||
import { Adapters, RequestAdapter } from '../../../inspector/common';
|
||||
import { TablesAdapter } from '../util/tables_adapter';
|
||||
import { ExpressionsInspectorAdapter } from '../util';
|
||||
|
||||
/**
|
||||
* `ExecutionContext` is an object available to all functions during a single execution;
|
||||
|
@ -79,7 +80,8 @@ export interface ExecutionContext<
|
|||
/**
|
||||
* Default inspector adapters created if inspector adapters are not set explicitly.
|
||||
*/
|
||||
export interface DefaultInspectorAdapters extends Adapters {
|
||||
export interface DefaultInspectorAdapters {
|
||||
requests: RequestAdapter;
|
||||
tables: TablesAdapter;
|
||||
expression: ExpressionsInspectorAdapter;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
|
||||
export interface CumulativeSumArgs {
|
||||
by?: string[];
|
||||
|
@ -22,7 +21,7 @@ export type ExpressionFunctionCumulativeSum = ExpressionFunctionDefinition<
|
|||
'cumulative_sum',
|
||||
Datatable,
|
||||
CumulativeSumArgs,
|
||||
Datatable
|
||||
Promise<Datatable>
|
||||
>;
|
||||
|
||||
/**
|
||||
|
@ -94,37 +93,8 @@ export const cumulativeSum: ExpressionFunctionCumulativeSum = {
|
|||
},
|
||||
},
|
||||
|
||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
|
||||
const resultColumns = buildResultColumns(
|
||||
input,
|
||||
outputColumnId,
|
||||
inputColumnId,
|
||||
outputColumnName
|
||||
);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const accumulators: Partial<Record<string, number>> = {};
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const accumulatorValue = accumulators[bucketIdentifier] ?? 0;
|
||||
const currentValue = newRow[inputColumnId];
|
||||
if (currentValue != null) {
|
||||
newRow[outputColumnId] = Number(currentValue) + accumulatorValue;
|
||||
accumulators[bucketIdentifier] = newRow[outputColumnId];
|
||||
} else {
|
||||
newRow[outputColumnId] = accumulatorValue;
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
async fn(input, args) {
|
||||
const { cumulativeSumFn } = await import('./cumulative_sum_fn');
|
||||
return cumulativeSumFn(input, args);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
import { CumulativeSumArgs } from './cumulative_sum';
|
||||
|
||||
export const cumulativeSumFn = (
|
||||
input: Datatable,
|
||||
{ by, inputColumnId, outputColumnId, outputColumnName }: CumulativeSumArgs
|
||||
): Datatable => {
|
||||
const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const accumulators: Partial<Record<string, number>> = {};
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const accumulatorValue = accumulators[bucketIdentifier] ?? 0;
|
||||
const currentValue = newRow[inputColumnId];
|
||||
if (currentValue != null) {
|
||||
newRow[outputColumnId] = Number(currentValue) + accumulatorValue;
|
||||
accumulators[bucketIdentifier] = newRow[outputColumnId];
|
||||
} else {
|
||||
newRow[outputColumnId] = accumulatorValue;
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -9,7 +9,6 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
|
||||
export interface DerivativeArgs {
|
||||
by?: string[];
|
||||
|
@ -22,7 +21,7 @@ export type ExpressionFunctionDerivative = ExpressionFunctionDefinition<
|
|||
'derivative',
|
||||
Datatable,
|
||||
DerivativeArgs,
|
||||
Datatable
|
||||
Promise<Datatable>
|
||||
>;
|
||||
|
||||
/**
|
||||
|
@ -95,43 +94,8 @@ export const derivative: ExpressionFunctionDerivative = {
|
|||
},
|
||||
},
|
||||
|
||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName }) {
|
||||
const resultColumns = buildResultColumns(
|
||||
input,
|
||||
outputColumnId,
|
||||
inputColumnId,
|
||||
outputColumnName
|
||||
);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const previousValues: Partial<Record<string, number>> = {};
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const previousValue = previousValues[bucketIdentifier];
|
||||
const currentValue = newRow[inputColumnId];
|
||||
|
||||
if (currentValue != null && previousValue != null) {
|
||||
newRow[outputColumnId] = Number(currentValue) - previousValue;
|
||||
} else {
|
||||
newRow[outputColumnId] = undefined;
|
||||
}
|
||||
|
||||
if (currentValue != null) {
|
||||
previousValues[bucketIdentifier] = Number(currentValue);
|
||||
} else {
|
||||
previousValues[bucketIdentifier] = undefined;
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
async fn(input, args) {
|
||||
const { derivativeFn } = await import('./derivative_fn');
|
||||
return derivativeFn(input, args);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
import { DerivativeArgs } from './derivative';
|
||||
|
||||
/**
|
||||
* Calculates the derivative of a specified column in the data table.
|
||||
*
|
||||
* Also supports multiple series in a single data table - use the `by` argument
|
||||
* to specify the columns to split the calculation by.
|
||||
* For each unique combination of all `by` columns a separate derivative will be calculated.
|
||||
* The order of rows won't be changed - this function is not modifying any existing columns, it's only
|
||||
* adding the specified `outputColumnId` column to every row of the table without adding or removing rows.
|
||||
*
|
||||
* Behavior:
|
||||
* * Will write the derivative of `inputColumnId` into `outputColumnId`
|
||||
* * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
|
||||
* * Derivative always start with an undefined value for the first row of a series, a cell will contain its own value minus the
|
||||
* value of the previous cell of the same series.
|
||||
*
|
||||
* Edge cases:
|
||||
* * Will return the input table if `inputColumnId` does not exist
|
||||
* * Will throw an error if `outputColumnId` exists already in provided data table
|
||||
* * If there is no previous row of the current series with a non `null` or `undefined` value, the output cell of the current row
|
||||
* will be set to `undefined`.
|
||||
* * If the row value contains `null` or `undefined`, it will be ignored and the output cell will be set to `undefined`
|
||||
* * If the value of the previous row of the same series contains `null` or `undefined`, the output cell of the current row will be set to `undefined` as well
|
||||
* * For all values besides `null` and `undefined`, the value will be cast to a number before it's used in the
|
||||
* calculation of the current series even if this results in `NaN` (like in case of objects).
|
||||
* * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings
|
||||
* before comparison. If the values are objects, the return value of their `toString` method will be used for comparison.
|
||||
* Missing values (`null` and `undefined`) will be treated as empty strings.
|
||||
*/
|
||||
export const derivativeFn = (
|
||||
input: Datatable,
|
||||
{ by, inputColumnId, outputColumnId, outputColumnName }: DerivativeArgs
|
||||
): Datatable => {
|
||||
const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const previousValues: Partial<Record<string, number>> = {};
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const previousValue = previousValues[bucketIdentifier];
|
||||
const currentValue = newRow[inputColumnId];
|
||||
|
||||
if (currentValue != null && previousValue != null) {
|
||||
newRow[outputColumnId] = Number(currentValue) - previousValue;
|
||||
} else {
|
||||
newRow[outputColumnId] = undefined;
|
||||
}
|
||||
|
||||
if (currentValue != null) {
|
||||
previousValues[bucketIdentifier] = Number(currentValue);
|
||||
} else {
|
||||
previousValues[bucketIdentifier] = undefined;
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -6,11 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { map, zipObject, isString } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { evaluate } from '@kbn/tinymath';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable, isDatatable } from '../../expression_types';
|
||||
import { Datatable } from '../../expression_types';
|
||||
|
||||
export type MathArguments = {
|
||||
expression: string;
|
||||
|
@ -23,63 +21,11 @@ const TINYMATH = '`TinyMath`';
|
|||
const TINYMATH_URL =
|
||||
'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html';
|
||||
|
||||
function pivotObjectArray<
|
||||
RowType extends { [key: string]: unknown },
|
||||
ReturnColumns extends keyof RowType & string
|
||||
>(rows: RowType[], columns?: ReturnColumns[]) {
|
||||
const columnNames = columns || Object.keys(rows[0]);
|
||||
if (!columnNames.every(isString)) {
|
||||
throw new Error('Columns should be an array of strings');
|
||||
}
|
||||
|
||||
const columnValues = map(columnNames, (name) => map(rows, name));
|
||||
|
||||
return zipObject(columnNames, columnValues) as { [K in ReturnColumns]: Array<RowType[K]> };
|
||||
}
|
||||
|
||||
export const errors = {
|
||||
emptyExpression: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.emptyExpressionErrorMessage', {
|
||||
defaultMessage: 'Empty expression',
|
||||
})
|
||||
),
|
||||
tooManyResults: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.tooManyResultsErrorMessage', {
|
||||
defaultMessage:
|
||||
'Expressions must return a single number. Try wrapping your expression in {mean} or {sum}',
|
||||
values: {
|
||||
mean: 'mean()',
|
||||
sum: 'sum()',
|
||||
},
|
||||
})
|
||||
),
|
||||
executionFailed: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.executionFailedErrorMessage', {
|
||||
defaultMessage: 'Failed to execute math expression. Check your column names',
|
||||
})
|
||||
),
|
||||
emptyDatatable: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.emptyDatatableErrorMessage', {
|
||||
defaultMessage: 'Empty datatable',
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
const fallbackValue = {
|
||||
null: null,
|
||||
zero: 0,
|
||||
false: false,
|
||||
} as const;
|
||||
|
||||
export const math: ExpressionFunctionDefinition<
|
||||
'math',
|
||||
MathInput,
|
||||
MathArguments,
|
||||
boolean | number | null
|
||||
Promise<boolean | number | null>
|
||||
> = {
|
||||
name: 'math',
|
||||
type: undefined,
|
||||
|
@ -121,47 +67,8 @@ export const math: ExpressionFunctionDefinition<
|
|||
}),
|
||||
},
|
||||
},
|
||||
fn: (input, args) => {
|
||||
const { expression, onError } = args;
|
||||
const onErrorValue = onError ?? 'throw';
|
||||
|
||||
if (!expression || expression.trim() === '') {
|
||||
throw errors.emptyExpression();
|
||||
}
|
||||
|
||||
// Use unique ID if available, otherwise fall back to names
|
||||
const mathContext = isDatatable(input)
|
||||
? pivotObjectArray(
|
||||
input.rows,
|
||||
input.columns.map((col) => col.id)
|
||||
)
|
||||
: { value: input };
|
||||
|
||||
try {
|
||||
const result = evaluate(expression, mathContext);
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 1) {
|
||||
return result[0];
|
||||
}
|
||||
throw errors.tooManyResults();
|
||||
}
|
||||
if (isNaN(result)) {
|
||||
// make TS happy
|
||||
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
||||
return fallbackValue[onErrorValue];
|
||||
}
|
||||
throw errors.executionFailed();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
||||
return fallbackValue[onErrorValue];
|
||||
}
|
||||
if (isDatatable(input) && input.rows.length === 0) {
|
||||
throw errors.emptyDatatable();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
fn: async (input, args) => {
|
||||
const { mathFn } = await import('./math_fn');
|
||||
return mathFn(input, args);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -21,7 +21,7 @@ export const mathColumn: ExpressionFunctionDefinition<
|
|||
'mathColumn',
|
||||
Datatable,
|
||||
MathColumnArguments,
|
||||
Datatable
|
||||
Promise<Datatable>
|
||||
> = {
|
||||
name: 'mathColumn',
|
||||
type: 'datatable',
|
||||
|
@ -63,7 +63,7 @@ export const mathColumn: ExpressionFunctionDefinition<
|
|||
default: null,
|
||||
},
|
||||
},
|
||||
fn: (input, args, context) => {
|
||||
fn: async (input, args, context) => {
|
||||
const columns = [...input.columns];
|
||||
const existingColumnIndex = columns.findIndex(({ id }) => {
|
||||
return id === args.id;
|
||||
|
@ -76,34 +76,36 @@ export const mathColumn: ExpressionFunctionDefinition<
|
|||
);
|
||||
}
|
||||
|
||||
const newRows = input.rows.map((row) => {
|
||||
const result = math.fn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: input.columns,
|
||||
rows: [row],
|
||||
},
|
||||
{
|
||||
expression: args.expression,
|
||||
onError: args.onError,
|
||||
},
|
||||
context
|
||||
);
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 1) {
|
||||
return { ...row, [args.id]: result[0] };
|
||||
}
|
||||
throw new Error(
|
||||
i18n.translate('expressions.functions.mathColumn.arrayValueError', {
|
||||
defaultMessage: 'Cannot perform math on array values at {name}',
|
||||
values: { name: args.name },
|
||||
})
|
||||
const newRows = await Promise.all(
|
||||
input.rows.map(async (row) => {
|
||||
const result = await math.fn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: input.columns,
|
||||
rows: [row],
|
||||
},
|
||||
{
|
||||
expression: args.expression,
|
||||
onError: args.onError,
|
||||
},
|
||||
context
|
||||
);
|
||||
}
|
||||
|
||||
return { ...row, [args.id]: result };
|
||||
});
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 1) {
|
||||
return { ...row, [args.id]: result[0] };
|
||||
}
|
||||
throw new Error(
|
||||
i18n.translate('expressions.functions.mathColumn.arrayValueError', {
|
||||
defaultMessage: 'Cannot perform math on array values at {name}',
|
||||
values: { name: args.name },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return { ...row, [args.id]: result };
|
||||
})
|
||||
);
|
||||
let type: DatatableColumnType = 'null';
|
||||
if (newRows.length) {
|
||||
for (const row of newRows) {
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { map, zipObject, isString } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { evaluate } from '@kbn/tinymath';
|
||||
import { isDatatable } from '../../expression_types';
|
||||
import { MathArguments, MathInput } from './math';
|
||||
|
||||
function pivotObjectArray<
|
||||
RowType extends { [key: string]: unknown },
|
||||
ReturnColumns extends keyof RowType & string
|
||||
>(rows: RowType[], columns?: ReturnColumns[]) {
|
||||
const columnNames = columns || Object.keys(rows[0]);
|
||||
if (!columnNames.every(isString)) {
|
||||
throw new Error('Columns should be an array of strings');
|
||||
}
|
||||
|
||||
const columnValues = map(columnNames, (name) => map(rows, name));
|
||||
|
||||
return zipObject(columnNames, columnValues) as { [K in ReturnColumns]: Array<RowType[K]> };
|
||||
}
|
||||
|
||||
export const errors = {
|
||||
emptyExpression: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.emptyExpressionErrorMessage', {
|
||||
defaultMessage: 'Empty expression',
|
||||
})
|
||||
),
|
||||
tooManyResults: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.tooManyResultsErrorMessage', {
|
||||
defaultMessage:
|
||||
'Expressions must return a single number. Try wrapping your expression in {mean} or {sum}',
|
||||
values: {
|
||||
mean: 'mean()',
|
||||
sum: 'sum()',
|
||||
},
|
||||
})
|
||||
),
|
||||
executionFailed: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.executionFailedErrorMessage', {
|
||||
defaultMessage: 'Failed to execute math expression. Check your column names',
|
||||
})
|
||||
),
|
||||
emptyDatatable: () =>
|
||||
new Error(
|
||||
i18n.translate('expressions.functions.math.emptyDatatableErrorMessage', {
|
||||
defaultMessage: 'Empty datatable',
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
const fallbackValue = {
|
||||
null: null,
|
||||
zero: 0,
|
||||
false: false,
|
||||
} as const;
|
||||
|
||||
export const mathFn = (input: MathInput, args: MathArguments): boolean | number | null => {
|
||||
const { expression, onError } = args;
|
||||
const onErrorValue = onError ?? 'throw';
|
||||
|
||||
if (!expression || expression.trim() === '') {
|
||||
throw errors.emptyExpression();
|
||||
}
|
||||
|
||||
// Use unique ID if available, otherwise fall back to names
|
||||
const mathContext = isDatatable(input)
|
||||
? pivotObjectArray(
|
||||
input.rows,
|
||||
input.columns.map((col) => col.id)
|
||||
)
|
||||
: { value: input };
|
||||
|
||||
try {
|
||||
const result = evaluate(expression, mathContext);
|
||||
if (Array.isArray(result)) {
|
||||
if (result.length === 1) {
|
||||
return result[0];
|
||||
}
|
||||
throw errors.tooManyResults();
|
||||
}
|
||||
if (isNaN(result)) {
|
||||
// make TS happy
|
||||
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
||||
return fallbackValue[onErrorValue];
|
||||
}
|
||||
throw errors.executionFailed();
|
||||
}
|
||||
return result;
|
||||
} catch (e) {
|
||||
if (onErrorValue !== 'throw' && onErrorValue in fallbackValue) {
|
||||
return fallbackValue[onErrorValue];
|
||||
}
|
||||
if (isDatatable(input) && input.rows.length === 0) {
|
||||
throw errors.emptyDatatable();
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
|
@ -9,7 +9,6 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
|
||||
export interface MovingAverageArgs {
|
||||
by?: string[];
|
||||
|
@ -23,7 +22,7 @@ export type ExpressionFunctionMovingAverage = ExpressionFunctionDefinition<
|
|||
'moving_average',
|
||||
Datatable,
|
||||
MovingAverageArgs,
|
||||
Datatable
|
||||
Promise<Datatable>
|
||||
>;
|
||||
|
||||
/**
|
||||
|
@ -100,43 +99,8 @@ export const movingAverage: ExpressionFunctionMovingAverage = {
|
|||
},
|
||||
},
|
||||
|
||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName, window }) {
|
||||
const resultColumns = buildResultColumns(
|
||||
input,
|
||||
outputColumnId,
|
||||
inputColumnId,
|
||||
outputColumnName
|
||||
);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const lastNValuesByBucket: Partial<Record<string, number[]>> = {};
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const lastNValues = lastNValuesByBucket[bucketIdentifier];
|
||||
const currentValue = newRow[inputColumnId];
|
||||
if (lastNValues != null && currentValue != null) {
|
||||
const sum = lastNValues.reduce((acc, current) => acc + current, 0);
|
||||
newRow[outputColumnId] = sum / lastNValues.length;
|
||||
} else {
|
||||
newRow[outputColumnId] = undefined;
|
||||
}
|
||||
|
||||
if (currentValue != null) {
|
||||
lastNValuesByBucket[bucketIdentifier] = [
|
||||
...(lastNValues || []),
|
||||
Number(currentValue),
|
||||
].slice(-window);
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
async fn(input, args) {
|
||||
const { movingAverageFn } = await import('./moving_average_fn');
|
||||
return movingAverageFn(input, args);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
import { MovingAverageArgs } from './moving_average';
|
||||
|
||||
/**
|
||||
* Calculates the moving average of a specified column in the data table.
|
||||
*
|
||||
* Also supports multiple series in a single data table - use the `by` argument
|
||||
* to specify the columns to split the calculation by.
|
||||
* For each unique combination of all `by` columns a separate moving average will be calculated.
|
||||
* The order of rows won't be changed - this function is not modifying any existing columns, it's only
|
||||
* adding the specified `outputColumnId` column to every row of the table without adding or removing rows.
|
||||
*
|
||||
* Behavior:
|
||||
* * Will write the moving average of `inputColumnId` into `outputColumnId`
|
||||
* * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
|
||||
* * Moving average always starts with an undefined value for the first row of a series. Each next cell will contain sum of the last
|
||||
* * [window] of values divided by [window] excluding the current bucket.
|
||||
* If either of window edges moves outside the borders of data series, the window shrinks to include available values only.
|
||||
*
|
||||
* Edge cases:
|
||||
* * Will return the input table if `inputColumnId` does not exist
|
||||
* * Will throw an error if `outputColumnId` exists already in provided data table
|
||||
* * If null or undefined value is encountered, skip the current row and do not change the window
|
||||
* * For all values besides `null` and `undefined`, the value will be cast to a number before it's used in the
|
||||
* calculation of the current series even if this results in `NaN` (like in case of objects).
|
||||
* * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings
|
||||
* before comparison. If the values are objects, the return value of their `toString` method will be used for comparison.
|
||||
*/
|
||||
export const movingAverageFn = (
|
||||
input: Datatable,
|
||||
{ by, inputColumnId, outputColumnId, outputColumnName, window }: MovingAverageArgs
|
||||
): Datatable => {
|
||||
const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const lastNValuesByBucket: Partial<Record<string, number[]>> = {};
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const lastNValues = lastNValuesByBucket[bucketIdentifier];
|
||||
const currentValue = newRow[inputColumnId];
|
||||
if (lastNValues != null && currentValue != null) {
|
||||
const sum = lastNValues.reduce((acc, current) => acc + current, 0);
|
||||
newRow[outputColumnId] = sum / lastNValues.length;
|
||||
} else {
|
||||
newRow[outputColumnId] = undefined;
|
||||
}
|
||||
|
||||
if (currentValue != null) {
|
||||
lastNValuesByBucket[bucketIdentifier] = [
|
||||
...(lastNValues || []),
|
||||
Number(currentValue),
|
||||
].slice(-window);
|
||||
}
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -9,7 +9,6 @@
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { ExpressionFunctionDefinition } from '../types';
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
|
||||
export interface OverallMetricArgs {
|
||||
by?: string[];
|
||||
|
@ -23,17 +22,9 @@ export type ExpressionFunctionOverallMetric = ExpressionFunctionDefinition<
|
|||
'overall_metric',
|
||||
Datatable,
|
||||
OverallMetricArgs,
|
||||
Datatable
|
||||
Promise<Datatable>
|
||||
>;
|
||||
|
||||
function getValueAsNumberArray(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((innerVal) => Number(innerVal));
|
||||
} else {
|
||||
return [Number(value)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the overall metric of a specified column in the data table.
|
||||
*
|
||||
|
@ -109,73 +100,8 @@ export const overallMetric: ExpressionFunctionOverallMetric = {
|
|||
},
|
||||
},
|
||||
|
||||
fn(input, { by, inputColumnId, outputColumnId, outputColumnName, metric }) {
|
||||
const resultColumns = buildResultColumns(
|
||||
input,
|
||||
outputColumnId,
|
||||
inputColumnId,
|
||||
outputColumnName
|
||||
);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const accumulators: Partial<Record<string, number>> = {};
|
||||
const valueCounter: Partial<Record<string, number>> = {};
|
||||
input.rows.forEach((row) => {
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const accumulatorValue = accumulators[bucketIdentifier];
|
||||
|
||||
const currentValue = row[inputColumnId];
|
||||
if (currentValue != null) {
|
||||
const currentNumberValues = getValueAsNumberArray(currentValue);
|
||||
switch (metric) {
|
||||
case 'average':
|
||||
valueCounter[bucketIdentifier] =
|
||||
(valueCounter[bucketIdentifier] ?? 0) + currentNumberValues.length;
|
||||
case 'sum':
|
||||
accumulators[bucketIdentifier] = currentNumberValues.reduce(
|
||||
(a, b) => a + b,
|
||||
accumulatorValue || 0
|
||||
);
|
||||
break;
|
||||
case 'min':
|
||||
if (typeof accumulatorValue !== 'undefined') {
|
||||
accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues);
|
||||
} else {
|
||||
accumulators[bucketIdentifier] = Math.min(...currentNumberValues);
|
||||
}
|
||||
break;
|
||||
case 'max':
|
||||
if (typeof accumulatorValue !== 'undefined') {
|
||||
accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues);
|
||||
} else {
|
||||
accumulators[bucketIdentifier] = Math.max(...currentNumberValues);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (metric === 'average') {
|
||||
Object.keys(accumulators).forEach((bucketIdentifier) => {
|
||||
const accumulatorValue = accumulators[bucketIdentifier];
|
||||
const valueCount = valueCounter[bucketIdentifier];
|
||||
if (typeof accumulatorValue !== 'undefined' && typeof valueCount !== 'undefined') {
|
||||
accumulators[bucketIdentifier] = accumulatorValue / valueCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
newRow[outputColumnId] = accumulators[bucketIdentifier];
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
async fn(input, args) {
|
||||
const { overallMetricFn } = await import('./overall_metric_fn');
|
||||
return overallMetricFn(input, args);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { Datatable } from '../../expression_types';
|
||||
import { buildResultColumns, getBucketIdentifier } from '../series_calculation_helpers';
|
||||
import { OverallMetricArgs } from './overall_metric';
|
||||
|
||||
function getValueAsNumberArray(value: unknown) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((innerVal) => Number(innerVal));
|
||||
} else {
|
||||
return [Number(value)];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the overall metric of a specified column in the data table.
|
||||
*
|
||||
* Also supports multiple series in a single data table - use the `by` argument
|
||||
* to specify the columns to split the calculation by.
|
||||
* For each unique combination of all `by` columns a separate overall metric will be calculated.
|
||||
* The order of rows won't be changed - this function is not modifying any existing columns, it's only
|
||||
* adding the specified `outputColumnId` column to every row of the table without adding or removing rows.
|
||||
*
|
||||
* Behavior:
|
||||
* * Will write the overall metric of `inputColumnId` into `outputColumnId`
|
||||
* * If provided will use `outputColumnName` as name for the newly created column. Otherwise falls back to `outputColumnId`
|
||||
* * Each cell will contain the calculated metric based on the values of all cells belonging to the current series.
|
||||
*
|
||||
* Edge cases:
|
||||
* * Will return the input table if `inputColumnId` does not exist
|
||||
* * Will throw an error if `outputColumnId` exists already in provided data table
|
||||
* * If the row value contains `null` or `undefined`, it will be ignored and overwritten with the overall metric of
|
||||
* all cells of the same series.
|
||||
* * For all values besides `null` and `undefined`, the value will be cast to a number before it's added to the
|
||||
* overall metric of the current series - if this results in `NaN` (like in case of objects), all cells of the
|
||||
* current series will be set to `NaN`.
|
||||
* * To determine separate series defined by the `by` columns, the values of these columns will be cast to strings
|
||||
* before comparison. If the values are objects, the return value of their `toString` method will be used for comparison.
|
||||
* Missing values (`null` and `undefined`) will be treated as empty strings.
|
||||
*/
|
||||
export const overallMetricFn = (
|
||||
input: Datatable,
|
||||
{ by, inputColumnId, outputColumnId, outputColumnName, metric }: OverallMetricArgs
|
||||
): Datatable => {
|
||||
const resultColumns = buildResultColumns(input, outputColumnId, inputColumnId, outputColumnName);
|
||||
|
||||
if (!resultColumns) {
|
||||
return input;
|
||||
}
|
||||
|
||||
const accumulators: Partial<Record<string, number>> = {};
|
||||
const valueCounter: Partial<Record<string, number>> = {};
|
||||
input.rows.forEach((row) => {
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
const accumulatorValue = accumulators[bucketIdentifier];
|
||||
|
||||
const currentValue = row[inputColumnId];
|
||||
if (currentValue != null) {
|
||||
const currentNumberValues = getValueAsNumberArray(currentValue);
|
||||
switch (metric) {
|
||||
case 'average':
|
||||
valueCounter[bucketIdentifier] =
|
||||
(valueCounter[bucketIdentifier] ?? 0) + currentNumberValues.length;
|
||||
case 'sum':
|
||||
accumulators[bucketIdentifier] = currentNumberValues.reduce(
|
||||
(a, b) => a + b,
|
||||
accumulatorValue || 0
|
||||
);
|
||||
break;
|
||||
case 'min':
|
||||
if (typeof accumulatorValue !== 'undefined') {
|
||||
accumulators[bucketIdentifier] = Math.min(accumulatorValue, ...currentNumberValues);
|
||||
} else {
|
||||
accumulators[bucketIdentifier] = Math.min(...currentNumberValues);
|
||||
}
|
||||
break;
|
||||
case 'max':
|
||||
if (typeof accumulatorValue !== 'undefined') {
|
||||
accumulators[bucketIdentifier] = Math.max(accumulatorValue, ...currentNumberValues);
|
||||
} else {
|
||||
accumulators[bucketIdentifier] = Math.max(...currentNumberValues);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (metric === 'average') {
|
||||
Object.keys(accumulators).forEach((bucketIdentifier) => {
|
||||
const accumulatorValue = accumulators[bucketIdentifier];
|
||||
const valueCount = valueCounter[bucketIdentifier];
|
||||
if (typeof accumulatorValue !== 'undefined' && typeof valueCount !== 'undefined') {
|
||||
accumulators[bucketIdentifier] = accumulatorValue / valueCount;
|
||||
}
|
||||
});
|
||||
}
|
||||
return {
|
||||
...input,
|
||||
columns: resultColumns,
|
||||
rows: input.rows.map((row) => {
|
||||
const newRow = { ...row };
|
||||
const bucketIdentifier = getBucketIdentifier(row, by);
|
||||
newRow[outputColumnId] = accumulators[bucketIdentifier];
|
||||
|
||||
return newRow;
|
||||
}),
|
||||
};
|
||||
};
|
|
@ -14,10 +14,10 @@ import { Datatable } from '../../../expression_types/specs/datatable';
|
|||
describe('interpreter/functions#cumulative_sum', () => {
|
||||
const fn = functionWrapper(cumulativeSum);
|
||||
const runFn = (input: Datatable, args: CumulativeSumArgs) =>
|
||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
||||
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||
|
||||
it('calculates cumulative sum', () => {
|
||||
const result = runFn(
|
||||
it('calculates cumulative sum', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -33,8 +33,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]);
|
||||
});
|
||||
|
||||
it('replaces null or undefined data with zeroes until there is real data', () => {
|
||||
const result = runFn(
|
||||
it('replaces null or undefined data with zeroes until there is real data', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -50,8 +50,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([0, 0, 0, 1]);
|
||||
});
|
||||
|
||||
it('calculates cumulative sum for multiple series', () => {
|
||||
const result = runFn(
|
||||
it('calculates cumulative sum for multiple series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -84,8 +84,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats missing split column as separate series', () => {
|
||||
const result = runFn(
|
||||
it('treats missing split column as separate series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -117,8 +117,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats null like undefined and empty string for split columns', () => {
|
||||
const result = runFn(
|
||||
it('treats null like undefined and empty string for split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -152,8 +152,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('calculates cumulative sum for multiple series by multiple split columns', () => {
|
||||
const result = runFn(
|
||||
it('calculates cumulative sum for multiple series by multiple split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -177,8 +177,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1 + 4, 5, 6, 7, 7 + 8]);
|
||||
});
|
||||
|
||||
it('splits separate series by the string representation of the cell values', () => {
|
||||
const result = runFn(
|
||||
it('splits separate series by the string representation of the cell values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -198,8 +198,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([1, 1 + 2, 10, 21]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating cumulative sum', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating cumulative sum', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -210,8 +210,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([5, 12, 15, 17]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating cumulative sum for NaN like values', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating cumulative sum for NaN like values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -222,8 +222,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([5, 12, NaN, NaN]);
|
||||
});
|
||||
|
||||
it('skips undefined and null values', () => {
|
||||
const result = runFn(
|
||||
it('skips undefined and null values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -244,8 +244,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([0, 7, 7, 7, 7, 7, 10, 12, 12]);
|
||||
});
|
||||
|
||||
it('copies over meta information from the source column', () => {
|
||||
const result = runFn(
|
||||
it('copies over meta information from the source column', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -286,8 +286,8 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets output name on output column if specified', () => {
|
||||
const result = runFn(
|
||||
it('sets output name on output column if specified', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -310,7 +310,7 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('returns source table if input column does not exist', () => {
|
||||
it('returns source table if input column does not exist', async () => {
|
||||
const input: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -324,11 +324,13 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
],
|
||||
rows: [{ val: 5 }],
|
||||
};
|
||||
expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input);
|
||||
expect(await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(
|
||||
input
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error if output column exists already', () => {
|
||||
expect(() =>
|
||||
it('throws an error if output column exists already', async () => {
|
||||
await expect(
|
||||
runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
|
@ -345,6 +347,6 @@ describe('interpreter/functions#cumulative_sum', () => {
|
|||
},
|
||||
{ inputColumnId: 'val', outputColumnId: 'val' }
|
||||
)
|
||||
).toThrow();
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,10 +14,10 @@ import { Datatable } from '../../../expression_types/specs/datatable';
|
|||
describe('interpreter/functions#derivative', () => {
|
||||
const fn = functionWrapper(derivative);
|
||||
const runFn = (input: Datatable, args: DerivativeArgs) =>
|
||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
||||
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||
|
||||
it('calculates derivative', () => {
|
||||
const result = runFn(
|
||||
it('calculates derivative', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -33,8 +33,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 2, -4, -1]);
|
||||
});
|
||||
|
||||
it('skips null or undefined values until there is real data', () => {
|
||||
const result = runFn(
|
||||
it('skips null or undefined values until there is real data', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -70,8 +70,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats 0 as real data', () => {
|
||||
const result = runFn(
|
||||
it('treats 0 as real data', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -108,8 +108,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('calculates derivative for multiple series', () => {
|
||||
const result = runFn(
|
||||
it('calculates derivative for multiple series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -142,8 +142,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats missing split column as separate series', () => {
|
||||
const result = runFn(
|
||||
it('treats missing split column as separate series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -175,8 +175,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats null like undefined and empty string for split columns', () => {
|
||||
const result = runFn(
|
||||
it('treats null like undefined and empty string for split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -210,8 +210,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('calculates derivative for multiple series by multiple split columns', () => {
|
||||
const result = runFn(
|
||||
it('calculates derivative for multiple series by multiple split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -244,8 +244,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('splits separate series by the string representation of the cell values', () => {
|
||||
const result = runFn(
|
||||
it('splits separate series by the string representation of the cell values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -265,8 +265,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 2 - 1, undefined, 11 - 10]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating derivative', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating derivative', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -277,8 +277,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, 3 - 7, 2 - 3]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating derivative for NaN like values', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating derivative for NaN like values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -289,8 +289,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 7 - 5, NaN, NaN, 5 - 2]);
|
||||
});
|
||||
|
||||
it('copies over meta information from the source column', () => {
|
||||
const result = runFn(
|
||||
it('copies over meta information from the source column', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -331,8 +331,8 @@ describe('interpreter/functions#derivative', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets output name on output column if specified', () => {
|
||||
const result = runFn(
|
||||
it('sets output name on output column if specified', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -355,7 +355,7 @@ describe('interpreter/functions#derivative', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('returns source table if input column does not exist', () => {
|
||||
it('returns source table if input column does not exist', async () => {
|
||||
const input: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -369,11 +369,13 @@ describe('interpreter/functions#derivative', () => {
|
|||
],
|
||||
rows: [{ val: 5 }],
|
||||
};
|
||||
expect(runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(input);
|
||||
expect(await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output' })).toBe(
|
||||
input
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error if output column exists already', () => {
|
||||
expect(() =>
|
||||
it('throws an error if output column exists already', async () => {
|
||||
await expect(
|
||||
runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
|
@ -390,6 +392,6 @@ describe('interpreter/functions#derivative', () => {
|
|||
},
|
||||
{ inputColumnId: 'val', outputColumnId: 'val' }
|
||||
)
|
||||
).toThrow();
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,129 +6,163 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { errors, math, MathArguments, MathInput } from '../math';
|
||||
import { math, MathArguments, MathInput } from '../math';
|
||||
import { errors } from '../math_fn';
|
||||
import { emptyTable, functionWrapper, testTable } from './utils';
|
||||
|
||||
describe('math', () => {
|
||||
const fn = functionWrapper(math);
|
||||
|
||||
it('evaluates math expressions without reference to context', () => {
|
||||
expect(fn(null as unknown as MathInput, { expression: '10.5345' })).toBe(10.5345);
|
||||
expect(fn(null as unknown as MathInput, { expression: '123 + 456' })).toBe(579);
|
||||
expect(fn(null as unknown as MathInput, { expression: '100 - 46' })).toBe(54);
|
||||
expect(fn(1, { expression: '100 / 5' })).toBe(20);
|
||||
expect(fn('foo' as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
||||
expect(fn(true as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
||||
expect(fn(testTable, { expression: '100 * 5' })).toBe(500);
|
||||
expect(fn(emptyTable, { expression: '100 * 5' })).toBe(500);
|
||||
it('evaluates math expressions without reference to context', async () => {
|
||||
expect(await fn(null as unknown as MathInput, { expression: '10.5345' })).toBe(10.5345);
|
||||
expect(await fn(null as unknown as MathInput, { expression: '123 + 456' })).toBe(579);
|
||||
expect(await fn(null as unknown as MathInput, { expression: '100 - 46' })).toBe(54);
|
||||
expect(await fn(1, { expression: '100 / 5' })).toBe(20);
|
||||
expect(await fn('foo' as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
||||
expect(await fn(true as unknown as MathInput, { expression: '100 / 5' })).toBe(20);
|
||||
expect(await fn(testTable, { expression: '100 * 5' })).toBe(500);
|
||||
expect(await fn(emptyTable, { expression: '100 * 5' })).toBe(500);
|
||||
});
|
||||
|
||||
it('evaluates math expressions with reference to the value of the context, must be a number', () => {
|
||||
expect(fn(-103, { expression: 'abs(value)' })).toBe(103);
|
||||
it('evaluates math expressions with reference to the value of the context, must be a number', async () => {
|
||||
expect(await fn(-103, { expression: 'abs(value)' })).toBe(103);
|
||||
});
|
||||
|
||||
it('evaluates math expressions with references to columns by id in a datatable', () => {
|
||||
expect(fn(testTable, { expression: 'unique(in_stock)' })).toBe(2);
|
||||
expect(fn(testTable, { expression: 'sum(quantity)' })).toBe(2508);
|
||||
expect(fn(testTable, { expression: 'mean(price)' })).toBe(320);
|
||||
expect(fn(testTable, { expression: 'min(price)' })).toBe(67);
|
||||
expect(fn(testTable, { expression: 'median(quantity)' })).toBe(256);
|
||||
expect(fn(testTable, { expression: 'max(price)' })).toBe(605);
|
||||
it('evaluates math expressions with references to columns by id in a datatable', async () => {
|
||||
expect(await fn(testTable, { expression: 'unique(in_stock)' })).toBe(2);
|
||||
expect(await fn(testTable, { expression: 'sum(quantity)' })).toBe(2508);
|
||||
expect(await fn(testTable, { expression: 'mean(price)' })).toBe(320);
|
||||
expect(await fn(testTable, { expression: 'min(price)' })).toBe(67);
|
||||
expect(await fn(testTable, { expression: 'median(quantity)' })).toBe(256);
|
||||
expect(await fn(testTable, { expression: 'max(price)' })).toBe(605);
|
||||
});
|
||||
|
||||
it('does not use the name for math', () => {
|
||||
expect(() => fn(testTable, { expression: 'unique("in_stock label")' })).toThrow(
|
||||
'Unknown variable'
|
||||
it('does not use the name for math', async () => {
|
||||
await expect(fn(testTable, { expression: 'unique("in_stock label")' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: in_stock label'
|
||||
);
|
||||
expect(() => fn(testTable, { expression: 'sum("quantity label")' })).toThrow(
|
||||
'Unknown variable'
|
||||
|
||||
await expect(fn(testTable, { expression: 'sum("quantity label")' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: quantity label'
|
||||
);
|
||||
expect(() => fn(testTable, { expression: 'mean("price label")' })).toThrow('Unknown variable');
|
||||
expect(() => fn(testTable, { expression: 'min("price label")' })).toThrow('Unknown variable');
|
||||
expect(() => fn(testTable, { expression: 'median("quantity label")' })).toThrow(
|
||||
'Unknown variable'
|
||||
|
||||
await expect(fn(testTable, { expression: 'mean("price label")' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: price label'
|
||||
);
|
||||
await expect(fn(testTable, { expression: 'min("price label")' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: price label'
|
||||
);
|
||||
|
||||
await expect(fn(testTable, { expression: 'median("quantity label")' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: quantity label'
|
||||
);
|
||||
|
||||
await expect(fn(testTable, { expression: 'max("price label")' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: price label'
|
||||
);
|
||||
expect(() => fn(testTable, { expression: 'max("price label")' })).toThrow('Unknown variable');
|
||||
});
|
||||
|
||||
describe('args', () => {
|
||||
describe('expression', () => {
|
||||
it('sets the math expression to be evaluted', () => {
|
||||
expect(fn(null as unknown as MathInput, { expression: '10' })).toBe(10);
|
||||
expect(fn(23.23, { expression: 'floor(value)' })).toBe(23);
|
||||
expect(fn(testTable, { expression: 'count(price)' })).toBe(9);
|
||||
expect(fn(testTable, { expression: 'count(name)' })).toBe(9);
|
||||
it('sets the math expression to be evaluted', async () => {
|
||||
expect(await fn(null as unknown as MathInput, { expression: '10' })).toBe(10);
|
||||
expect(await fn(23.23, { expression: 'floor(value)' })).toBe(23);
|
||||
expect(await fn(testTable, { expression: 'count(price)' })).toBe(9);
|
||||
expect(await fn(testTable, { expression: 'count(name)' })).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onError', () => {
|
||||
it('should return the desired fallback value, for invalid expressions', () => {
|
||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'zero' })).toBe(0);
|
||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'null' })).toBe(null);
|
||||
expect(fn(testTable, { expression: 'mean(name)', onError: 'false' })).toBe(false);
|
||||
it('should return the desired fallback value, for invalid expressions', async () => {
|
||||
expect(await fn(testTable, { expression: 'mean(name)', onError: 'zero' })).toBe(0);
|
||||
expect(await fn(testTable, { expression: 'mean(name)', onError: 'null' })).toBe(null);
|
||||
expect(await fn(testTable, { expression: 'mean(name)', onError: 'false' })).toBe(false);
|
||||
});
|
||||
it('should return the desired fallback value, for division by zero', () => {
|
||||
expect(fn(testTable, { expression: '1/0', onError: 'zero' })).toBe(0);
|
||||
expect(fn(testTable, { expression: '1/0', onError: 'null' })).toBe(null);
|
||||
expect(fn(testTable, { expression: '1/0', onError: 'false' })).toBe(false);
|
||||
it('should return the desired fallback value, for division by zero', async () => {
|
||||
expect(await fn(testTable, { expression: '1/0', onError: 'zero' })).toBe(0);
|
||||
expect(await fn(testTable, { expression: '1/0', onError: 'null' })).toBe(null);
|
||||
expect(await fn(testTable, { expression: '1/0', onError: 'false' })).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid expressions', () => {
|
||||
it('throws when expression evaluates to an array', () => {
|
||||
expect(() => fn(testTable, { expression: 'multiply(price, 2)' })).toThrow(
|
||||
new RegExp(errors.tooManyResults().message.replace(/[()]/g, '\\$&'))
|
||||
it('throws when expression evaluates to an array', async () => {
|
||||
await expect(fn(testTable, { expression: 'multiply(price, 2)' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
errors.tooManyResults().message
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when using an unknown context variable', () => {
|
||||
expect(() => fn(testTable, { expression: 'sum(foo)' })).toThrow('Unknown variable: foo');
|
||||
});
|
||||
|
||||
it('throws when using non-numeric data', () => {
|
||||
expect(() => fn(testTable, { expression: 'mean(name)' })).toThrow(
|
||||
new RegExp(errors.executionFailed().message)
|
||||
);
|
||||
|
||||
expect(() => fn(testTable, { expression: 'mean(in_stock)' })).toThrow(
|
||||
new RegExp(errors.executionFailed().message)
|
||||
it('throws when using an unknown context variable', async () => {
|
||||
await expect(fn(testTable, { expression: 'sum(foo)' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Unknown variable: foo'
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when missing expression', () => {
|
||||
expect(() => fn(testTable)).toThrow(new RegExp(errors.emptyExpression().message));
|
||||
|
||||
expect(() => fn(testTable, { expession: '' } as unknown as MathArguments)).toThrow(
|
||||
new RegExp(errors.emptyExpression().message)
|
||||
it('throws when using non-numeric data', async () => {
|
||||
await expect(fn(testTable, { expression: 'mean(name)' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
errors.executionFailed().message
|
||||
);
|
||||
|
||||
expect(() => fn(testTable, { expession: ' ' } as unknown as MathArguments)).toThrow(
|
||||
new RegExp(errors.emptyExpression().message)
|
||||
await expect(fn(testTable, { expression: 'mean(in_stock)' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
errors.executionFailed().message
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when passing a context variable from an empty datatable', () => {
|
||||
expect(() => fn(emptyTable, { expression: 'mean(foo)' })).toThrow(
|
||||
new RegExp(errors.emptyDatatable().message)
|
||||
it('throws when missing expression', async () => {
|
||||
await expect(fn(testTable)).rejects.toHaveProperty(
|
||||
'message',
|
||||
errors.emptyExpression().message
|
||||
);
|
||||
|
||||
await expect(
|
||||
fn(testTable, { expession: '' } as unknown as MathArguments)
|
||||
).rejects.toHaveProperty('message', errors.emptyExpression().message);
|
||||
|
||||
await expect(
|
||||
fn(testTable, { expession: ' ' } as unknown as MathArguments)
|
||||
).rejects.toHaveProperty('message', errors.emptyExpression().message);
|
||||
});
|
||||
|
||||
it('throws when passing a context variable from an empty datatable', async () => {
|
||||
await expect(() => fn(emptyTable, { expression: 'mean(foo)' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
errors.emptyDatatable().message
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw when requesting fallback values for invalid expression', () => {
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'zero' })).not.toThrow();
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'false' })).not.toThrow();
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'null' })).not.toThrow();
|
||||
it('should not throw when requesting fallback values for invalid expression', async () => {
|
||||
await expect(
|
||||
fn(testTable, { expression: 'mean(name)', onError: 'zero' })
|
||||
).resolves.toBeDefined();
|
||||
await expect(
|
||||
fn(testTable, { expression: 'mean(name)', onError: 'false' })
|
||||
).resolves.toBeDefined();
|
||||
await expect(
|
||||
fn(testTable, { expression: 'mean(name)', onError: 'null' })
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('should throw when declared in the onError argument', () => {
|
||||
expect(() => fn(testTable, { expression: 'mean(name)', onError: 'throw' })).toThrow(
|
||||
new RegExp(errors.executionFailed().message)
|
||||
);
|
||||
it('should throw when declared in the onError argument', async () => {
|
||||
await expect(
|
||||
fn(testTable, { expression: 'mean(name)', onError: 'throw' })
|
||||
).rejects.toHaveProperty('message', errors.executionFailed().message);
|
||||
});
|
||||
|
||||
it('should throw when dividing by zero', () => {
|
||||
expect(() => fn(testTable, { expression: '1/0', onError: 'throw' })).toThrow(
|
||||
new RegExp('Cannot divide by 0')
|
||||
it('should throw when dividing by zero', async () => {
|
||||
await expect(fn(testTable, { expression: '1/0', onError: 'throw' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
'Cannot divide by 0'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,14 +12,18 @@ import { functionWrapper, testTable, tableWithNulls } from './utils';
|
|||
describe('mathColumn', () => {
|
||||
const fn = functionWrapper(mathColumn);
|
||||
|
||||
it('throws if the id is used', () => {
|
||||
expect(() => fn(testTable, { id: 'price', name: 'price', expression: 'price * 2' })).toThrow(
|
||||
`ID must be unique`
|
||||
);
|
||||
it('throws if the id is used', async () => {
|
||||
await expect(
|
||||
fn(testTable, { id: 'price', name: 'price', expression: 'price * 2' })
|
||||
).rejects.toHaveProperty('message', `ID must be unique`);
|
||||
});
|
||||
|
||||
it('applies math to each row by id', () => {
|
||||
const result = fn(testTable, { id: 'output', name: 'output', expression: 'quantity * price' });
|
||||
it('applies math to each row by id', async () => {
|
||||
const result = await fn(testTable, {
|
||||
id: 'output',
|
||||
name: 'output',
|
||||
expression: 'quantity * price',
|
||||
});
|
||||
expect(result.columns).toEqual([
|
||||
...testTable.columns,
|
||||
{ id: 'output', name: 'output', meta: { params: { id: 'number' }, type: 'number' } },
|
||||
|
@ -34,7 +38,7 @@ describe('mathColumn', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('extracts a single array value, but not a multi-value array', () => {
|
||||
it('extracts a single array value, but not a multi-value array', async () => {
|
||||
const arrayTable = {
|
||||
...testTable,
|
||||
rows: [
|
||||
|
@ -52,23 +56,24 @@ describe('mathColumn', () => {
|
|||
name: 'output',
|
||||
expression: 'quantity',
|
||||
};
|
||||
expect(fn(arrayTable, args).rows[0].output).toEqual(100);
|
||||
expect(() => fn(arrayTable, { ...args, expression: 'price' })).toThrowError(
|
||||
`Cannot perform math on array values`
|
||||
expect((await fn(arrayTable, args)).rows[0].output).toEqual(100);
|
||||
await expect(fn(arrayTable, { ...args, expression: 'price' })).rejects.toHaveProperty(
|
||||
'message',
|
||||
`Cannot perform math on array values at output`
|
||||
);
|
||||
});
|
||||
|
||||
it('handles onError', () => {
|
||||
it('handles onError', async () => {
|
||||
const args = {
|
||||
id: 'output',
|
||||
name: 'output',
|
||||
expression: 'quantity / 0',
|
||||
};
|
||||
expect(() => fn(testTable, args)).toThrowError(`Cannot divide by 0`);
|
||||
expect(() => fn(testTable, { ...args, onError: 'throw' })).toThrow();
|
||||
expect(fn(testTable, { ...args, onError: 'zero' }).rows[0].output).toEqual(0);
|
||||
expect(fn(testTable, { ...args, onError: 'false' }).rows[0].output).toEqual(false);
|
||||
expect(fn(testTable, { ...args, onError: 'null' }).rows[0].output).toEqual(null);
|
||||
await expect(fn(testTable, args)).rejects.toHaveProperty('message', `Cannot divide by 0`);
|
||||
await expect(fn(testTable, { ...args, onError: 'throw' })).rejects.toBeDefined();
|
||||
expect((await fn(testTable, { ...args, onError: 'zero' })).rows[0].output).toEqual(0);
|
||||
expect((await fn(testTable, { ...args, onError: 'false' })).rows[0].output).toEqual(false);
|
||||
expect((await fn(testTable, { ...args, onError: 'null' })).rows[0].output).toEqual(null);
|
||||
});
|
||||
|
||||
it('should copy over the meta information from the specified column', async () => {
|
||||
|
@ -96,9 +101,14 @@ describe('mathColumn', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should correctly infer the type from the first non-null row', () => {
|
||||
it('should correctly infer the type from the first non-null row', async () => {
|
||||
expect(
|
||||
fn(tableWithNulls, { id: 'value', name: 'value', expression: 'price + 2', onError: 'null' })
|
||||
await fn(tableWithNulls, {
|
||||
id: 'value',
|
||||
name: 'value',
|
||||
expression: 'price + 2',
|
||||
onError: 'null',
|
||||
})
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
type: 'datatable',
|
||||
|
|
|
@ -16,10 +16,10 @@ const defaultArgs = { window: 5, inputColumnId: 'val', outputColumnId: 'output'
|
|||
describe('interpreter/functions#movingAverage', () => {
|
||||
const fn = functionWrapper(movingAverage);
|
||||
const runFn = (input: Datatable, args: MovingAverageArgs) =>
|
||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
||||
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||
|
||||
it('calculates movingAverage', () => {
|
||||
const result = runFn(
|
||||
it('calculates movingAverage', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -40,8 +40,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('skips null or undefined values until there is real data', () => {
|
||||
const result = runFn(
|
||||
it('skips null or undefined values until there is real data', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -77,8 +77,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats 0 as real data', () => {
|
||||
const result = runFn(
|
||||
it('treats 0 as real data', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -115,8 +115,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('calculates movingAverage for multiple series', () => {
|
||||
const result = runFn(
|
||||
it('calculates movingAverage for multiple series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -149,8 +149,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats missing split column as separate series', () => {
|
||||
const result = runFn(
|
||||
it('treats missing split column as separate series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -182,8 +182,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats null like undefined and empty string for split columns', () => {
|
||||
const result = runFn(
|
||||
it('treats null like undefined and empty string for split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -217,8 +217,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('calculates movingAverage for multiple series by multiple split columns', () => {
|
||||
const result = runFn(
|
||||
it('calculates movingAverage for multiple series by multiple split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -251,8 +251,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('splits separate series by the string representation of the cell values', () => {
|
||||
const result = runFn(
|
||||
it('splits separate series by the string representation of the cell values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -272,8 +272,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 1, undefined, 10]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating movingAverage', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating movingAverage', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -289,8 +289,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('skips NaN like values', () => {
|
||||
const result = runFn(
|
||||
it('skips NaN like values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -301,8 +301,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([undefined, 5, (5 + 7) / 2, NaN, NaN]);
|
||||
});
|
||||
|
||||
it('copies over meta information from the source column', () => {
|
||||
const result = runFn(
|
||||
it('copies over meta information from the source column', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -343,8 +343,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets output name on output column if specified', () => {
|
||||
const result = runFn(
|
||||
it('sets output name on output column if specified', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -367,7 +367,7 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('returns source table if input column does not exist', () => {
|
||||
it('returns source table if input column does not exist', async () => {
|
||||
const input: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -382,12 +382,12 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
rows: [{ val: 5 }],
|
||||
};
|
||||
expect(
|
||||
runFn(input, { ...defaultArgs, inputColumnId: 'nonexisting', outputColumnId: 'output' })
|
||||
await runFn(input, { ...defaultArgs, inputColumnId: 'nonexisting', outputColumnId: 'output' })
|
||||
).toBe(input);
|
||||
});
|
||||
|
||||
it('throws an error if output column exists already', () => {
|
||||
expect(() =>
|
||||
it('throws an error if output column exists already', async () => {
|
||||
await expect(
|
||||
runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
|
@ -404,11 +404,11 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
},
|
||||
{ ...defaultArgs, inputColumnId: 'val', outputColumnId: 'val' }
|
||||
)
|
||||
).toThrow();
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
|
||||
it('calculates moving average for window equal to 1', () => {
|
||||
const result = runFn(
|
||||
it('calculates moving average for window equal to 1', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -441,8 +441,8 @@ describe('interpreter/functions#movingAverage', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('calculates moving average for window bigger than array', () => {
|
||||
const result = runFn(
|
||||
it('calculates moving average for window bigger than array', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
|
|
@ -14,10 +14,10 @@ import { overallMetric, OverallMetricArgs } from '../overall_metric';
|
|||
describe('interpreter/functions#overall_metric', () => {
|
||||
const fn = functionWrapper(overallMetric);
|
||||
const runFn = (input: Datatable, args: OverallMetricArgs) =>
|
||||
fn(input, args, {} as ExecutionContext) as Datatable;
|
||||
fn(input, args, {} as ExecutionContext) as Promise<Datatable>;
|
||||
|
||||
it('ignores null or undefined with sum', () => {
|
||||
const result = runFn(
|
||||
it('ignores null or undefined with sum', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -33,8 +33,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([12, 12, 12, 12]);
|
||||
});
|
||||
|
||||
it('ignores null or undefined with average', () => {
|
||||
const result = runFn(
|
||||
it('ignores null or undefined with average', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -50,8 +50,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([3, 3, 3, 3, 3]);
|
||||
});
|
||||
|
||||
it('ignores null or undefined with min', () => {
|
||||
const result = runFn(
|
||||
it('ignores null or undefined with min', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -67,8 +67,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([1, 1, 1, 1, 1]);
|
||||
});
|
||||
|
||||
it('ignores null or undefined with max', () => {
|
||||
const result = runFn(
|
||||
it('ignores null or undefined with max', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -84,8 +84,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([-1, -1, -1, -1, -1]);
|
||||
});
|
||||
|
||||
it('calculates overall sum for multiple series', () => {
|
||||
const result = runFn(
|
||||
it('calculates overall sum for multiple series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -118,8 +118,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('treats missing split column as separate series', () => {
|
||||
const result = runFn(
|
||||
it('treats missing split column as separate series', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -142,7 +142,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([1, 2, 3, 1, 3, 1, 2, 2]);
|
||||
});
|
||||
|
||||
it('treats null like undefined and empty string for split columns', () => {
|
||||
it('treats null like undefined and empty string for split columns', async () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -162,7 +162,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
],
|
||||
};
|
||||
|
||||
const result = runFn(table, {
|
||||
const result = await runFn(table, {
|
||||
inputColumnId: 'val',
|
||||
outputColumnId: 'output',
|
||||
by: ['split'],
|
||||
|
@ -180,7 +180,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
3 + 5 + 7 + 9,
|
||||
]);
|
||||
|
||||
const result2 = runFn(table, {
|
||||
const result2 = await runFn(table, {
|
||||
inputColumnId: 'val',
|
||||
outputColumnId: 'output',
|
||||
by: ['split'],
|
||||
|
@ -188,7 +188,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
});
|
||||
expect(result2.rows.map((row) => row.output)).toEqual([6, 8, 9, 6, 9, 6, 9, 8, 9]);
|
||||
|
||||
const result3 = runFn(table, {
|
||||
const result3 = await runFn(table, {
|
||||
inputColumnId: 'val',
|
||||
outputColumnId: 'output',
|
||||
by: ['split'],
|
||||
|
@ -207,8 +207,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('handles array values', () => {
|
||||
const result = runFn(
|
||||
it('handles array values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -224,8 +224,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([28, 28, 28, 28]);
|
||||
});
|
||||
|
||||
it('takes array values into account for average calculation', () => {
|
||||
const result = runFn(
|
||||
it('takes array values into account for average calculation', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -241,7 +241,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([3, 3]);
|
||||
});
|
||||
|
||||
it('handles array values for split columns', () => {
|
||||
it('handles array values for split columns', async () => {
|
||||
const table: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -261,7 +261,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
],
|
||||
};
|
||||
|
||||
const result = runFn(table, {
|
||||
const result = await runFn(table, {
|
||||
inputColumnId: 'val',
|
||||
outputColumnId: 'output',
|
||||
by: ['split'],
|
||||
|
@ -279,7 +279,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
3 + 5 + 7 + 9 + 99,
|
||||
]);
|
||||
|
||||
const result2 = runFn(table, {
|
||||
const result2 = await runFn(table, {
|
||||
inputColumnId: 'val',
|
||||
outputColumnId: 'output',
|
||||
by: ['split'],
|
||||
|
@ -288,8 +288,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result2.rows.map((row) => row.output)).toEqual([6, 11, 99, 6, 99, 6, 99, 11, 99]);
|
||||
});
|
||||
|
||||
it('calculates cumulative sum for multiple series by multiple split columns', () => {
|
||||
const result = runFn(
|
||||
it('calculates cumulative sum for multiple series by multiple split columns', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -313,8 +313,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([1 + 4, 2, 3, 1 + 4, 5, 6, 7 + 8, 7 + 8]);
|
||||
});
|
||||
|
||||
it('splits separate series by the string representation of the cell values', () => {
|
||||
const result = runFn(
|
||||
it('splits separate series by the string representation of the cell values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -334,8 +334,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([1 + 2, 1 + 2, 10 + 11, 10 + 11]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating cumulative sum', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating cumulative sum', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -346,8 +346,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([7, 7, 7, 7]);
|
||||
});
|
||||
|
||||
it('casts values to number before calculating metric for NaN like values', () => {
|
||||
const result = runFn(
|
||||
it('casts values to number before calculating metric for NaN like values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -358,8 +358,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([NaN, NaN, NaN, NaN]);
|
||||
});
|
||||
|
||||
it('skips undefined and null values', () => {
|
||||
const result = runFn(
|
||||
it('skips undefined and null values', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [{ id: 'val', name: 'val', meta: { type: 'number' } }],
|
||||
|
@ -380,8 +380,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
expect(result.rows.map((row) => row.output)).toEqual([4, 4, 4, 4, 4, 4, 4, 4, 4]);
|
||||
});
|
||||
|
||||
it('copies over meta information from the source column', () => {
|
||||
const result = runFn(
|
||||
it('copies over meta information from the source column', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -422,8 +422,8 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('sets output name on output column if specified', () => {
|
||||
const result = runFn(
|
||||
it('sets output name on output column if specified', async () => {
|
||||
const result = await runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -451,7 +451,7 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('returns source table if input column does not exist', () => {
|
||||
it('returns source table if input column does not exist', async () => {
|
||||
const input: Datatable = {
|
||||
type: 'datatable',
|
||||
columns: [
|
||||
|
@ -466,12 +466,12 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
rows: [{ val: 5 }],
|
||||
};
|
||||
expect(
|
||||
runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' })
|
||||
await runFn(input, { inputColumnId: 'nonexisting', outputColumnId: 'output', metric: 'sum' })
|
||||
).toBe(input);
|
||||
});
|
||||
|
||||
it('throws an error if output column exists already', () => {
|
||||
expect(() =>
|
||||
it('throws an error if output column exists already', async () => {
|
||||
await expect(
|
||||
runFn(
|
||||
{
|
||||
type: 'datatable',
|
||||
|
@ -488,6 +488,6 @@ describe('interpreter/functions#overall_metric', () => {
|
|||
},
|
||||
{ inputColumnId: 'val', outputColumnId: 'val', metric: 'max' }
|
||||
)
|
||||
).toThrow();
|
||||
).rejects.toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,20 +22,23 @@ export function plugin(initializerContext: PluginInitializerContext) {
|
|||
}
|
||||
|
||||
// Static exports.
|
||||
export { ExpressionExecutor, IExpressionLoaderParams, ExpressionRenderError } from './types';
|
||||
export {
|
||||
export type {
|
||||
ExpressionExecutor,
|
||||
IExpressionLoaderParams,
|
||||
ExpressionRenderError,
|
||||
ExpressionRendererEvent,
|
||||
} from './types';
|
||||
export type { ExpressionLoader } from './loader';
|
||||
export type { ExpressionRenderHandler } from './render';
|
||||
export type {
|
||||
ExpressionRendererComponent,
|
||||
ReactExpressionRenderer,
|
||||
ReactExpressionRendererProps,
|
||||
ReactExpressionRendererType,
|
||||
} from './react_expression_renderer';
|
||||
export { ExpressionRenderHandler, ExpressionRendererEvent } from './render';
|
||||
export {
|
||||
export type {
|
||||
AnyExpressionFunctionDefinition,
|
||||
AnyExpressionTypeDefinition,
|
||||
ArgumentType,
|
||||
buildExpression,
|
||||
buildExpressionFunction,
|
||||
Datatable,
|
||||
DatatableColumn,
|
||||
DatatableColumnType,
|
||||
|
@ -79,17 +82,12 @@ export {
|
|||
FontStyle,
|
||||
FontValue,
|
||||
FontWeight,
|
||||
format,
|
||||
formatExpression,
|
||||
FunctionsRegistry,
|
||||
IInterpreterRenderHandlers,
|
||||
InterpreterErrorType,
|
||||
IRegistry,
|
||||
isExpressionAstBuilder,
|
||||
KnownTypeToString,
|
||||
Overflow,
|
||||
parse,
|
||||
parseExpression,
|
||||
PointSeries,
|
||||
PointSeriesColumn,
|
||||
PointSeriesColumnName,
|
||||
|
@ -109,6 +107,13 @@ export {
|
|||
ExpressionsServiceSetup,
|
||||
ExpressionsServiceStart,
|
||||
TablesAdapter,
|
||||
ExpressionsInspectorAdapter,
|
||||
} from '../common';
|
||||
|
||||
export {
|
||||
buildExpression,
|
||||
buildExpressionFunction,
|
||||
formatExpression,
|
||||
isExpressionAstBuilder,
|
||||
parseExpression,
|
||||
createDefaultInspectorAdapters,
|
||||
} from '../common';
|
||||
|
|
|
@ -88,8 +88,8 @@ jest.mock('./services', () => {
|
|||
});
|
||||
|
||||
describe('execute helper function', () => {
|
||||
it('returns ExpressionLoader instance', () => {
|
||||
const response = loader(element, '', {});
|
||||
it('returns ExpressionLoader instance', async () => {
|
||||
const response = await loader(element, '', {});
|
||||
expect(response).toBeInstanceOf(ExpressionLoader);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -194,10 +194,10 @@ export class ExpressionLoader {
|
|||
|
||||
export type IExpressionLoader = (
|
||||
element: HTMLElement,
|
||||
expression: string | ExpressionAstExpression,
|
||||
params: IExpressionLoaderParams
|
||||
) => ExpressionLoader;
|
||||
expression?: string | ExpressionAstExpression,
|
||||
params?: IExpressionLoaderParams
|
||||
) => Promise<ExpressionLoader>;
|
||||
|
||||
export const loader: IExpressionLoader = (element, expression, params) => {
|
||||
export const loader: IExpressionLoader = async (element, expression?, params?) => {
|
||||
return new ExpressionLoader(element, expression, params);
|
||||
};
|
||||
|
|
|
@ -30,8 +30,6 @@ const createSetupContract = (): Setup => {
|
|||
const createStartContract = (): Start => {
|
||||
return {
|
||||
execute: jest.fn(),
|
||||
ExpressionLoader: jest.fn(),
|
||||
ExpressionRenderHandler: jest.fn(),
|
||||
getFunction: jest.fn(),
|
||||
getFunctions: jest.fn(),
|
||||
getRenderer: jest.fn(),
|
||||
|
@ -39,8 +37,8 @@ const createStartContract = (): Start => {
|
|||
getType: jest.fn(),
|
||||
getTypes: jest.fn(),
|
||||
loader: jest.fn(),
|
||||
ReactExpressionRenderer: jest.fn((props) => <></>),
|
||||
render: jest.fn(),
|
||||
ReactExpressionRenderer: jest.fn((props) => <></>),
|
||||
run: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
|
|
@ -8,16 +8,17 @@
|
|||
|
||||
import { pick } from 'lodash';
|
||||
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public';
|
||||
import { ExpressionsServiceSetup, ExpressionsServiceStart } from '../common';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import type { ExpressionsServiceSetup, ExpressionsServiceStart } from '../common';
|
||||
import {
|
||||
ExpressionsService,
|
||||
setRenderersRegistry,
|
||||
setNotifications,
|
||||
setExpressionsService,
|
||||
} from './services';
|
||||
import { ReactExpressionRenderer } from './react_expression_renderer';
|
||||
import { ExpressionLoader, IExpressionLoader, loader } from './loader';
|
||||
import { render, ExpressionRenderHandler } from './render';
|
||||
import { ReactExpressionRenderer } from './react_expression_renderer_wrapper';
|
||||
import type { IExpressionLoader } from './loader';
|
||||
import type { IExpressionRenderer } from './render';
|
||||
|
||||
/**
|
||||
* Expressions public setup contract, extends {@link ExpressionsServiceSetup}
|
||||
|
@ -28,11 +29,9 @@ export type ExpressionsSetup = ExpressionsServiceSetup;
|
|||
* Expressions public start contrect, extends {@link ExpressionServiceStart}
|
||||
*/
|
||||
export interface ExpressionsStart extends ExpressionsServiceStart {
|
||||
ExpressionLoader: typeof ExpressionLoader;
|
||||
ExpressionRenderHandler: typeof ExpressionRenderHandler;
|
||||
loader: IExpressionLoader;
|
||||
render: IExpressionRenderer;
|
||||
ReactExpressionRenderer: typeof ReactExpressionRenderer;
|
||||
render: typeof render;
|
||||
}
|
||||
|
||||
export class ExpressionsPublicPlugin implements Plugin<ExpressionsSetup, ExpressionsStart> {
|
||||
|
@ -66,13 +65,24 @@ export class ExpressionsPublicPlugin implements Plugin<ExpressionsSetup, Express
|
|||
setNotifications(core.notifications);
|
||||
|
||||
const { expressions } = this;
|
||||
|
||||
const loader: IExpressionLoader = async (element, expression, params) => {
|
||||
const { ExpressionLoader } = await import('./loader');
|
||||
return new ExpressionLoader(element, expression, params);
|
||||
};
|
||||
|
||||
const render: IExpressionRenderer = async (element, data, options) => {
|
||||
const { ExpressionRenderHandler } = await import('./render');
|
||||
const handler = new ExpressionRenderHandler(element, options);
|
||||
handler.render(data as SerializableRecord);
|
||||
return handler;
|
||||
};
|
||||
|
||||
const start = {
|
||||
...expressions.start(),
|
||||
ExpressionLoader,
|
||||
ExpressionRenderHandler,
|
||||
loader,
|
||||
ReactExpressionRenderer,
|
||||
render,
|
||||
ReactExpressionRenderer,
|
||||
};
|
||||
|
||||
return Object.freeze(start);
|
||||
|
|
|
@ -10,13 +10,12 @@ import React from 'react';
|
|||
import { act } from 'react-dom/test-utils';
|
||||
import { Subject } from 'rxjs';
|
||||
import { share } from 'rxjs/operators';
|
||||
import { ReactExpressionRenderer } from './react_expression_renderer';
|
||||
import { default as ReactExpressionRenderer } from './react_expression_renderer';
|
||||
import { ExpressionLoader } from './loader';
|
||||
import { mount } from 'enzyme';
|
||||
import { EuiProgress } from '@elastic/eui';
|
||||
import { IInterpreterRenderHandlers } from '../common';
|
||||
import { RenderErrorHandlerFnType } from './types';
|
||||
import { ExpressionRendererEvent } from './render';
|
||||
import { RenderErrorHandlerFnType, ExpressionRendererEvent } from './types';
|
||||
|
||||
jest.mock('./loader', () => {
|
||||
return {
|
||||
|
|
|
@ -13,10 +13,9 @@ import { filter } from 'rxjs/operators';
|
|||
import useShallowCompareEffect from 'react-use/lib/useShallowCompareEffect';
|
||||
import { EuiLoadingChart, EuiProgress } from '@elastic/eui';
|
||||
import theme from '@elastic/eui/dist/eui_theme_light.json';
|
||||
import { IExpressionLoaderParams, ExpressionRenderError } from './types';
|
||||
import { IExpressionLoaderParams, ExpressionRenderError, ExpressionRendererEvent } from './types';
|
||||
import { ExpressionAstExpression, IInterpreterRenderHandlers } from '../common';
|
||||
import { ExpressionLoader } from './loader';
|
||||
import { ExpressionRendererEvent } from './render';
|
||||
|
||||
// Accept all options of the runner as props except for the
|
||||
// dom element which is provided by the component itself
|
||||
|
@ -58,7 +57,8 @@ const defaultState: State = {
|
|||
error: null,
|
||||
};
|
||||
|
||||
export const ReactExpressionRenderer = ({
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ReactExpressionRenderer({
|
||||
className,
|
||||
dataAttrs,
|
||||
padding,
|
||||
|
@ -69,7 +69,7 @@ export const ReactExpressionRenderer = ({
|
|||
reload$,
|
||||
debounce,
|
||||
...expressionLoaderOptions
|
||||
}: ReactExpressionRendererProps) => {
|
||||
}: ReactExpressionRendererProps) {
|
||||
const mountpoint: React.MutableRefObject<null | HTMLDivElement> = useRef(null);
|
||||
const [state, setState] = useState<State>({ ...defaultState });
|
||||
const hasCustomRenderErrorHandler = !!renderError;
|
||||
|
@ -237,4 +237,4 @@ export const ReactExpressionRenderer = ({
|
|||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import { EuiLoadingSpinner } from '@elastic/eui';
|
||||
import type { ReactExpressionRendererProps } from './react_expression_renderer';
|
||||
|
||||
const ReactExpressionRendererComponent = lazy(() => import('./react_expression_renderer'));
|
||||
|
||||
export const ReactExpressionRenderer = (props: ReactExpressionRendererProps) => (
|
||||
<Suspense fallback={<EuiLoadingSpinner />}>
|
||||
<ReactExpressionRendererComponent {...props} />
|
||||
</Suspense>
|
||||
);
|
|
@ -53,8 +53,8 @@ const getHandledError = () => {
|
|||
};
|
||||
|
||||
describe('render helper function', () => {
|
||||
it('returns ExpressionRenderHandler instance', () => {
|
||||
const response = render(element, {});
|
||||
it('returns ExpressionRenderHandler instance', async () => {
|
||||
const response = await render(element, {});
|
||||
expect(response).toBeInstanceOf(ExpressionRenderHandler);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,14 +11,14 @@ import { Observable } from 'rxjs';
|
|||
import { filter } from 'rxjs/operators';
|
||||
import { isNumber } from 'lodash';
|
||||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import { ExpressionRenderError, RenderErrorHandlerFnType, IExpressionLoaderParams } from './types';
|
||||
import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler';
|
||||
import {
|
||||
IInterpreterRenderHandlers,
|
||||
IInterpreterRenderEvent,
|
||||
IInterpreterRenderUpdateParams,
|
||||
RenderMode,
|
||||
} from '../common';
|
||||
ExpressionRenderError,
|
||||
RenderErrorHandlerFnType,
|
||||
IExpressionLoaderParams,
|
||||
ExpressionRendererEvent,
|
||||
} from './types';
|
||||
import { renderErrorHandler as defaultRenderErrorHandler } from './render_error_handler';
|
||||
import { IInterpreterRenderHandlers, IInterpreterRenderUpdateParams, RenderMode } from '../common';
|
||||
|
||||
import { getRenderersRegistry } from './services';
|
||||
|
||||
|
@ -32,9 +32,6 @@ export interface ExpressionRenderHandlerParams {
|
|||
hasCompatibleActions?: (event: ExpressionRendererEvent) => Promise<boolean>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ExpressionRendererEvent = IInterpreterRenderEvent<any>;
|
||||
|
||||
type UpdateValue = IInterpreterRenderUpdateParams<IExpressionLoaderParams>;
|
||||
|
||||
export class ExpressionRenderHandler {
|
||||
|
@ -154,12 +151,14 @@ export class ExpressionRenderHandler {
|
|||
};
|
||||
}
|
||||
|
||||
export function render(
|
||||
export type IExpressionRenderer = (
|
||||
element: HTMLElement,
|
||||
data: unknown,
|
||||
options?: ExpressionRenderHandlerParams
|
||||
): ExpressionRenderHandler {
|
||||
) => Promise<ExpressionRenderHandler>;
|
||||
|
||||
export const render: IExpressionRenderer = async (element, data, options) => {
|
||||
const handler = new ExpressionRenderHandler(element, options);
|
||||
handler.render(data as SerializableRecord);
|
||||
return handler;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
ExpressionValue,
|
||||
ExpressionsService,
|
||||
RenderMode,
|
||||
IInterpreterRenderEvent,
|
||||
} from '../../common';
|
||||
import { ExpressionRenderHandlerParams } from '../render';
|
||||
|
||||
|
@ -75,3 +76,6 @@ export type RenderErrorHandlerFnType = (
|
|||
error: ExpressionRenderError,
|
||||
handlers: IInterpreterRenderHandlers
|
||||
) => void;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type ExpressionRendererEvent = IInterpreterRenderEvent<any>;
|
||||
|
|
|
@ -30,7 +30,7 @@ import {
|
|||
} from '../../../../plugins/embeddable/public';
|
||||
import {
|
||||
IExpressionLoaderParams,
|
||||
ExpressionsStart,
|
||||
ExpressionLoader,
|
||||
ExpressionRenderError,
|
||||
ExpressionAstExpression,
|
||||
} from '../../../../plugins/expressions/public';
|
||||
|
@ -81,8 +81,6 @@ export type VisualizeSavedObjectAttributes = SavedObjectAttributes & {
|
|||
export type VisualizeByValueInput = { attributes: VisualizeSavedObjectAttributes } & VisualizeInput;
|
||||
export type VisualizeByReferenceInput = SavedObjectEmbeddableInput & VisualizeInput;
|
||||
|
||||
type ExpressionLoader = InstanceType<ExpressionsStart['ExpressionLoader']>;
|
||||
|
||||
export class VisualizeEmbeddable
|
||||
extends Embeddable<VisualizeInput, VisualizeOutput>
|
||||
implements ReferenceOrValueEmbeddable<VisualizeByValueInput, VisualizeByReferenceInput>
|
||||
|
@ -302,7 +300,7 @@ export class VisualizeEmbeddable
|
|||
super.render(this.domNode);
|
||||
|
||||
const expressions = getExpressions();
|
||||
this.handler = new expressions.ExpressionLoader(this.domNode, undefined, {
|
||||
this.handler = await expressions.loader(this.domNode, undefined, {
|
||||
onRenderError: (element: HTMLElement, error: ExpressionRenderError) => {
|
||||
this.onContainerError(error);
|
||||
},
|
||||
|
|
|
@ -9,5 +9,5 @@
|
|||
"requiredPlugins": ["data", "savedObjects", "kibanaUtils", "expressions"],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"requiredBundles": ["inspector"]
|
||||
"requiredBundles": []
|
||||
}
|
||||
|
|
|
@ -13,10 +13,8 @@ import { first, pluck } from 'rxjs/operators';
|
|||
import {
|
||||
IInterpreterRenderHandlers,
|
||||
ExpressionValue,
|
||||
TablesAdapter,
|
||||
} from '../../../../../../../src/plugins/expressions/public';
|
||||
import { RequestAdapter } from '../../../../../../../src/plugins/inspector/public';
|
||||
import { Adapters, ExpressionRenderHandler } from '../../types';
|
||||
import { ExpressionRenderHandler } from '../../types';
|
||||
import { getExpressions } from '../../services';
|
||||
|
||||
declare global {
|
||||
|
@ -50,13 +48,9 @@ class Main extends React.Component<{}, State> {
|
|||
initialContext: ExpressionValue = {}
|
||||
) => {
|
||||
this.setState({ expression });
|
||||
const adapters: Adapters = {
|
||||
requests: new RequestAdapter(),
|
||||
tables: new TablesAdapter(),
|
||||
};
|
||||
|
||||
return getExpressions()
|
||||
.execute(expression, context || { type: 'null' }, {
|
||||
inspectorAdapters: adapters,
|
||||
searchContext: initialContext as any,
|
||||
})
|
||||
.getData()
|
||||
|
@ -70,7 +64,7 @@ class Main extends React.Component<{}, State> {
|
|||
lastRenderHandler.destroy();
|
||||
}
|
||||
|
||||
lastRenderHandler = getExpressions().render(this.chartRef.current!, context, {
|
||||
lastRenderHandler = await getExpressions().render(this.chartRef.current!, context, {
|
||||
onRenderError: (el: HTMLElement, error: unknown, handler: IInterpreterRenderHandlers) => {
|
||||
this.setState({
|
||||
expression: 'Render error!\n\n' + JSON.stringify(error),
|
||||
|
|
|
@ -58,6 +58,7 @@ describe('lens_merge_tables', () => {
|
|||
const adapters: DefaultInspectorAdapters = {
|
||||
tables: new TablesAdapter(),
|
||||
requests: {} as never,
|
||||
expression: {} as never,
|
||||
};
|
||||
mergeTables.fn(null, { layerIds: ['first', 'second'], tables: [sampleTable1, sampleTable2] }, {
|
||||
inspectorAdapters: adapters,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue