[8.x] [ML] Adds ability to toggle visibility for empty fields when choosing an aggregation or field in Anomaly detection, data frame analytics (#186670) (#196180)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ML] Adds ability to toggle visibility for empty fields when choosing
an aggregation or field in Anomaly detection, data frame analytics
(#186670)](https://github.com/elastic/kibana/pull/186670)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Quynh Nguyen
(Quinn)","email":"43350163+qn895@users.noreply.github.com"},"sourceCommit":{"committedDate":"2024-10-14T16:17:38Z","message":"[ML]
Adds ability to toggle visibility for empty fields when choosing an
aggregation or field in Anomaly detection, data frame analytics
(#186670)\n\n## Summary\r\n\r\nThis PR adds new ability to toggle
visibility for empty fields when\r\nchoosing an aggregation or field in
Anomaly detection and Data
frame\r\nanalytics\r\n\r\n\r\n5d8b0788-dd59-44e4-b324-3a4035b7a0ec\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f922089c5f088738acd30aeb17de7c7ec07604ce","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement",":ml","Feature:Anomaly
Detection","Feature:Data Frame
Analytics","v9.0.0","backport:prev-major","v8.16.0","backport:current-major"],"title":"[ML]
Adds ability to toggle visibility for empty fields when choosing an
aggregation or field in Anomaly detection, data frame
analytics","number":186670,"url":"https://github.com/elastic/kibana/pull/186670","mergeCommit":{"message":"[ML]
Adds ability to toggle visibility for empty fields when choosing an
aggregation or field in Anomaly detection, data frame analytics
(#186670)\n\n## Summary\r\n\r\nThis PR adds new ability to toggle
visibility for empty fields when\r\nchoosing an aggregation or field in
Anomaly detection and Data
frame\r\nanalytics\r\n\r\n\r\n5d8b0788-dd59-44e4-b324-3a4035b7a0ec\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f922089c5f088738acd30aeb17de7c7ec07604ce"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/186670","number":186670,"mergeCommit":{"message":"[ML]
Adds ability to toggle visibility for empty fields when choosing an
aggregation or field in Anomaly detection, data frame analytics
(#186670)\n\n## Summary\r\n\r\nThis PR adds new ability to toggle
visibility for empty fields when\r\nchoosing an aggregation or field in
Anomaly detection and Data
frame\r\nanalytics\r\n\r\n\r\n5d8b0788-dd59-44e4-b324-3a4035b7a0ec\r\n\r\n\r\n\r\n###
Checklist\r\n\r\nDelete any items that are not applicable to this
PR.\r\n\r\n- [ ] Any text added follows [EUI's
writing\r\nguidelines](https://elastic.github.io/eui/#/guidelines/writing),
uses\r\nsentence case text and includes
[i18n\r\nsupport](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)\r\n-
[
]\r\n[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)\r\nwas
added for features that require explanation or tutorials\r\n- [ ] [Unit
or
functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere
updated or added to match the most common scenarios\r\n- [ ] [Flaky
Test\r\nRunner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1)
was\r\nused on any tests changed\r\n- [ ] Any UI touched in this PR is
usable by keyboard only (learn more\r\nabout [keyboard
accessibility](https://webaim.org/techniques/keyboard/))\r\n- [ ] Any UI
touched in this PR does not create any new axe failures\r\n(run axe in
browser:\r\n[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),\r\n[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))\r\n-
[ ] If a plugin configuration key changed, check if it needs to
be\r\nallowlisted in the cloud and added to the
[docker\r\nlist](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker)\r\n-
[ ] This renders correctly on smaller devices using a
responsive\r\nlayout. (You can test this [in
your\r\nbrowser](https://www.browserstack.com/guide/responsive-testing-on-local-server))\r\n-
[ ] This was checked for
[cross-browser\r\ncompatibility](https://www.elastic.co/support/matrix#matrix_browsers)\r\n\r\n\r\n###
Risk Matrix\r\n\r\nDelete this section if it is not applicable to this
PR.\r\n\r\nBefore closing this PR, invite QA, stakeholders, and other
developers to\r\nidentify risks that should be tested prior to the
change/feature\r\nrelease.\r\n\r\nWhen forming the risk matrix, consider
some of the following examples\r\nand how they may potentially impact
the change:\r\n\r\n| Risk | Probability | Severity | Mitigation/Notes
|\r\n\r\n|---------------------------|-------------|----------|-------------------------|\r\n|
Multiple Spaces&mdash;unexpected behavior in non-default Kibana
Space.\r\n| Low | High | Integration tests will verify that all features
are still\r\nsupported in non-default Kibana Space and when user
switches between\r\nspaces. |\r\n| Multiple nodes&mdash;Elasticsearch
polling might have race conditions\r\nwhen multiple Kibana nodes are
polling for the same tasks. | High | Low\r\n| Tasks are idempotent, so
executing them multiple times will not result\r\nin logical error, but
will degrade performance. To test for this case we\r\nadd plenty of unit
tests around this logic and document manual testing\r\nprocedure. |\r\n|
Code should gracefully handle cases when feature X or plugin Y
are\r\ndisabled. | Medium | High | Unit tests will verify that any
feature flag\r\nor plugin combination still results in our service
operational. |\r\n| [See more potential
risk\r\nexamples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx)
|\r\n\r\n\r\n### For maintainers\r\n\r\n- [ ] This was checked for
breaking API changes and was
[labeled\r\nappropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<42973632+kibanamachine@users.noreply.github.com>","sha":"f922089c5f088738acd30aeb17de7c7ec07604ce"}},{"branch":"8.x","label":"v8.16.0","branchLabelMappingKey":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->

Co-authored-by: Quynh Nguyen (Quinn) <43350163+qn895@users.noreply.github.com>
This commit is contained in:
Kibana Machine 2024-10-15 05:14:58 +11:00 committed by GitHub
parent 77f241184e
commit df849c5431
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 695 additions and 274 deletions

View file

@ -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
* <EuiComboBoxWithFieldStats options={options} />
* ```
* @param {EuiComboBoxWithFieldStatsProps} props - The component props.
*/
export const EuiComboBoxWithFieldStats: FC<EuiComboBoxWithFieldStatsProps> = (props) => {
const { options, ...restProps } = props;
const { renderOption } = useFieldStatsTrigger();
const comboBoxOptions: EuiComboBoxOptionOption[] = useMemo(
() =>
Array.isArray(options)
? options.map((o) => ({
...o,
css: optionCss,
}))
: [],
[options]
);
return (
<EuiComboBox
{...restProps}
options={comboBoxOptions}
renderOption={renderOption}
singleSelection={{ asPlainText: true }}
/>
);
};

View file

@ -142,6 +142,7 @@ export const FieldStatsFlyoutProvider: FC<FieldStatsFlyoutProviderProps> = (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);

View file

@ -88,6 +88,7 @@ export const FieldStatsInfoButton: FC<FieldStatsInfoButtonProps> = (props) => {
defaultMessage: '(no data found in 1000 sample records)',
})
: '';
return (
<EuiFlexGroup gutterSize="none" alignItems="center">
<EuiFlexItem grow={false}>
@ -135,14 +136,15 @@ export const FieldStatsInfoButton: FC<FieldStatsInfoButtonProps> = (props) => {
grow={false}
css={{
paddingRight: themeVars.euiTheme.euiSizeXS,
paddingBottom: themeVars.euiTheme.euiSizeXS,
}}
>
<FieldIcon
color={isEmpty ? themeVars.euiTheme.euiColorDisabled : undefined}
type={getKbnFieldIconType(field.type)}
fill="none"
/>
{!hideTrigger ? (
<FieldIcon
color={isEmpty ? themeVars.euiTheme.euiColorDisabled : undefined}
type={getKbnFieldIconType(field.type)}
fill="none"
/>
) : null}
</EuiFlexItem>
<EuiText
color={isEmpty ? 'subdued' : undefined}
@ -150,7 +152,6 @@ export const FieldStatsInfoButton: FC<FieldStatsInfoButtonProps> = (props) => {
aria-label={label}
title={label}
className="euiComboBoxOption__content"
css={{ paddingBottom: themeVars.euiTheme.euiSizeXS }}
>
{label}
</EuiText>

View file

@ -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';

View file

@ -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<EuiComboBoxOptionOption<string | number | string[] | undefined>>
) => 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<EuiComboBoxOptionOption<string | number | string[] | undefined>>
) => void);
setPopoverOpen: (open: boolean) => void;
}
const OptionsListPopoverSuggestions: FC<OptionsListPopoverSuggestionsProps> = ({
options,
renderOption,
singleSelection,
onChange,
setPopoverOpen,
}) => {
const [selectableOptions, setSelectableOptions] = useState<DropDownLabel[]>([]); // 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 (
<EuiSelectable
searchProps={{ 'data-test-subj': 'optionsListFilterInput' }}
singleSelection={Boolean(singleSelection)}
searchable
options={selectableOptions as Array<EuiSelectableOption<string>>}
renderOption={renderOption}
listProps={{ onFocusBadge: false }}
onChange={(opts, _, changedOption) => {
const option = changedOption as DropDownLabel;
if (singleSelection) {
if (onChange) {
onChange([option as EuiComboBoxOptionOption<string | number | string[] | undefined>]);
setPopoverOpen(false);
}
} else {
if (onChange) {
onChange([option as EuiComboBoxOptionOption<string | number | string[] | undefined>]);
setPopoverOpen(false);
}
}
}}
>
{(list, search) => (
<>
{search}
{list}
</>
)}
</EuiSelectable>
);
};
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 (
<div
id={`control-popover-${id}`}
className={'optionsList__popover'}
data-test-subj={`optionsListControlPopover`}
>
<EuiFlexItem
data-test-subj={`optionsListControlAvailableOptions`}
css={css({ width: '100%', height: '100%' })}
>
<OptionsListPopoverSuggestions
renderOption={renderOption}
options={filteredOptions}
singleSelection={singleSelection}
onChange={onChange}
setPopoverOpen={setPopoverOpen}
/>
</EuiFlexItem>
<OptionsListPopoverFooter
showEmptyFields={showEmptyFields}
setShowEmptyFields={setShowEmptyFields}
isLoading={isLoading}
/>
</div>
);
};

View file

@ -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 (
<EuiPopoverFooter
paddingSize="none"
css={css({
height: euiThemeVars.euiButtonHeight,
backgroundColor: useEuiBackgroundColor('subdued'),
alignItems: 'center',
display: 'flex',
paddingLeft: euiThemeVars.euiSizeS,
})}
>
{isLoading ? (
// @ts-expect-error css should be ok
<div css={css({ position: 'absolute', width: '100%' })}>
<EuiProgress
data-test-subj="optionsList-control-popover-loading"
size="xs"
color="accent"
/>
</div>
) : null}
<EuiSwitch
data-test-subj="optionsListIncludeEmptyFields"
label={i18n.translate('xpack.ml.controls.optionsList.popover.includeEmptyFieldsLabel', {
defaultMessage: 'Include empty fields',
})}
checked={showEmptyFields}
onChange={(e) => setShowEmptyFields(e.target.checked)}
/>
</EuiPopoverFooter>
);
};

View file

@ -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<OptionListWithFieldStatsProps> = ({
options,
placeholder,
singleSelection = false,
onChange,
selectedOptions,
fullWidth,
isDisabled,
isLoading,
isClearable = true,
'aria-label': ariaLabel,
'data-test-subj': dataTestSubj,
}) => {
const { renderOption } = useFieldStatsTrigger<DropDownLabel>();
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 (
<EuiInputPopover
fullWidth={fullWidth}
data-test-subj={dataTestSubj}
id={popoverId}
input={
<EuiFormControlLayout
fullWidth={fullWidth}
// Adding classname to make functional tests similar to EuiComboBox
className={singleSelection ? 'euiComboBox__inputWrap--plainText' : ''}
data-test-subj="comboBoxInput"
clear={isClearable && hasSelections ? { onClick: onChange.bind(null, []) } : undefined}
isDropdown={true}
>
<EuiFieldText
fullWidth={fullWidth}
disabled={isDisabled}
placeholder={placeholder}
data-test-subj="comboBoxSearchInput"
onClick={setPopoverOpen.bind(null, true)}
type="text"
role="combobox"
controlOnly
aria-expanded={isPopoverOpen ? 'true' : 'false'}
aria-label={
placeholder ??
i18n.translate('xpack.ml.controls.optionsList.popover.selectOptionAriaLabel', {
defaultMessage: 'Select an option',
})
}
onChange={() => {}}
value={value}
/>
</EuiFormControlLayout>
}
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 ? (
<OptionsListPopover
options={comboBoxOptions}
renderOption={renderOption}
singleSelection={singleSelection}
onChange={onChange}
setPopoverOpen={setPopoverOpen}
isLoading={isLoading}
/>
) : null}
</EuiInputPopover>
);
};

View file

@ -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<T> {
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<T> = EuiSelectableOption<BaseOption<T>>;
export type DropDownLabel<T = string> =
| (EuiComboBoxOptionOption & BaseOption<Aggregation>)
| SelectableOption<T>;
export function isSelectableOption<T>(option: unknown): option is SelectableOption<T> {
return typeof option === 'object' && option !== null && Object.hasOwn(option, 'label');
}

View file

@ -32,5 +32,6 @@
"@kbn/ml-query-utils",
"@kbn/ml-is-defined",
"@kbn/field-types",
"@kbn/ui-theme",
]
}

View file

@ -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<string> {
field: Field;
}
@ -30,7 +44,7 @@ interface Option extends EuiComboBoxOptionOption<string> {
* - `optionCss`: CSS styles for the options in the combo box.
* - `populatedFields`: A set of populated fields.
*/
export const useFieldStatsTrigger = () => {
export function useFieldStatsTrigger<T = DropDownLabel>() {
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
) : (
<FieldStatsInfoButton
isEmpty={populatedFields && !populatedFields.has(field.id)}
field={field}
label={option.label}
onButtonClick={handleFieldStatsButtonClick}
/>
);
(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
) : (
<FieldStatsInfoButton
isEmpty={isEmpty}
field={field}
label={option.label}
onButtonClick={handleFieldStatsButtonClick}
hideTrigger={shouldHideInpectButton ?? isInternalEventRateFieldId}
/>
);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[handleFieldStatsButtonClick, populatedFields?.size]
@ -71,4 +93,4 @@ export const useFieldStatsTrigger = () => {
optionCss,
populatedFields,
};
};
}

View file

@ -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';

View file

@ -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<ConfigurationStepProps> = ({
: []),
]}
>
<EuiComboBoxWithFieldStats
<OptionListWithFieldStats
fullWidth
aria-label={i18n.translate(
'xpack.ml.dataframe.analytics.create.dependentVariableInputAriaLabel',
@ -694,7 +695,7 @@ export const ConfigurationStepForm: FC<ConfigurationStepProps> = ({
singleSelection={true}
options={dependentVariableOptions}
selectedOptions={dependentVariable ? [{ label: dependentVariable }] : []}
onChange={(selectedOptions) => {
onChange={(selectedOptions: DropDownLabel[]) => {
setFormState({
dependentVariable: selectedOptions[0].label || '',
});

View file

@ -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<Props> = ({
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlAdvancedFieldSelect">
<FieldDescription>
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={currentFieldOptions}
selectedOptions={createSelectedOptions(fieldOption)}
onChange={onOptionChange(setFieldOption)}
isClearable={true}
isDisabled={fieldOptionEnabled === false}
renderOption={renderOption}
/>
</FieldDescription>
</EuiFlexItem>
@ -277,53 +276,49 @@ export const AdvancedDetectorModal: FC<Props> = ({
<EuiFlexGrid columns={2}>
<EuiFlexItem data-test-subj="mlAdvancedByFieldSelect">
<ByFieldDescription>
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={splitFieldOptions}
selectedOptions={createSelectedOptions(byFieldOption)}
onChange={onOptionChange(setByFieldOption)}
isClearable={true}
isDisabled={splitFieldsEnabled === false}
renderOption={renderOption}
/>
</ByFieldDescription>
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlAdvancedOverFieldSelect">
<OverFieldDescription>
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={splitFieldOptions}
selectedOptions={createSelectedOptions(overFieldOption)}
onChange={onOptionChange(setOverFieldOption)}
isClearable={true}
isDisabled={splitFieldsEnabled === false}
renderOption={renderOption}
/>
</OverFieldDescription>
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlAdvancedPartitionFieldSelect">
<PartitionFieldDescription>
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={splitFieldOptions}
selectedOptions={createSelectedOptions(partitionFieldOption)}
onChange={onOptionChange(setPartitionFieldOption)}
isClearable={true}
isDisabled={splitFieldsEnabled === false}
renderOption={renderOption}
/>
</PartitionFieldDescription>
</EuiFlexItem>
<EuiFlexItem data-test-subj="mlAdvancedExcludeFrequentSelect">
<ExcludeFrequentDescription>
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={excludeFrequentOptions}
selectedOptions={createSelectedOptions(excludeFrequentOption)}
onChange={onOptionChange(setExcludeFrequentOption)}
isClearable={true}
isDisabled={splitFieldsEnabled === false || excludeFrequentEnabled === false}
renderOption={renderOption}
/>
</ExcludeFrequentDescription>
</EuiFlexItem>

View file

@ -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<Props> = ({ 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: (
<FieldStatsInfoButton
hideTrigger={f.id === EVENT_RATE_FIELD_ID}
isEmpty={f.id === EVENT_RATE_FIELD_ID ? false : !populatedFields?.has(f.name)}
isEmpty={isEmpty}
field={f}
label={f.name}
onButtonClick={handleFieldStatsButtonClick}
/>
),
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<Props> = ({ fields, changeHandler, selectedOptions, r
isInvalid={validation.valid === false}
data-test-subj="mlJobWizardAggSelection"
>
<EuiComboBox
singleSelection={{ asPlainText: true }}
<OptionListWithFieldStats
aria-label={i18n.translate('xpack.ml.newJob.wizard.aggSelect.ariaLabel', {
defaultMessage: 'Select an aggregation',
})}
singleSelection={true}
options={options}
selectedOptions={selectedOptions}
onChange={changeHandler}

View file

@ -4,6 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
export type { DropDownLabel, DropDownProps } from './agg_select';
export type { DropDownLabel, DropDownOption, DropDownProps } from './agg_select';
export { AggSelect, createLabel } from './agg_select';

View file

@ -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';
@ -24,7 +23,7 @@ interface Props {
export const CategorizationFieldSelect: FC<Props> = ({ 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<Props> = ({ fields, changeHandler, se
);
return (
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChange}
isClearable={true}
data-test-subj="mlCategorizationFieldNameSelect"
renderOption={renderOption}
/>
);
};

View file

@ -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<Props> = ({
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<Props> = ({
);
return (
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChange}
isClearable={true}
data-test-subj="mlJobWizardCategorizationPerPartitionFieldNameSelect"
renderOption={renderOption}
/>
);
};

View file

@ -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<Props> = ({ fields, changeHandler, selectedField }) => {
const { renderOption, optionCss } = useFieldStatsTrigger();
const { optionCss } = useFieldStatsTrigger();
const options: EuiComboBoxOptionOption[] = useMemo(
() =>
@ -60,14 +59,13 @@ export const GeoFieldSelect: FC<Props> = ({ fields, changeHandler, selectedField
);
return (
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChange}
isClearable={true}
data-test-subj="mlGeoFieldNameSelect"
renderOption={renderOption}
/>
);
};

View file

@ -42,6 +42,7 @@ export const InfluencersSelect: FC<Props> = ({ fields, changeHandler, selectedIn
return (
<EuiComboBox
singleSelection={false}
options={options}
selectedOptions={selection}
onChange={onChange}

View file

@ -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 { MultiMetricJobCreator } from '../../../../../common/job_creator';
@ -64,8 +65,11 @@ export const MultiMetricDetectors: FC<Props> = ({ 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 {

View file

@ -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<Props> = ({ 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 {

View file

@ -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<Props> = ({
testSubject,
placeholder,
}) => {
const { renderOption, optionCss } = useFieldStatsTrigger();
const { optionCss } = useFieldStatsTrigger();
const options: EuiComboBoxOptionOption[] = fields.map(
(f) =>
@ -58,7 +57,7 @@ export const RareFieldSelect: FC<Props> = ({
}
return (
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
@ -66,7 +65,6 @@ export const RareFieldSelect: FC<Props> = ({
placeholder={placeholder}
data-test-subj={testSubject}
isClearable={false}
renderOption={renderOption}
/>
);
};

View file

@ -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<Props> = ({ 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<Aggregation>;
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);
}

View file

@ -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<Props> = ({
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<Props> = ({
} 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<Props> = ({
}
return (
<EuiComboBox
singleSelection={{ asPlainText: true }}
<OptionListWithFieldStats
singleSelection={true}
options={options}
selectedOptions={selection}
onChange={onChange}
isClearable={isClearable}
placeholder={placeholder}
data-test-subj={testSubject}
renderOption={renderOption}
/>
);
};

View file

@ -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<Props> = ({ 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<Props> = ({ fields, changeHandler, sele
}
return (
<EuiComboBox
<OptionListWithFieldStats
singleSelection={{ asPlainText: true }}
options={options}
selectedOptions={selection}
onChange={onChange}
isClearable={true}
data-test-subj="mlSummaryCountFieldNameSelect"
renderOption={renderOption}
/>
);
};

View file

@ -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}')`
);
},
};
}

View file

@ -397,7 +397,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
async selectDependentVariable(dependentVariable: string) {
await this.waitForDependentVariableInputLoaded();
await comboBox.set(
await mlCommonUI.setOptionsListWithFieldStatsValue(
'~mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput',
dependentVariable
);

View file

@ -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
);

View file

@ -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]);
},

View file

@ -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]);
},

View file

@ -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] : []);
},

View file

@ -7,10 +7,14 @@
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../ftr_provider_context';
import type { FtrProviderContext } from '../../ftr_provider_context';
import type { MlCommonUI } from './common_ui';
export function MachineLearningJobWizardGeoProvider({ getService }: FtrProviderContext) {
const comboBox = getService('comboBox');
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 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');
await testSubjects.existOrFail('mlGeoJobWizardMap');
await testSubjects.existOrFail('mapContainer');
});
},
};
}

View file

@ -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]);
},

View file

@ -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
);