mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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—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—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—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—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—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—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:
parent
77f241184e
commit
df849c5431
34 changed files with 695 additions and 274 deletions
|
@ -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 }}
|
||||
/>
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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');
|
||||
}
|
|
@ -32,5 +32,6 @@
|
|||
"@kbn/ml-query-utils",
|
||||
"@kbn/ml-is-defined",
|
||||
"@kbn/field-types",
|
||||
"@kbn/ui-theme",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 || '',
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -42,6 +42,7 @@ export const InfluencersSelect: FC<Props> = ({ fields, changeHandler, selectedIn
|
|||
|
||||
return (
|
||||
<EuiComboBox
|
||||
singleSelection={false}
|
||||
options={options}
|
||||
selectedOptions={selection}
|
||||
onChange={onChange}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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}')`
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -397,7 +397,7 @@ export function MachineLearningDataFrameAnalyticsCreationProvider(
|
|||
|
||||
async selectDependentVariable(dependentVariable: string) {
|
||||
await this.waitForDependentVariableInputLoaded();
|
||||
await comboBox.set(
|
||||
await mlCommonUI.setOptionsListWithFieldStatsValue(
|
||||
'~mlAnalyticsCreateJobWizardDependentVariableSelect > comboBoxInput',
|
||||
dependentVariable
|
||||
);
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
|
|
|
@ -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] : []);
|
||||
},
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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]);
|
||||
},
|
||||
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue