From 9810a72720c63a72ef5c5cc43c7af9d09ff165db Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 4 Jun 2021 16:04:53 +0200 Subject: [PATCH] [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 --- .../transform/common/types/pivot_aggs.ts | 1 + .../public/app/common/pivot_aggs.test.ts | 11 +- .../transform/public/app/common/pivot_aggs.ts | 70 ++++++- .../advanced_runtime_mappings_settings.tsx | 22 +- .../aggregation_list/popover_form.tsx | 71 +++++-- .../step_define/common/common.test.ts | 3 + .../step_define/common/get_agg_form_config.ts | 3 + .../common/get_default_aggregation_config.ts | 3 + .../common/get_pivot_dropdown_options.ts | 1 + .../components/top_metrics_agg_form.tsx | 195 +++++++++++++++++ .../common/top_metrics_agg/config.test.ts | 196 ++++++++++++++++++ .../common/top_metrics_agg/config.ts | 118 +++++++++++ .../common/top_metrics_agg/types.ts | 24 +++ .../step_define/hooks/use_pivot_config.ts | 4 +- 14 files changed, 693 insertions(+), 29 deletions(-) create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/components/top_metrics_agg_form.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.test.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts create mode 100644 x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/types.ts diff --git a/x-pack/plugins/transform/common/types/pivot_aggs.ts b/x-pack/plugins/transform/common/types/pivot_aggs.ts index c50852e53254af..ced4d0a9bce0c2 100644 --- a/x-pack/plugins/transform/common/types/pivot_aggs.ts +++ b/x-pack/plugins/transform/common/types/pivot_aggs.ts @@ -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]; diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts index dba9fa5dd83ba4..f92bf1cdf59d90 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.test.ts @@ -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); + }); +}); diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index 03e06d36f9319c..97685096a5d223 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -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 extends PivotAggsConfigWithUiBase { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx index 29e341fdaeaea9..4e70b7d7fe9b7a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/advanced_runtime_mappings_settings/advanced_runtime_mappings_settings.tsx @@ -46,7 +46,7 @@ export const AdvancedRuntimeMappingsSettings: FC = (props) = }, } = props.runtimeMappingsEditor; const { - actions: { deleteAggregation, deleteGroupBy }, + actions: { deleteAggregation, deleteGroupBy, updateAggregation }, state: { groupByList, aggList }, } = props.pivotConfig; @@ -55,6 +55,9 @@ export const AdvancedRuntimeMappingsSettings: FC = (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 = (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); + } + } } }); }; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 553581f58d55e1..fd11255374a517 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -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 = ({ defaultData, otherAggNames, onCha const [aggName, setAggName] = useState(defaultData.aggName); const [agg, setAgg] = useState(defaultData.agg); - const [field, setField] = useState( + const [field, setField] = useState( isPivotAggsConfigWithUiSupport(defaultData) ? defaultData.field : '' ); @@ -148,13 +149,21 @@ export const PopoverForm: React.FC = ({ 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 = ({ defaultData, otherAggNames, onCha data-test-subj="transformAggName" /> - {availableFields.length > 0 && ( - - setField(e.target.value)} - data-test-subj="transformAggField" - /> - - )} + {availableFields.length > 0 ? ( + aggConfigDef.isMultiField ? ( + + { + 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" + /> + + ) : ( + + setField(e.target.value)} + data-test-subj="transformAggField" + /> + + ) + ) : null} {availableAggs.length > 0 && ( = ({ defaultData, otherAggNames, onCha {isPivotAggsWithExtendedForm(aggConfigDef) && ( { setAggConfigDef({ ...aggConfigDef, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts index fcdbac8c7ff39c..5891e8b330b949 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/common.test.ts @@ -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)' }, ], }, ], diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts index ab69d22b1f3d7b..5d8d7cb967b658 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts @@ -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; } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts index 53350f0238bf06..39594dcbff9ae2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts @@ -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; } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts index 300626e0570ae2..b17f30d115f4a2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_pivot_dropdown_options.ts @@ -141,6 +141,7 @@ export function getPivotDropdownOptions( }); return { + fields: combinedFields, groupByOptions, groupByOptionsData, aggOptions, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/components/top_metrics_agg_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/components/top_metrics_agg_form.tsx new file mode 100644 index 00000000000000..0ec66a3d59a113 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/components/top_metrics_agg_form.tsx @@ -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) => { + onChange({ + ...aggConfig, + sortSettings: { + ...(aggConfig.sortSettings ?? {}), + ...update, + }, + }); + }, + [aggConfig, onChange] + ); + + return ( + <> + + } + > + { + onChange({ ...aggConfig, sortField: e.target.value }); + }} + data-test-subj="transformSortFieldTopMetricsLabel" + /> + + + {aggConfig.sortField ? ( + <> + {isSpecialFieldSelected ? null : ( + <> + + } + > + { + updateSortSettings({ order: id as SortDirection }); + }} + color="text" + /> + + + + + + } + > + + } + helpText={ + + } + > + { + updateSortSettings({ mode: id as SortMode }); + }} + color="text" + /> + + + {sortFieldType && NUMERIC_TYPES_OPTIONS.hasOwnProperty(sortFieldType) ? ( + + } + > + ({ + text: v, + name: v, + }))} + value={sortSettings.numericType} + onChange={(e) => { + updateSortSettings({ + numericType: e.target.value as SortNumericFieldType, + }); + }} + data-test-subj="transformSortNumericTypeTopMetricsLabel" + /> + + ) : null} + + + )} + + ) : null} + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.test.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.test.ts new file mode 100644 index 00000000000000..ef57e6d1295c1a --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.test.ts @@ -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', + }); + }); + }); +}); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts new file mode 100644 index 00000000000000..56d17e7973e160 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/config.ts @@ -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) + ); + }, + }; +} diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/types.ts new file mode 100644 index 00000000000000..a90ee5307a18ec --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/top_metrics_agg/types.ts @@ -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; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts index 4bd8f5cea60926..0c31b4fe2da819 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts @@ -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, ]); };