diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts
index eb1f7b886d427..8835c7f5767f6 100644
--- a/src/plugins/controls/common/options_list/types.ts
+++ b/src/plugins/controls/common/options_list/types.ts
@@ -40,6 +40,7 @@ export interface OptionsListSuggestions {
* The Options list response is returned from the serverside Options List route.
*/
export interface OptionsListResponse {
+ rejected: boolean;
suggestions: OptionsListSuggestions;
totalCardinality: number;
invalidSelections?: string[];
diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx
index 02a13125ba49c..7f091abe34a56 100644
--- a/src/plugins/controls/public/__stories__/controls.stories.tsx
+++ b/src/plugins/controls/public/__stories__/controls.stories.tsx
@@ -61,6 +61,7 @@ const storybookStubOptionsListRequest = async (
{}
),
totalCardinality: 100,
+ rejected: false,
}),
120
)
diff --git a/src/plugins/controls/public/options_list/components/options_list.scss b/src/plugins/controls/public/options_list/components/options_list.scss
index c737dd6dc0215..e88208ee4c623 100644
--- a/src/plugins/controls/public/options_list/components/options_list.scss
+++ b/src/plugins/controls/public/options_list/components/options_list.scss
@@ -7,15 +7,6 @@
height: 100%;
}
-.optionsList__items {
- @include euiScrollBar;
-
- overflow-y: auto;
- max-height: $euiSize * 30;
- width: $euiSize * 25;
- max-width: 100%;
-}
-
.optionsList__actions {
padding: $euiSizeS;
border-bottom: $euiBorderThin;
diff --git a/src/plugins/controls/public/options_list/components/options_list_control.tsx b/src/plugins/controls/public/options_list/components/options_list_control.tsx
index 43742a817e3ec..3035eada20caf 100644
--- a/src/plugins/controls/public/options_list/components/options_list_control.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_control.tsx
@@ -43,18 +43,22 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub
const existsSelected = select((state) => state.explicitInput.existsSelected);
const controlStyle = select((state) => state.explicitInput.controlStyle);
const singleSelect = select((state) => state.explicitInput.singleSelect);
+ const fieldName = select((state) => state.explicitInput.fieldName);
const exclude = select((state) => state.explicitInput.exclude);
const id = select((state) => state.explicitInput.id);
const loading = select((state) => state.output.loading);
// debounce loading state so loading doesn't flash when user types
- const [buttonLoading, setButtonLoading] = useState(true);
- const debounceSetButtonLoading = useMemo(
- () => debounce((latestLoading: boolean) => setButtonLoading(latestLoading), 100),
+ const [debouncedLoading, setDebouncedLoading] = useState(true);
+ const debounceSetLoading = useMemo(
+ () =>
+ debounce((latestLoading: boolean) => {
+ setDebouncedLoading(latestLoading);
+ }, 100),
[]
);
- useEffect(() => debounceSetButtonLoading(loading ?? false), [loading, debounceSetButtonLoading]);
+ useEffect(() => debounceSetLoading(loading ?? false), [loading, debounceSetLoading]);
// remove all other selections if this control is single select
useEffect(() => {
@@ -111,7 +115,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub
setIsPopoverOpen(false)}
anchorClassName="optionsList__anchorOverride"
- aria-labelledby={`control-popover-${id}`}
+ aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)}
>
-
+
);
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx b/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx
index b1315be51ae1e..acb5d24d80659 100644
--- a/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_popover.test.tsx
@@ -21,6 +21,7 @@ import { ControlOutput, OptionsListEmbeddableInput } from '../..';
describe('Options list popover', () => {
const defaultProps = {
width: 500,
+ isLoading: false,
updateSearchString: jest.fn(),
};
@@ -56,13 +57,13 @@ describe('Options list popover', () => {
test('available options list width responds to container size', async () => {
let popover = await mountComponent({ popoverProps: { width: 301 } });
- let availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
- expect(availableOptionsDiv.getDOMNode().getAttribute('style')).toBe('width: 301px;');
+ let popoverDiv = findTestSubject(popover, 'optionsList-control-popover');
+ expect(popoverDiv.getDOMNode().getAttribute('style')).toBe('width: 301px;');
// the div cannot be smaller than 301 pixels wide
popover = await mountComponent({ popoverProps: { width: 300 } });
- availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
- expect(availableOptionsDiv.getDOMNode().getAttribute('style')).toBe(null);
+ popoverDiv = findTestSubject(popover, 'optionsList-control-available-options');
+ expect(popoverDiv.getDOMNode().getAttribute('style')).toBe(null);
});
test('no available options', async () => {
@@ -92,13 +93,12 @@ describe('Options list popover', () => {
explicitInput: { selectedOptions: selections },
});
clickShowOnlySelections(popover);
- const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
- availableOptionsDiv
- .childAt(0)
- .children()
- .forEach((child, i) => {
- expect(child.text()).toBe(selections[i]);
- });
+ const availableOptions = popover.find(
+ '[data-test-subj="optionsList-control-available-options"] ul'
+ );
+ availableOptions.children().forEach((child, i) => {
+ expect(child.text()).toBe(`${selections[i]} - Checked option.`);
+ });
});
test('disable search and sort when show only selected toggle is true', async () => {
@@ -132,11 +132,18 @@ describe('Options list popover', () => {
},
});
const validSelection = findTestSubject(popover, 'optionsList-control-selection-bark');
- expect(validSelection.text()).toEqual('bark75');
+ expect(validSelection.find('.euiSelectableListItem__text').text()).toEqual(
+ 'bark - Checked option.'
+ );
+ expect(
+ validSelection.find('div[data-test-subj="optionsList-document-count-badge"]').text().trim()
+ ).toEqual('75');
const title = findTestSubject(popover, 'optionList__ignoredSelectionLabel').text();
expect(title).toEqual('Ignored selection');
const invalidSelection = findTestSubject(popover, 'optionsList-control-ignored-selection-woof');
- expect(invalidSelection.text()).toEqual('woof');
+ expect(invalidSelection.find('.euiSelectableListItem__text').text()).toEqual(
+ 'woof - Checked option.'
+ );
expect(invalidSelection.hasClass('optionsList__selectionInvalid')).toBe(true);
});
@@ -221,8 +228,10 @@ describe('Options list popover', () => {
explicitInput: { existsSelected: true },
});
clickShowOnlySelections(popover);
- const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options');
- expect(availableOptionsDiv.children().at(0).text()).toBe('Exists');
+ const availableOptions = popover.find(
+ '[data-test-subj="optionsList-control-available-options"] ul'
+ );
+ expect(availableOptions.text()).toBe('Exists - Checked option.');
});
test('when sorting suggestions, show both sorting types for keyword field', async () => {
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover.tsx b/src/plugins/controls/public/options_list/components/options_list_popover.tsx
index 6ad39e0b3dbd9..70353524068cd 100644
--- a/src/plugins/controls/public/options_list/components/options_list_popover.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_popover.tsx
@@ -22,10 +22,15 @@ import { OptionsListPopoverInvalidSelections } from './options_list_popover_inva
export interface OptionsListPopoverProps {
width: number;
+ isLoading: boolean;
updateSearchString: (newSearchString: string) => void;
}
-export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPopoverProps) => {
+export const OptionsListPopover = ({
+ width,
+ isLoading,
+ updateSearchString,
+}: OptionsListPopoverProps) => {
// Redux embeddable container Context
const { useEmbeddableSelector: select } = useReduxEmbeddableContext<
OptionsListReduxState,
@@ -45,9 +50,10 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop
const [showOnlySelected, setShowOnlySelected] = useState(false);
return (
- 300 ? width : undefined }}
+ data-test-subj={`optionsList-control-popover`}
aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)}
>
{title}
@@ -59,17 +65,15 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop
/>
)}
300 ? width : undefined }}
data-test-subj={`optionsList-control-available-options`}
- data-option-count={Object.keys(availableOptions ?? {}).length}
+ data-option-count={isLoading ? 0 : Object.keys(availableOptions ?? {}).length}
>
-
+
{!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && (
)}
{!hideExclude && }
-
+
);
};
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_empty_message.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_empty_message.tsx
new file mode 100644
index 0000000000000..69c819a3caca2
--- /dev/null
+++ b/src/plugins/controls/public/options_list/components/options_list_popover_empty_message.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+import { EuiIcon, EuiSpacer } from '@elastic/eui';
+
+import { OptionsListStrings } from './options_list_strings';
+
+export const OptionsListPopoverEmptyMessage = ({
+ showOnlySelected,
+}: {
+ showOnlySelected: boolean;
+}) => {
+ return (
+
+
+
+
+ {showOnlySelected
+ ? OptionsListStrings.popover.getSelectionsEmptyMessage()
+ : OptionsListStrings.popover.getEmptyMessage()}
+
+
+ );
+};
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_invalid_selections.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_invalid_selections.tsx
index 01c9f14363a4c..424ae37da4bcb 100644
--- a/src/plugins/controls/public/options_list/components/options_list_popover_invalid_selections.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_popover_invalid_selections.tsx
@@ -6,9 +6,15 @@
* Side Public License, v 1.
*/
-import React from 'react';
+import React, { useEffect, useState } from 'react';
-import { EuiFilterSelectItem, EuiSpacer, EuiTitle } from '@elastic/eui';
+import {
+ EuiSelectableOption,
+ EuiSelectable,
+ EuiSpacer,
+ EuiTitle,
+ EuiScreenReaderOnly,
+} from '@elastic/eui';
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
import { OptionsListReduxState } from '../types';
@@ -26,6 +32,31 @@ export const OptionsListPopoverInvalidSelections = () => {
// Select current state from Redux using multiple selectors to avoid rerenders.
const invalidSelections = select((state) => state.componentState.invalidSelections);
+ const fieldName = select((state) => state.explicitInput.fieldName);
+
+ const [selectableOptions, setSelectableOptions] = useState([]); // will be set in following useEffect
+ useEffect(() => {
+ /* This useEffect makes selectableOptions responsive to unchecking options */
+ const options: EuiSelectableOption[] = (invalidSelections ?? []).map((key) => {
+ return {
+ key,
+ label: key,
+ checked: 'on',
+ className: 'optionsList__selectionInvalid',
+ 'data-test-subj': `optionsList-control-ignored-selection-${key}`,
+ prepend: (
+
+
+ {OptionsListStrings.popover.getInvalidSelectionScreenReaderText()}
+ {'" "'} {/* Adds a pause for the screen reader */}
+
+
+ ),
+ };
+ });
+ setSelectableOptions(options);
+ }, [invalidSelections]);
+
return (
<>
@@ -40,18 +71,20 @@ export const OptionsListPopoverInvalidSelections = () => {
)}
- {invalidSelections?.map((ignoredSelection, index) => (
- dispatch(deselectOption(ignoredSelection))}
- aria-label={OptionsListStrings.popover.getInvalidSelectionAriaLabel(ignoredSelection)}
- >
- {`${ignoredSelection}`}
-
- ))}
+ {
+ setSelectableOptions(newSuggestions);
+ dispatch(deselectOption(changedOption.label));
+ }}
+ >
+ {(list) => list}
+
>
);
};
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_suggestion_badge.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_suggestion_badge.tsx
new file mode 100644
index 0000000000000..6c50d92ba81b5
--- /dev/null
+++ b/src/plugins/controls/public/options_list/components/options_list_popover_suggestion_badge.tsx
@@ -0,0 +1,46 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import React from 'react';
+
+import { css } from '@emotion/react';
+import { EuiScreenReaderOnly, EuiText, EuiToolTip, useEuiTheme } from '@elastic/eui';
+
+import { OptionsListStrings } from './options_list_strings';
+
+export const OptionsListPopoverSuggestionBadge = ({ documentCount }: { documentCount: number }) => {
+ const { euiTheme } = useEuiTheme();
+
+ return (
+ <>
+
+
+ {`${documentCount.toLocaleString()}`}
+
+
+
+
+ {'" "'} {/* Adds a pause for the screen reader */}
+ {OptionsListStrings.popover.getDocumentCountScreenReaderText(documentCount)}
+
+
+ >
+ );
+};
diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx
index 7983043ae1d8a..8bd8e361e7081 100644
--- a/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx
+++ b/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx
@@ -6,30 +6,25 @@
* Side Public License, v 1.
*/
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
-import {
- EuiFilterSelectItem,
- EuiFlexGroup,
- EuiFlexItem,
- EuiToolTip,
- EuiSpacer,
- EuiIcon,
- useEuiTheme,
- EuiText,
-} from '@elastic/eui';
-import { css } from '@emotion/react';
+import { EuiLoadingSpinner, EuiSelectable, EuiSpacer } from '@elastic/eui';
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
+import { EuiSelectableOption } from '@elastic/eui/src/components/selectable/selectable_option';
import { OptionsListReduxState } from '../types';
import { OptionsListStrings } from './options_list_strings';
import { optionsListReducers } from '../options_list_reducers';
+import { OptionsListPopoverEmptyMessage } from './options_list_popover_empty_message';
+import { OptionsListPopoverSuggestionBadge } from './options_list_popover_suggestion_badge';
interface OptionsListPopoverSuggestionsProps {
+ isLoading: boolean;
showOnlySelected: boolean;
}
export const OptionsListPopoverSuggestions = ({
+ isLoading,
showOnlySelected,
}: OptionsListPopoverSuggestionsProps) => {
// Redux embeddable container Context
@@ -39,7 +34,6 @@ export const OptionsListPopoverSuggestions = ({
actions: { replaceSelection, deselectOption, selectOption, selectExists },
} = useReduxEmbeddableContext();
const dispatch = useEmbeddableDispatch();
- const { euiTheme } = useEuiTheme();
// Select current state from Redux using multiple selectors to avoid rerenders.
const invalidSelections = select((state) => state.componentState.invalidSelections);
@@ -49,130 +43,97 @@ export const OptionsListPopoverSuggestions = ({
const existsSelected = select((state) => state.explicitInput.existsSelected);
const singleSelect = select((state) => state.explicitInput.singleSelect);
const hideExists = select((state) => state.explicitInput.hideExists);
+ const fieldName = select((state) => state.explicitInput.fieldName);
- const loading = select((state) => state.output.loading);
// track selectedOptions and invalidSelections in sets for more efficient lookup
const selectedOptionsSet = useMemo(() => new Set(selectedOptions), [selectedOptions]);
const invalidSelectionsSet = useMemo(
() => new Set(invalidSelections),
[invalidSelections]
);
- const suggestions = showOnlySelected ? selectedOptions : Object.keys(availableOptions ?? {});
- if (
- !loading &&
- (!suggestions || suggestions.length === 0) &&
- !(showOnlySelected && existsSelected)
- ) {
- return (
-
-
-
-
-
- {showOnlySelected
- ? OptionsListStrings.popover.getSelectionsEmptyMessage()
- : OptionsListStrings.popover.getEmptyMessage()}
-
-
-
- );
- }
+ const suggestions = useMemo(() => {
+ return showOnlySelected ? selectedOptions : Object.keys(availableOptions ?? {});
+ }, [availableOptions, selectedOptions, showOnlySelected]);
+
+ const existsSelectableOption = useMemo(() => {
+ if (hideExists || (!existsSelected && (showOnlySelected || suggestions?.length === 0))) return;
+
+ return {
+ key: 'exists-option',
+ checked: existsSelected ? 'on' : undefined,
+ label: OptionsListStrings.controlAndPopover.getExists(),
+ className: 'optionsList__existsFilter',
+ 'data-test-subj': 'optionsList-control-selection-exists',
+ };
+ }, [suggestions, existsSelected, showOnlySelected, hideExists]);
+
+ const [selectableOptions, setSelectableOptions] = useState([]); // will be set in following useEffect
+ useEffect(() => {
+ /* This useEffect makes selectableOptions responsive to search, show only selected, and clear selections */
+ const options: EuiSelectableOption[] = (suggestions ?? []).map((key) => {
+ return {
+ key,
+ label: key,
+ checked: selectedOptionsSet?.has(key) ? 'on' : undefined,
+ 'data-test-subj': `optionsList-control-selection-${key}`,
+ className:
+ showOnlySelected && invalidSelectionsSet.has(key)
+ ? 'optionsList__selectionInvalid'
+ : 'optionsList__validSuggestion',
+ append:
+ !showOnlySelected && availableOptions?.[key] ? (
+
+ ) : undefined,
+ };
+ });
+ const suggestionsSelectableOptions = existsSelectableOption
+ ? [existsSelectableOption, ...options]
+ : options;
+ setSelectableOptions(suggestionsSelectableOptions);
+ }, [
+ suggestions,
+ availableOptions,
+ showOnlySelected,
+ selectedOptionsSet,
+ invalidSelectionsSet,
+ existsSelectableOption,
+ ]);
return (
- <>
- {!hideExists && !(showOnlySelected && !existsSelected) && (
- {
- dispatch(selectExists(!Boolean(existsSelected)));
- }}
- className="optionsList__existsFilter"
- >
- {OptionsListStrings.controlAndPopover.getExists()}
-
+
+
+
+ {OptionsListStrings.popover.getLoadingMessage()}
+
+ }
+ options={selectableOptions}
+ listProps={{ onFocusBadge: false }}
+ aria-label={OptionsListStrings.popover.getSuggestionsAriaLabel(
+ fieldName,
+ selectableOptions.length
)}
- {suggestions?.map((key: string) => (
- {
- if (showOnlySelected) {
- dispatch(deselectOption(key));
- return;
- }
- if (singleSelect) {
- dispatch(replaceSelection(key));
- return;
- }
- if (selectedOptionsSet.has(key)) {
- dispatch(deselectOption(key));
- return;
- }
- dispatch(selectOption(key));
- }}
- className={
- showOnlySelected && invalidSelectionsSet.has(key)
- ? 'optionsList__selectionInvalid'
- : 'optionsList__validSuggestion'
- }
- aria-label={
- availableOptions?.[key]
- ? OptionsListStrings.popover.getSuggestionAriaLabel(
- key,
- availableOptions[key].doc_count ?? 0
- )
- : key
- }
- >
-
-
- {`${key}`}
-
- {!showOnlySelected && (
-
- {availableOptions && availableOptions[key] && (
-
-
- {`${availableOptions[key].doc_count.toLocaleString()}`}
-
-
- )}
-
- )}
-
-
- ))}
- >
+ emptyMessage={}
+ onChange={(newSuggestions, _, changedOption) => {
+ setSelectableOptions(newSuggestions);
+
+ const key = changedOption.key ?? changedOption.label;
+ // the order of these checks matters, so be careful if rearranging them
+ if (key === 'exists-option') {
+ dispatch(selectExists(!Boolean(existsSelected)));
+ } else if (showOnlySelected || selectedOptionsSet.has(key)) {
+ dispatch(deselectOption(key));
+ } else if (singleSelect) {
+ dispatch(replaceSelection(key));
+ } else {
+ dispatch(selectOption(key));
+ }
+ }}
+ >
+ {(list) => list}
+
);
};
diff --git a/src/plugins/controls/public/options_list/components/options_list_strings.ts b/src/plugins/controls/public/options_list/components/options_list_strings.ts
index a75eb7913064c..bef8a2cbc26ff 100644
--- a/src/plugins/controls/public/options_list/components/options_list_strings.ts
+++ b/src/plugins/controls/public/options_list/components/options_list_strings.ts
@@ -48,11 +48,15 @@ export const OptionsListStrings = {
defaultMessage: 'Popover for {fieldName} control',
values: { fieldName },
}),
- getSuggestionAriaLabel: (key: string, documentCount: number) =>
+ getSuggestionsAriaLabel: (fieldName: string, optionCount: number) =>
i18n.translate('controls.optionsList.popover.suggestionsAriaLabel', {
defaultMessage:
- '{key}, which appears in {documentCount} {documentCount, plural, one {document} other {documents}}.',
- values: { key, documentCount },
+ 'Available {optionCount, plural, one {option} other {options}} for {fieldName}',
+ values: { fieldName, optionCount },
+ }),
+ getLoadingMessage: () =>
+ i18n.translate('controls.optionsList.popover.loading', {
+ defaultMessage: 'Loading options',
}),
getEmptyMessage: () =>
i18n.translate('controls.optionsList.popover.empty', {
@@ -80,6 +84,12 @@ export const OptionsListStrings = {
'Search {totalOptions} available {totalOptions, plural, one {option} other {options}}',
values: { totalOptions },
}),
+ getInvalidSelectionsSectionAriaLabel: (fieldName: string, invalidSelectionCount: number) =>
+ i18n.translate('controls.optionsList.popover.invalidSelectionsAriaLabel', {
+ defaultMessage:
+ 'Ignored {invalidSelectionCount, plural, one {selection} other {selections}} for {fieldName}',
+ values: { fieldName, invalidSelectionCount },
+ }),
getInvalidSelectionsSectionTitle: (invalidSelectionCount: number) =>
i18n.translate('controls.optionsList.popover.invalidSelectionsSectionTitle', {
defaultMessage:
@@ -92,10 +102,9 @@ export const OptionsListStrings = {
'{selectedOptions} selected {selectedOptions, plural, one {option} other {options}} {selectedOptions, plural, one {is} other {are}} ignored because {selectedOptions, plural, one {it is} other {they are}} no longer in the data.',
values: { selectedOptions },
}),
- getInvalidSelectionAriaLabel: (option: string) =>
- i18n.translate('controls.optionsList.popover.invalidSelectionAriaLabel', {
- defaultMessage: 'Ignored selection: {option}',
- values: { option },
+ getInvalidSelectionScreenReaderText: () =>
+ i18n.translate('controls.optionsList.popover.invalidSelectionScreenReaderText', {
+ defaultMessage: 'Invalid selection.',
}),
getIncludeLabel: () =>
i18n.translate('controls.optionsList.popover.includeLabel', {
@@ -127,6 +136,12 @@ export const OptionsListStrings = {
'This value appears in {documentCount, number} {documentCount, plural, one {document} other {documents}}',
values: { documentCount },
}),
+ getDocumentCountScreenReaderText: (documentCount: number) =>
+ i18n.translate('controls.optionsList.popover.documentCountScreenReaderText', {
+ defaultMessage:
+ 'Appears in {documentCount, number} {documentCount, plural, one {document} other {documents}}',
+ values: { documentCount },
+ }),
},
controlAndPopover: {
getExists: (negate: number = +false) =>
diff --git a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx
index 1153150143f89..2f710a56c4f87 100644
--- a/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx
+++ b/src/plugins/controls/public/options_list/embeddable/options_list_embeddable.tsx
@@ -307,7 +307,7 @@ export class OptionsListEmbeddable extends Embeddable
) => {
state.explicitInput.selectedOptions = [action.payload];
+ if (state.explicitInput.existsSelected) state.explicitInput.existsSelected = false;
},
clearSelections: (state: WritableDraft) => {
if (state.explicitInput.existsSelected) state.explicitInput.existsSelected = false;
diff --git a/src/plugins/controls/public/services/options_list/options_list.story.ts b/src/plugins/controls/public/services/options_list/options_list.story.ts
index a44f698c93395..62686feee7495 100644
--- a/src/plugins/controls/public/services/options_list/options_list.story.ts
+++ b/src/plugins/controls/public/services/options_list/options_list.story.ts
@@ -20,6 +20,7 @@ let optionsListRequestMethod = async (request: OptionsListRequest, abortSignal:
r({
suggestions: {},
totalCardinality: 100,
+ rejected: false,
}),
120
)
diff --git a/src/plugins/controls/public/services/options_list/options_list_service.ts b/src/plugins/controls/public/services/options_list/options_list_service.ts
index bc2934e9295a6..ab8e67666140b 100644
--- a/src/plugins/controls/public/services/options_list/options_list_service.ts
+++ b/src/plugins/controls/public/services/options_list/options_list_service.ts
@@ -101,7 +101,7 @@ class OptionsListService implements ControlsOptionsListService {
} catch (error) {
// Remove rejected results from memoize cache
this.cachedOptionsListRequest.cache.delete(this.optionsListCacheResolver(request));
- return {} as OptionsListResponse;
+ return { rejected: true } as OptionsListResponse;
}
};
}
diff --git a/src/plugins/controls/server/options_list/options_list_suggestions_route.ts b/src/plugins/controls/server/options_list/options_list_suggestions_route.ts
index 6e2f8f769815d..0893d24ebacf0 100644
--- a/src/plugins/controls/server/options_list/options_list_suggestions_route.ts
+++ b/src/plugins/controls/server/options_list/options_list_suggestions_route.ts
@@ -144,6 +144,7 @@ export const setupOptionsListSuggestionsRoute = (
suggestions,
totalCardinality,
invalidSelections,
+ rejected: false,
};
};
};
diff --git a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts
index b70404f1338bf..4fc9101ed67a5 100644
--- a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts
+++ b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts
@@ -196,7 +196,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('Creating "does not exist" query from first control filters the second and third controls', async () => {
await dashboardControls.optionsListOpenPopover(controlIds[0]);
- await dashboardControls.optionsListPopoverSelectOption('exists');
+ await dashboardControls.optionsListPopoverSelectExists();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await dashboard.waitForRenderComplete();
diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts
deleted file mode 100644
index 8186d9702ae3e..0000000000000
--- a/test/functional/apps/dashboard_elements/controls/options_list.ts
+++ /dev/null
@@ -1,783 +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 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { pick } from 'lodash';
-
-import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
-import expect from '@kbn/expect';
-
-import { OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS } from '../../../page_objects/dashboard_page_controls';
-import { FtrProviderContext } from '../../../ftr_provider_context';
-
-export default function ({ getService, getPageObjects }: FtrProviderContext) {
- const retry = getService('retry');
- const queryBar = getService('queryBar');
- const pieChart = getService('pieChart');
- const security = getService('security');
- const elasticChart = getService('elasticChart');
- const filterBar = getService('filterBar');
- const testSubjects = getService('testSubjects');
- const dashboardAddPanel = getService('dashboardAddPanel');
- const dashboardPanelActions = getService('dashboardPanelActions');
-
- const { dashboardControls, timePicker, console, common, dashboard, header, settings } =
- getPageObjects([
- 'dashboardControls',
- 'timePicker',
- 'dashboard',
- 'settings',
- 'console',
- 'common',
- 'header',
- ]);
-
- const DASHBOARD_NAME = 'Test Options List Control';
-
- describe('Dashboard options list integration', () => {
- let controlId: string;
-
- const returnToDashboard = async () => {
- await common.navigateToApp('dashboard');
- await header.waitUntilLoadingHasFinished();
- await elasticChart.setNewChartUiDebugFlag();
- await dashboard.loadSavedDashboard(DASHBOARD_NAME);
- if (await dashboard.getIsInViewMode()) {
- await dashboard.switchToEditMode();
- }
- await dashboard.waitForRenderComplete();
- };
-
- before(async () => {
- await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']);
-
- await common.navigateToApp('dashboard');
- await dashboard.gotoDashboardLandingPage();
- await dashboard.clickNewDashboard();
- await timePicker.setDefaultDataRange();
- await elasticChart.setNewChartUiDebugFlag();
- await dashboard.saveDashboard(DASHBOARD_NAME, {
- exitFromEditMode: false,
- storeTimeWithDashboard: true,
- });
- });
-
- describe('Options List Control Editor selects relevant data views', async () => {
- it('selects the default data view when the dashboard is blank', async () => {
- expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
- 'logstash-*'
- );
- });
-
- it('selects a relevant data view based on the panels on the dashboard', async () => {
- await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
- await dashboard.waitForRenderComplete();
- expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
- 'animals-*'
- );
- await dashboardPanelActions.removePanelByTitle('Rendering Test: animal sounds pie');
- await dashboard.waitForRenderComplete();
- expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
- 'logstash-*'
- );
- });
-
- it('selects the last used data view by default', async () => {
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- dataViewTitle: 'animals-*',
- fieldName: 'sound.keyword',
- });
- expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
- 'animals-*'
- );
- await dashboardControls.deleteAllControls();
- });
- });
-
- // Skip on cloud until issue is fixed
- // Issue: https://github.com/elastic/kibana/issues/141280
- describe('Options List Control creation and editing experience', function () {
- this.tags(['skipCloudFailedTest']);
- it('can add a new options list control from a blank state', async () => {
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- dataViewTitle: 'logstash-*',
- fieldName: 'machine.os.raw',
- });
- expect(await dashboardControls.getControlsCount()).to.be(1);
- await dashboard.clearUnsavedChanges();
- });
-
- it('can add a second options list control with a non-default data view', async () => {
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- dataViewTitle: 'animals-*',
- fieldName: 'sound.keyword',
- });
- expect(await dashboardControls.getControlsCount()).to.be(2);
-
- // data views should be properly propagated from the control group to the dashboard
- expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*');
- await dashboard.clearUnsavedChanges();
- });
-
- it('renames an existing control', async () => {
- const secondId = (await dashboardControls.getAllControlIds())[1];
-
- const newTitle = 'wow! Animal sounds?';
- await dashboardControls.editExistingControl(secondId);
- await dashboardControls.controlEditorSetTitle(newTitle);
- await dashboardControls.controlEditorSave();
- expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
- await dashboard.clearUnsavedChanges();
- });
-
- it('can change the data view and field of an existing options list', async () => {
- const firstId = (await dashboardControls.getAllControlIds())[0];
- await dashboardControls.editExistingControl(firstId);
-
- const saveButton = await testSubjects.find('control-editor-save');
- expect(await saveButton.isEnabled()).to.be(true);
- await dashboardControls.controlsEditorSetDataView('animals-*');
- expect(await saveButton.isEnabled()).to.be(false);
- await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
- await dashboardControls.controlEditorSave();
-
- // when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
- await retry.try(async () => {
- await testSubjects.click('addFilter');
- const indexPatternSelectExists = await testSubjects.exists('filterIndexPatternsSelect');
- await filterBar.ensureFieldEditorModalIsClosed();
- expect(indexPatternSelectExists).to.be(false);
- });
- await dashboard.clearUnsavedChanges();
- });
-
- it('editing field clears selections', async () => {
- const secondId = (await dashboardControls.getAllControlIds())[1];
- await dashboardControls.optionsListOpenPopover(secondId);
- await dashboardControls.optionsListPopoverSelectOption('hiss');
- await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
-
- await dashboardControls.editExistingControl(secondId);
- await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
- await dashboardControls.controlEditorSave();
-
- const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);
- expect(selectionString).to.be('Any');
- });
-
- it('editing other control settings keeps selections', async () => {
- const secondId = (await dashboardControls.getAllControlIds())[1];
- await dashboardControls.optionsListOpenPopover(secondId);
- await dashboardControls.optionsListPopoverSelectOption('dog');
- await dashboardControls.optionsListPopoverSelectOption('cat');
- await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
-
- await dashboardControls.editExistingControl(secondId);
- await dashboardControls.controlEditorSetTitle('Animal');
- await dashboardControls.controlEditorSetWidth('large');
- await dashboardControls.controlEditorSave();
-
- const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);
- expect(selectionString).to.be('dog, cat');
- });
-
- it('deletes an existing control', async () => {
- const firstId = (await dashboardControls.getAllControlIds())[0];
-
- await dashboardControls.removeExistingControl(firstId);
- expect(await dashboardControls.getControlsCount()).to.be(1);
- await dashboard.clearUnsavedChanges();
- });
-
- it('cannot create options list for scripted field', async () => {
- await dashboardControls.openCreateControlFlyout();
- expect(await dashboardControls.optionsListEditorGetCurrentDataView(false)).to.eql(
- 'animals-*'
- );
- await testSubjects.missingOrFail('field-picker-select-isDog');
- await dashboardControls.controlEditorCancel(true);
- });
-
- after(async () => {
- await dashboardControls.clearAllControls();
- });
- });
-
- describe('Options List Control suggestions', async () => {
- before(async () => {
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- dataViewTitle: 'animals-*',
- fieldName: 'sound.keyword',
- });
- controlId = (await dashboardControls.getAllControlIds())[0];
- await dashboard.clickQuickSave();
- await header.waitUntilLoadingHasFinished();
-
- await dashboardControls.optionsListOpenPopover(controlId);
- });
-
- it('sort alphabetically - descending', async () => {
- await dashboardControls.optionsListPopoverSetSort({ by: '_key', direction: 'desc' });
- await dashboardControls.optionsListWaitForLoading(controlId);
-
- const sortedSuggestions = Object.keys(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS)
- .sort()
- .reverse()
- .reduce((result, key) => {
- return { ...result, [key]: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS[key] };
- }, {});
- await dashboardControls.ensureAvailableOptionsEqual(
- controlId,
- { suggestions: sortedSuggestions, invalidSelections: [] },
- true
- );
- });
-
- it('sort alphabetically - ascending', async () => {
- await dashboardControls.optionsListPopoverSetSort({ by: '_key', direction: 'asc' });
- await dashboardControls.optionsListWaitForLoading(controlId);
-
- const sortedSuggestions = Object.keys(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS)
- .sort()
- .reduce((result, key) => {
- return { ...result, [key]: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS[key] };
- }, {});
- await dashboardControls.ensureAvailableOptionsEqual(
- controlId,
- { suggestions: sortedSuggestions, invalidSelections: [] },
- true
- );
- });
-
- it('sort by document count - descending', async () => {
- await dashboardControls.optionsListPopoverSetSort({ by: '_count', direction: 'desc' });
- await dashboardControls.optionsListWaitForLoading(controlId);
- await dashboardControls.ensureAvailableOptionsEqual(
- controlId,
- {
- suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, // keys are already sorted descending by doc count
- invalidSelections: [],
- },
- true
- );
- });
-
- it('sort by document count - ascending', async () => {
- await dashboardControls.optionsListPopoverSetSort({ by: '_count', direction: 'asc' });
- await dashboardControls.optionsListWaitForLoading(controlId);
- const sortedSuggestions = Object.entries(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS)
- .sort(([, docCountA], [, docCountB]) => {
- return docCountB - docCountA;
- })
- .reduce((result, [key, docCount]) => {
- return { ...result, [key]: docCount };
- }, {});
- await dashboardControls.ensureAvailableOptionsEqual(
- controlId,
- { suggestions: sortedSuggestions, invalidSelections: [] },
- true
- );
- });
-
- it('non-default value should cause unsaved changes', async () => {
- await testSubjects.existOrFail('dashboardUnsavedChangesBadge');
- });
-
- it('returning to default value should remove unsaved changes', async () => {
- await dashboardControls.optionsListPopoverSetSort({ by: '_count', direction: 'desc' });
- await dashboardControls.optionsListWaitForLoading(controlId);
- await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
- });
-
- after(async () => {
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- });
- });
-
- describe('Interactions between options list and dashboard', async () => {
- before(async () => {
- await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
- });
-
- describe('Applies query settings to controls', async () => {
- it('Applies dashboard query to options list control', async () => {
- await queryBar.setQuery('animal.keyword : "dog" ');
- await queryBar.submitQuery();
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
-
- const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
- 'ruff',
- 'bark',
- 'grrr',
- 'bow ow ow',
- 'grr',
- ]);
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: { ...suggestions, grr: suggestions.grr - 1 },
- invalidSelections: [],
- });
- await queryBar.setQuery('');
- await queryBar.submitQuery();
-
- // using the query hides the time range. Clicking anywhere else shows it again.
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- });
-
- it('Applies dashboard time range to options list control', async () => {
- // set time range to time with no documents
- await timePicker.setAbsoluteRange(
- 'Jan 1, 2017 @ 00:00:00.000',
- 'Jan 1, 2017 @ 00:00:00.000'
- );
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
-
- await dashboardControls.optionsListOpenPopover(controlId);
- expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0);
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await timePicker.setDefaultDataRange();
- });
-
- describe('dashboard filters', async () => {
- before(async () => {
- await filterBar.addFilter({
- field: 'sound.keyword',
- operation: 'is one of',
- value: ['bark', 'bow ow ow', 'ruff'],
- });
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- });
-
- it('Applies dashboard filters to options list control', async () => {
- const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
- 'ruff',
- 'bark',
- 'bow ow ow',
- ]);
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions,
- invalidSelections: [],
- });
- });
-
- it('Does not apply disabled dashboard filters to options list control', async () => {
- await filterBar.toggleFilterEnabled('sound.keyword');
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS,
- invalidSelections: [],
- });
- await filterBar.toggleFilterEnabled('sound.keyword');
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- });
-
- it('Negated filters apply to options control', async () => {
- await filterBar.toggleFilterNegated('sound.keyword');
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
-
- const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
- 'hiss',
- 'grrr',
- 'meow',
- 'growl',
- 'grr',
- ]);
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions,
- invalidSelections: [],
- });
- });
-
- after(async () => {
- await filterBar.removeAllFilters();
- });
- });
- });
-
- describe('Selections made in control apply to dashboard', async () => {
- it('Shows available options in options list', async () => {
- await queryBar.setQuery('');
- await queryBar.submitQuery();
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS,
- invalidSelections: [],
- });
- });
-
- it('Can search options list for available options', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSearchForOption('meo');
- await dashboardControls.ensureAvailableOptionsEqual(
- controlId,
- {
- suggestions: { meow: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.meow },
- invalidSelections: [],
- },
- true
- );
- await dashboardControls.optionsListPopoverClearSearch();
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- });
-
- it('Can search options list for available options case insensitive', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSearchForOption('MEO');
- await dashboardControls.ensureAvailableOptionsEqual(
- controlId,
- {
- suggestions: { meow: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.meow },
- invalidSelections: [],
- },
- true
- );
- await dashboardControls.optionsListPopoverClearSearch();
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- });
-
- it('Can select multiple available options', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSelectOption('hiss');
- await dashboardControls.optionsListPopoverSelectOption('grr');
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- });
-
- it('Selected options appear in control', async () => {
- const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
- expect(selectionString).to.be('hiss, grr');
- });
-
- it('Applies options list control options to dashboard', async () => {
- await retry.try(async () => {
- expect(await pieChart.getPieSliceCount()).to.be(2);
- });
- });
-
- it('Applies options list control options to dashboard by default on open', async () => {
- await dashboard.gotoDashboardLandingPage();
- await header.waitUntilLoadingHasFinished();
- await dashboard.clickUnsavedChangesContinueEditing(DASHBOARD_NAME);
- await header.waitUntilLoadingHasFinished();
- expect(await pieChart.getPieSliceCount()).to.be(2);
-
- const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
- expect(selectionString).to.be('hiss, grr');
- });
-
- it('excluding selections has expected results', async () => {
- await dashboard.clickQuickSave();
- await dashboard.waitForRenderComplete();
-
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSetIncludeSelections(false);
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await dashboard.waitForRenderComplete();
-
- expect(await pieChart.getPieSliceCount()).to.be(5);
- await dashboard.clearUnsavedChanges();
- });
-
- it('including selections has expected results', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSetIncludeSelections(true);
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await dashboard.waitForRenderComplete();
-
- expect(await pieChart.getPieSliceCount()).to.be(2);
- await dashboard.clearUnsavedChanges();
- });
-
- it('changes to selections can be discarded', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSelectOption('bark');
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- let selections = await dashboardControls.optionsListGetSelectionsString(controlId);
- expect(selections).to.equal('hiss, grr, bark');
-
- await dashboard.clickCancelOutOfEditMode();
- selections = await dashboardControls.optionsListGetSelectionsString(controlId);
- expect(selections).to.equal('hiss, grr');
- });
-
- it('dashboard does not load with unsaved changes when changes are discarded', async () => {
- await dashboard.switchToEditMode();
- await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
- });
- });
-
- describe('test data view runtime field', async () => {
- const FIELD_NAME = 'testRuntimeField';
- const FIELD_VALUES = {
- G:
- OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.growl +
- OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.grr +
- OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.grrr,
- H: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.hiss,
- B:
- OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.bark +
- OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS['bow ow ow'],
- R: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.ruff,
- M: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.meow,
- };
-
- before(async () => {
- await common.navigateToApp('settings');
- await settings.clickKibanaIndexPatterns();
- await settings.clickIndexPatternByName('animals-*');
- await settings.addRuntimeField(
- FIELD_NAME,
- 'keyword',
- `emit(doc['sound.keyword'].value.substring(0, 1).toUpperCase())`
- );
- await header.waitUntilLoadingHasFinished();
-
- await returnToDashboard();
- await dashboardControls.deleteAllControls();
- });
-
- it('can create options list control on runtime field', async () => {
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- fieldName: FIELD_NAME,
- dataViewTitle: 'animals-*',
- });
- expect(await dashboardControls.getControlsCount()).to.be(1);
- });
-
- it('new control has expected suggestions', async () => {
- controlId = (await dashboardControls.getAllControlIds())[0];
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: FIELD_VALUES,
- invalidSelections: [],
- });
- });
-
- it('making selection has expected results', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSelectOption('B');
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await dashboard.waitForRenderComplete();
-
- expect(await pieChart.getPieChartLabels()).to.eql(['bark', 'bow ow ow']);
- });
-
- after(async () => {
- await dashboardControls.deleteAllControls();
- await dashboard.clickQuickSave();
- await header.waitUntilLoadingHasFinished();
-
- await common.navigateToApp('settings');
- await settings.clickKibanaIndexPatterns();
- await settings.clickIndexPatternByName('animals-*');
- await settings.filterField('testRuntimeField');
- await testSubjects.click('deleteField');
- await settings.confirmDelete();
- });
- });
-
- describe('test exists query', async () => {
- const newDocuments: Array<{ index: string; id: string }> = [];
-
- const addDocument = async (index: string, document: string) => {
- await console.enterRequest('\nPOST ' + index + '/_doc/ \n{\n ' + document);
- await console.clickPlay();
- await header.waitUntilLoadingHasFinished();
- const response = JSON.parse(await console.getResponse());
- newDocuments.push({ index, id: response._id });
- };
-
- before(async () => {
- await common.navigateToApp('console');
- await console.collapseHelp();
- await console.clearTextArea();
- await addDocument(
- 'animals-cats-2018-01-01',
- '"@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss"'
- );
- await returnToDashboard();
-
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- dataViewTitle: 'animals-*',
- fieldName: 'animal.keyword',
- title: 'Animal',
- });
- controlId = (await dashboardControls.getAllControlIds())[0];
- await header.waitUntilLoadingHasFinished();
- await dashboard.waitForRenderComplete();
- });
-
- it('creating exists query has expected results', async () => {
- expect((await pieChart.getPieChartValues())[0]).to.be(6);
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSelectOption('exists');
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await dashboard.waitForRenderComplete();
-
- expect(await pieChart.getPieSliceCount()).to.be(5);
- expect((await pieChart.getPieChartValues())[0]).to.be(5);
- });
-
- it('negating exists query has expected results', async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSetIncludeSelections(false);
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await dashboard.waitForRenderComplete();
-
- expect(await pieChart.getPieSliceCount()).to.be(1);
- expect((await pieChart.getPieChartValues())[0]).to.be(1);
- });
-
- after(async () => {
- await common.navigateToApp('console');
- await console.clearTextArea();
- for (const { index, id } of newDocuments) {
- await console.enterRequest(`\nDELETE /${index}/_doc/${id}`);
- await console.clickPlay();
- await header.waitUntilLoadingHasFinished();
- }
-
- await returnToDashboard();
- await dashboardControls.deleteAllControls();
- });
- });
-
- describe('Options List dashboard validation', async () => {
- before(async () => {
- await dashboardControls.createControl({
- controlType: OPTIONS_LIST_CONTROL,
- dataViewTitle: 'animals-*',
- fieldName: 'sound.keyword',
- title: 'Animal Sounds',
- });
- controlId = (await dashboardControls.getAllControlIds())[0];
-
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSelectOption('meow');
- await dashboardControls.optionsListPopoverSelectOption('bark');
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- });
-
- after(async () => {
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverClearSelections();
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await filterBar.removeAllFilters();
- });
-
- it('Can mark selections invalid with Query', async () => {
- await queryBar.setQuery('NOT animal.keyword : "dog" ');
- await queryBar.submitQuery();
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
-
- const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
- 'hiss',
- 'meow',
- 'growl',
- 'grr',
- ]);
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: { ...suggestions, grr: suggestions.grr - 1 },
- invalidSelections: ['bark'],
- });
- // only valid selections are applied as filters.
- expect(await pieChart.getPieSliceCount()).to.be(1);
- });
-
- it('can make invalid selections valid again if the parent filter changes', async () => {
- await queryBar.setQuery('');
- await queryBar.submitQuery();
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS,
- invalidSelections: [],
- });
- expect(await pieChart.getPieSliceCount()).to.be(2);
- });
-
- it('Can mark multiple selections invalid with Filter', async () => {
- await filterBar.addFilter({ field: 'sound.keyword', operation: 'is', value: 'hiss' });
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: {
- hiss: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.hiss,
- },
- invalidSelections: ['meow', 'bark'],
- });
- // only valid selections are applied as filters.
- expect(await pieChart.getPieSliceCount()).to.be(1);
- });
- });
-
- describe('Options List dashboard no validation', async () => {
- before(async () => {
- await filterBar.removeAllFilters();
- await queryBar.clickQuerySubmitButton();
- await dashboardControls.optionsListOpenPopover(controlId);
- await dashboardControls.optionsListPopoverSelectOption('meow');
- await dashboardControls.optionsListPopoverSelectOption('bark');
- await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
- await dashboardControls.updateValidationSetting(false);
- });
-
- it('Does not mark selections invalid with Query', async () => {
- await queryBar.setQuery('NOT animal.keyword : "dog" ');
- await queryBar.submitQuery();
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
-
- const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
- 'hiss',
- 'meow',
- 'growl',
- 'grr',
- ]);
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: { ...suggestions, grr: suggestions.grr - 1 },
- invalidSelections: [],
- });
- });
-
- it('Does not mark multiple selections invalid with Filter', async () => {
- await filterBar.addFilter({ field: 'sound.keyword', operation: 'is', value: 'hiss' });
- await dashboard.waitForRenderComplete();
- await header.waitUntilLoadingHasFinished();
- await dashboardControls.ensureAvailableOptionsEqual(controlId, {
- suggestions: {
- hiss: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.hiss,
- },
- invalidSelections: [],
- });
- });
- });
-
- after(async () => {
- await filterBar.removeAllFilters();
- await queryBar.clickQuerySubmitButton();
- await dashboardControls.clearAllControls();
- });
- });
-
- after(async () => {
- await security.testUser.restoreDefaults();
- });
- });
-}
diff --git a/test/functional/apps/dashboard_elements/controls/options_list/index.ts b/test/functional/apps/dashboard_elements/controls/options_list/index.ts
new file mode 100644
index 0000000000000..9d4cb7d18d525
--- /dev/null
+++ b/test/functional/apps/dashboard_elements/controls/options_list/index.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export const OPTIONS_LIST_DASHBOARD_NAME = 'Test Options List Control';
+
+export default function ({ loadTestFile, getService, getPageObjects }: FtrProviderContext) {
+ const elasticChart = getService('elasticChart');
+ const security = getService('security');
+
+ const { timePicker, dashboard } = getPageObjects([
+ 'dashboardControls',
+ 'timePicker',
+ 'dashboard',
+ 'common',
+ ]);
+
+ async function setup() {
+ await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']);
+
+ await dashboard.gotoDashboardLandingPage();
+ await dashboard.clickNewDashboard();
+ await timePicker.setDefaultDataRange();
+ await elasticChart.setNewChartUiDebugFlag();
+ await dashboard.saveDashboard(OPTIONS_LIST_DASHBOARD_NAME, {
+ exitFromEditMode: false,
+ storeTimeWithDashboard: true,
+ });
+ }
+
+ async function teardown() {
+ await security.testUser.restoreDefaults();
+ }
+
+ describe('Options list control', function () {
+ before(setup);
+ after(teardown);
+
+ loadTestFile(require.resolve('./options_list_creation_and_editing'));
+ loadTestFile(require.resolve('./options_list_dashboard_interaction'));
+ loadTestFile(require.resolve('./options_list_suggestions'));
+ loadTestFile(require.resolve('./options_list_validation'));
+ });
+}
diff --git a/test/functional/apps/dashboard_elements/controls/options_list/options_list_creation_and_editing.ts b/test/functional/apps/dashboard_elements/controls/options_list/options_list_creation_and_editing.ts
new file mode 100644
index 0000000000000..2281c5ce112be
--- /dev/null
+++ b/test/functional/apps/dashboard_elements/controls/options_list/options_list_creation_and_editing.ts
@@ -0,0 +1,181 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
+import expect from '@kbn/expect';
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const retry = getService('retry');
+ const filterBar = getService('filterBar');
+ const testSubjects = getService('testSubjects');
+ const dashboardAddPanel = getService('dashboardAddPanel');
+ const dashboardPanelActions = getService('dashboardPanelActions');
+
+ const { dashboardControls, dashboard } = getPageObjects([
+ 'dashboardControls',
+ 'timePicker',
+ 'dashboard',
+ 'settings',
+ 'console',
+ 'common',
+ 'header',
+ ]);
+
+ describe('Dashboard options list creation and editing', () => {
+ before(async () => {
+ await dashboard.ensureDashboardIsInEditMode();
+ });
+
+ after(async () => {
+ await dashboardControls.deleteAllControls();
+ await dashboard.clickQuickSave();
+ });
+
+ describe('Options List Control Editor selects relevant data views', async () => {
+ it('selects the default data view when the dashboard is blank', async () => {
+ expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
+ 'logstash-*'
+ );
+ });
+
+ it('selects a relevant data view based on the panels on the dashboard', async () => {
+ await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
+ await dashboard.waitForRenderComplete();
+ expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
+ 'animals-*'
+ );
+ await dashboard.waitForRenderComplete();
+ await dashboardPanelActions.removePanelByTitle('Rendering Test: animal sounds pie');
+ expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
+ 'logstash-*'
+ );
+ });
+
+ it('selects the last used data view by default', async () => {
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'animals-*',
+ fieldName: 'sound.keyword',
+ });
+ expect(await dashboardControls.optionsListEditorGetCurrentDataView(true)).to.eql(
+ 'animals-*'
+ );
+ await dashboardControls.deleteAllControls();
+ });
+ });
+
+ // Skip on cloud until issue is fixed
+ // Issue: https://github.com/elastic/kibana/issues/141280
+ describe('Options List Control creation and editing experience', function () {
+ this.tags(['skipCloudFailedTest']);
+ it('can add a new options list control from a blank state', async () => {
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'logstash-*',
+ fieldName: 'machine.os.raw',
+ });
+ expect(await dashboardControls.getControlsCount()).to.be(1);
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('can add a second options list control with a non-default data view', async () => {
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'animals-*',
+ fieldName: 'sound.keyword',
+ });
+ expect(await dashboardControls.getControlsCount()).to.be(2);
+
+ // data views should be properly propagated from the control group to the dashboard
+ expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*');
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('renames an existing control', async () => {
+ const secondId = (await dashboardControls.getAllControlIds())[1];
+
+ const newTitle = 'wow! Animal sounds?';
+ await dashboardControls.editExistingControl(secondId);
+ await dashboardControls.controlEditorSetTitle(newTitle);
+ await dashboardControls.controlEditorSave();
+ expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true);
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('can change the data view and field of an existing options list', async () => {
+ const firstId = (await dashboardControls.getAllControlIds())[0];
+ await dashboardControls.editExistingControl(firstId);
+
+ const saveButton = await testSubjects.find('control-editor-save');
+ expect(await saveButton.isEnabled()).to.be(true);
+ await dashboardControls.controlsEditorSetDataView('animals-*');
+ expect(await saveButton.isEnabled()).to.be(false);
+ await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
+ await dashboardControls.controlEditorSave();
+
+ // when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view
+ await retry.try(async () => {
+ await testSubjects.click('addFilter');
+ const indexPatternSelectExists = await testSubjects.exists('filterIndexPatternsSelect');
+ await filterBar.ensureFieldEditorModalIsClosed();
+ expect(indexPatternSelectExists).to.be(false);
+ });
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('editing field clears selections', async () => {
+ const secondId = (await dashboardControls.getAllControlIds())[1];
+ await dashboardControls.optionsListOpenPopover(secondId);
+ await dashboardControls.optionsListPopoverSelectOption('hiss');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
+
+ await dashboardControls.editExistingControl(secondId);
+ await dashboardControls.controlsEditorSetfield('animal.keyword', OPTIONS_LIST_CONTROL);
+ await dashboardControls.controlEditorSave();
+
+ const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);
+ expect(selectionString).to.be('Any');
+ });
+
+ it('editing other control settings keeps selections', async () => {
+ const secondId = (await dashboardControls.getAllControlIds())[1];
+ await dashboardControls.optionsListOpenPopover(secondId);
+ await dashboardControls.optionsListPopoverSelectOption('dog');
+ await dashboardControls.optionsListPopoverSelectOption('cat');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(secondId);
+
+ await dashboardControls.editExistingControl(secondId);
+ await dashboardControls.controlEditorSetTitle('Animal');
+ await dashboardControls.controlEditorSetWidth('large');
+ await dashboardControls.controlEditorSave();
+
+ const selectionString = await dashboardControls.optionsListGetSelectionsString(secondId);
+ expect(selectionString).to.be('dog, cat');
+ });
+
+ it('deletes an existing control', async () => {
+ const firstId = (await dashboardControls.getAllControlIds())[0];
+
+ await dashboardControls.removeExistingControl(firstId);
+ expect(await dashboardControls.getControlsCount()).to.be(1);
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('cannot create options list for scripted field', async () => {
+ await dashboardControls.openCreateControlFlyout();
+ expect(await dashboardControls.optionsListEditorGetCurrentDataView(false)).to.eql(
+ 'animals-*'
+ );
+ await testSubjects.missingOrFail('field-picker-select-isDog');
+ await dashboardControls.controlEditorCancel(true);
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/dashboard_elements/controls/options_list/options_list_dashboard_interaction.ts b/test/functional/apps/dashboard_elements/controls/options_list/options_list_dashboard_interaction.ts
new file mode 100644
index 0000000000000..43bde4798e3d5
--- /dev/null
+++ b/test/functional/apps/dashboard_elements/controls/options_list/options_list_dashboard_interaction.ts
@@ -0,0 +1,413 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { pick } from 'lodash';
+
+import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
+import expect from '@kbn/expect';
+
+import { OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS } from '../../../../page_objects/dashboard_page_controls';
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import { OPTIONS_LIST_DASHBOARD_NAME } from '.';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const retry = getService('retry');
+ const queryBar = getService('queryBar');
+ const pieChart = getService('pieChart');
+ const elasticChart = getService('elasticChart');
+ const filterBar = getService('filterBar');
+ const testSubjects = getService('testSubjects');
+ const dashboardAddPanel = getService('dashboardAddPanel');
+ const dashboardPanelActions = getService('dashboardPanelActions');
+
+ const { dashboardControls, timePicker, console, common, dashboard, header, settings } =
+ getPageObjects([
+ 'dashboardControls',
+ 'timePicker',
+ 'dashboard',
+ 'settings',
+ 'console',
+ 'common',
+ 'header',
+ ]);
+
+ describe('Interactions between options list and dashboard', () => {
+ let controlId: string;
+
+ const returnToDashboard = async () => {
+ await common.navigateToApp('dashboard');
+ await header.waitUntilLoadingHasFinished();
+ await elasticChart.setNewChartUiDebugFlag();
+ await dashboard.loadSavedDashboard(OPTIONS_LIST_DASHBOARD_NAME);
+ await dashboard.ensureDashboardIsInEditMode();
+ };
+
+ before(async () => {
+ await dashboard.ensureDashboardIsInEditMode();
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'animals-*',
+ fieldName: 'sound.keyword',
+ });
+ controlId = (await dashboardControls.getAllControlIds())[0];
+ await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
+ await dashboard.clickQuickSave();
+ await header.waitUntilLoadingHasFinished();
+ });
+
+ after(async () => {
+ await dashboardControls.deleteAllControls();
+ await dashboardPanelActions.removePanelByTitle('Rendering Test: animal sounds pie');
+ await dashboard.clickQuickSave();
+ });
+
+ describe('Applies query settings to controls', async () => {
+ it('Applies dashboard query to options list control', async () => {
+ await queryBar.setQuery('animal.keyword : "dog" ');
+ await queryBar.submitQuery(); // quicker than clicking the submit button, but hides the time picker
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+
+ const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
+ 'ruff',
+ 'bark',
+ 'grrr',
+ 'bow ow ow',
+ 'grr',
+ ]);
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: { ...suggestions, grr: suggestions.grr - 1 },
+ invalidSelections: [],
+ });
+ await queryBar.setQuery('');
+ await queryBar.clickQuerySubmitButton(); // ensures that the time picker is visible for the next test
+ });
+
+ it('Applies dashboard time range to options list control', async () => {
+ // set time range to time with no documents
+ await timePicker.setAbsoluteRange(
+ 'Jan 1, 2017 @ 00:00:00.000',
+ 'Jan 1, 2017 @ 00:00:00.000'
+ );
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+
+ await dashboardControls.optionsListOpenPopover(controlId);
+ expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0);
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await timePicker.setDefaultDataRange();
+ });
+
+ describe('dashboard filters', async () => {
+ before(async () => {
+ await filterBar.addFilter({
+ field: 'sound.keyword',
+ operation: 'is one of',
+ value: ['bark', 'bow ow ow', 'ruff'],
+ });
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ });
+
+ it('Applies dashboard filters to options list control', async () => {
+ const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
+ 'ruff',
+ 'bark',
+ 'bow ow ow',
+ ]);
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions,
+ invalidSelections: [],
+ });
+ });
+
+ it('Does not apply disabled dashboard filters to options list control', async () => {
+ await filterBar.toggleFilterEnabled('sound.keyword');
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS,
+ invalidSelections: [],
+ });
+ await filterBar.toggleFilterEnabled('sound.keyword');
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ });
+
+ it('Negated filters apply to options control', async () => {
+ await filterBar.toggleFilterNegated('sound.keyword');
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+
+ const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
+ 'hiss',
+ 'grrr',
+ 'meow',
+ 'growl',
+ 'grr',
+ ]);
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions,
+ invalidSelections: [],
+ });
+ });
+
+ after(async () => {
+ await filterBar.removeAllFilters();
+ });
+ });
+ });
+
+ describe('Selections made in control apply to dashboard', async () => {
+ it('Shows available options in options list', async () => {
+ await queryBar.setQuery('');
+ await queryBar.submitQuery();
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS,
+ invalidSelections: [],
+ });
+ });
+
+ it('Can search options list for available options', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSearchForOption('meo');
+ await dashboardControls.ensureAvailableOptionsEqual(
+ controlId,
+ {
+ suggestions: { meow: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.meow },
+ invalidSelections: [],
+ },
+ true
+ );
+ await dashboardControls.optionsListPopoverClearSearch();
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ });
+
+ it('Can search options list for available options case insensitive', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSearchForOption('MEO');
+ await dashboardControls.ensureAvailableOptionsEqual(
+ controlId,
+ {
+ suggestions: { meow: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.meow },
+ invalidSelections: [],
+ },
+ true
+ );
+ await dashboardControls.optionsListPopoverClearSearch();
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ });
+
+ it('Can select multiple available options', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSelectOption('hiss');
+ await dashboardControls.optionsListPopoverSelectOption('grr');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ });
+
+ it('Selected options appear in control', async () => {
+ const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
+ expect(selectionString).to.be('hiss, grr');
+ });
+
+ it('Applies options list control options to dashboard', async () => {
+ await retry.try(async () => {
+ expect(await pieChart.getPieSliceCount()).to.be(2);
+ });
+ });
+
+ it('Applies options list control options to dashboard by default on open', async () => {
+ await dashboard.gotoDashboardLandingPage();
+ await header.waitUntilLoadingHasFinished();
+ await dashboard.clickUnsavedChangesContinueEditing(OPTIONS_LIST_DASHBOARD_NAME);
+ await header.waitUntilLoadingHasFinished();
+ expect(await pieChart.getPieSliceCount()).to.be(2);
+
+ const selectionString = await dashboardControls.optionsListGetSelectionsString(controlId);
+ expect(selectionString).to.be('hiss, grr');
+ });
+
+ it('excluding selections has expected results', async () => {
+ await dashboard.clickQuickSave();
+ await dashboard.waitForRenderComplete();
+
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSetIncludeSelections(false);
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboard.waitForRenderComplete();
+
+ expect(await pieChart.getPieSliceCount()).to.be(5);
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('including selections has expected results', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSetIncludeSelections(true);
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboard.waitForRenderComplete();
+
+ expect(await pieChart.getPieSliceCount()).to.be(2);
+ await dashboard.clearUnsavedChanges();
+ });
+
+ it('changes to selections can be discarded', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSelectOption('bark');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ let selections = await dashboardControls.optionsListGetSelectionsString(controlId);
+ expect(selections).to.equal('hiss, grr, bark');
+
+ await dashboard.clickCancelOutOfEditMode();
+ selections = await dashboardControls.optionsListGetSelectionsString(controlId);
+ expect(selections).to.equal('hiss, grr');
+ });
+
+ it('dashboard does not load with unsaved changes when changes are discarded', async () => {
+ await dashboard.switchToEditMode();
+ await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
+ });
+ });
+
+ describe('Test data view runtime field', async () => {
+ const FIELD_NAME = 'testRuntimeField';
+ const FIELD_VALUES = {
+ G:
+ OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.growl +
+ OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.grr +
+ OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.grrr,
+ H: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.hiss,
+ B:
+ OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.bark +
+ OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS['bow ow ow'],
+ R: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.ruff,
+ M: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.meow,
+ };
+
+ before(async () => {
+ await common.navigateToApp('settings');
+ await settings.clickKibanaIndexPatterns();
+ await settings.clickIndexPatternByName('animals-*');
+ await settings.addRuntimeField(
+ FIELD_NAME,
+ 'keyword',
+ `emit(doc['sound.keyword'].value.substring(0, 1).toUpperCase())`
+ );
+ await header.waitUntilLoadingHasFinished();
+
+ await returnToDashboard();
+ await dashboardControls.deleteAllControls();
+ });
+
+ it('can create options list control on runtime field', async () => {
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ fieldName: FIELD_NAME,
+ dataViewTitle: 'animals-*',
+ });
+ expect(await dashboardControls.getControlsCount()).to.be(1);
+ });
+
+ it('new control has expected suggestions', async () => {
+ controlId = (await dashboardControls.getAllControlIds())[0];
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: FIELD_VALUES,
+ invalidSelections: [],
+ });
+ });
+
+ it('making selection has expected results', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSelectOption('B');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboard.waitForRenderComplete();
+
+ expect(await pieChart.getPieChartLabels()).to.eql(['bark', 'bow ow ow']);
+ });
+
+ after(async () => {
+ await dashboardControls.deleteAllControls();
+ await dashboard.clickQuickSave();
+ await header.waitUntilLoadingHasFinished();
+
+ await common.navigateToApp('settings');
+ await settings.clickKibanaIndexPatterns();
+ await settings.clickIndexPatternByName('animals-*');
+ await settings.filterField('testRuntimeField');
+ await testSubjects.click('deleteField');
+ await settings.confirmDelete();
+ });
+ });
+
+ describe('Test exists query', async () => {
+ const newDocuments: Array<{ index: string; id: string }> = [];
+
+ const addDocument = async (index: string, document: string) => {
+ await console.enterRequest('\nPOST ' + index + '/_doc/ \n{\n ' + document);
+ await console.clickPlay();
+ await header.waitUntilLoadingHasFinished();
+ const response = JSON.parse(await console.getResponse());
+ newDocuments.push({ index, id: response._id });
+ };
+
+ before(async () => {
+ await common.navigateToApp('console');
+ await console.collapseHelp();
+ await console.clearTextArea();
+ await addDocument(
+ 'animals-cats-2018-01-01',
+ '"@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss"'
+ );
+ await returnToDashboard();
+
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'animals-*',
+ fieldName: 'animal.keyword',
+ title: 'Animal',
+ });
+ controlId = (await dashboardControls.getAllControlIds())[0];
+ await header.waitUntilLoadingHasFinished();
+ await dashboard.waitForRenderComplete();
+ });
+
+ it('creating exists query has expected results', async () => {
+ expect((await pieChart.getPieChartValues())[0]).to.be(6);
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSelectExists();
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboard.waitForRenderComplete();
+
+ expect(await pieChart.getPieSliceCount()).to.be(5);
+ expect((await pieChart.getPieChartValues())[0]).to.be(5);
+ });
+
+ it('negating exists query has expected results', async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSetIncludeSelections(false);
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboard.waitForRenderComplete();
+
+ expect(await pieChart.getPieSliceCount()).to.be(1);
+ expect((await pieChart.getPieChartValues())[0]).to.be(1);
+ });
+
+ after(async () => {
+ await common.navigateToApp('console');
+ await console.clearTextArea();
+ for (const { index, id } of newDocuments) {
+ await console.enterRequest(`\nDELETE /${index}/_doc/${id}`);
+ await console.clickPlay();
+ await header.waitUntilLoadingHasFinished();
+ }
+ await returnToDashboard();
+ });
+ });
+ });
+}
diff --git a/test/functional/apps/dashboard_elements/controls/options_list/options_list_suggestions.ts b/test/functional/apps/dashboard_elements/controls/options_list/options_list_suggestions.ts
new file mode 100644
index 0000000000000..6c69dc99bc4d3
--- /dev/null
+++ b/test/functional/apps/dashboard_elements/controls/options_list/options_list_suggestions.ts
@@ -0,0 +1,116 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
+
+import { OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS } from '../../../../page_objects/dashboard_page_controls';
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const testSubjects = getService('testSubjects');
+
+ const { dashboardControls, dashboard, header } = getPageObjects([
+ 'dashboardControls',
+ 'timePicker',
+ 'dashboard',
+ 'settings',
+ 'console',
+ 'common',
+ 'header',
+ ]);
+
+ describe('Dashboard options list suggestions', () => {
+ let controlId: string;
+
+ before(async () => {
+ await dashboard.ensureDashboardIsInEditMode();
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'animals-*',
+ fieldName: 'sound.keyword',
+ });
+ controlId = (await dashboardControls.getAllControlIds())[0];
+ await dashboard.clickQuickSave();
+ await header.waitUntilLoadingHasFinished();
+
+ await dashboardControls.optionsListOpenPopover(controlId);
+ });
+
+ after(async () => {
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboardControls.deleteAllControls();
+ await dashboard.clickQuickSave();
+ });
+
+ it('sort alphabetically - descending', async () => {
+ await dashboardControls.optionsListPopoverSetSort({ by: '_key', direction: 'desc' });
+ const sortedSuggestions = Object.keys(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS)
+ .sort()
+ .reverse()
+ .reduce((result, key) => {
+ return { ...result, [key]: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS[key] };
+ }, {});
+ await dashboardControls.ensureAvailableOptionsEqual(
+ controlId,
+ { suggestions: sortedSuggestions, invalidSelections: [] },
+ true
+ );
+ });
+
+ it('sort alphabetically - ascending', async () => {
+ await dashboardControls.optionsListPopoverSetSort({ by: '_key', direction: 'asc' });
+ const sortedSuggestions = Object.keys(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS)
+ .sort()
+ .reduce((result, key) => {
+ return { ...result, [key]: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS[key] };
+ }, {});
+ await dashboardControls.ensureAvailableOptionsEqual(
+ controlId,
+ { suggestions: sortedSuggestions, invalidSelections: [] },
+ true
+ );
+ });
+
+ it('sort by document count - descending', async () => {
+ await dashboardControls.optionsListPopoverSetSort({ by: '_count', direction: 'desc' });
+ await dashboardControls.ensureAvailableOptionsEqual(
+ controlId,
+ {
+ suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, // keys are already sorted descending by doc count
+ invalidSelections: [],
+ },
+ true
+ );
+ });
+
+ it('sort by document count - ascending', async () => {
+ await dashboardControls.optionsListPopoverSetSort({ by: '_count', direction: 'asc' });
+ const sortedSuggestions = Object.entries(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS)
+ .sort(([, docCountA], [, docCountB]) => {
+ return docCountB - docCountA;
+ })
+ .reduce((result, [key, docCount]) => {
+ return { ...result, [key]: docCount };
+ }, {});
+ await dashboardControls.ensureAvailableOptionsEqual(
+ controlId,
+ { suggestions: sortedSuggestions, invalidSelections: [] },
+ true
+ );
+ });
+
+ it('non-default sort value should cause unsaved changes', async () => {
+ await testSubjects.existOrFail('dashboardUnsavedChangesBadge');
+ });
+
+ it('returning to default sort value should remove unsaved changes', async () => {
+ await dashboardControls.optionsListPopoverSetSort({ by: '_count', direction: 'desc' });
+ await testSubjects.missingOrFail('dashboardUnsavedChangesBadge');
+ });
+ });
+}
diff --git a/test/functional/apps/dashboard_elements/controls/options_list/options_list_validation.ts b/test/functional/apps/dashboard_elements/controls/options_list/options_list_validation.ts
new file mode 100644
index 0000000000000..f48bc5d9e4c42
--- /dev/null
+++ b/test/functional/apps/dashboard_elements/controls/options_list/options_list_validation.ts
@@ -0,0 +1,161 @@
+/*
+ * 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 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { pick } from 'lodash';
+
+import expect from '@kbn/expect';
+import { OPTIONS_LIST_CONTROL } from '@kbn/controls-plugin/common';
+
+import { FtrProviderContext } from '../../../../ftr_provider_context';
+import { OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS } from '../../../../page_objects/dashboard_page_controls';
+
+export default function ({ getService, getPageObjects }: FtrProviderContext) {
+ const queryBar = getService('queryBar');
+ const pieChart = getService('pieChart');
+ const filterBar = getService('filterBar');
+ const dashboardAddPanel = getService('dashboardAddPanel');
+ const dashboardPanelActions = getService('dashboardPanelActions');
+
+ const { dashboardControls, dashboard, header } = getPageObjects([
+ 'dashboardControls',
+ 'timePicker',
+ 'dashboard',
+ 'settings',
+ 'console',
+ 'common',
+ 'header',
+ ]);
+
+ describe('Dashboard options list validation', () => {
+ let controlId: string;
+
+ before(async () => {
+ await dashboard.ensureDashboardIsInEditMode();
+ await dashboardControls.createControl({
+ controlType: OPTIONS_LIST_CONTROL,
+ dataViewTitle: 'animals-*',
+ fieldName: 'sound.keyword',
+ title: 'Animal Sounds',
+ });
+ controlId = (await dashboardControls.getAllControlIds())[0];
+ await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie');
+ await dashboard.clickQuickSave();
+ await header.waitUntilLoadingHasFinished();
+ });
+
+ after(async () => {
+ await filterBar.removeAllFilters();
+ await dashboardControls.deleteAllControls();
+ await dashboardPanelActions.removePanelByTitle('Rendering Test: animal sounds pie');
+ await dashboard.clickQuickSave();
+ });
+
+ describe('Options List dashboard validation', async () => {
+ before(async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSelectOption('meow');
+ await dashboardControls.optionsListPopoverSelectOption('bark');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ });
+
+ after(async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverClearSelections();
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await filterBar.removeAllFilters();
+ await queryBar.clickQuerySubmitButton();
+ });
+
+ it('Can mark selections invalid with Query', async () => {
+ await queryBar.setQuery('NOT animal.keyword : "dog" ');
+ await queryBar.submitQuery();
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+
+ const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
+ 'hiss',
+ 'meow',
+ 'growl',
+ 'grr',
+ ]);
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: { ...suggestions, grr: suggestions.grr - 1 },
+ invalidSelections: ['bark'],
+ });
+ // only valid selections are applied as filters.
+ expect(await pieChart.getPieSliceCount()).to.be(1);
+ });
+
+ it('can make invalid selections valid again if the parent filter changes', async () => {
+ await queryBar.setQuery('');
+ await queryBar.submitQuery();
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS,
+ invalidSelections: [],
+ });
+ expect(await pieChart.getPieSliceCount()).to.be(2);
+ });
+
+ it('Can mark multiple selections invalid with Filter', async () => {
+ await filterBar.addFilter({ field: 'sound.keyword', operation: 'is', value: 'hiss' });
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: {
+ hiss: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.hiss,
+ },
+ invalidSelections: ['meow', 'bark'],
+ });
+ // only valid selections are applied as filters.
+ expect(await pieChart.getPieSliceCount()).to.be(1);
+ });
+ });
+
+ describe('Options List dashboard no validation', async () => {
+ before(async () => {
+ await dashboardControls.optionsListOpenPopover(controlId);
+ await dashboardControls.optionsListPopoverSelectOption('meow');
+ await dashboardControls.optionsListPopoverSelectOption('bark');
+ await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
+ await dashboardControls.updateValidationSetting(false);
+ });
+
+ it('Does not mark selections invalid with Query', async () => {
+ await queryBar.setQuery('NOT animal.keyword : "dog" ');
+ await queryBar.submitQuery();
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+
+ const suggestions = pick(OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS, [
+ 'hiss',
+ 'meow',
+ 'growl',
+ 'grr',
+ ]);
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: { ...suggestions, grr: suggestions.grr - 1 },
+ invalidSelections: [],
+ });
+ });
+
+ it('Does not mark multiple selections invalid with Filter', async () => {
+ await filterBar.addFilter({ field: 'sound.keyword', operation: 'is', value: 'hiss' });
+ await dashboard.waitForRenderComplete();
+ await header.waitUntilLoadingHasFinished();
+ await dashboardControls.ensureAvailableOptionsEqual(controlId, {
+ suggestions: {
+ hiss: OPTIONS_LIST_ANIMAL_SOUND_SUGGESTIONS.hiss,
+ },
+ invalidSelections: [],
+ });
+ });
+ });
+ });
+}
diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts
index 0f5a6d41e6fe0..1f9656c9ff548 100644
--- a/test/functional/page_objects/dashboard_page.ts
+++ b/test/functional/page_objects/dashboard_page.ts
@@ -281,6 +281,13 @@ export class DashboardPageObject extends FtrService {
return await this.testSubjects.exists('dashboardEditMode');
}
+ public async ensureDashboardIsInEditMode() {
+ if (await this.getIsInViewMode()) {
+ await this.switchToEditMode();
+ }
+ await this.waitForRenderComplete();
+ }
+
public async clickCancelOutOfEditMode(accept = true) {
this.log.debug('clickCancelOutOfEditMode');
if (await this.getIsInViewMode()) return;
diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts
index cdb48b067aec5..85908bce3d35c 100644
--- a/test/functional/page_objects/dashboard_page_controls.ts
+++ b/test/functional/page_objects/dashboard_page_controls.ts
@@ -14,8 +14,8 @@ import {
} from '@kbn/controls-plugin/common';
import { ControlGroupChainingSystem } from '@kbn/controls-plugin/common/control_group/types';
import { OptionsListSortingType } from '@kbn/controls-plugin/common/options_list/suggestions_sorting';
-import { WebElementWrapper } from '../services/lib/web_element_wrapper';
+import { WebElementWrapper } from '../services/lib/web_element_wrapper';
import { FtrService } from '../ftr_provider_context';
const CONTROL_DISPLAY_NAMES: { [key: string]: string } = {
@@ -48,6 +48,7 @@ export class DashboardPageControls extends FtrService {
private readonly log = this.ctx.getService('log');
private readonly find = this.ctx.getService('find');
private readonly retry = this.ctx.getService('retry');
+ private readonly browser = this.ctx.getService('browser');
private readonly testSubjects = this.ctx.getService('testSubjects');
private readonly common = this.ctx.getPageObject('common');
@@ -354,6 +355,7 @@ export class DashboardPageControls extends FtrService {
public async optionsListGetSelectionsString(controlId: string) {
this.log.debug(`Getting selections string for Options List: ${controlId}`);
+ await this.optionsListWaitForLoading(controlId);
const controlElement = await this.getControlElementById(controlId);
return (await controlElement.getVisibleText()).split('\n')[1];
}
@@ -362,7 +364,7 @@ export class DashboardPageControls extends FtrService {
this.log.debug(`Opening popover for Options List: ${controlId}`);
await this.testSubjects.click(`optionsList-control-${controlId}`);
await this.retry.try(async () => {
- await this.testSubjects.existOrFail(`optionsList-control-available-options`);
+ await this.testSubjects.existOrFail(`optionsList-control-popover`);
});
}
@@ -382,25 +384,27 @@ export class DashboardPageControls extends FtrService {
public async optionsListPopoverGetAvailableOptionsCount() {
this.log.debug(`getting available options count from options list`);
+ await this.optionsListPopoverWaitForLoading();
const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`);
return +(await availableOptions.getAttribute('data-option-count'));
}
public async optionsListPopoverGetAvailableOptions() {
this.log.debug(`getting available options from options list`);
+ await this.optionsListPopoverWaitForLoading();
const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`);
-
- const suggestionElements = await availableOptions.findAllByClassName(
- 'optionsList__validSuggestion'
- );
- const suggestions: { [key: string]: number } = await suggestionElements.reduce(
- async (promise, option) => {
- const acc = await promise;
- const [key, docCount] = (await option.getVisibleText()).split('\n');
- return { ...acc, [key]: Number(docCount) };
- },
- Promise.resolve({} as { [key: string]: number })
- );
+ const optionsCount = await this.optionsListPopoverGetAvailableOptionsCount();
+
+ const selectableListItems = await availableOptions.findByClassName('euiSelectableList__list');
+ const suggestions: { [key: string]: number } = {};
+ while (Object.keys(suggestions).length < optionsCount) {
+ await selectableListItems._webElement.sendKeys(this.browser.keys.ARROW_DOWN);
+ const currentOption = await selectableListItems.findByCssSelector('[aria-selected="true"]');
+ const [suggestion, docCount] = (await currentOption.getVisibleText()).split('\n');
+ if (suggestion !== 'Exists') {
+ suggestions[suggestion] = Number(docCount);
+ }
+ }
const invalidSelectionElements = await availableOptions.findAllByClassName(
'optionsList__selectionInvalid'
@@ -419,6 +423,7 @@ export class DashboardPageControls extends FtrService {
expectation: { suggestions: { [key: string]: number }; invalidSelections: string[] },
skipOpen?: boolean
) {
+ await this.optionsListWaitForLoading(controlId);
if (!skipOpen) await this.optionsListOpenPopover(controlId);
await this.retry.try(async () => {
expect(await this.optionsListPopoverGetAvailableOptions()).to.eql(expectation);
@@ -456,10 +461,25 @@ export class DashboardPageControls extends FtrService {
});
}
+ public async optionsListPopoverSelectExists() {
+ await this.retry.try(async () => {
+ await this.testSubjects.existOrFail(`optionsList-control-selection-exists`);
+ await this.testSubjects.click(`optionsList-control-selection-exists`);
+ });
+ }
+
public async optionsListPopoverSelectOption(availableOption: string) {
this.log.debug(`selecting ${availableOption} from options list`);
- await this.optionsListPopoverAssertOpen();
- await this.testSubjects.click(`optionsList-control-selection-${availableOption}`);
+ await this.optionsListPopoverSearchForOption(availableOption);
+ await this.optionsListPopoverWaitForLoading();
+
+ await this.retry.try(async () => {
+ await this.testSubjects.existOrFail(`optionsList-control-selection-${availableOption}`);
+ await this.testSubjects.click(`optionsList-control-selection-${availableOption}`);
+ });
+
+ await this.optionsListPopoverClearSearch();
+ await this.optionsListPopoverWaitForLoading();
}
public async optionsListPopoverClearSelections() {
@@ -482,9 +502,16 @@ export class DashboardPageControls extends FtrService {
}
public async optionsListWaitForLoading(controlId: string) {
+ this.log.debug(`wait for ${controlId} to load`);
await this.testSubjects.waitForEnabled(`optionsList-control-${controlId}`);
}
+ public async optionsListPopoverWaitForLoading() {
+ this.log.debug(`wait for the suggestions in the popover to load`);
+ await this.optionsListPopoverAssertOpen();
+ await this.testSubjects.waitForDeleted('optionsList-control-popover-loading');
+ }
+
/* -----------------------------------------------------------
Control editor flyout
----------------------------------------------------------- */