diff --git a/x-pack/packages/ml/field_stats_flyout/eui_combo_box_with_field_stats.tsx b/x-pack/packages/ml/field_stats_flyout/eui_combo_box_with_field_stats.tsx deleted file mode 100644 index a09710da8e398..0000000000000 --- a/x-pack/packages/ml/field_stats_flyout/eui_combo_box_with_field_stats.tsx +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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 { FC } from 'react'; -import React, { useMemo } from 'react'; -import type { EuiComboBoxProps } from '@elastic/eui/src/components/combo_box/combo_box'; -import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; -import { css } from '@emotion/react'; -import { useFieldStatsTrigger } from './use_field_stats_trigger'; - -export const optionCss = css` - .euiComboBoxOption__enterBadge { - display: none; - } - .euiFlexGroup { - gap: 0px; - } - .euiComboBoxOption__content { - margin-left: 2px; - } -`; - -/** - * Props for the EuiComboBoxWithFieldStats component. - */ -export type EuiComboBoxWithFieldStatsProps = EuiComboBoxProps< - string | number | string[] | undefined ->; - -/** - * React component that wraps the EuiComboBox component and adds field statistics functionality. - * - * @component - * @example - * ```tsx - * - * ``` - * @param {EuiComboBoxWithFieldStatsProps} props - The component props. - */ -export const EuiComboBoxWithFieldStats: FC = (props) => { - const { options, ...restProps } = props; - const { renderOption } = useFieldStatsTrigger(); - const comboBoxOptions: EuiComboBoxOptionOption[] = useMemo( - () => - Array.isArray(options) - ? options.map((o) => ({ - ...o, - css: optionCss, - })) - : [], - [options] - ); - - return ( - - ); -}; diff --git a/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx b/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx index 9dd947f0872f3..678dec7d36f42 100644 --- a/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx +++ b/x-pack/packages/ml/field_stats_flyout/field_stats_flyout_provider.tsx @@ -142,6 +142,7 @@ export const FieldStatsFlyoutProvider: FC = (prop // Get all field names for each returned doc and flatten it // to a list of unique field names used across all docs. const fieldsWithData = new Set(docs.map(Object.keys).flat(1)); + manager.set(cacheKey, fieldsWithData); if (!unmounted) { setPopulatedFields(fieldsWithData); diff --git a/x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx b/x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx index 936f9550cdda1..7863f358708d6 100644 --- a/x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx +++ b/x-pack/packages/ml/field_stats_flyout/field_stats_info_button.tsx @@ -88,6 +88,7 @@ export const FieldStatsInfoButton: FC = (props) => { defaultMessage: '(no data found in 1000 sample records)', }) : ''; + return ( @@ -135,14 +136,15 @@ export const FieldStatsInfoButton: FC = (props) => { grow={false} css={{ paddingRight: themeVars.euiTheme.euiSizeXS, - paddingBottom: themeVars.euiTheme.euiSizeXS, }} > - + {!hideTrigger ? ( + + ) : null} = (props) => { aria-label={label} title={label} className="euiComboBoxOption__content" - css={{ paddingBottom: themeVars.euiTheme.euiSizeXS }} > {label} diff --git a/x-pack/packages/ml/field_stats_flyout/index.ts b/x-pack/packages/ml/field_stats_flyout/index.ts index db4d3c5ee7b15..53ed8c7ce877b 100644 --- a/x-pack/packages/ml/field_stats_flyout/index.ts +++ b/x-pack/packages/ml/field_stats_flyout/index.ts @@ -21,7 +21,6 @@ export { type FieldStatsInfoButtonProps, } from './field_stats_info_button'; export { useFieldStatsTrigger } from './use_field_stats_trigger'; -export { - EuiComboBoxWithFieldStats, - type EuiComboBoxWithFieldStatsProps, -} from './eui_combo_box_with_field_stats'; + +export { OptionListWithFieldStats } from './options_list_with_stats/option_list_with_stats'; +export type { DropDownLabel } from './options_list_with_stats/types'; diff --git a/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_popover.tsx b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_popover.tsx new file mode 100644 index 0000000000000..77b5f8a0d8b15 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_popover.tsx @@ -0,0 +1,158 @@ +/* + * 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 { FC } from 'react'; +import React, { useMemo, useState, useEffect } from 'react'; +import { isDefined } from '@kbn/ml-is-defined'; +import type { + EuiComboBoxOptionOption, + EuiComboBoxSingleSelectionShape, + EuiSelectableOption, +} from '@elastic/eui'; +import { EuiFlexItem, EuiSelectable, htmlIdGenerator } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { type DropDownLabel } from './types'; +import { useFieldStatsFlyoutContext } from '../use_field_stats_flyout_context'; +import { OptionsListPopoverFooter } from './option_list_popover_footer'; + +interface OptionsListPopoverProps { + options: DropDownLabel[]; + renderOption: (option: DropDownLabel) => React.ReactNode; + singleSelection?: boolean | EuiComboBoxSingleSelectionShape; + onChange?: + | ((newSuggestions: DropDownLabel[]) => void) + | (( + newSuggestions: Array> + ) => void); + setPopoverOpen: (open: boolean) => void; + isLoading?: boolean; +} + +interface OptionsListPopoverSuggestionsProps { + options: DropDownLabel[]; + renderOption: (option: DropDownLabel) => React.ReactNode; + singleSelection?: boolean | EuiComboBoxSingleSelectionShape; + onChange?: + | ((newSuggestions: DropDownLabel[]) => void) + | (( + newSuggestions: Array> + ) => void); + setPopoverOpen: (open: boolean) => void; +} +const OptionsListPopoverSuggestions: FC = ({ + options, + renderOption, + singleSelection, + onChange, + setPopoverOpen, +}) => { + const [selectableOptions, setSelectableOptions] = useState([]); // will be set in following useEffect + useEffect(() => { + /* This useEffect makes selectableOptions responsive to search, show only selected, and clear selections */ + const _selectableOptions = (options ?? []).map((suggestion) => { + const key = suggestion.label ?? suggestion.field?.id; + return { + ...suggestion, + key, + checked: undefined, + 'data-test-subj': `optionsListControlSelection-${key}`, + }; + }); + setSelectableOptions(_selectableOptions); + }, [options]); + + return ( + >} + renderOption={renderOption} + listProps={{ onFocusBadge: false }} + onChange={(opts, _, changedOption) => { + const option = changedOption as DropDownLabel; + if (singleSelection) { + if (onChange) { + onChange([option as EuiComboBoxOptionOption]); + setPopoverOpen(false); + } + } else { + if (onChange) { + onChange([option as EuiComboBoxOptionOption]); + setPopoverOpen(false); + } + } + }} + > + {(list, search) => ( + <> + {search} + {list} + + )} + + ); +}; + +export const OptionsListPopover = ({ + options, + renderOption, + singleSelection, + onChange, + setPopoverOpen, + isLoading, +}: OptionsListPopoverProps) => { + const { populatedFields } = useFieldStatsFlyoutContext(); + + const [showEmptyFields, setShowEmptyFields] = useState(false); + const id = useMemo(() => htmlIdGenerator()(), []); + + const filteredOptions = useMemo(() => { + return showEmptyFields + ? options + : options.filter((option) => { + if (isDefined(option['data-is-empty'])) { + return !option['data-is-empty']; + } + if ( + Object.hasOwn(option, 'isGroupLabel') || + Object.hasOwn(option, 'isGroupLabelOption') + ) { + const key = option.key ?? option.searchableLabel; + return key ? populatedFields?.has(key) : false; + } + if (option.field) { + return populatedFields?.has(option.field.id); + } + return true; + }); + }, [options, showEmptyFields, populatedFields]); + return ( +
+ + + + +
+ ); +}; diff --git a/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_popover_footer.tsx b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_popover_footer.tsx new file mode 100644 index 0000000000000..0bed94223b0c5 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_popover_footer.tsx @@ -0,0 +1,51 @@ +/* + * 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 from 'react'; +import type { FC } from 'react'; +import { EuiPopoverFooter, EuiSwitch, EuiProgress, useEuiBackgroundColor } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { euiThemeVars } from '@kbn/ui-theme'; + +export const OptionsListPopoverFooter: FC<{ + showEmptyFields: boolean; + setShowEmptyFields: (showEmptyFields: boolean) => void; + isLoading?: boolean; +}> = ({ showEmptyFields, setShowEmptyFields, isLoading }) => { + return ( + + {isLoading ? ( + // @ts-expect-error css should be ok +
+ +
+ ) : null} + + setShowEmptyFields(e.target.checked)} + /> +
+ ); +}; diff --git a/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_with_stats.tsx b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_with_stats.tsx new file mode 100644 index 0000000000000..244b2d6a511a9 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/option_list_with_stats.tsx @@ -0,0 +1,146 @@ +/* + * 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 { FC } from 'react'; +import React, { useMemo, useState } from 'react'; +import type { EuiComboBoxOptionOption, EuiComboBoxSingleSelectionShape } from '@elastic/eui'; +import { EuiInputPopover, htmlIdGenerator, EuiFormControlLayout, EuiFieldText } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { i18n } from '@kbn/i18n'; +import { useFieldStatsTrigger } from '../use_field_stats_trigger'; +import { OptionsListPopover } from './option_list_popover'; +import type { DropDownLabel } from './types'; + +const MIN_POPOVER_WIDTH = 400; + +export const optionCss = css` + display: flex; + align-items: center; + .euiComboBoxOption__enterBadge { + display: none; + } + .euiFlexGroup { + gap: 0px; + } + .euiComboBoxOption__content { + margin-left: 2px; + } +`; + +interface OptionListWithFieldStatsProps { + options: DropDownLabel[]; + placeholder?: string; + 'aria-label'?: string; + singleSelection?: boolean | EuiComboBoxSingleSelectionShape; + onChange: + | ((newSuggestions: DropDownLabel[]) => void) + | ((newSuggestions: EuiComboBoxOptionOption[]) => void); + selectedOptions?: Array<{ label: string }>; + fullWidth?: boolean; + isDisabled?: boolean; + isLoading?: boolean; + isClearable?: boolean; + isInvalid?: boolean; + 'data-test-subj'?: string; +} + +export const OptionListWithFieldStats: FC = ({ + options, + placeholder, + singleSelection = false, + onChange, + selectedOptions, + fullWidth, + isDisabled, + isLoading, + isClearable = true, + 'aria-label': ariaLabel, + 'data-test-subj': dataTestSubj, +}) => { + const { renderOption } = useFieldStatsTrigger(); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const popoverId = useMemo(() => htmlIdGenerator()(), []); + const comboBoxOptions: DropDownLabel[] = useMemo( + () => + Array.isArray(options) + ? options.map(({ isEmpty, hideTrigger: hideInspectButton, ...o }) => ({ + ...o, + css: optionCss, + // Change data-is-empty- because EUI is passing all props to dom element + // so isEmpty is invalid, but we need this info to render option correctly + 'data-is-empty': isEmpty, + 'data-hide-inspect': hideInspectButton, + })) + : [], + [options] + ); + const hasSelections = useMemo(() => selectedOptions?.length ?? 0 > 0, [selectedOptions]); + + const value = singleSelection && selectedOptions?.[0]?.label ? selectedOptions?.[0]?.label : ''; + return ( + + {}} + value={value} + /> + + } + hasArrow={false} + repositionOnScroll + isOpen={isPopoverOpen} + panelPaddingSize="none" + panelMinWidth={MIN_POPOVER_WIDTH} + initialFocus={'[data-test-subj=optionsList-control-search-input]'} + closePopover={setPopoverOpen.bind(null, false)} + panelProps={{ + 'aria-label': i18n.translate('xpack.ml.controls.optionsList.popover.ariaLabel', { + defaultMessage: 'Popover for {ariaLabel}', + values: { ariaLabel }, + }), + }} + > + {isPopoverOpen ? ( + + ) : null} + + ); +}; diff --git a/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/types.ts b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/types.ts new file mode 100644 index 0000000000000..ef95daa38ea03 --- /dev/null +++ b/x-pack/packages/ml/field_stats_flyout/options_list_with_stats/types.ts @@ -0,0 +1,31 @@ +/* + * 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 { EuiComboBoxOptionOption, EuiSelectableOption } from '@elastic/eui'; +import type { Aggregation, Field } from '@kbn/ml-anomaly-utils'; + +interface BaseOption { + key?: string; + label: string | React.ReactNode; + isEmpty?: boolean; + hideTrigger?: boolean; + 'data-is-empty'?: boolean; + 'data-hide-inspect'?: boolean; + isGroupLabelOption?: boolean; + isGroupLabel?: boolean; + field?: Field; + agg?: Aggregation; + searchableLabel?: string; +} +export type SelectableOption = EuiSelectableOption>; +export type DropDownLabel = + | (EuiComboBoxOptionOption & BaseOption) + | SelectableOption; + +export function isSelectableOption(option: unknown): option is SelectableOption { + return typeof option === 'object' && option !== null && Object.hasOwn(option, 'label'); +} diff --git a/x-pack/packages/ml/field_stats_flyout/tsconfig.json b/x-pack/packages/ml/field_stats_flyout/tsconfig.json index b0920fac0ad2a..0010d79432e34 100644 --- a/x-pack/packages/ml/field_stats_flyout/tsconfig.json +++ b/x-pack/packages/ml/field_stats_flyout/tsconfig.json @@ -32,5 +32,6 @@ "@kbn/ml-query-utils", "@kbn/ml-is-defined", "@kbn/field-types", + "@kbn/ui-theme", ] } diff --git a/x-pack/packages/ml/field_stats_flyout/use_field_stats_trigger.tsx b/x-pack/packages/ml/field_stats_flyout/use_field_stats_trigger.tsx index 78c2f6772049a..546dc36ce9e4b 100644 --- a/x-pack/packages/ml/field_stats_flyout/use_field_stats_trigger.tsx +++ b/x-pack/packages/ml/field_stats_flyout/use_field_stats_trigger.tsx @@ -7,13 +7,27 @@ import type { ReactNode } from 'react'; import React, { useCallback } from 'react'; -import type { EuiComboBoxOptionOption } from '@elastic/eui'; +import { type EuiComboBoxOptionOption } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { optionCss } from './eui_combo_box_with_field_stats'; +import { css } from '@emotion/react'; +import { EVENT_RATE_FIELD_ID } from '@kbn/ml-anomaly-utils/fields'; +import type { DropDownLabel } from '.'; import { useFieldStatsFlyoutContext } from '.'; import type { FieldForStats } from './field_stats_info_button'; import { FieldStatsInfoButton } from './field_stats_info_button'; +import { isSelectableOption } from './options_list_with_stats/types'; +export const optionCss = css` + .euiComboBoxOption__enterBadge { + display: none; + } + .euiFlexGroup { + gap: 0px; + } + .euiComboBoxOption__content { + margin-left: 2px; + } +`; interface Option extends EuiComboBoxOptionOption { field: Field; } @@ -30,7 +44,7 @@ interface Option extends EuiComboBoxOptionOption { * - `optionCss`: CSS styles for the options in the combo box. * - `populatedFields`: A set of populated fields. */ -export const useFieldStatsTrigger = () => { +export function useFieldStatsTrigger() { const { setIsFlyoutVisible, setFieldName, populatedFields } = useFieldStatsFlyoutContext(); const closeFlyout = useCallback(() => setIsFlyoutVisible(false), [setIsFlyoutVisible]); @@ -46,18 +60,26 @@ export const useFieldStatsTrigger = () => { ); const renderOption = useCallback( - (option: EuiComboBoxOptionOption, searchValue: string): ReactNode => { - const field = (option as Option).field; - return option.isGroupLabelOption || !field ? ( - option.label - ) : ( - - ); + (option: T): ReactNode => { + if (isSelectableOption(option)) { + const field = (option as Option).field; + const isInternalEventRateFieldId = field?.id === EVENT_RATE_FIELD_ID; + const isEmpty = isInternalEventRateFieldId + ? false + : !populatedFields?.has(field?.id ?? field?.name); + const shouldHideInpectButton = option.hideTrigger ?? option['data-hide-inspect']; + return option.isGroupLabel || !field ? ( + option.label + ) : ( + + ); + } }, // eslint-disable-next-line react-hooks/exhaustive-deps [handleFieldStatsButtonClick, populatedFields?.size] @@ -71,4 +93,4 @@ export const useFieldStatsTrigger = () => { optionCss, populatedFields, }; -}; +} diff --git a/x-pack/plugins/aiops/public/components/change_point_detection/partitions_selector.tsx b/x-pack/plugins/aiops/public/components/change_point_detection/partitions_selector.tsx index 2034930913d6c..dddada9cd83db 100644 --- a/x-pack/plugins/aiops/public/components/change_point_detection/partitions_selector.tsx +++ b/x-pack/plugins/aiops/public/components/change_point_detection/partitions_selector.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { type SearchRequest } from '@elastic/elasticsearch/lib/api/types'; -import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; +import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { debounce } from 'lodash'; import usePrevious from 'react-use/lib/usePrevious'; import { useAiopsAppContext } from '../../hooks/use_aiops_app_context'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index b2b60e95dceda..3212eba8b2ddd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -30,11 +30,12 @@ import { import { DataGrid } from '@kbn/ml-data-grid'; import { SEARCH_QUERY_LANGUAGE } from '@kbn/ml-query-utils'; import { - EuiComboBoxWithFieldStats, + OptionListWithFieldStats, FieldStatsFlyoutProvider, type FieldForStats, } from '@kbn/ml-field-stats-flyout'; +import type { DropDownLabel } from '../../../../../jobs/new_job/pages/components/pick_fields_step/components/agg_select'; import { useMlApi, useMlKibana } from '../../../../../contexts/kibana'; import { useNewJobCapsServiceAnalytics } from '../../../../../services/new_job_capabilities/new_job_capabilities_service_analytics'; import { useDataSource } from '../../../../../contexts/ml'; @@ -665,7 +666,7 @@ export const ConfigurationStepForm: FC = ({ : []), ]} > - = ({ singleSelection={true} options={dependentVariableOptions} selectedOptions={dependentVariable ? [{ label: dependentVariable }] : []} - onChange={(selectedOptions) => { + onChange={(selectedOptions: DropDownLabel[]) => { setFormState({ dependentVariable: selectedOptions[0].label || '', }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx index 667840a6ca486..51146ee8992dc 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/advanced_detector_modal/advanced_detector_modal.tsx @@ -11,12 +11,12 @@ import React, { Fragment, useState, useContext, useEffect } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; import { - EuiComboBox, EuiFlexItem, EuiFlexGroup, EuiFlexGrid, EuiHorizontalRule, EuiTextArea, + EuiComboBox, } from '@elastic/eui'; import { @@ -25,7 +25,7 @@ import { EVENT_RATE_FIELD_ID, mlCategory, } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; +import { OptionListWithFieldStats, useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../../../job_creator_context'; import type { AdvancedJobCreator } from '../../../../../common/job_creator'; @@ -261,14 +261,13 @@ export const AdvancedDetectorModal: FC = ({ - @@ -277,53 +276,49 @@ export const AdvancedDetectorModal: FC = ({ - - - - diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx index 257b075ed4511..1d112a68fdf8a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/agg_select/agg_select.tsx @@ -8,34 +8,25 @@ import type { FC } from 'react'; import React, { useContext, useState, useEffect, useMemo } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox, EuiFormRow } from '@elastic/eui'; -import type { Field, Aggregation, AggFieldPair } from '@kbn/ml-anomaly-utils'; +import { EuiFormRow } from '@elastic/eui'; +import type { Field, AggFieldPair } from '@kbn/ml-anomaly-utils'; import { EVENT_RATE_FIELD_ID } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger, FieldStatsInfoButton } from '@kbn/ml-field-stats-flyout'; +import { i18n } from '@kbn/i18n'; +import { omit } from 'lodash'; +import type { DropDownLabel } from '@kbn/ml-field-stats-flyout'; +import { + OptionListWithFieldStats, + FieldStatsInfoButton, + useFieldStatsTrigger, +} from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../../../job_creator_context'; - -// The display label used for an aggregation e.g. sum(bytes). -export type Label = string; - -// Label object structured for EUI's ComboBox. -export interface DropDownLabel { - label: Label; - agg: Aggregation; - field: Field; -} - -// Label object structure for EUI's ComboBox with support for nesting. -export interface DropDownOption extends EuiComboBoxOptionOption { - label: Label; - options: DropDownLabel[]; -} - +export type { DropDownLabel }; export type DropDownProps = DropDownLabel[] | EuiComboBoxOptionOption[]; interface Props { fields: Field[]; - changeHandler(d: EuiComboBoxOptionOption[]): void; - selectedOptions: EuiComboBoxOptionOption[]; + changeHandler(d: DropDownLabel[]): void; + selectedOptions: DropDownLabel[]; removeOptions: AggFieldPair[]; } @@ -47,40 +38,51 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r const removeLabels = removeOptions.map(createLabel); const { handleFieldStatsButtonClick, populatedFields } = useFieldStatsTrigger(); - const options: EuiComboBoxOptionOption[] = useMemo( - () => - fields.map((f) => { - const aggOption: DropDownOption = { - isGroupLabelOption: true, + const options: DropDownLabel[] = useMemo( + () => { + const opts: DropDownLabel[] = []; + fields.forEach((f) => { + const isEmpty = f.id === EVENT_RATE_FIELD_ID ? false : !populatedFields?.has(f.name); + const aggOption: DropDownLabel = { + isGroupLabel: true, key: f.name, + searchableLabel: f.name, + isEmpty, // @ts-ignore Purposefully passing label as element instead of string // for more robust rendering label: ( ), - options: [], }; - if (typeof f.aggs !== 'undefined') { - aggOption.options = f.aggs - .filter((a) => a.dslName !== null) // don't include aggs which have no ES equivalent - .map( - (a) => - ({ - label: `${a.title}(${f.name})`, - agg: a, - field: f, - } as DropDownLabel) - ) - .filter((o) => removeLabels.includes(o.label) === false); + if (typeof f.aggs !== 'undefined' && f.aggs.length > 0) { + opts.push(aggOption); + + f.aggs.forEach((a) => { + const label = `${a.title}(${f.name})`; + if (removeLabels.includes(label) === true) return; + if (a.dslName !== null) { + const agg: DropDownLabel = { + key: label, + isEmpty, + hideTrigger: true, + isGroupLabel: false, + label, + agg: omit(a, 'fields'), + field: omit(f, 'aggs'), + }; + opts.push(agg); + } + }); } - return aggOption; - }), + }); + return opts; + }, // eslint-disable-next-line react-hooks/exhaustive-deps [handleFieldStatsButtonClick, fields, removeLabels, populatedFields?.size] ); @@ -96,8 +98,11 @@ export const AggSelect: FC = ({ fields, changeHandler, selectedOptions, r isInvalid={validation.valid === false} data-test-subj="mlJobWizardAggSelection" > - = ({ fields, changeHandler, selectedField }) => { const { jobCreator, jobCreatorUpdated } = useContext(JobCreatorContext); - const { renderOption, optionCss } = useFieldStatsTrigger(); + const { optionCss } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = useMemo( () => @@ -51,14 +50,13 @@ export const CategorizationFieldSelect: FC = ({ fields, changeHandler, se ); return ( - ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx index 237688b215511..33a7a04fc5640 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_partition_field/categorization_per_partition_input.tsx @@ -8,10 +8,9 @@ import type { FC } from 'react'; import React, { useCallback, useContext, useMemo } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; +import { OptionListWithFieldStats, useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../../../job_creator_context'; import { createFieldOptions } from '../../../../../common/job_creator/util/general'; @@ -27,7 +26,7 @@ export const CategorizationPerPartitionFieldSelect: FC = ({ selectedField, }) => { const { jobCreator, jobCreatorUpdated } = useContext(JobCreatorContext); - const { renderOption, optionCss } = useFieldStatsTrigger(); + const { optionCss } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = useMemo( () => @@ -54,14 +53,13 @@ export const CategorizationPerPartitionFieldSelect: FC = ({ ); return ( - ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx index f40e28f2fea28..3c866d6605083 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/geo_field/geo_field_select.tsx @@ -8,9 +8,8 @@ import type { FC } from 'react'; import React, { useCallback, useMemo } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; +import { OptionListWithFieldStats, useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; interface DropDownLabel { label: string; @@ -24,7 +23,7 @@ interface Props { } export const GeoFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { - const { renderOption, optionCss } = useFieldStatsTrigger(); + const { optionCss } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = useMemo( () => @@ -60,14 +59,13 @@ export const GeoFieldSelect: FC = ({ fields, changeHandler, selectedField ); return ( - ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx index 4c0946657d8e6..b667c8667eeee 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/influencers/influencers_select.tsx @@ -42,6 +42,7 @@ export const InfluencersSelect: FC = ({ fields, changeHandler, selectedIn return ( = ({ setIsValid }) => { function addDetector(selectedOptionsIn: DropDownLabel[]) { if (selectedOptionsIn !== null && selectedOptionsIn.length) { const option = selectedOptionsIn[0] as DropDownLabel; - if (typeof option !== 'undefined') { - const newPair = { agg: option.agg, field: option.field }; + if (typeof option !== 'undefined' && isPopulatedObject(option, ['agg', 'field'])) { + const newPair = { + agg: option.agg as Aggregation, + field: option.field as Field, + }; setAggFieldPairList([...aggFieldPairList, newPair]); setSelectedOptions([]); } else { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index 81dd83b9e157c..f193c0c49bbc9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -8,8 +8,9 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState, useReducer, useMemo } from 'react'; import { EuiHorizontalRule } from '@elastic/eui'; -import type { Field, AggFieldPair } from '@kbn/ml-anomaly-utils'; +import type { Field, AggFieldPair, Aggregation } from '@kbn/ml-anomaly-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { PopulationJobCreator } from '../../../../../common/job_creator'; @@ -72,9 +73,13 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { function addDetector(selectedOptionsIn: DropDownLabel[]) { if (selectedOptionsIn !== null && selectedOptionsIn.length) { - const option = selectedOptionsIn[0] as DropDownLabel; - if (typeof option !== 'undefined') { - const newPair = { agg: option.agg, field: option.field, by: { field: null, value: null } }; + const option = selectedOptionsIn[0] as DropDownLabel & { field: Field }; + if (typeof option !== 'undefined' && isPopulatedObject(option, ['agg', 'field'])) { + const newPair: AggFieldPair = { + agg: option.agg as Aggregation, + field: option.field, + by: { field: null, value: null }, + }; setAggFieldPairList([...aggFieldPairList, newPair]); setSelectedOptions([]); } else { diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx index a834a0d3bbdd4..e4a2f7588496c 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/rare_field/rare_field_select.tsx @@ -8,9 +8,8 @@ import type { FC } from 'react'; import React from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; import type { Field, SplitField } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; +import { OptionListWithFieldStats, useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; interface DropDownLabel { label: string; @@ -32,7 +31,7 @@ export const RareFieldSelect: FC = ({ testSubject, placeholder, }) => { - const { renderOption, optionCss } = useFieldStatsTrigger(); + const { optionCss } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = fields.map( (f) => @@ -58,7 +57,7 @@ export const RareFieldSelect: FC = ({ } return ( - = ({ placeholder={placeholder} data-test-subj={testSubject} isClearable={false} - renderOption={renderOption} /> ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index e37b1722e9bc2..dcf325adf41d1 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -7,8 +7,9 @@ import type { FC } from 'react'; import React, { Fragment, useContext, useEffect, useState, useMemo } from 'react'; -import type { AggFieldPair } from '@kbn/ml-anomaly-utils'; +import type { AggFieldPair, Aggregation, Field } from '@kbn/ml-anomaly-utils'; +import { isPopulatedObject } from '@kbn/ml-is-populated-object'; import { useUiSettings } from '../../../../../../../contexts/kibana'; import { JobCreatorContext } from '../../../job_creator_context'; import type { SingleMetricJobCreator } from '../../../../../common/job_creator'; @@ -58,9 +59,13 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { function detectorChangeHandler(selectedOptionsIn: DropDownLabel[]) { setSelectedOptions(selectedOptionsIn); if (selectedOptionsIn.length) { - const option = selectedOptionsIn[0]; - if (typeof option !== 'undefined') { - setAggFieldPair({ agg: option.agg, field: option.field }); + const option = selectedOptionsIn[0] as DropDownLabel; + if (typeof option !== 'undefined' && isPopulatedObject(option, ['agg', 'field'])) { + setAggFieldPair({ + agg: option.agg as Aggregation, + field: option.field as Field, + by: { field: null, value: null }, + }); } else { setAggFieldPair(null); } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx index d621e85b3f56a..72c236d690979 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/split_field_select/split_field_select.tsx @@ -8,15 +8,10 @@ import type { FC } from 'react'; import React from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; import type { Field, SplitField } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; - -interface DropDownLabel { - label: string; - field: Field; -} +import type { DropDownLabel } from '@kbn/ml-field-stats-flyout'; +import { OptionListWithFieldStats, useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; interface Props { fields: Field[]; @@ -35,8 +30,8 @@ export const SplitFieldSelect: FC = ({ testSubject, placeholder, }) => { - const { renderOption, optionCss } = useFieldStatsTrigger(); - const options: EuiComboBoxOptionOption[] = fields.map( + const { optionCss } = useFieldStatsTrigger(); + const options: DropDownLabel[] = fields.map( (f) => ({ label: f.name, @@ -45,14 +40,14 @@ export const SplitFieldSelect: FC = ({ } as DropDownLabel) ); - const selection: EuiComboBoxOptionOption[] = []; + const selection: DropDownLabel[] = []; if (selectedField !== null) { selection.push({ label: selectedField.name, field: selectedField } as DropDownLabel); } function onChange(selectedOptions: EuiComboBoxOptionOption[]) { const option = selectedOptions[0] as DropDownLabel; - if (typeof option !== 'undefined') { + if (typeof option?.field !== 'undefined') { changeHandler(option.field); } else { changeHandler(null); @@ -60,15 +55,14 @@ export const SplitFieldSelect: FC = ({ } return ( - ); }; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx index 8fc91b74d1a2c..59c761a46e75a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/summary_count_field/summary_count_field_select.tsx @@ -8,10 +8,8 @@ import type { FC } from 'react'; import React, { useContext } from 'react'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; -import { EuiComboBox } from '@elastic/eui'; - import type { Field } from '@kbn/ml-anomaly-utils'; -import { useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; +import { OptionListWithFieldStats, useFieldStatsTrigger } from '@kbn/ml-field-stats-flyout'; import { JobCreatorContext } from '../../../job_creator_context'; import { @@ -27,7 +25,7 @@ interface Props { export const SummaryCountFieldSelect: FC = ({ fields, changeHandler, selectedField }) => { const { jobCreator } = useContext(JobCreatorContext); - const { renderOption, optionCss } = useFieldStatsTrigger(); + const { optionCss } = useFieldStatsTrigger(); const options: EuiComboBoxOptionOption[] = [ ...createDocCountFieldOption(jobCreator.aggregationFields.length > 0), @@ -49,14 +47,13 @@ export const SummaryCountFieldSelect: FC = ({ fields, changeHandler, sele } return ( - ); }; diff --git a/x-pack/test/functional/services/ml/common_ui.ts b/x-pack/test/functional/services/ml/common_ui.ts index 7c12e406227d6..735c061419b14 100644 --- a/x-pack/test/functional/services/ml/common_ui.ts +++ b/x-pack/test/functional/services/ml/common_ui.ts @@ -451,5 +451,48 @@ export function MachineLearningCommonUIProvider({ async toggleSwitchIfNeeded(testSubj: string, targetState: boolean) { await testSubjects.setEuiSwitch(testSubj, targetState ? 'check' : 'uncheck'); }, + + /** Set value for OptionListWithFieldStats component */ + async setOptionsListWithFieldStatsValue(selector: string, value: string) { + await testSubjects.click(selector); + await testSubjects.existOrFail('optionsListControlAvailableOptions'); + + await retry.tryForTime(1000, async () => { + const enabled = + (await testSubjects.getAttribute(`optionsListIncludeEmptyFields`, 'aria-checked')) === + 'true'; + if (!enabled) { + await testSubjects.click(`optionsListIncludeEmptyFields`); + expect( + (await testSubjects.getAttribute(`optionsListIncludeEmptyFields`, 'aria-checked')) === + 'true' + ).to.eql(true, `Expected optionsListIncludeEmptyFields to be enabled.`); + } + }); + await retry.tryForTime(5 * 1000, async () => { + await testSubjects.find('optionsListFilterInput'); + + await testSubjects.setValue('optionsListFilterInput', value); + await testSubjects.click(`optionsListControlSelection-${value}`); + }); + }, + + async assertOptionsListWithFieldStatsValue( + selector: string, + expectedIdentifiers?: string[] | string, + label?: string + ) { + const expectedValue = + (Array.isArray(expectedIdentifiers) ? expectedIdentifiers.join('') : expectedIdentifiers) ?? + ''; + const actualValue = await testSubjects.getAttribute( + `${selector} > comboBoxSearchInput`, + 'value' + ); + expect(actualValue).to.eql( + expectedValue, + `Expected ${label ?? selector} value should be '${expectedValue}' (got '${actualValue}')` + ); + }, }; } diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index bda9bb2b350d1..4292480c9c61c 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -397,7 +397,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider( async selectDependentVariable(dependentVariable: string) { await this.waitForDependentVariableInputLoaded(); - await comboBox.set( + await mlCommonUI.setOptionsListWithFieldStatsValue( '~mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput', dependentVariable ); diff --git a/x-pack/test/functional/services/ml/index.ts b/x-pack/test/functional/services/ml/index.ts index 7249a493368d9..d62dfe921f69c 100644 --- a/x-pack/test/functional/services/ml/index.ts +++ b/x-pack/test/functional/services/ml/index.ts @@ -134,6 +134,7 @@ export function MachineLearningProvider(context: FtrProviderContext) { const jobWizardAdvanced = MachineLearningJobWizardAdvancedProvider(context, commonUI); const jobWizardCategorization = MachineLearningJobWizardCategorizationProvider( context, + commonUI, commonFieldStatsFlyout ); const jobWizardRecognizer = MachineLearningJobWizardRecognizerProvider(context, commonUI); @@ -143,13 +144,15 @@ export function MachineLearningProvider(context: FtrProviderContext) { customUrls, commonFieldStatsFlyout ); - const jobWizardGeo = MachineLearningJobWizardGeoProvider(context); + const jobWizardGeo = MachineLearningJobWizardGeoProvider(context, commonUI); const jobWizardMultiMetric = MachineLearningJobWizardMultiMetricProvider( context, + commonUI, commonFieldStatsFlyout ); const jobWizardPopulation = MachineLearningJobWizardPopulationProvider( context, + commonUI, commonFieldStatsFlyout ); diff --git a/x-pack/test/functional/services/ml/job_wizard_advanced.ts b/x-pack/test/functional/services/ml/job_wizard_advanced.ts index 317001efd75f8..a629494f3d6a2 100644 --- a/x-pack/test/functional/services/ml/job_wizard_advanced.ts +++ b/x-pack/test/functional/services/ml/job_wizard_advanced.ts @@ -7,8 +7,8 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; -import { MlCommonUI } from './common_ui'; +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlCommonUI } from './common_ui'; export function MachineLearningJobWizardAdvancedProvider( { getService }: FtrProviderContext, @@ -125,17 +125,16 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertCategorizationFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlCategorizationFieldNameSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlCategorizationFieldNameSelect > comboBoxInput', expectedIdentifier, - `Expected categorization field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'categorization field selection' ); }, async selectCategorizationField(identifier: string) { - await comboBox.set('mlCategorizationFieldNameSelect > comboBoxInput', identifier); + const selector = 'mlCategorizationFieldNameSelect > comboBoxInput'; + await mlCommonUI.setOptionsListWithFieldStatsValue(selector, identifier); await this.assertCategorizationFieldSelection([identifier]); }, @@ -144,18 +143,19 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertSummaryCountFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlSummaryCountFieldNameSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlSummaryCountFieldNameSelect > comboBoxInput', expectedIdentifier, - `Expected summary count field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'summary count field selection' ); }, async selectSummaryCountField(identifier: string) { await retry.tryForTime(15 * 1000, async () => { - await comboBox.set('mlSummaryCountFieldNameSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlSummaryCountFieldNameSelect > comboBoxInput', + identifier + ); await this.assertSummaryCountFieldSelection([identifier]); }); }, @@ -199,17 +199,18 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertDetectorFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlAdvancedFieldSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlAdvancedFieldSelect > comboBoxInput', expectedIdentifier, - `Expected detector field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'detector field selection' ); }, async selectDetectorField(identifier: string) { - await comboBox.set('mlAdvancedFieldSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlAdvancedFieldSelect > comboBoxInput', + identifier + ); await this.assertDetectorFieldSelection([identifier]); }, @@ -218,17 +219,18 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertDetectorByFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlAdvancedByFieldSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlAdvancedByFieldSelect > comboBoxInput', expectedIdentifier, - `Expected detector by field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'detector by field selection' ); }, async selectDetectorByField(identifier: string) { - await comboBox.set('mlAdvancedByFieldSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlAdvancedByFieldSelect > comboBoxInput', + identifier + ); await this.assertDetectorByFieldSelection([identifier]); }, @@ -237,17 +239,18 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertDetectorOverFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlAdvancedOverFieldSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlAdvancedOverFieldSelect > comboBoxInput', expectedIdentifier, - `Expected detector over field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'detector over field selection' ); }, async selectDetectorOverField(identifier: string) { - await comboBox.set('mlAdvancedOverFieldSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlAdvancedOverFieldSelect > comboBoxInput', + identifier + ); await this.assertDetectorOverFieldSelection([identifier]); }, @@ -256,17 +259,18 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertDetectorPartitionFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlAdvancedPartitionFieldSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlAdvancedPartitionFieldSelect > comboBoxInput', expectedIdentifier, - `Expected detector partition field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'detector partition field selection' ); }, async selectDetectorPartitionField(identifier: string) { - await comboBox.set('mlAdvancedPartitionFieldSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlAdvancedPartitionFieldSelect > comboBoxInput', + identifier + ); await this.assertDetectorPartitionFieldSelection([identifier]); }, @@ -275,17 +279,18 @@ export function MachineLearningJobWizardAdvancedProvider( }, async assertDetectorExcludeFrequentSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlAdvancedExcludeFrequentSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlAdvancedExcludeFrequentSelect > comboBoxInput', expectedIdentifier, - `Expected detector exclude frequent selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'detector exclude frequent selection' ); }, async selectDetectorExcludeFrequent(identifier: string) { - await comboBox.set('mlAdvancedExcludeFrequentSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlAdvancedExcludeFrequentSelect > comboBoxInput', + identifier + ); await this.assertDetectorExcludeFrequentSelection([identifier]); }, diff --git a/x-pack/test/functional/services/ml/job_wizard_categorization.ts b/x-pack/test/functional/services/ml/job_wizard_categorization.ts index 8c4e4a6386da7..7a24df49e7c05 100644 --- a/x-pack/test/functional/services/ml/job_wizard_categorization.ts +++ b/x-pack/test/functional/services/ml/job_wizard_categorization.ts @@ -10,9 +10,11 @@ import expect from '@kbn/expect'; import { CATEGORY_EXAMPLES_VALIDATION_STATUS } from '@kbn/ml-category-validator'; import type { FtrProviderContext } from '../../ftr_provider_context'; import type { MlCommonFieldStatsFlyout } from './field_stats_flyout'; +import type { MlCommonUI } from './common_ui'; export function MachineLearningJobWizardCategorizationProvider( { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, mlCommonFieldStatsFlyout: MlCommonFieldStatsFlyout ) { const comboBox = getService('comboBox'); @@ -50,7 +52,10 @@ export function MachineLearningJobWizardCategorizationProvider( }, async selectCategorizationField(identifier: string) { - await comboBox.set('mlCategorizationFieldNameSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlCategorizationFieldNameSelect > comboBoxInput', + identifier + ); await this.assertCategorizationFieldSelection([identifier]); }, diff --git a/x-pack/test/functional/services/ml/job_wizard_common.ts b/x-pack/test/functional/services/ml/job_wizard_common.ts index b1671626f191f..6dd2aaf1d1826 100644 --- a/x-pack/test/functional/services/ml/job_wizard_common.ts +++ b/x-pack/test/functional/services/ml/job_wizard_common.ts @@ -127,7 +127,10 @@ export function MachineLearningJobWizardCommonProvider( }, async selectAggAndField(identifier: string, isIdentifierKeptInField: boolean) { - await comboBox.set('mlJobWizardAggSelection > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlJobWizardAggSelection > comboBoxInput', + identifier + ); await this.assertAggAndFieldSelection(isIdentifierKeptInField ? [identifier] : []); }, diff --git a/x-pack/test/functional/services/ml/job_wizard_geo.ts b/x-pack/test/functional/services/ml/job_wizard_geo.ts index be2848985a6a2..3e4717942e990 100644 --- a/x-pack/test/functional/services/ml/job_wizard_geo.ts +++ b/x-pack/test/functional/services/ml/job_wizard_geo.ts @@ -7,10 +7,14 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export function MachineLearningJobWizardGeoProvider({ getService }: FtrProviderContext) { - const comboBox = getService('comboBox'); +import type { FtrProviderContext } from '../../ftr_provider_context'; +import type { MlCommonUI } from './common_ui'; + +export function MachineLearningJobWizardGeoProvider( + { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI +) { + const retry = getService('retry'); const testSubjects = getService('testSubjects'); return { @@ -19,18 +23,21 @@ export function MachineLearningJobWizardGeoProvider({ getService }: FtrProviderC }, async assertGeoFieldSelection(expectedIdentifier: string[]) { - const comboBoxSelectedOptions = await comboBox.getComboBoxSelectedOptions( - 'mlGeoFieldNameSelect > comboBoxInput' - ); - expect(comboBoxSelectedOptions).to.eql( + await mlCommonUI.assertOptionsListWithFieldStatsValue( + 'mlGeoFieldNameSelect > comboBoxInput', expectedIdentifier, - `Expected geo field selection to be '${expectedIdentifier}' (got '${comboBoxSelectedOptions}')` + 'geo field selection' ); }, async selectGeoField(identifier: string) { - await comboBox.set('mlGeoFieldNameSelect > comboBoxInput', identifier); - await this.assertGeoFieldSelection([identifier]); + await retry.tryForTime(5 * 1000, async () => { + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlGeoFieldNameSelect > comboBoxInput', + identifier + ); + await this.assertGeoFieldSelection([identifier]); + }); }, async assertSplitCardWithMapExampleExists() { @@ -40,13 +47,15 @@ export function MachineLearningJobWizardGeoProvider({ getService }: FtrProviderC async assertDetectorPreviewExists(detectorDescription: string) { await testSubjects.existOrFail('mlGeoMap > mlDetectorTitle'); const actualDetectorTitle = await testSubjects.getVisibleText('mlGeoMap > mlDetectorTitle'); - expect(actualDetectorTitle).to.eql( - detectorDescription, - `Expected detector title to be '${detectorDescription}' (got '${actualDetectorTitle}')` - ); - - await testSubjects.existOrFail('mlGeoJobWizardMap'); - await testSubjects.existOrFail('mapContainer'); + await retry.tryForTime(5 * 1000, async () => { + expect(actualDetectorTitle).to.eql( + detectorDescription, + `Expected detector title to be '${detectorDescription}' (got '${actualDetectorTitle}')` + ); + + await testSubjects.existOrFail('mlGeoJobWizardMap'); + await testSubjects.existOrFail('mapContainer'); + }); }, }; } diff --git a/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts b/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts index f5c347ad697b2..0390f716ddc04 100644 --- a/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts +++ b/x-pack/test/functional/services/ml/job_wizard_multi_metric.ts @@ -9,9 +9,11 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; import type { MlCommonFieldStatsFlyout } from './field_stats_flyout'; +import type { MlCommonUI } from './common_ui'; export function MachineLearningJobWizardMultiMetricProvider( { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, mlCommonFieldStatsFlyout: MlCommonFieldStatsFlyout ) { const comboBox = getService('comboBox'); @@ -46,7 +48,11 @@ export function MachineLearningJobWizardMultiMetricProvider( }, async selectSplitField(identifier: string) { - await comboBox.set('mlSplitFieldSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlSplitFieldSelect > comboBoxInput', + identifier + ); + await this.assertSplitFieldSelection([identifier]); }, diff --git a/x-pack/test/functional/services/ml/job_wizard_population.ts b/x-pack/test/functional/services/ml/job_wizard_population.ts index 418369bbf905f..0039138e94ce1 100644 --- a/x-pack/test/functional/services/ml/job_wizard_population.ts +++ b/x-pack/test/functional/services/ml/job_wizard_population.ts @@ -9,9 +9,11 @@ import expect from '@kbn/expect'; import type { FtrProviderContext } from '../../ftr_provider_context'; import type { MlCommonFieldStatsFlyout } from './field_stats_flyout'; +import type { MlCommonUI } from './common_ui'; export function MachineLearningJobWizardPopulationProvider( { getService }: FtrProviderContext, + mlCommonUI: MlCommonUI, mlCommonFieldStatsFlyout: MlCommonFieldStatsFlyout ) { const comboBox = getService('comboBox'); @@ -46,7 +48,10 @@ export function MachineLearningJobWizardPopulationProvider( }, async selectPopulationField(identifier: string) { - await comboBox.set('mlPopulationSplitFieldSelect > comboBoxInput', identifier); + await mlCommonUI.setOptionsListWithFieldStatsValue( + 'mlPopulationSplitFieldSelect > comboBoxInput', + identifier + ); await this.assertPopulationFieldSelection([identifier]); }, @@ -70,7 +75,7 @@ export function MachineLearningJobWizardPopulationProvider( }, async selectDetectorSplitField(detectorPosition: number, identifier: string) { - await comboBox.set( + await mlCommonUI.setOptionsListWithFieldStatsValue( `mlDetector ${detectorPosition} > mlByFieldSelect > comboBoxInput`, identifier );