[Transform] Support for the top_metrics
aggregation (#101152)
* [ML] init top_metrics agg * [ML] support sort * [ML] support _score sorting * [ML] support sort mode * [ML] support numeric type sorting * [ML] update field label, hide additional sorting controls * [ML] preserve advanced config * [ML] update agg fields after runtime fields edit * [ML] fix TS issue with EuiButtonGroup * [ML] fix Field label * [ML] refactor setUiConfig * [ML] update unit tests * [ML] wrap advanced sorting settings with accordion * [ML] config validation with tests * [ML] fix preserving of the unsupported config * [ML] update translation message * [ML] fix level of the custom config * [ML] preserve unsupported config for sorting
This commit is contained in:
parent
93df9a32a4
commit
9810a72720
|
@ -17,6 +17,7 @@ export const PIVOT_SUPPORTED_AGGS = {
|
|||
SUM: 'sum',
|
||||
VALUE_COUNT: 'value_count',
|
||||
FILTER: 'filter',
|
||||
TOP_METRICS: 'top_metrics',
|
||||
} as const;
|
||||
|
||||
export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS];
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getAggConfigFromEsAgg } from './pivot_aggs';
|
||||
import { getAggConfigFromEsAgg, isSpecialSortField } from './pivot_aggs';
|
||||
import {
|
||||
FilterAggForm,
|
||||
FilterTermForm,
|
||||
|
@ -67,3 +67,12 @@ describe('getAggConfigFromEsAgg', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSpecialSortField', () => {
|
||||
test('detects special sort field', () => {
|
||||
expect(isSpecialSortField('_score')).toBe(true);
|
||||
});
|
||||
test('rejects special fields that not supported yet', () => {
|
||||
expect(isSpecialSortField('_doc')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { FC } from 'react';
|
||||
|
||||
import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
|
||||
import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common';
|
||||
|
||||
import type { AggName } from '../../../common/types/aggregations';
|
||||
import type { Dictionary } from '../../../common/types/common';
|
||||
|
@ -43,6 +43,7 @@ export const pivotAggsFieldSupport = {
|
|||
PIVOT_SUPPORTED_AGGS.CARDINALITY,
|
||||
PIVOT_SUPPORTED_AGGS.VALUE_COUNT,
|
||||
PIVOT_SUPPORTED_AGGS.FILTER,
|
||||
PIVOT_SUPPORTED_AGGS.TOP_METRICS,
|
||||
],
|
||||
[KBN_FIELD_TYPES.MURMUR3]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER],
|
||||
[KBN_FIELD_TYPES.NUMBER]: [
|
||||
|
@ -54,17 +55,78 @@ export const pivotAggsFieldSupport = {
|
|||
PIVOT_SUPPORTED_AGGS.SUM,
|
||||
PIVOT_SUPPORTED_AGGS.VALUE_COUNT,
|
||||
PIVOT_SUPPORTED_AGGS.FILTER,
|
||||
PIVOT_SUPPORTED_AGGS.TOP_METRICS,
|
||||
],
|
||||
[KBN_FIELD_TYPES.STRING]: [
|
||||
PIVOT_SUPPORTED_AGGS.CARDINALITY,
|
||||
PIVOT_SUPPORTED_AGGS.VALUE_COUNT,
|
||||
PIVOT_SUPPORTED_AGGS.FILTER,
|
||||
PIVOT_SUPPORTED_AGGS.TOP_METRICS,
|
||||
],
|
||||
[KBN_FIELD_TYPES._SOURCE]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER],
|
||||
[KBN_FIELD_TYPES.UNKNOWN]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER],
|
||||
[KBN_FIELD_TYPES.CONFLICT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER],
|
||||
};
|
||||
|
||||
export const TOP_METRICS_SORT_FIELD_TYPES = [
|
||||
KBN_FIELD_TYPES.NUMBER,
|
||||
KBN_FIELD_TYPES.DATE,
|
||||
KBN_FIELD_TYPES.GEO_POINT,
|
||||
];
|
||||
|
||||
export const SORT_DIRECTION = {
|
||||
ASC: 'asc',
|
||||
DESC: 'desc',
|
||||
} as const;
|
||||
|
||||
export type SortDirection = typeof SORT_DIRECTION[keyof typeof SORT_DIRECTION];
|
||||
|
||||
export const SORT_MODE = {
|
||||
MIN: 'min',
|
||||
MAX: 'max',
|
||||
AVG: 'avg',
|
||||
SUM: 'sum',
|
||||
MEDIAN: 'median',
|
||||
} as const;
|
||||
|
||||
export const NUMERIC_TYPES_OPTIONS = {
|
||||
[KBN_FIELD_TYPES.NUMBER]: [ES_FIELD_TYPES.DOUBLE, ES_FIELD_TYPES.LONG],
|
||||
[KBN_FIELD_TYPES.DATE]: [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS],
|
||||
};
|
||||
|
||||
export type KbnNumericType = typeof KBN_FIELD_TYPES.NUMBER | typeof KBN_FIELD_TYPES.DATE;
|
||||
|
||||
const SORT_NUMERIC_FIELD_TYPES = [
|
||||
ES_FIELD_TYPES.DOUBLE,
|
||||
ES_FIELD_TYPES.LONG,
|
||||
ES_FIELD_TYPES.DATE,
|
||||
ES_FIELD_TYPES.DATE_NANOS,
|
||||
] as const;
|
||||
|
||||
export type SortNumericFieldType = typeof SORT_NUMERIC_FIELD_TYPES[number];
|
||||
|
||||
export type SortMode = typeof SORT_MODE[keyof typeof SORT_MODE];
|
||||
|
||||
export const TOP_METRICS_SPECIAL_SORT_FIELDS = {
|
||||
_SCORE: '_score',
|
||||
} as const;
|
||||
|
||||
export const isSpecialSortField = (sortField: unknown) => {
|
||||
return Object.values(TOP_METRICS_SPECIAL_SORT_FIELDS).some((v) => v === sortField);
|
||||
};
|
||||
|
||||
export const isValidSortDirection = (arg: unknown): arg is SortDirection => {
|
||||
return Object.values(SORT_DIRECTION).some((v) => v === arg);
|
||||
};
|
||||
|
||||
export const isValidSortMode = (arg: unknown): arg is SortMode => {
|
||||
return Object.values(SORT_MODE).some((v) => v === arg);
|
||||
};
|
||||
|
||||
export const isValidSortNumericType = (arg: unknown): arg is SortNumericFieldType => {
|
||||
return SORT_NUMERIC_FIELD_TYPES.some((v) => v === arg);
|
||||
};
|
||||
|
||||
/**
|
||||
* The maximum level of sub-aggregations
|
||||
*/
|
||||
|
@ -75,6 +137,10 @@ export interface PivotAggsConfigBase {
|
|||
agg: PivotSupportedAggs;
|
||||
aggName: AggName;
|
||||
dropDownName: string;
|
||||
/**
|
||||
* Indicates if aggregation supports multiple fields
|
||||
*/
|
||||
isMultiField?: boolean;
|
||||
/** Indicates if aggregation supports sub-aggregations */
|
||||
isSubAggsSupported?: boolean;
|
||||
/** Dictionary of the sub-aggregations */
|
||||
|
@ -130,7 +196,7 @@ export function getAggConfigFromEsAgg(
|
|||
}
|
||||
|
||||
export interface PivotAggsConfigWithUiBase extends PivotAggsConfigBase {
|
||||
field: EsFieldName;
|
||||
field: EsFieldName | EsFieldName[];
|
||||
}
|
||||
|
||||
export interface PivotAggsConfigWithExtra<T> extends PivotAggsConfigWithUiBase {
|
||||
|
|
|
@ -46,7 +46,7 @@ export const AdvancedRuntimeMappingsSettings: FC<StepDefineFormHook> = (props) =
|
|||
},
|
||||
} = props.runtimeMappingsEditor;
|
||||
const {
|
||||
actions: { deleteAggregation, deleteGroupBy },
|
||||
actions: { deleteAggregation, deleteGroupBy, updateAggregation },
|
||||
state: { groupByList, aggList },
|
||||
} = props.pivotConfig;
|
||||
|
||||
|
@ -55,6 +55,9 @@ export const AdvancedRuntimeMappingsSettings: FC<StepDefineFormHook> = (props) =
|
|||
advancedRuntimeMappingsConfig === '' ? {} : JSON.parse(advancedRuntimeMappingsConfig);
|
||||
const previousConfig = runtimeMappings;
|
||||
|
||||
const isFieldDeleted = (field: string) =>
|
||||
previousConfig?.hasOwnProperty(field) && !nextConfig.hasOwnProperty(field);
|
||||
|
||||
applyRuntimeMappingsEditorChanges();
|
||||
|
||||
// If the user updates the name of the runtime mapping fields
|
||||
|
@ -71,13 +74,16 @@ export const AdvancedRuntimeMappingsSettings: FC<StepDefineFormHook> = (props) =
|
|||
});
|
||||
Object.keys(aggList).forEach((aggName) => {
|
||||
const agg = aggList[aggName] as PivotAggsConfigWithUiSupport;
|
||||
if (
|
||||
isPivotAggConfigWithUiSupport(agg) &&
|
||||
agg.field !== undefined &&
|
||||
previousConfig?.hasOwnProperty(agg.field) &&
|
||||
!nextConfig.hasOwnProperty(agg.field)
|
||||
) {
|
||||
deleteAggregation(aggName);
|
||||
|
||||
if (isPivotAggConfigWithUiSupport(agg)) {
|
||||
if (Array.isArray(agg.field)) {
|
||||
const newFields = agg.field.filter((f) => !isFieldDeleted(f));
|
||||
updateAggregation(aggName, { ...agg, field: newFields });
|
||||
} else {
|
||||
if (agg.field !== undefined && isFieldDeleted(agg.field)) {
|
||||
deleteAggregation(aggName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import {
|
||||
EuiButton,
|
||||
EuiCodeEditor,
|
||||
EuiComboBox,
|
||||
EuiFieldText,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
|
@ -79,7 +80,7 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
|
|||
|
||||
const [aggName, setAggName] = useState(defaultData.aggName);
|
||||
const [agg, setAgg] = useState(defaultData.agg);
|
||||
const [field, setField] = useState(
|
||||
const [field, setField] = useState<string | string[]>(
|
||||
isPivotAggsConfigWithUiSupport(defaultData) ? defaultData.field : ''
|
||||
);
|
||||
|
||||
|
@ -148,13 +149,21 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
|
|||
|
||||
if (!isUnsupportedAgg) {
|
||||
const optionsArr = dictionaryToArray(options);
|
||||
|
||||
optionsArr
|
||||
.filter((o) => o.agg === defaultData.agg)
|
||||
.forEach((o) => {
|
||||
availableFields.push({ text: o.field });
|
||||
});
|
||||
|
||||
optionsArr
|
||||
.filter((o) => isPivotAggsConfigWithUiSupport(defaultData) && o.field === defaultData.field)
|
||||
.filter(
|
||||
(o) =>
|
||||
isPivotAggsConfigWithUiSupport(defaultData) &&
|
||||
(Array.isArray(defaultData.field)
|
||||
? defaultData.field.includes(o.field as string)
|
||||
: o.field === defaultData.field)
|
||||
)
|
||||
.forEach((o) => {
|
||||
availableAggs.push({ text: o.agg });
|
||||
});
|
||||
|
@ -217,20 +226,48 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
|
|||
data-test-subj="transformAggName"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
{availableFields.length > 0 && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.transform.agg.popoverForm.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={availableFields}
|
||||
value={field}
|
||||
onChange={(e) => setField(e.target.value)}
|
||||
data-test-subj="transformAggField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)}
|
||||
{availableFields.length > 0 ? (
|
||||
aggConfigDef.isMultiField ? (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.transform.agg.popoverForm.fieldsLabel', {
|
||||
defaultMessage: 'Fields',
|
||||
})}
|
||||
>
|
||||
<EuiComboBox
|
||||
fullWidth
|
||||
options={availableFields.map((v) => {
|
||||
return {
|
||||
value: v.text,
|
||||
label: v.text as string,
|
||||
};
|
||||
})}
|
||||
selectedOptions={(typeof field === 'string' ? [field] : field).map((v) => ({
|
||||
value: v,
|
||||
label: v,
|
||||
}))}
|
||||
onChange={(e) => {
|
||||
const res = e.map((v) => v.value as string);
|
||||
setField(res);
|
||||
}}
|
||||
isClearable={false}
|
||||
data-test-subj="transformAggFields"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.transform.agg.popoverForm.fieldLabel', {
|
||||
defaultMessage: 'Field',
|
||||
})}
|
||||
>
|
||||
<EuiSelect
|
||||
options={availableFields}
|
||||
value={field as string}
|
||||
onChange={(e) => setField(e.target.value)}
|
||||
data-test-subj="transformAggField"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
)
|
||||
) : null}
|
||||
{availableAggs.length > 0 && (
|
||||
<EuiFormRow
|
||||
label={i18n.translate('xpack.transform.agg.popoverForm.aggLabel', {
|
||||
|
@ -248,7 +285,7 @@ export const PopoverForm: React.FC<Props> = ({ defaultData, otherAggNames, onCha
|
|||
{isPivotAggsWithExtendedForm(aggConfigDef) && (
|
||||
<aggConfigDef.AggFormComponent
|
||||
aggConfig={aggConfigDef.aggConfig}
|
||||
selectedField={field}
|
||||
selectedField={field as string}
|
||||
onChange={(update) => {
|
||||
setAggConfigDef({
|
||||
...aggConfigDef,
|
||||
|
|
|
@ -44,6 +44,7 @@ describe('Transform: Define Pivot Common', () => {
|
|||
{ label: 'sum( the-f[i]e>ld )' },
|
||||
{ label: 'value_count( the-f[i]e>ld )' },
|
||||
{ label: 'filter( the-f[i]e>ld )' },
|
||||
{ label: 'top_metrics( the-f[i]e>ld )' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
@ -133,6 +134,7 @@ describe('Transform: Define Pivot Common', () => {
|
|||
{ label: 'sum( the-f[i]e>ld )' },
|
||||
{ label: 'value_count( the-f[i]e>ld )' },
|
||||
{ label: 'filter( the-f[i]e>ld )' },
|
||||
{ label: 'top_metrics( the-f[i]e>ld )' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -146,6 +148,7 @@ describe('Transform: Define Pivot Common', () => {
|
|||
{ label: 'sum(rt_bytes_bigger)' },
|
||||
{ label: 'value_count(rt_bytes_bigger)' },
|
||||
{ label: 'filter(rt_bytes_bigger)' },
|
||||
{ label: 'top_metrics(rt_bytes_bigger)' },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
|
||||
import { PivotAggsConfigBase, PivotAggsConfigWithUiBase } from '../../../../../common/pivot_aggs';
|
||||
import { getFilterAggConfig } from './filter_agg/config';
|
||||
import { getTopMetricsAggConfig } from './top_metrics_agg/config';
|
||||
|
||||
/**
|
||||
* Gets form configuration for provided aggregation type.
|
||||
|
@ -23,6 +24,8 @@ export function getAggFormConfig(
|
|||
switch (agg) {
|
||||
case PIVOT_SUPPORTED_AGGS.FILTER:
|
||||
return getFilterAggConfig(commonConfig);
|
||||
case PIVOT_SUPPORTED_AGGS.TOP_METRICS:
|
||||
return getTopMetricsAggConfig(commonConfig);
|
||||
default:
|
||||
return commonConfig;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
PivotAggsConfigWithUiSupport,
|
||||
} from '../../../../../common';
|
||||
import { getFilterAggConfig } from './filter_agg/config';
|
||||
import { getTopMetricsAggConfig } from './top_metrics_agg/config';
|
||||
|
||||
/**
|
||||
* Provides a configuration based on the aggregation type.
|
||||
|
@ -41,6 +42,8 @@ export function getDefaultAggregationConfig(
|
|||
};
|
||||
case PIVOT_SUPPORTED_AGGS.FILTER:
|
||||
return getFilterAggConfig(commonConfig);
|
||||
case PIVOT_SUPPORTED_AGGS.TOP_METRICS:
|
||||
return getTopMetricsAggConfig(commonConfig);
|
||||
default:
|
||||
return commonConfig;
|
||||
}
|
||||
|
|
|
@ -141,6 +141,7 @@ export function getPivotDropdownOptions(
|
|||
});
|
||||
|
||||
return {
|
||||
fields: combinedFields,
|
||||
groupByOptions,
|
||||
groupByOptionsData,
|
||||
aggOptions,
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
import { EuiFormRow, EuiSelect, EuiButtonGroup, EuiAccordion, EuiSpacer } from '@elastic/eui';
|
||||
import { PivotAggsConfigTopMetrics, TopMetricsAggConfig } from '../types';
|
||||
import { PivotConfigurationContext } from '../../../../pivot_configuration/pivot_configuration';
|
||||
import {
|
||||
isSpecialSortField,
|
||||
KbnNumericType,
|
||||
NUMERIC_TYPES_OPTIONS,
|
||||
SORT_DIRECTION,
|
||||
SORT_MODE,
|
||||
SortDirection,
|
||||
SortMode,
|
||||
SortNumericFieldType,
|
||||
TOP_METRICS_SORT_FIELD_TYPES,
|
||||
TOP_METRICS_SPECIAL_SORT_FIELDS,
|
||||
} from '../../../../../../../common/pivot_aggs';
|
||||
|
||||
export const TopMetricsAggForm: PivotAggsConfigTopMetrics['AggFormComponent'] = ({
|
||||
onChange,
|
||||
aggConfig,
|
||||
}) => {
|
||||
const {
|
||||
state: { fields },
|
||||
} = useContext(PivotConfigurationContext)!;
|
||||
|
||||
const sortFieldOptions = fields
|
||||
.filter((v) => TOP_METRICS_SORT_FIELD_TYPES.includes(v.type))
|
||||
.map(({ name }) => ({ text: name, value: name }));
|
||||
|
||||
Object.values(TOP_METRICS_SPECIAL_SORT_FIELDS).forEach((v) => {
|
||||
sortFieldOptions.unshift({ text: v, value: v });
|
||||
});
|
||||
sortFieldOptions.unshift({ text: '', value: '' });
|
||||
|
||||
const isSpecialFieldSelected = isSpecialSortField(aggConfig.sortField);
|
||||
|
||||
const sortDirectionOptions = Object.values(SORT_DIRECTION).map((v) => ({
|
||||
id: v,
|
||||
label: v,
|
||||
}));
|
||||
|
||||
const sortModeOptions = Object.values(SORT_MODE).map((v) => ({
|
||||
id: v,
|
||||
label: v,
|
||||
}));
|
||||
|
||||
const sortFieldType = fields.find((f) => f.name === aggConfig.sortField)?.type;
|
||||
|
||||
const sortSettings = aggConfig.sortSettings ?? {};
|
||||
|
||||
const updateSortSettings = useCallback(
|
||||
(update: Partial<TopMetricsAggConfig['sortSettings']>) => {
|
||||
onChange({
|
||||
...aggConfig,
|
||||
sortSettings: {
|
||||
...(aggConfig.sortSettings ?? {}),
|
||||
...update,
|
||||
},
|
||||
});
|
||||
},
|
||||
[aggConfig, onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.agg.popoverForm.sortFieldTopMetricsLabel"
|
||||
defaultMessage="Sort field"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
options={sortFieldOptions}
|
||||
value={aggConfig.sortField}
|
||||
onChange={(e) => {
|
||||
onChange({ ...aggConfig, sortField: e.target.value });
|
||||
}}
|
||||
data-test-subj="transformSortFieldTopMetricsLabel"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{aggConfig.sortField ? (
|
||||
<>
|
||||
{isSpecialFieldSelected ? null : (
|
||||
<>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.agg.popoverForm.sortDirectionTopMetricsLabel"
|
||||
defaultMessage="Sort direction"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
type="single"
|
||||
legend={i18n.translate(
|
||||
'xpack.transform.agg.popoverForm.sortDirectionTopMetricsLabel',
|
||||
{
|
||||
defaultMessage: 'Sort direction',
|
||||
}
|
||||
)}
|
||||
options={sortDirectionOptions}
|
||||
idSelected={sortSettings.order ?? ''}
|
||||
onChange={(id: string) => {
|
||||
updateSortSettings({ order: id as SortDirection });
|
||||
}}
|
||||
color="text"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
<EuiAccordion
|
||||
id="sortAdvancedSettings"
|
||||
buttonContent={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.agg.popoverForm.advancedSortingSettingsLabel"
|
||||
defaultMessage="Advanced sorting settings"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.agg.popoverForm.sortModeTopMetricsLabel"
|
||||
defaultMessage="Sort mode"
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.agg.popoverForm.sortModeTopMetricsHelpText"
|
||||
defaultMessage="Only relevant if the sorting field is an array."
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
type="single"
|
||||
legend={i18n.translate(
|
||||
'xpack.transform.agg.popoverForm.sortModeTopMetricsLabel',
|
||||
{
|
||||
defaultMessage: 'Sort mode',
|
||||
}
|
||||
)}
|
||||
options={sortModeOptions}
|
||||
idSelected={sortSettings.mode ?? ''}
|
||||
onChange={(id: string) => {
|
||||
updateSortSettings({ mode: id as SortMode });
|
||||
}}
|
||||
color="text"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
|
||||
{sortFieldType && NUMERIC_TYPES_OPTIONS.hasOwnProperty(sortFieldType) ? (
|
||||
<EuiFormRow
|
||||
label={
|
||||
<FormattedMessage
|
||||
id="xpack.transform.agg.popoverForm.numericSortFieldTopMetricsLabel"
|
||||
defaultMessage="Numeric field"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<EuiSelect
|
||||
options={NUMERIC_TYPES_OPTIONS[sortFieldType as KbnNumericType].map((v) => ({
|
||||
text: v,
|
||||
name: v,
|
||||
}))}
|
||||
value={sortSettings.numericType}
|
||||
onChange={(e) => {
|
||||
updateSortSettings({
|
||||
numericType: e.target.value as SortNumericFieldType,
|
||||
});
|
||||
}}
|
||||
data-test-subj="transformSortNumericTypeTopMetricsLabel"
|
||||
/>
|
||||
</EuiFormRow>
|
||||
) : null}
|
||||
</EuiAccordion>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getTopMetricsAggConfig } from './config';
|
||||
import { PivotAggsConfigTopMetrics } from './types';
|
||||
|
||||
describe('top metrics agg config', () => {
|
||||
let config: PivotAggsConfigTopMetrics;
|
||||
|
||||
beforeEach(() => {
|
||||
config = getTopMetricsAggConfig({
|
||||
agg: 'top_metrics',
|
||||
aggName: 'test-agg',
|
||||
field: ['test-field'],
|
||||
dropDownName: 'test-agg',
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setUiConfigFromEs', () => {
|
||||
test('sets config with a special field', () => {
|
||||
// act
|
||||
config.setUiConfigFromEs({
|
||||
metrics: {
|
||||
field: 'test-field-01',
|
||||
},
|
||||
sort: '_score',
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(config.field).toEqual(['test-field-01']);
|
||||
expect(config.aggConfig).toEqual({
|
||||
sortField: '_score',
|
||||
});
|
||||
});
|
||||
|
||||
test('sets config with a simple sort direction definition', () => {
|
||||
// act
|
||||
config.setUiConfigFromEs({
|
||||
metrics: [
|
||||
{
|
||||
field: 'test-field-01',
|
||||
},
|
||||
{
|
||||
field: 'test-field-02',
|
||||
},
|
||||
],
|
||||
sort: {
|
||||
'sort-field': 'asc',
|
||||
},
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(config.field).toEqual(['test-field-01', 'test-field-02']);
|
||||
expect(config.aggConfig).toEqual({
|
||||
sortField: 'sort-field',
|
||||
sortSettings: {
|
||||
order: 'asc',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('sets config with a sort definition params not supported by the UI', () => {
|
||||
// act
|
||||
config.setUiConfigFromEs({
|
||||
metrics: [
|
||||
{
|
||||
field: 'test-field-01',
|
||||
},
|
||||
],
|
||||
sort: {
|
||||
'offer.price': {
|
||||
order: 'desc',
|
||||
mode: 'avg',
|
||||
nested: {
|
||||
path: 'offer',
|
||||
filter: {
|
||||
term: { 'offer.color': 'blue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// assert
|
||||
expect(config.field).toEqual(['test-field-01']);
|
||||
expect(config.aggConfig).toEqual({
|
||||
sortField: 'offer.price',
|
||||
sortSettings: {
|
||||
order: 'desc',
|
||||
mode: 'avg',
|
||||
nested: {
|
||||
path: 'offer',
|
||||
filter: {
|
||||
term: { 'offer.color': 'blue' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getEsAggConfig', () => {
|
||||
test('rejects invalid config', () => {
|
||||
// arrange
|
||||
config.field = ['field-01', 'field-02'];
|
||||
config.aggConfig = {
|
||||
sortSettings: {
|
||||
order: 'asc',
|
||||
},
|
||||
};
|
||||
|
||||
// act and assert
|
||||
expect(config.getEsAggConfig()).toEqual(null);
|
||||
});
|
||||
|
||||
test('rejects invalid config with missing sort direction', () => {
|
||||
// arrange
|
||||
config.field = ['field-01', 'field-02'];
|
||||
config.aggConfig = {
|
||||
sortField: 'sort-field',
|
||||
};
|
||||
|
||||
// act and assert
|
||||
expect(config.getEsAggConfig()).toEqual(null);
|
||||
});
|
||||
|
||||
test('converts valid config', () => {
|
||||
// arrange
|
||||
config.field = ['field-01', 'field-02'];
|
||||
config.aggConfig = {
|
||||
sortField: 'sort-field',
|
||||
sortSettings: {
|
||||
order: 'asc',
|
||||
},
|
||||
};
|
||||
|
||||
// act and assert
|
||||
expect(config.getEsAggConfig()).toEqual({
|
||||
metrics: [{ field: 'field-01' }, { field: 'field-02' }],
|
||||
sort: {
|
||||
'sort-field': 'asc',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('preserves unsupported config', () => {
|
||||
// arrange
|
||||
config.field = ['field-01', 'field-02'];
|
||||
|
||||
config.aggConfig = {
|
||||
sortField: 'sort-field',
|
||||
sortSettings: {
|
||||
order: 'asc',
|
||||
// @ts-ignore
|
||||
nested: {
|
||||
path: 'order',
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
size: 2,
|
||||
};
|
||||
|
||||
// act and assert
|
||||
expect(config.getEsAggConfig()).toEqual({
|
||||
metrics: [{ field: 'field-01' }, { field: 'field-02' }],
|
||||
sort: {
|
||||
'sort-field': {
|
||||
order: 'asc',
|
||||
nested: {
|
||||
path: 'order',
|
||||
},
|
||||
},
|
||||
},
|
||||
size: 2,
|
||||
});
|
||||
});
|
||||
|
||||
test('converts configs with a special sorting field', () => {
|
||||
// arrange
|
||||
config.field = ['field-01', 'field-02'];
|
||||
config.aggConfig = {
|
||||
sortField: '_score',
|
||||
};
|
||||
|
||||
// act and assert
|
||||
expect(config.getEsAggConfig()).toEqual({
|
||||
metrics: [{ field: 'field-01' }, { field: 'field-02' }],
|
||||
sort: '_score',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,118 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
isPivotAggsConfigWithUiSupport,
|
||||
isSpecialSortField,
|
||||
isValidSortDirection,
|
||||
isValidSortMode,
|
||||
isValidSortNumericType,
|
||||
PivotAggsConfigBase,
|
||||
PivotAggsConfigWithUiBase,
|
||||
} from '../../../../../../common/pivot_aggs';
|
||||
import { PivotAggsConfigTopMetrics } from './types';
|
||||
import { TopMetricsAggForm } from './components/top_metrics_agg_form';
|
||||
import { isPopulatedObject } from '../../../../../../../../common/shared_imports';
|
||||
|
||||
/**
|
||||
* Gets initial basic configuration of the top_metrics aggregation.
|
||||
*/
|
||||
export function getTopMetricsAggConfig(
|
||||
commonConfig: PivotAggsConfigWithUiBase | PivotAggsConfigBase
|
||||
): PivotAggsConfigTopMetrics {
|
||||
return {
|
||||
...commonConfig,
|
||||
isSubAggsSupported: false,
|
||||
isMultiField: true,
|
||||
field: isPivotAggsConfigWithUiSupport(commonConfig) ? commonConfig.field : '',
|
||||
AggFormComponent: TopMetricsAggForm,
|
||||
aggConfig: {},
|
||||
getEsAggConfig() {
|
||||
// ensure the configuration has been completed
|
||||
if (!this.isValid()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sortField, sortSettings = {}, ...unsupportedConfig } = this.aggConfig;
|
||||
|
||||
let sort = null;
|
||||
|
||||
if (isSpecialSortField(sortField)) {
|
||||
sort = sortField;
|
||||
} else {
|
||||
const { mode, numericType, order, ...rest } = sortSettings;
|
||||
|
||||
if (mode || numericType || isPopulatedObject(rest)) {
|
||||
sort = {
|
||||
[sortField!]: {
|
||||
...rest,
|
||||
order,
|
||||
...(mode ? { mode } : {}),
|
||||
...(numericType ? { numeric_type: numericType } : {}),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
sort = { [sortField!]: sortSettings.order };
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
metrics: (Array.isArray(this.field) ? this.field : [this.field]).map((f) => ({ field: f })),
|
||||
sort,
|
||||
...(unsupportedConfig ?? {}),
|
||||
};
|
||||
},
|
||||
setUiConfigFromEs(esAggDefinition) {
|
||||
const { metrics, sort, ...unsupportedConfig } = esAggDefinition;
|
||||
|
||||
this.field = (Array.isArray(metrics) ? metrics : [metrics]).map((v) => v.field);
|
||||
|
||||
if (isSpecialSortField(sort)) {
|
||||
this.aggConfig.sortField = sort;
|
||||
return;
|
||||
}
|
||||
|
||||
const sortField = Object.keys(sort)[0];
|
||||
|
||||
this.aggConfig.sortField = sortField;
|
||||
|
||||
const sortDefinition = sort[sortField];
|
||||
|
||||
this.aggConfig.sortSettings = this.aggConfig.sortSettings ?? {};
|
||||
|
||||
if (isValidSortDirection(sortDefinition)) {
|
||||
this.aggConfig.sortSettings.order = sortDefinition;
|
||||
}
|
||||
|
||||
if (isPopulatedObject(sortDefinition)) {
|
||||
const { order, mode, numeric_type: numType, ...rest } = sortDefinition;
|
||||
this.aggConfig.sortSettings = rest;
|
||||
|
||||
if (isValidSortDirection(order)) {
|
||||
this.aggConfig.sortSettings.order = order;
|
||||
}
|
||||
if (isValidSortMode(mode)) {
|
||||
this.aggConfig.sortSettings.mode = mode;
|
||||
}
|
||||
if (isValidSortNumericType(numType)) {
|
||||
this.aggConfig.sortSettings.numericType = numType;
|
||||
}
|
||||
}
|
||||
|
||||
this.aggConfig = {
|
||||
...this.aggConfig,
|
||||
...(unsupportedConfig ?? {}),
|
||||
};
|
||||
},
|
||||
isValid() {
|
||||
return (
|
||||
!!this.aggConfig.sortField &&
|
||||
(isSpecialSortField(this.aggConfig.sortField) ? true : !!this.aggConfig.sortSettings?.order)
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
PivotAggsConfigWithExtra,
|
||||
SortDirection,
|
||||
SortMode,
|
||||
SortNumericFieldType,
|
||||
} from '../../../../../../common/pivot_aggs';
|
||||
|
||||
export interface TopMetricsAggConfig {
|
||||
sortField: string;
|
||||
sortSettings?: {
|
||||
order?: SortDirection;
|
||||
mode?: SortMode;
|
||||
numericType?: SortNumericFieldType;
|
||||
};
|
||||
}
|
||||
|
||||
export type PivotAggsConfigTopMetrics = PivotAggsConfigWithExtra<TopMetricsAggConfig>;
|
|
@ -97,7 +97,7 @@ export const usePivotConfig = (
|
|||
) => {
|
||||
const toastNotifications = useToastNotifications();
|
||||
|
||||
const { aggOptions, aggOptionsData, groupByOptions, groupByOptionsData } = useMemo(
|
||||
const { aggOptions, aggOptionsData, groupByOptions, groupByOptionsData, fields } = useMemo(
|
||||
() => getPivotDropdownOptions(indexPattern, defaults.runtimeMappings),
|
||||
[defaults.runtimeMappings, indexPattern]
|
||||
);
|
||||
|
@ -347,6 +347,7 @@ export const usePivotConfig = (
|
|||
pivotGroupByArr,
|
||||
validationStatus,
|
||||
requestPayload,
|
||||
fields,
|
||||
},
|
||||
};
|
||||
}, [
|
||||
|
@ -361,6 +362,7 @@ export const usePivotConfig = (
|
|||
pivotGroupByArr,
|
||||
validationStatus,
|
||||
requestPayload,
|
||||
fields,
|
||||
]);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue