[Lens] Allow date functions in formula (#143632)

* allow date functions in formula

* fix tests

* fix test

* fix review comments

* fix test

Co-authored-by: Marco Liberati <dej611@users.noreply.github.com>
This commit is contained in:
Joe Reuter 2022-11-03 11:35:46 +01:00 committed by GitHub
parent 5ceda2b237
commit cc4be7e612
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 70 additions and 13 deletions

View file

@ -14,6 +14,7 @@ import { Datatable, DatatableColumn, DatatableColumnType, getType } from '../../
export type MathColumnArguments = MathArguments & { export type MathColumnArguments = MathArguments & {
id: string; id: string;
name?: string; name?: string;
castColumns?: string[];
copyMetaFrom?: string | null; copyMetaFrom?: string | null;
}; };
@ -52,6 +53,14 @@ export const mathColumn: ExpressionFunctionDefinition<
}), }),
required: true, required: true,
}, },
castColumns: {
types: ['string'],
multi: true,
help: i18n.translate('expressions.functions.mathColumn.args.castColumnsHelpText', {
defaultMessage: 'The ids of columns to cast to numbers before applying the formula',
}),
required: false,
},
copyMetaFrom: { copyMetaFrom: {
types: ['string', 'null'], types: ['string', 'null'],
help: i18n.translate('expressions.functions.mathColumn.args.copyMetaFromHelpText', { help: i18n.translate('expressions.functions.mathColumn.args.copyMetaFromHelpText', {
@ -77,11 +86,31 @@ export const mathColumn: ExpressionFunctionDefinition<
const newRows = await Promise.all( const newRows = await Promise.all(
input.rows.map(async (row) => { input.rows.map(async (row) => {
let preparedRow = row;
if (args.castColumns) {
preparedRow = { ...row };
args.castColumns.forEach((columnId) => {
switch (typeof row[columnId]) {
case 'string':
const parsedAsDate = Number(new Date(preparedRow[columnId]));
if (!isNaN(parsedAsDate)) {
preparedRow[columnId] = parsedAsDate;
return;
} else {
preparedRow[columnId] = Number(preparedRow[columnId]);
return;
}
case 'boolean':
preparedRow[columnId] = Number(preparedRow[columnId]);
return;
}
});
}
const result = await math.fn( const result = await math.fn(
{ {
...input, ...input,
columns: input.columns, columns: input.columns,
rows: [row], rows: [preparedRow],
}, },
{ {
expression: args.expression, expression: args.expression,

View file

@ -353,7 +353,7 @@ describe('math completion', () => {
expect(results.list).toEqual(['bytes', 'memory']); expect(results.list).toEqual(['bytes', 'memory']);
}); });
it('should autocomplete only operations that provide numeric output', async () => { it('should autocomplete only operations that provide numeric or date output', async () => {
const results = await suggest({ const results = await suggest({
expression: 'last_value()', expression: 'last_value()',
zeroIndexedOffset: 11, zeroIndexedOffset: 11,
@ -366,7 +366,7 @@ describe('math completion', () => {
unifiedSearch: unifiedSearchPluginMock.createStartContract(), unifiedSearch: unifiedSearchPluginMock.createStartContract(),
dataViews: dataViewPluginMocks.createStartContract(), dataViews: dataViewPluginMocks.createStartContract(),
}); });
expect(results.list).toEqual(['bytes', 'memory']); expect(results.list).toEqual(['bytes', 'memory', 'timestamp', 'start_date']);
}); });
}); });

View file

@ -306,12 +306,14 @@ function getArgumentSuggestions(
operationDefinitionMap operationDefinitionMap
); );
// TODO: This only allow numeric functions, will reject last_value(string) for example. // TODO: This only allow numeric functions, will reject last_value(string) for example.
const validOperation = available.find( const validOperation = available.filter(
({ operationMetaData }) => ({ operationMetaData }) =>
operationMetaData.dataType === 'number' && !operationMetaData.isBucketed (operationMetaData.dataType === 'number' || operationMetaData.dataType === 'date') &&
!operationMetaData.isBucketed
); );
if (validOperation) { if (validOperation.length) {
const fields = validOperation.operations const fields = validOperation
.flatMap((op) => op.operations)
.filter((op) => op.operationType === operation.type) .filter((op) => op.operationType === operation.type)
.map((op) => ('field' in op ? op.field : undefined)) .map((op) => ('field' in op ? op.field : undefined))
.filter(nonNullable); .filter(nonNullable);

View file

@ -157,7 +157,7 @@ describe('formula', () => {
formulaOperation.buildColumn({ formulaOperation.buildColumn({
previousColumn: { previousColumn: {
...layer.columns.col1, ...layer.columns.col1,
dataType: 'date', dataType: 'boolean',
filter: { language: 'kuery', query: 'ABC: DEF' }, filter: { language: 'kuery', query: 'ABC: DEF' },
}, },
layer, layer,

View file

@ -139,6 +139,11 @@ export const formulaOperation: OperationDefinition<FormulaIndexPatternColumn, 'm
arguments: { arguments: {
id: [columnId], id: [columnId],
name: [label || defaultLabel], name: [label || defaultLabel],
...(currentColumn.references.length
? {
castColumns: [currentColumn.references[0]],
}
: {}),
expression: [currentColumn.references.length ? `"${currentColumn.references[0]}"` : ''], expression: [currentColumn.references.length ? `"${currentColumn.references[0]}"` : ''],
}, },
}, },

View file

@ -54,7 +54,11 @@ export function generateFormula(
previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`; previousFormula += `${previousColumn.operationType}(${metric.operationType}(${fieldName})`;
} }
} else { } else {
if (previousColumn && 'sourceField' in previousColumn && previousColumn.dataType === 'number') { if (
previousColumn &&
'sourceField' in previousColumn &&
(previousColumn.dataType === 'number' || previousColumn.dataType === 'date')
) {
previousFormula += `${previousColumn.operationType}(${getSafeFieldName(previousColumn)}`; previousFormula += `${previousColumn.operationType}(${getSafeFieldName(previousColumn)}`;
} else { } else {
// couldn't find formula function to call, exit early because adding args is going to fail anyway // couldn't find formula function to call, exit early because adding args is going to fail anyway

View file

@ -163,6 +163,7 @@ describe('math operation', () => {
arguments: { arguments: {
id: ['myColumnId'], id: ['myColumnId'],
name: ['Math'], name: ['Math'],
castColumns: [],
expression: [ expression: [
'(((((((((((((((("columnX0" + "columnX1") + "columnX2") + "columnX3") + "columnX4") + "columnX5") + "columnX6") + "columnX7") + "columnX8") + "columnX9") + "columnX10") + "columnX11") + "columnX12") + "columnX13") + "columnX14") + "columnX15") + "columnX16")', '(((((((((((((((("columnX0" + "columnX1") + "columnX2") + "columnX3") + "columnX4") + "columnX5") + "columnX6") + "columnX7") + "columnX8") + "columnX9") + "columnX10") + "columnX11") + "columnX12") + "columnX13") + "columnX14") + "columnX15") + "columnX16")',
], ],
@ -243,6 +244,7 @@ describe('math operation', () => {
arguments: { arguments: {
id: ['myColumnId'], id: ['myColumnId'],
name: ['Math'], name: ['Math'],
castColumns: [],
expression: [ expression: [
`("columnX0" + (("columnX1" - "columnX2") / ("columnX3" - ("columnX4" * "columnX5"))))`, `("columnX0" + (("columnX1" - "columnX2") / ("columnX3" - ("columnX4" * "columnX5"))))`,
], ],
@ -298,6 +300,7 @@ describe('math operation', () => {
arguments: { arguments: {
id: ['myColumnId'], id: ['myColumnId'],
name: ['Math'], name: ['Math'],
castColumns: [],
expression: [`max(min("columnX0","columnX1"),abs("columnX2"))`], expression: [`max(min("columnX0","columnX1"),abs("columnX2"))`],
onError: ['null'], onError: ['null'],
}, },
@ -342,6 +345,7 @@ describe('math operation', () => {
arguments: { arguments: {
id: ['myColumnId'], id: ['myColumnId'],
name: ['Math'], name: ['Math'],
castColumns: [],
expression: [`(5 + (3 / 8))`], expression: [`(5 + (3 / 8))`],
onError: ['null'], onError: ['null'],
}, },
@ -425,6 +429,7 @@ describe('math operation', () => {
arguments: { arguments: {
id: ['myColumnId'], id: ['myColumnId'],
name: ['Math'], name: ['Math'],
castColumns: [],
expression: [ expression: [
'ifelse(("columnX0" == 0),ifelse(("columnX1" < 0),ifelse(("columnX2" <= 0),"columnX3","columnX4"),"columnX5"),ifelse(("columnX6" > 0),ifelse(("columnX7" >= 0),"columnX8","columnX9"),"columnX10"))', 'ifelse(("columnX0" == 0),ifelse(("columnX1" < 0),ifelse(("columnX2" <= 0),"columnX3","columnX4"),"columnX5"),ifelse(("columnX6" > 0),ifelse(("columnX7" >= 0),"columnX8","columnX9"),"columnX10"))',
], ],

View file

@ -46,6 +46,8 @@ export const mathOperation: OperationDefinition<MathIndexPatternColumn, 'managed
name: [column.label], name: [column.label],
expression: [astToString(column.params.tinymathAst)], expression: [astToString(column.params.tinymathAst)],
onError: ['null'], onError: ['null'],
// cast everything into number
castColumns: column.references,
}, },
}, },
]; ];

View file

@ -319,7 +319,7 @@ describe('last_value', () => {
).toEqual({ ).toEqual({
dataType: 'ip', dataType: 'ip',
isBucketed: false, isBucketed: false,
scale: 'ratio', scale: 'ordinal',
}); });
}); });

View file

@ -124,6 +124,16 @@ function getExistsFilter(field: string) {
}; };
} }
function getScale(type: string) {
return type === 'string' ||
type === 'ip' ||
type === 'ip_range' ||
type === 'date_range' ||
type === 'number_range'
? 'ordinal'
: 'ratio';
}
export const lastValueOperation: OperationDefinition< export const lastValueOperation: OperationDefinition<
LastValueIndexPatternColumn, LastValueIndexPatternColumn,
'field', 'field',
@ -155,7 +165,7 @@ export const lastValueOperation: OperationDefinition<
label: ofName(field.displayName, oldColumn.timeShift, oldColumn.reducedTimeRange), label: ofName(field.displayName, oldColumn.timeShift, oldColumn.reducedTimeRange),
sourceField: field.name, sourceField: field.name,
params: newParams, params: newParams,
scale: field.type === 'string' ? 'ordinal' : 'ratio', scale: getScale(field.type),
filter: filter:
oldColumn.filter && isEqual(oldColumn.filter, getExistsFilter(oldColumn.sourceField)) oldColumn.filter && isEqual(oldColumn.filter, getExistsFilter(oldColumn.sourceField))
? getExistsFilter(field.name) ? getExistsFilter(field.name)
@ -167,7 +177,7 @@ export const lastValueOperation: OperationDefinition<
return { return {
dataType: type as DataType, dataType: type as DataType,
isBucketed: false, isBucketed: false,
scale: type === 'string' ? 'ordinal' : 'ratio', scale: getScale(type),
}; };
} }
}, },
@ -218,7 +228,7 @@ export const lastValueOperation: OperationDefinition<
dataType: field.type as DataType, dataType: field.type as DataType,
operationType: 'last_value', operationType: 'last_value',
isBucketed: false, isBucketed: false,
scale: field.type === 'string' ? 'ordinal' : 'ratio', scale: getScale(field.type),
sourceField: field.name, sourceField: field.name,
filter: getFilter(previousColumn, columnParams) || getExistsFilter(field.name), filter: getFilter(previousColumn, columnParams) || getExistsFilter(field.name),
timeShift: columnParams?.shift || previousColumn?.timeShift, timeShift: columnParams?.shift || previousColumn?.timeShift,