[Lens] Add percentile function (#86490) (#86914)

This commit is contained in:
Joe Reuter 2020-12-23 23:48:25 +01:00 committed by GitHub
parent a3a78e1d94
commit b9c61dc4f2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 631 additions and 56 deletions

View file

@ -1329,8 +1329,10 @@ describe('IndexPatternDimensionEditorPanel', () => {
'Median',
'Minimum',
'Moving average',
'Percentile',
'Sum',
'Unique count',
'\u00a0',
]);
});

View file

@ -474,6 +474,53 @@ describe('IndexPattern Data Source', () => {
expect(ast.chain[0].arguments.timeFields).toEqual(['timestamp', 'another_datefield']);
});
it('should add the suffix to the remap column id if provided by the operation', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',
layers: {
first: {
indexPatternId: '1',
columnOrder: ['def', 'abc'],
columns: {
abc: {
label: '23rd percentile',
dataType: 'number',
isBucketed: false,
sourceField: 'bytes',
operationType: 'percentile',
params: {
percentile: 23,
},
},
def: {
label: 'Terms',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
sourceField: 'source',
params: {
size: 5,
orderBy: {
type: 'alphabetical',
},
orderDirection: 'asc',
},
},
},
},
},
};
const state = enrichBaseState(queryBaseState);
const ast = indexPatternDatasource.toExpression(state, 'first') as Ast;
expect(Object.keys(JSON.parse(ast.chain[1].arguments.idMap[0] as string))).toEqual([
'col-0-def',
// col-1 is the auto naming of esasggs, abc is the specified column id, .23 is the generated suffix
'col-1-abc.23',
]);
});
it('should add time_scale and format function if time scale is set and supported', async () => {
const queryBaseState: IndexPatternBaseState = {
currentIndexPatternId: '1',

View file

@ -19,7 +19,7 @@ import {
hasDateField,
} from './utils';
import { updateColumnParam } from '../../layer_helpers';
import { useDebounceWithOptions } from '../helpers';
import { isValidNumber, useDebounceWithOptions } from '../helpers';
import { adjustTimeScaleOnOtherColumnChange } from '../../time_scale_utils';
import type { OperationDefinition, ParamEditorProps } from '..';
@ -122,19 +122,6 @@ export const movingAverageOperation: OperationDefinition<
timeScalingMode: 'optional',
};
function isValidNumber(input: string) {
if (input === '') return false;
try {
const val = parseFloat(input);
if (isNaN(val)) return false;
if (val < 1) return false;
if (val.toString().includes('.')) return false;
} catch (e) {
return false;
}
return true;
}
function MovingAverageParamEditor({
layer,
updateLayer,
@ -145,7 +132,7 @@ function MovingAverageParamEditor({
useDebounceWithOptions(
() => {
if (!isValidNumber(inputValue)) return;
if (!isValidNumber(inputValue, true, undefined, 1)) return;
const inputNumber = parseInt(inputValue, 10);
updateLayer(
updateColumnParam({

View file

@ -199,7 +199,8 @@ describe('date_histogram', () => {
const esAggsFn = dateHistogramOperation.toEsAggsFn(
layer.columns.col1 as DateHistogramIndexPatternColumn,
'col1',
indexPattern1
indexPattern1,
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({
@ -250,7 +251,8 @@ describe('date_histogram', () => {
},
},
]),
}
},
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({

View file

@ -83,7 +83,8 @@ describe('filters', () => {
const esAggsFn = filtersOperation.toEsAggsFn(
layer.columns.col1 as FiltersIndexPatternColumn,
'col1',
createMockedIndexPattern()
createMockedIndexPattern(),
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({

View file

@ -7,7 +7,7 @@
import { useRef } from 'react';
import useDebounce from 'react-use/lib/useDebounce';
import { i18n } from '@kbn/i18n';
import { operationDefinitionMap } from '.';
import { IndexPatternColumn, operationDefinitionMap } from '.';
import { FieldBasedIndexPatternColumn } from './column_types';
import { IndexPattern } from '../../types';
@ -63,6 +63,13 @@ export function getInvalidFieldMessage(
: undefined;
}
export function getEsAggsSuffix(column: IndexPatternColumn) {
const operationDefinition = operationDefinitionMap[column.operationType];
return operationDefinition.input === 'field' && operationDefinition.getEsAggsSuffix
? operationDefinition.getEsAggsSuffix(column)
: '';
}
export function getSafeName(name: string, indexPattern: IndexPattern): string {
const field = indexPattern.getFieldByName(name);
return field
@ -71,3 +78,22 @@ export function getSafeName(name: string, indexPattern: IndexPattern): string {
defaultMessage: 'Missing field',
});
}
export function isValidNumber(
inputValue: string | number | null | undefined,
integer?: boolean,
upperBound?: number,
lowerBound?: number
) {
const inputValueAsNumber = Number(inputValue);
return (
inputValue !== '' &&
inputValue !== null &&
inputValue !== undefined &&
!Number.isNaN(inputValueAsNumber) &&
Number.isFinite(inputValueAsNumber) &&
(!integer || Number.isInteger(inputValueAsNumber)) &&
(upperBound === undefined || inputValueAsNumber <= upperBound) &&
(lowerBound === undefined || inputValueAsNumber >= lowerBound)
);
}

View file

@ -9,6 +9,7 @@ import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { termsOperation, TermsIndexPatternColumn } from './terms';
import { filtersOperation, FiltersIndexPatternColumn } from './filters';
import { cardinalityOperation, CardinalityIndexPatternColumn } from './cardinality';
import { percentileOperation, PercentileIndexPatternColumn } from './percentile';
import {
minOperation,
MinIndexPatternColumn,
@ -58,6 +59,7 @@ export type IndexPatternColumn =
| CardinalityIndexPatternColumn
| SumIndexPatternColumn
| MedianIndexPatternColumn
| PercentileIndexPatternColumn
| CountIndexPatternColumn
| LastValueIndexPatternColumn
| CumulativeSumIndexPatternColumn
@ -82,6 +84,7 @@ const internalOperationDefinitions = [
cardinalityOperation,
sumOperation,
medianOperation,
percentileOperation,
lastValueOperation,
countOperation,
rangeOperation,
@ -96,6 +99,7 @@ export { rangeOperation } from './ranges';
export { filtersOperation } from './filters';
export { dateHistogramOperation } from './date_histogram';
export { minOperation, averageOperation, sumOperation, maxOperation } from './metrics';
export { percentileOperation } from './percentile';
export { countOperation } from './count';
export { lastValueOperation } from './last_value';
export {
@ -223,7 +227,12 @@ interface FieldlessOperationDefinition<C extends BaseIndexPatternColumn> {
* Function turning a column into an agg config passed to the `esaggs` function
* together with the agg configs returned from other columns.
*/
toEsAggsFn: (column: C, columnId: string, indexPattern: IndexPattern) => ExpressionAstFunction;
toEsAggsFn: (
column: C,
columnId: string,
indexPattern: IndexPattern,
layer: IndexPatternLayer
) => ExpressionAstFunction;
}
interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
@ -262,7 +271,19 @@ interface FieldBasedOperationDefinition<C extends BaseIndexPatternColumn> {
* Function turning a column into an agg config passed to the `esaggs` function
* together with the agg configs returned from other columns.
*/
toEsAggsFn: (column: C, columnId: string, indexPattern: IndexPattern) => ExpressionAstFunction;
toEsAggsFn: (
column: C,
columnId: string,
indexPattern: IndexPattern,
layer: IndexPatternLayer
) => ExpressionAstFunction;
/**
* Optional function to return the suffix used for ES bucket paths and esaggs column id.
* This is relevant for multi metrics to pick the right value.
*
* @param column The current column
*/
getEsAggsSuffix?: (column: C) => string;
/**
* Validate that the operation has the right preconditions in the state. For example:
*

View file

@ -69,7 +69,8 @@ describe('last_value', () => {
const esAggsFn = lastValueOperation.toEsAggsFn(
{ ...lastValueColumn, params: { ...lastValueColumn.params } },
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({

View file

@ -83,21 +83,24 @@ function buildMetricOperation<T extends MetricColumn<string>>({
},
onOtherColumnChanged: (layer, thisColumnId, changedColumnId) =>
optionalTimeScaling
? adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId)
: layer.columns[thisColumnId],
? (adjustTimeScaleOnOtherColumnChange(layer, thisColumnId, changedColumnId) as T)
: (layer.columns[thisColumnId] as T),
getDefaultLabel: (column, indexPattern, columns) =>
labelLookup(getSafeName(column.sourceField, indexPattern), column),
buildColumn: ({ field, previousColumn }) => ({
label: labelLookup(field.displayName, previousColumn),
dataType: 'number',
operationType: type,
sourceField: field.name,
isBucketed: false,
scale: 'ratio',
timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined,
params:
previousColumn && previousColumn.dataType === 'number' ? previousColumn.params : undefined,
}),
buildColumn: ({ field, previousColumn }) =>
({
label: labelLookup(field.displayName, previousColumn),
dataType: 'number',
operationType: type,
sourceField: field.name,
isBucketed: false,
scale: 'ratio',
timeScale: optionalTimeScaling ? previousColumn?.timeScale : undefined,
params:
previousColumn && previousColumn.dataType === 'number'
? previousColumn.params
: undefined,
} as T),
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,

View file

@ -0,0 +1,237 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallow, mount } from 'enzyme';
import { IUiSettingsClient, SavedObjectsClientContract, HttpSetup } from 'kibana/public';
import { IStorageWrapper } from 'src/plugins/kibana_utils/public';
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
import { createMockedIndexPattern } from '../../mocks';
import { percentileOperation } from './index';
import { IndexPattern, IndexPatternLayer } from '../../types';
import { PercentileIndexPatternColumn } from './percentile';
import { EuiFieldNumber } from '@elastic/eui';
import { act } from 'react-dom/test-utils';
import { EuiFormRow } from '@elastic/eui';
const defaultProps = {
storage: {} as IStorageWrapper,
uiSettings: {} as IUiSettingsClient,
savedObjectsClient: {} as SavedObjectsClientContract,
dateRange: { fromDate: 'now-1d', toDate: 'now' },
data: dataPluginMock.createStartContract(),
http: {} as HttpSetup,
indexPattern: {
...createMockedIndexPattern(),
hasRestrictions: false,
} as IndexPattern,
};
describe('percentile', () => {
let layer: IndexPatternLayer;
const InlineOptions = percentileOperation.paramEditor!;
beforeEach(() => {
layer = {
indexPatternId: '1',
columnOrder: ['col1', 'col2'],
columns: {
col1: {
label: 'Top value of category',
dataType: 'string',
isBucketed: true,
operationType: 'terms',
params: {
orderBy: { type: 'alphabetical' },
size: 3,
orderDirection: 'asc',
},
sourceField: 'category',
},
col2: {
label: '23rd percentile of a',
dataType: 'number',
isBucketed: false,
sourceField: 'a',
operationType: 'percentile',
params: {
percentile: 23,
},
},
},
};
});
describe('toEsAggsFn', () => {
it('should reflect params correctly', () => {
const percentileColumn = layer.columns.col2 as PercentileIndexPatternColumn;
const esAggsFn = percentileOperation.toEsAggsFn(
percentileColumn,
'col1',
{} as IndexPattern,
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({
arguments: expect.objectContaining({
percents: [23],
field: ['a'],
}),
})
);
});
});
describe('onFieldChange', () => {
it('should change correctly to new field', () => {
const oldColumn: PercentileIndexPatternColumn = {
operationType: 'percentile',
sourceField: 'bytes',
label: '23rd percentile of bytes',
isBucketed: true,
dataType: 'number',
params: {
percentile: 23,
},
};
const indexPattern = createMockedIndexPattern();
const newNumberField = indexPattern.getFieldByName('memory')!;
const column = percentileOperation.onFieldChange(oldColumn, newNumberField);
expect(column).toEqual(
expect.objectContaining({
dataType: 'number',
sourceField: 'memory',
params: expect.objectContaining({
percentile: 23,
}),
})
);
expect(column.label).toContain('memory');
});
});
describe('buildColumn', () => {
it('should set default percentile', () => {
const indexPattern = createMockedIndexPattern();
const bytesField = indexPattern.fields.find(({ name }) => name === 'bytes')!;
bytesField.displayName = 'test';
const percentileColumn = percentileOperation.buildColumn({
indexPattern,
field: bytesField,
layer: { columns: {}, columnOrder: [], indexPatternId: '' },
});
expect(percentileColumn.dataType).toEqual('number');
expect(percentileColumn.params.percentile).toEqual(95);
expect(percentileColumn.label).toEqual('95th percentile of test');
});
});
describe('param editor', () => {
it('should render current percentile', () => {
const updateLayerSpy = jest.fn();
const instance = shallow(
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
/>
);
const input = instance.find('[data-test-subj="lns-indexPattern-percentile-input"]');
expect(input.prop('value')).toEqual('23');
});
it('should update state on change', async () => {
jest.useFakeTimers();
const updateLayerSpy = jest.fn();
const instance = mount(
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
/>
);
jest.runAllTimers();
const input = instance
.find('[data-test-subj="lns-indexPattern-percentile-input"]')
.find(EuiFieldNumber);
await act(async () => {
input.prop('onChange')!({ target: { value: '27' } } as React.ChangeEvent<HTMLInputElement>);
});
instance.update();
jest.runAllTimers();
expect(updateLayerSpy).toHaveBeenCalledWith({
...layer,
columns: {
...layer.columns,
col2: {
...layer.columns.col2,
params: {
percentile: 27,
},
label: '27th percentile of a',
},
},
});
});
it('should not update on invalid input, but show invalid value locally', async () => {
const updateLayerSpy = jest.fn();
const instance = mount(
<InlineOptions
{...defaultProps}
layer={layer}
updateLayer={updateLayerSpy}
columnId="col2"
currentColumn={layer.columns.col2 as PercentileIndexPatternColumn}
/>
);
jest.runAllTimers();
const input = instance
.find('[data-test-subj="lns-indexPattern-percentile-input"]')
.find(EuiFieldNumber);
await act(async () => {
input.prop('onChange')!({
target: { value: '12.12' },
} as React.ChangeEvent<HTMLInputElement>);
});
instance.update();
jest.runAllTimers();
expect(updateLayerSpy).not.toHaveBeenCalled();
expect(
instance
.find('[data-test-subj="lns-indexPattern-percentile-form"]')
.find(EuiFormRow)
.prop('isInvalid')
).toEqual(true);
expect(
instance
.find('[data-test-subj="lns-indexPattern-percentile-input"]')
.find(EuiFieldNumber)
.prop('value')
).toEqual('12.12');
});
});
});

View file

@ -0,0 +1,189 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiFieldNumber, EuiFormRow } from '@elastic/eui';
import React, { useCallback, useState } from 'react';
import { i18n } from '@kbn/i18n';
import { AggFunctionsMapping } from 'src/plugins/data/public';
import { buildExpressionFunction } from '../../../../../../../src/plugins/expressions/public';
import { OperationDefinition } from './index';
import {
getInvalidFieldMessage,
getSafeName,
isValidNumber,
useDebounceWithOptions,
} from './helpers';
import { FieldBasedIndexPatternColumn } from './column_types';
export interface PercentileIndexPatternColumn extends FieldBasedIndexPatternColumn {
operationType: 'percentile';
params: {
percentile: number;
format?: {
id: string;
params?: {
decimals: number;
};
};
};
}
function ofName(name: string, percentile: number) {
return i18n.translate('xpack.lens.indexPattern.percentileOf', {
defaultMessage:
'{percentile, selectordinal, one {#st} two {#nd} few {#rd} other {#th}} percentile of {name}',
values: { name, percentile },
});
}
const DEFAULT_PERCENTILE_VALUE = 95;
export const percentileOperation: OperationDefinition<PercentileIndexPatternColumn, 'field'> = {
type: 'percentile',
displayName: i18n.translate('xpack.lens.indexPattern.percentile', {
defaultMessage: 'Percentile',
}),
input: 'field',
getPossibleOperationForField: ({ aggregationRestrictions, aggregatable, type: fieldType }) => {
if (fieldType === 'number' && aggregatable && !aggregationRestrictions) {
return {
dataType: 'number',
isBucketed: false,
scale: 'ratio',
};
}
},
isTransferable: (column, newIndexPattern) => {
const newField = newIndexPattern.getFieldByName(column.sourceField);
return Boolean(
newField &&
newField.type === 'number' &&
newField.aggregatable &&
!newField.aggregationRestrictions
);
},
getDefaultLabel: (column, indexPattern, columns) =>
ofName(getSafeName(column.sourceField, indexPattern), column.params.percentile),
buildColumn: ({ field, previousColumn, indexPattern }) => {
const existingFormat =
previousColumn?.params && 'format' in previousColumn?.params
? previousColumn?.params?.format
: undefined;
const existingPercentileParam =
previousColumn?.operationType === 'percentile' && previousColumn?.params.percentile;
const newPercentileParam = existingPercentileParam || DEFAULT_PERCENTILE_VALUE;
return {
label: ofName(getSafeName(field.name, indexPattern), newPercentileParam),
dataType: 'number',
operationType: 'percentile',
sourceField: field.name,
isBucketed: false,
scale: 'ratio',
params: {
format: existingFormat,
percentile: newPercentileParam,
},
};
},
onFieldChange: (oldColumn, field) => {
return {
...oldColumn,
label: ofName(field.displayName, oldColumn.params.percentile),
sourceField: field.name,
};
},
toEsAggsFn: (column, columnId, _indexPattern) => {
return buildExpressionFunction<AggFunctionsMapping['aggPercentiles']>('aggPercentiles', {
id: columnId,
enabled: true,
schema: 'metric',
field: column.sourceField,
percents: [column.params.percentile],
}).toAst();
},
getEsAggsSuffix: (column) => {
const value = column.params.percentile;
return `.${value}`;
},
getErrorMessage: (layer, columnId, indexPattern) =>
getInvalidFieldMessage(layer.columns[columnId] as FieldBasedIndexPatternColumn, indexPattern),
paramEditor: function PercentileParamEditor({
layer,
updateLayer,
currentColumn,
columnId,
indexPattern,
}) {
const [inputValue, setInputValue] = useState(String(currentColumn.params.percentile));
const inputValueAsNumber = Number(inputValue);
// an input is value if it's not an empty string, parses to a valid number, is between 0 and 100 (exclusive)
// and is an integer
const inputValueIsValid = isValidNumber(inputValue, true, 99, 1);
useDebounceWithOptions(
() => {
if (!inputValueIsValid) return;
updateLayer({
...layer,
columns: {
...layer.columns,
[columnId]: {
...currentColumn,
label: currentColumn.customLabel
? currentColumn.label
: ofName(
indexPattern.getFieldByName(currentColumn.sourceField)?.displayName ||
currentColumn.sourceField,
inputValueAsNumber
),
params: {
...currentColumn.params,
percentile: inputValueAsNumber,
},
},
},
});
},
{ skipFirstRender: true },
256,
[inputValue]
);
const handleInputChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
const val = String(e.target.value);
setInputValue(val);
}, []);
return (
<EuiFormRow
label={i18n.translate('xpack.lens.indexPattern.percentile.percentileValue', {
defaultMessage: 'Percentile',
})}
data-test-subj="lns-indexPattern-percentile-form"
display="columnCompressed"
fullWidth
isInvalid={!inputValueIsValid}
error={
!inputValueIsValid &&
i18n.translate('xpack.lens.indexPattern.percentile.errorMessage', {
defaultMessage: 'Percentile has to be an integer between 1 and 99',
})
}
>
<EuiFieldNumber
data-test-subj="lns-indexPattern-percentile-input"
compressed
value={inputValue}
min={1}
max={99}
step={1}
onChange={handleInputChange}
/>
</EuiFormRow>
);
},
};

View file

@ -22,7 +22,7 @@ import {
keys,
} from '@elastic/eui';
import { IFieldFormat } from '../../../../../../../../src/plugins/data/common';
import { RangeTypeLens, isValidRange, isValidNumber } from './ranges';
import { RangeTypeLens, isValidRange } from './ranges';
import { FROM_PLACEHOLDER, TO_PLACEHOLDER, TYPING_DEBOUNCE_TIME } from './constants';
import {
NewBucketButton,
@ -30,7 +30,7 @@ import {
DraggableBucketContainer,
LabelInput,
} from '../shared_components';
import { useDebounceWithOptions } from '../helpers';
import { isValidNumber, useDebounceWithOptions } from '../helpers';
const generateId = htmlIdGenerator();

View file

@ -142,7 +142,8 @@ describe('ranges', () => {
const esAggsFn = rangeOperation.toEsAggsFn(
layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect(esAggsFn).toMatchInlineSnapshot(`
Object {
@ -184,7 +185,8 @@ describe('ranges', () => {
const esAggsFn = rangeOperation.toEsAggsFn(
layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect(esAggsFn).toEqual(
@ -203,7 +205,8 @@ describe('ranges', () => {
const esAggsFn = rangeOperation.toEsAggsFn(
layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect(esAggsFn).toEqual(
@ -222,7 +225,8 @@ describe('ranges', () => {
const esAggsFn = rangeOperation.toEsAggsFn(
layer.columns.col1 as RangeIndexPatternColumn,
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect((esAggsFn as { arguments: unknown }).arguments).toEqual(

View file

@ -19,7 +19,7 @@ import { updateColumnParam } from '../../layer_helpers';
import { supportedFormats } from '../../../format_column';
import { MODES, AUTO_BARS, DEFAULT_INTERVAL, MIN_HISTOGRAM_BARS, SLICES } from './constants';
import { IndexPattern, IndexPatternField } from '../../../types';
import { getInvalidFieldMessage } from '../helpers';
import { getInvalidFieldMessage, isValidNumber } from '../helpers';
type RangeType = Omit<Range, 'type'>;
// Try to cover all possible serialized states for ranges
@ -52,10 +52,6 @@ export type UpdateParamsFnType = <K extends keyof RangeColumnParams>(
value: RangeColumnParams[K]
) => void;
// on initialization values can be null (from the Infinity serialization), so handle it correctly
// or they will be casted to 0 by the editor ( see #78867 )
export const isValidNumber = (value: number | '' | null): value is number =>
value != null && value !== '' && !isNaN(value) && isFinite(value);
export const isRangeWithin = (range: RangeType): boolean => range.from <= range.to;
const isFullRange = (range: RangeTypeLens): range is FullRangeTypeLens =>
isValidNumber(range.from) && isValidNumber(range.to);
@ -152,10 +148,10 @@ export const rangeOperation: OperationDefinition<RangeIndexPatternColumn, 'field
const partialRange: Partial<RangeType> = { label: range.label };
// be careful with the fields to set on partial ranges
if (isValidNumber(range.from)) {
partialRange.from = range.from;
partialRange.from = Number(range.from);
}
if (isValidNumber(range.to)) {
partialRange.to = range.to;
partialRange.to = Number(range.to);
}
return partialRange;
})

View file

@ -23,7 +23,7 @@ import { DataType } from '../../../../types';
import { OperationDefinition } from '../index';
import { FieldBasedIndexPatternColumn } from '../column_types';
import { ValuesRangeInput } from './values_range_input';
import { getInvalidFieldMessage } from '../helpers';
import { getEsAggsSuffix, getInvalidFieldMessage } from '../helpers';
import type { IndexPatternLayer } from '../../../types';
function ofName(name?: string) {
@ -119,7 +119,10 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
params: {
size: previousBucketsLength === 0 ? 5 : DEFAULT_SIZE,
orderBy: existingMetricColumn
? { type: 'column', columnId: existingMetricColumn }
? {
type: 'column',
columnId: existingMetricColumn,
}
: { type: 'alphabetical' },
orderDirection: existingMetricColumn ? 'desc' : 'asc',
otherBucket: !indexPattern.hasRestrictions,
@ -127,14 +130,18 @@ export const termsOperation: OperationDefinition<TermsIndexPatternColumn, 'field
},
};
},
toEsAggsFn: (column, columnId, _indexPattern) => {
toEsAggsFn: (column, columnId, _indexPattern, layer) => {
return buildExpressionFunction<AggFunctionsMapping['aggTerms']>('aggTerms', {
id: columnId,
enabled: true,
schema: 'segment',
field: column.sourceField,
orderBy:
column.params.orderBy.type === 'alphabetical' ? '_key' : column.params.orderBy.columnId,
column.params.orderBy.type === 'alphabetical'
? '_key'
: `${column.params.orderBy.columnId}${getEsAggsSuffix(
layer.columns[column.params.orderBy.columnId]
)}`,
order: column.params.orderDirection,
size: column.params.size,
otherBucket: Boolean(column.params.otherBucket),

View file

@ -65,7 +65,8 @@ describe('terms', () => {
const esAggsFn = termsOperation.toEsAggsFn(
{ ...termsColumn, params: { ...termsColumn.params, otherBucket: true } },
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({
@ -87,7 +88,8 @@ describe('terms', () => {
params: { ...termsColumn.params, otherBucket: false, missingBucket: true },
},
'col1',
{} as IndexPattern
{} as IndexPattern,
layer
);
expect(esAggsFn).toEqual(
expect.objectContaining({
@ -98,6 +100,45 @@ describe('terms', () => {
})
);
});
it('should include esaggs suffix from other columns in orderby argument', () => {
const termsColumn = layer.columns.col1 as TermsIndexPatternColumn;
const esAggsFn = termsOperation.toEsAggsFn(
{
...termsColumn,
params: {
...termsColumn.params,
otherBucket: true,
orderBy: { type: 'column', columnId: 'abcde' },
},
},
'col1',
{} as IndexPattern,
{
...layer,
columns: {
...layer.columns,
abcde: {
dataType: 'number',
isBucketed: false,
operationType: 'percentile',
sourceField: 'abc',
label: '',
params: {
percentile: 12,
},
},
},
}
);
expect(esAggsFn).toEqual(
expect.objectContaining({
arguments: expect.objectContaining({
orderBy: ['abcde.12'],
}),
})
);
});
});
describe('onFieldChange', () => {

View file

@ -293,6 +293,11 @@ describe('getOperationTypesForField', () => {
"operationType": "median",
"type": "field",
},
Object {
"field": "bytes",
"operationType": "percentile",
"type": "field",
},
Object {
"field": "bytes",
"operationType": "last_value",

View file

@ -20,6 +20,7 @@ import { operationDefinitionMap } from './operations';
import { IndexPattern, IndexPatternPrivateState, IndexPatternLayer } from './types';
import { OriginalColumn } from './rename_columns';
import { dateHistogramOperation } from './operations/definitions';
import { getEsAggsSuffix } from './operations/definitions/helpers';
function getExpressionForLayer(
layer: IndexPatternLayer,
@ -41,15 +42,20 @@ function getExpressionForLayer(
expressions.push(...def.toExpression(layer, colId, indexPattern));
} else {
aggs.push(
buildExpression({ type: 'expression', chain: [def.toEsAggsFn(col, colId, indexPattern)] })
buildExpression({
type: 'expression',
chain: [def.toEsAggsFn(col, colId, indexPattern, layer)],
})
);
}
});
const idMap = columnEntries.reduce((currentIdMap, [colId, column], index) => {
const esAggsId = `col-${columnEntries.length === 1 ? 0 : index}-${colId}`;
const suffix = getEsAggsSuffix(column);
return {
...currentIdMap,
[`col-${columnEntries.length === 1 ? 0 : index}-${colId}`]: {
[`${esAggsId}${suffix}`]: {
...column,
id: colId,
},