diff --git a/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts b/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts index c412a5589cc32..2b89bc55ba2c0 100644 --- a/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts +++ b/src/plugins/controls/common/control_group/control_group_panel_diff_system.ts @@ -8,7 +8,7 @@ import deepEqual from 'fast-deep-equal'; import { omit, isEqual } from 'lodash'; -import { DEFAULT_SORT } from '../options_list/suggestions_sorting'; +import { OPTIONS_LIST_DEFAULT_SORT } from '../options_list/suggestions_sorting'; import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from '../options_list/types'; import { ControlPanelState } from './types'; @@ -65,7 +65,7 @@ export const ControlPanelDiffSystems: { Boolean(singleSelectA) === Boolean(singleSelectB) && Boolean(existsSelectedA) === Boolean(existsSelectedB) && Boolean(runPastTimeoutA) === Boolean(runPastTimeoutB) && - deepEqual(sortA ?? DEFAULT_SORT, sortB ?? DEFAULT_SORT) && + deepEqual(sortA ?? OPTIONS_LIST_DEFAULT_SORT, sortB ?? OPTIONS_LIST_DEFAULT_SORT) && isEqual(selectedA ?? [], selectedB ?? []) && deepEqual(inputA, inputB) ); diff --git a/src/plugins/controls/common/options_list/mocks.tsx b/src/plugins/controls/common/options_list/mocks.tsx index ac80ac3873968..943e78c370fc6 100644 --- a/src/plugins/controls/common/options_list/mocks.tsx +++ b/src/plugins/controls/common/options_list/mocks.tsx @@ -21,7 +21,13 @@ const mockOptionsListComponentState = { ...getDefaultComponentState(), field: undefined, totalCardinality: 0, - availableOptions: ['woof', 'bark', 'meow', 'quack', 'moo'], + availableOptions: { + woof: { doc_count: 100 }, + bark: { doc_count: 75 }, + meow: { doc_count: 50 }, + quack: { doc_count: 25 }, + moo: { doc_count: 5 }, + }, invalidSelections: [], validSelections: [], } as OptionsListComponentState; diff --git a/src/plugins/controls/common/options_list/suggestions_sorting.ts b/src/plugins/controls/common/options_list/suggestions_sorting.ts index 5289beeeb2a29..a66fe1bdf2891 100644 --- a/src/plugins/controls/common/options_list/suggestions_sorting.ts +++ b/src/plugins/controls/common/options_list/suggestions_sorting.ts @@ -10,13 +10,14 @@ import { Direction } from '@elastic/eui'; export type OptionsListSortBy = '_count' | '_key'; -export const DEFAULT_SORT: SortingType = { by: '_count', direction: 'desc' }; +export const OPTIONS_LIST_DEFAULT_SORT: OptionsListSortingType = { + by: '_count', + direction: 'desc', +}; -export const sortDirections: Readonly = ['asc', 'desc'] as const; -export type SortDirection = typeof sortDirections[number]; -export interface SortingType { +export interface OptionsListSortingType { by: OptionsListSortBy; - direction: SortDirection; + direction: Direction; } export const getCompatibleSortingTypes = (type?: string): OptionsListSortBy[] => { diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index be5f252af3cc6..eb1f7b886d427 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -9,12 +9,13 @@ import { FieldSpec, DataView, RuntimeFieldSpec } from '@kbn/data-views-plugin/common'; import type { Filter, Query, BoolQuery, TimeRange } from '@kbn/es-query'; -import { SortingType } from './suggestions_sorting'; +import { OptionsListSortingType } from './suggestions_sorting'; import { DataControlInput } from '../types'; export const OPTIONS_LIST_CONTROL = 'optionsListControl'; export interface OptionsListEmbeddableInput extends DataControlInput { + sort?: OptionsListSortingType; selectedOptions?: string[]; existsSelected?: boolean; runPastTimeout?: boolean; @@ -22,7 +23,6 @@ export interface OptionsListEmbeddableInput extends DataControlInput { hideExclude?: boolean; hideExists?: boolean; hideSort?: boolean; - sort?: SortingType; exclude?: boolean; } @@ -32,11 +32,15 @@ export type OptionsListField = FieldSpec & { childFieldName?: string; }; +export interface OptionsListSuggestions { + [key: string]: { doc_count: number }; +} + /** * The Options list response is returned from the serverside Options List route. */ export interface OptionsListResponse { - suggestions: string[]; + suggestions: OptionsListSuggestions; totalCardinality: number; invalidSelections?: string[]; } @@ -61,6 +65,7 @@ export type OptionsListRequest = Omit< */ export interface OptionsListRequestBody { runtimeFieldMap?: Record; + sort?: OptionsListSortingType; filters?: Array<{ bool: BoolQuery }>; selectedOptions?: string[]; runPastTimeout?: boolean; @@ -68,6 +73,5 @@ export interface OptionsListRequestBody { textFieldName?: string; searchString?: string; fieldSpec?: FieldSpec; - sort?: SortingType; fieldName: string; } diff --git a/src/plugins/controls/public/__stories__/controls.stories.tsx b/src/plugins/controls/public/__stories__/controls.stories.tsx index e891e3ba36685..02a13125ba49c 100644 --- a/src/plugins/controls/public/__stories__/controls.stories.tsx +++ b/src/plugins/controls/public/__stories__/controls.stories.tsx @@ -54,7 +54,12 @@ const storybookStubOptionsListRequest = async ( setTimeout( () => r({ - suggestions: getFlightSearchOptions(request.field.name, request.searchString), + suggestions: getFlightSearchOptions(request.field.name, request.searchString).reduce( + (o, current, index) => { + return { ...o, [current]: { doc_count: index } }; + }, + {} + ), totalCardinality: 100, }), 120 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 1f19382ab506b..43742a817e3ec 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 @@ -93,7 +93,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub ) : ( <> {validSelections && ( - {validSelections?.join(OptionsListStrings.control.getSeparator())} + {validSelections.join(OptionsListStrings.control.getSeparator())} )} {invalidSelections && ( diff --git a/src/plugins/controls/public/options_list/components/options_list_editor_options.tsx b/src/plugins/controls/public/options_list/components/options_list_editor_options.tsx index dcfd98eedf395..8496971d131b3 100644 --- a/src/plugins/controls/public/options_list/components/options_list_editor_options.tsx +++ b/src/plugins/controls/public/options_list/components/options_list_editor_options.tsx @@ -21,7 +21,7 @@ import { css } from '@emotion/react'; import { getCompatibleSortingTypes, - DEFAULT_SORT, + OPTIONS_LIST_DEFAULT_SORT, OptionsListSortBy, } from '../../../common/options_list/suggestions_sorting'; import { OptionsListStrings } from './options_list_strings'; @@ -48,8 +48,8 @@ export const OptionsListEditorOptions = ({ fieldType, }: ControlEditorProps) => { const [state, setState] = useState({ - sortDirection: initialInput?.sort?.direction ?? DEFAULT_SORT.direction, - sortBy: initialInput?.sort?.by ?? DEFAULT_SORT.by, + sortDirection: initialInput?.sort?.direction ?? OPTIONS_LIST_DEFAULT_SORT.direction, + sortBy: initialInput?.sort?.by ?? OPTIONS_LIST_DEFAULT_SORT.by, runPastTimeout: initialInput?.runPastTimeout, singleSelect: initialInput?.singleSelect, hideExclude: initialInput?.hideExclude, @@ -60,8 +60,12 @@ export const OptionsListEditorOptions = ({ useEffect(() => { // when field type changes, ensure that the selected sort type is still valid if (!getCompatibleSortingTypes(fieldType).includes(state.sortBy)) { - onChange({ sort: DEFAULT_SORT }); - setState((s) => ({ ...s, sortBy: DEFAULT_SORT.by, sortDirection: DEFAULT_SORT.direction })); + onChange({ sort: OPTIONS_LIST_DEFAULT_SORT }); + setState((s) => ({ + ...s, + sortBy: OPTIONS_LIST_DEFAULT_SORT.by, + sortDirection: OPTIONS_LIST_DEFAULT_SORT.direction, + })); } }, [fieldType, onChange, state.sortBy]); 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 ef4e0a8ed0b36..b1315be51ae1e 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 @@ -66,7 +66,7 @@ describe('Options list popover', () => { }); test('no available options', async () => { - const popover = await mountComponent({ componentState: { availableOptions: [] } }); + const popover = await mountComponent({ componentState: { availableOptions: {} } }); const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); const noOptionsDiv = findTestSubject( availableOptionsDiv, @@ -118,6 +118,43 @@ describe('Options list popover', () => { expect(sortButton.prop('disabled')).toBe(true); }); + test('test single invalid selection', async () => { + const popover = await mountComponent({ + explicitInput: { + selectedOptions: ['bark', 'woof'], + }, + componentState: { + availableOptions: { + bark: { doc_count: 75 }, + }, + validSelections: ['bark'], + invalidSelections: ['woof'], + }, + }); + const validSelection = findTestSubject(popover, 'optionsList-control-selection-bark'); + expect(validSelection.text()).toEqual('bark75'); + 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.hasClass('optionsList__selectionInvalid')).toBe(true); + }); + + test('test title when multiple invalid selections', async () => { + const popover = await mountComponent({ + explicitInput: { selectedOptions: ['bark', 'woof', 'meow'] }, + componentState: { + availableOptions: { + bark: { doc_count: 75 }, + }, + validSelections: ['bark'], + invalidSelections: ['woof', 'meow'], + }, + }); + const title = findTestSubject(popover, 'optionList__ignoredSelectionLabel').text(); + expect(title).toEqual('Ignored selections'); + }); + test('should default to exclude = false', async () => { const popover = await mountComponent(); const includeButton = findTestSubject(popover, 'optionsList__includeResults'); @@ -172,7 +209,7 @@ describe('Options list popover', () => { test('if existsSelected = false and no suggestions, then "Exists" does not show up', async () => { const popover = await mountComponent({ - componentState: { availableOptions: [] }, + componentState: { availableOptions: {} }, explicitInput: { existsSelected: false }, }); const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); 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 bc1e62fccfda7..6ad39e0b3dbd9 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 @@ -46,8 +46,8 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop return ( {title} @@ -59,10 +59,10 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop /> )}
300 ? width : undefined }} className="optionsList __items" - data-option-count={availableOptions?.length ?? 0} + style={{ width: width > 300 ? width : undefined }} data-test-subj={`optionsList-control-available-options`} + data-option-count={Object.keys(availableOptions ?? {}).length} > {!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && ( diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_action_bar.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_action_bar.tsx index f6be1cb0e7a62..375a2a2058692 100644 --- a/src/plugins/controls/public/options_list/components/options_list_popover_action_bar.tsx +++ b/src/plugins/controls/public/options_list/components/options_list_popover_action_bar.tsx @@ -112,7 +112,11 @@ export const OptionsListPopoverActionBar = ({ display={showOnlySelected ? 'base' : 'empty'} onClick={() => setShowOnlySelected(!showOnlySelected)} data-test-subj="optionsList-control-show-only-selected" - aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()} + aria-label={ + showOnlySelected + ? OptionsListStrings.popover.getAllOptionsButtonTitle() + : OptionsListStrings.popover.getSelectedOptionsButtonTitle() + } /> 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 1a6ec2176dd42..01c9f14363a4c 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 @@ -26,11 +26,14 @@ export const OptionsListPopoverInvalidSelections = () => { // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); - return ( <> - +