forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Expressions] [Lens] Add id and copyMetaFrom arg to mapColumn fn + ad…
…d configurable onError argument to math fn (elastic#90481) (elastic#92810) Co-authored-by: Kibana Machine <[email protected]> Co-authored-by: Kibana Machine <[email protected]>
- Loading branch information
1 parent
31dc410
commit 7592e8c
Showing
17 changed files
with
706 additions
and
365 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
123 changes: 123 additions & 0 deletions
123
src/plugins/expressions/common/expression_functions/specs/map_column.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
/* | ||
* 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 { i18n } from '@kbn/i18n'; | ||
import { ExpressionFunctionDefinition } from '../types'; | ||
import { Datatable, getType } from '../../expression_types'; | ||
|
||
export interface MapColumnArguments { | ||
id?: string | null; | ||
name: string; | ||
expression?: (datatable: Datatable) => Promise<boolean | number | string | null>; | ||
copyMetaFrom?: string | null; | ||
} | ||
|
||
export const mapColumn: ExpressionFunctionDefinition< | ||
'mapColumn', | ||
Datatable, | ||
MapColumnArguments, | ||
Promise<Datatable> | ||
> = { | ||
name: 'mapColumn', | ||
aliases: ['mc'], // midnight commander. So many times I've launched midnight commander instead of moving a file. | ||
type: 'datatable', | ||
inputTypes: ['datatable'], | ||
help: i18n.translate('expressions.functions.mapColumnHelpText', { | ||
defaultMessage: | ||
'Adds a column calculated as the result of other columns. ' + | ||
'Changes are made only when you provide arguments.' + | ||
'See also {alterColumnFn} and {staticColumnFn}.', | ||
values: { | ||
alterColumnFn: '`alterColumn`', | ||
staticColumnFn: '`staticColumn`', | ||
}, | ||
}), | ||
args: { | ||
id: { | ||
types: ['string', 'null'], | ||
help: i18n.translate('expressions.functions.mapColumn.args.idHelpText', { | ||
defaultMessage: | ||
'An optional id of the resulting column. When `null` the name/column argument is used as id.', | ||
}), | ||
required: false, | ||
default: null, | ||
}, | ||
name: { | ||
types: ['string'], | ||
aliases: ['_', 'column'], | ||
help: i18n.translate('expressions.functions.mapColumn.args.nameHelpText', { | ||
defaultMessage: 'The name of the resulting column.', | ||
}), | ||
required: true, | ||
}, | ||
expression: { | ||
types: ['boolean', 'number', 'string', 'null'], | ||
resolve: false, | ||
aliases: ['exp', 'fn', 'function'], | ||
help: i18n.translate('expressions.functions.mapColumn.args.expressionHelpText', { | ||
defaultMessage: | ||
'An expression that is executed on every row, provided with a single-row {DATATABLE} context and returning the cell value.', | ||
values: { | ||
DATATABLE: '`datatable`', | ||
}, | ||
}), | ||
required: true, | ||
}, | ||
copyMetaFrom: { | ||
types: ['string', 'null'], | ||
help: i18n.translate('expressions.functions.mapColumn.args.copyMetaFromHelpText', { | ||
defaultMessage: | ||
"If set, the meta object from the specified column id is copied over to the specified target column. If the column doesn't exist it silently fails.", | ||
}), | ||
required: false, | ||
default: null, | ||
}, | ||
}, | ||
fn: (input, args) => { | ||
const expression = args.expression || (() => Promise.resolve(null)); | ||
const columnId = args.id != null ? args.id : args.name; | ||
|
||
const columns = [...input.columns]; | ||
const rowPromises = input.rows.map((row) => { | ||
return expression({ | ||
type: 'datatable', | ||
columns, | ||
rows: [row], | ||
}).then((val) => ({ | ||
...row, | ||
[columnId]: val, | ||
})); | ||
}); | ||
|
||
return Promise.all(rowPromises).then((rows) => { | ||
const existingColumnIndex = columns.findIndex(({ name }) => name === args.name); | ||
const type = rows.length ? getType(rows[0][columnId]) : 'null'; | ||
const newColumn = { | ||
id: columnId, | ||
name: args.name, | ||
meta: { type }, | ||
}; | ||
if (args.copyMetaFrom) { | ||
const metaSourceFrom = columns.find(({ id }) => id === args.copyMetaFrom); | ||
newColumn.meta = { ...newColumn.meta, ...(metaSourceFrom?.meta || {}) }; | ||
} | ||
|
||
if (existingColumnIndex === -1) { | ||
columns.push(newColumn); | ||
} else { | ||
columns[existingColumnIndex] = newColumn; | ||
} | ||
|
||
return { | ||
type: 'datatable', | ||
columns, | ||
rows, | ||
} as Datatable; | ||
}); | ||
}, | ||
}; |
167 changes: 167 additions & 0 deletions
167
src/plugins/expressions/common/expression_functions/specs/math.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
/* | ||
* 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 } from 'lodash'; | ||
import { i18n } from '@kbn/i18n'; | ||
import { evaluate } from '@kbn/tinymath'; | ||
import { ExpressionFunctionDefinition } from '../types'; | ||
import { Datatable, isDatatable } from '../../expression_types'; | ||
|
||
export type MathArguments = { | ||
expression: string; | ||
onError?: 'null' | 'zero' | 'false' | 'throw'; | ||
}; | ||
|
||
export type MathInput = number | Datatable; | ||
|
||
const TINYMATH = '`TinyMath`'; | ||
const TINYMATH_URL = | ||
'https://www.elastic.co/guide/en/kibana/current/canvas-tinymath-functions.html'; | ||
|
||
const isString = (val: any): boolean => typeof val === 'string'; | ||
|
||
function pivotObjectArray< | ||
RowType extends { [key: string]: any }, | ||
ReturnColumns extends string | number | symbol = keyof RowType | ||
>(rows: RowType[], columns?: string[]): Record<string, 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); | ||
} | ||
|
||
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 | ||
> = { | ||
name: 'math', | ||
type: undefined, | ||
inputTypes: ['number', 'datatable'], | ||
help: i18n.translate('expressions.functions.mathHelpText', { | ||
defaultMessage: | ||
'Interprets a {TINYMATH} math expression using a {TYPE_NUMBER} or {DATATABLE} as {CONTEXT}. ' + | ||
'The {DATATABLE} columns are available by their column name. ' + | ||
'If the {CONTEXT} is a number it is available as {value}.', | ||
values: { | ||
TINYMATH, | ||
CONTEXT: '_context_', | ||
DATATABLE: '`datatable`', | ||
value: '`value`', | ||
TYPE_NUMBER: '`number`', | ||
}, | ||
}), | ||
args: { | ||
expression: { | ||
aliases: ['_'], | ||
types: ['string'], | ||
help: i18n.translate('expressions.functions.math.args.expressionHelpText', { | ||
defaultMessage: 'An evaluated {TINYMATH} expression. See {TINYMATH_URL}.', | ||
values: { | ||
TINYMATH, | ||
TINYMATH_URL, | ||
}, | ||
}), | ||
}, | ||
onError: { | ||
types: ['string'], | ||
options: ['throw', 'false', 'zero', 'null'], | ||
help: i18n.translate('expressions.functions.math.args.onErrorHelpText', { | ||
defaultMessage: | ||
"In case the {TINYMATH} evaluation fails or returns NaN, the return value is specified by onError. When `'throw'`, it will throw an exception, terminating expression execution (default).", | ||
values: { | ||
TINYMATH, | ||
}, | ||
}), | ||
}, | ||
}, | ||
fn: (input, args) => { | ||
const { expression, onError } = args; | ||
const onErrorValue = onError ?? 'throw'; | ||
|
||
if (!expression || expression.trim() === '') { | ||
throw errors.emptyExpression(); | ||
} | ||
|
||
const mathContext = isDatatable(input) | ||
? pivotObjectArray( | ||
input.rows, | ||
input.columns.map((col) => col.name) | ||
) | ||
: { 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; | ||
} | ||
} | ||
}, | ||
}; |
Oops, something went wrong.