From 7a260b378cac7e7828f53224158d40e66addf6cb Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 19 Oct 2022 14:21:50 -0600 Subject: [PATCH 01/33] Mock first attempt at UI --- .../controls/common/options_list/types.ts | 1 + .../components/options_list_popover.tsx | 80 ++++++++++++------- .../options_list/options_list_reducers.ts | 27 ++++++- .../controls/public/options_list/types.ts | 1 + 4 files changed, 81 insertions(+), 28 deletions(-) diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index 59b78ee38d0b9..2b820fee14a5e 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -15,6 +15,7 @@ export const OPTIONS_LIST_CONTROL = 'optionsListControl'; export interface OptionsListEmbeddableInput extends DataControlInput { selectedOptions?: string[]; + existsSelected?: boolean; runPastTimeout?: boolean; singleSelect?: boolean; hideExclude?: boolean; 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 48be0d9253285..fdc0437b0f51b 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 @@ -54,13 +54,21 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const { useEmbeddableDispatch, useEmbeddableSelector: select, - actions: { selectOption, deselectOption, clearSelections, replaceSelection, setExclude }, + actions: { + selectOption, + deselectOption, + clearSelections, + replaceSelection, + setExclude, + selectExists, + }, } = useReduxEmbeddableContext(); const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); + const ignoredSelections = select((state) => state.componentState.ignoredSelections); const totalCardinality = select((state) => state.componentState.totalCardinality); const availableOptions = select((state) => state.componentState.availableOptions); const searchString = select((state) => state.componentState.searchString); @@ -68,6 +76,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const selectedOptions = select((state) => state.explicitInput.selectedOptions); const hideExclude = select((state) => state.explicitInput.hideExclude); + const existsSelected = select((state) => state.explicitInput.existsSelected); const singleSelect = select((state) => state.explicitInput.singleSelect); const title = select((state) => state.explicitInput.title); const exclude = select((state) => state.explicitInput.exclude); @@ -84,6 +93,10 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const [showOnlySelected, setShowOnlySelected] = useState(false); const euiBackgroundColor = useEuiBackgroundColor('subdued'); + const combinedIgnoredSelections = useMemo(() => { + return (invalidSelections ?? []).concat(ignoredSelections ?? []); + }, [ignoredSelections, invalidSelections]); + return ( <> {title} @@ -114,14 +127,14 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop /> - {invalidSelections && invalidSelections.length > 0 && ( + {combinedIgnoredSelections.length > 0 && ( - {invalidSelections.length} + {combinedIgnoredSelections.length} )} @@ -174,26 +187,39 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop > {!showOnlySelected && ( <> - {availableOptions?.map((availableOption, index) => ( - { - if (singleSelect) { - dispatch(replaceSelection(availableOption)); - return; - } - if (selectedOptionsSet.has(availableOption)) { - dispatch(deselectOption(availableOption)); - return; - } - dispatch(selectOption(availableOption)); - }} - > - {`${availableOption}`} - - ))} + { + console.log(existsSelected); + dispatch(selectExists(!Boolean(existsSelected))); + console.log(existsSelected); + }} + > + {`Exists (*)`} + + {!existsSelected && + availableOptions?.map((availableOption, index) => ( + { + if (singleSelect) { + dispatch(replaceSelection(availableOption)); + return; + } + if (selectedOptionsSet.has(availableOption)) { + dispatch(deselectOption(availableOption)); + return; + } + dispatch(selectOption(availableOption)); + }} + > + {`${availableOption}`} + + ))} {!loading && (!availableOptions || availableOptions.length === 0) && (
)} - {!isEmpty(invalidSelections) && ( + {!isEmpty(combinedIgnoredSelections) && ( <> <> - {invalidSelections?.map((ignoredSelection, index) => ( + {combinedIgnoredSelections?.map((ignoredSelection, index) => ( ({ searchString: { value: '', valid: true }, @@ -51,6 +52,29 @@ export const optionsListReducers = { state.componentState.searchString.valid = getIpRangeQuery(action.payload).validSearch; } }, + selectExists: (state: WritableDraft, action: PayloadAction) => { + console.log( + 'BEFORE:', + { ...state.componentState.validSelections }, + { ...state.componentState.ignoredSelections } + ); + + state.explicitInput.existsSelected = action.payload; + + if (action.payload) { + state.componentState.ignoredSelections = state.componentState.validSelections; + state.componentState.validSelections = []; + } else { + state.componentState.validSelections = state.componentState.ignoredSelections; + state.componentState.ignoredSelections = []; + } + + console.log( + 'AFTER', + { ...state.componentState.validSelections }, + { ...state.componentState.ignoredSelections } + ); + }, selectOption: (state: WritableDraft, action: PayloadAction) => { if (!state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; state.explicitInput.selectedOptions?.push(action.payload); @@ -62,7 +86,8 @@ export const optionsListReducers = { state.explicitInput.selectedOptions = [action.payload]; }, clearSelections: (state: WritableDraft) => { - if (state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; + if (state.explicitInput.existsSelected) state.explicitInput.existsSelected = false; + else if (state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; }, setExclude: (state: WritableDraft, action: PayloadAction) => { state.explicitInput.exclude = action.payload; diff --git a/src/plugins/controls/public/options_list/types.ts b/src/plugins/controls/public/options_list/types.ts index 4001299a9ab53..a868448b1af11 100644 --- a/src/plugins/controls/public/options_list/types.ts +++ b/src/plugins/controls/public/options_list/types.ts @@ -22,6 +22,7 @@ export interface OptionsListComponentState { availableOptions?: string[]; invalidSelections?: string[]; validSelections?: string[]; + ignoredSelections?: string[]; searchString: SearchString; } From 98c71124cd4d8536fc1c98549645e4770e7b913f Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 20 Oct 2022 09:04:19 -0600 Subject: [PATCH 02/33] Add `exists` filter functionality --- .../components/options_list_popover.tsx | 3 +- .../embeddable/options_list_embeddable.tsx | 58 ++++++++++++++----- .../options_list/options_list_reducers.ts | 24 +++----- .../page_objects/dashboard_page_controls.ts | 7 ++- 4 files changed, 55 insertions(+), 37 deletions(-) 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 fdc0437b0f51b..596b315941cb8 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 @@ -192,9 +192,8 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop checked={existsSelected ? 'on' : undefined} key={'exists-option'} onClick={() => { - console.log(existsSelected); dispatch(selectExists(!Boolean(existsSelected))); - console.log(existsSelected); + // console.log(existsSelected); }} > {`Exists (*)`} 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 ffead8d9c20bc..b6fd8d916f0e5 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 @@ -21,6 +21,7 @@ import { buildPhraseFilter, buildPhrasesFilter, COMPARE_ALL_OPTIONS, + buildExistsFilter, } from '@kbn/es-query'; import { ReduxEmbeddableTools, ReduxEmbeddablePackage } from '@kbn/presentation-util-plugin/public'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -156,7 +157,10 @@ export class OptionsListEmbeddable extends Embeddable isEqual(a.selectedOptions, b.selectedOptions) && a.exclude === b.exclude + (a, b) => + isEqual(a.selectedOptions, b.selectedOptions) && + a.exclude === b.exclude && + a.existsSelected === b.existsSelected ) ) .subscribe(async ({ selectedOptions: newSelectedOptions }) => { @@ -164,32 +168,47 @@ export class OptionsListEmbeddable extends Embeddable { + dispatch(clearValidAndInvalidSelections({})); + dispatch(setIgnoredSelections([])); + }); } else { - const { invalidSelections } = this.reduxEmbeddableTools.getState().componentState ?? {}; + const { invalidSelections, ignoredSelections } = + this.reduxEmbeddableTools.getState().componentState ?? {}; const newValidSelections: string[] = []; const newInvalidSelections: string[] = []; + const newIgnoredSelections: string[] = []; for (const selectedOption of newSelectedOptions) { if (invalidSelections?.includes(selectedOption)) { newInvalidSelections.push(selectedOption); continue; } + if (ignoredSelections?.includes(selectedOption)) { + newIgnoredSelections.push(selectedOption); + continue; + } newValidSelections.push(selectedOption); } - dispatch( - setValidAndInvalidSelections({ - validSelections: newValidSelections, - invalidSelections: newInvalidSelections, - }) - ); + batch(() => { + dispatch( + setValidAndInvalidSelections({ + validSelections: newValidSelections, + invalidSelections: newInvalidSelections, + }) + ); + dispatch(setIgnoredSelections(newIgnoredSelections)); + }); } const newFilters = await this.buildFilter(); + // console.log('new filter 1:', newFilters); + dispatch(publishFilters(newFilters)); }) ); @@ -350,6 +369,7 @@ export class OptionsListEmbeddable extends Embeddable { dispatch(setLoading(false)); dispatch(publishFilters(newFilters)); @@ -370,22 +390,28 @@ export class OptionsListEmbeddable extends Embeddable ({ searchString: { value: '', valid: true }, @@ -53,27 +52,20 @@ export const optionsListReducers = { } }, selectExists: (state: WritableDraft, action: PayloadAction) => { - console.log( - 'BEFORE:', - { ...state.componentState.validSelections }, - { ...state.componentState.ignoredSelections } - ); - state.explicitInput.existsSelected = action.payload; - - if (action.payload) { + if (state.explicitInput.existsSelected) { state.componentState.ignoredSelections = state.componentState.validSelections; state.componentState.validSelections = []; } else { state.componentState.validSelections = state.componentState.ignoredSelections; state.componentState.ignoredSelections = []; } - - console.log( - 'AFTER', - { ...state.componentState.validSelections }, - { ...state.componentState.ignoredSelections } - ); + }, + setIgnoredSelections: ( + state: WritableDraft, + action: PayloadAction + ) => { + state.componentState.ignoredSelections = action.payload; }, selectOption: (state: WritableDraft, action: PayloadAction) => { if (!state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; @@ -87,7 +79,7 @@ export const optionsListReducers = { }, clearSelections: (state: WritableDraft) => { if (state.explicitInput.existsSelected) state.explicitInput.existsSelected = false; - else if (state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; + if (state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; }, setExclude: (state: WritableDraft, action: PayloadAction) => { state.explicitInput.exclude = action.payload; diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts index 1e04ebb467d89..461734c61d0ce 100644 --- a/test/functional/page_objects/dashboard_page_controls.ts +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -346,10 +346,11 @@ export class DashboardPageControls extends FtrService { return +(await availableOptions.getAttribute('data-option-count')); } - public async optionsListPopoverGetAvailableOptions() { - this.log.debug(`getting available options count from options list`); + public async optionsListPopoverGetAvailableOptions(filterOutExists: boolean = true) { + this.log.debug(`getting available options from options list`); const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`); - return (await availableOptions.getVisibleText()).split('\n'); + const availableOptionsArray = (await availableOptions.getVisibleText()).split('\n'); + return filterOutExists ? availableOptionsArray.slice(1) : availableOptionsArray; } public async optionsListPopoverSearchForOption(search: string) { From efa54624e9bc5469634ffe445b0bf1860a866278 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 20 Oct 2022 15:29:01 -0600 Subject: [PATCH 03/33] Make `exists` selection change button --- .../options_list/components/options_list.scss | 4 +++ .../components/options_list_control.tsx | 27 ++++++++++++------- .../components/options_list_popover.tsx | 2 +- 3 files changed, 23 insertions(+), 10 deletions(-) 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 53ad3990cd371..1f1ea7f200a40 100644 --- a/src/plugins/controls/public/options_list/components/options_list.scss +++ b/src/plugins/controls/public/options_list/components/options_list.scss @@ -35,6 +35,10 @@ font-weight: 300; } +.optionsList__existsFilter { + font-style: italic; +} + .optionsList__ignoredBadge { margin-left: $euiSizeS; } 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 93bf0cbef864e..f1c6b2987ef11 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 @@ -46,6 +46,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub const validSelections = select((state) => state.componentState.validSelections); const selectedOptions = select((state) => state.explicitInput.selectedOptions); + const existsSelected = select((state) => state.explicitInput.existsSelected); const controlStyle = select((state) => state.explicitInput.controlStyle); const singleSelect = select((state) => state.explicitInput.singleSelect); const id = select((state) => state.explicitInput.id); @@ -87,18 +88,24 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub {OptionsListStrings.control.getNegate()}{' '} )} - {validSelections && ( - {validSelections?.join(OptionsListStrings.control.getSeparator())} - )} - {invalidSelections && ( - - {invalidSelections.join(OptionsListStrings.control.getSeparator())} - + {existsSelected ? ( + {'Exists (*)'} + ) : ( + <> + {validSelections && ( + {validSelections?.join(OptionsListStrings.control.getSeparator())} + )} + {invalidSelections && ( + + {invalidSelections.join(OptionsListStrings.control.getSeparator())} + + )} + )} ), }; - }, [exclude, validSelections, invalidSelections]); + }, [exclude, existsSelected, validSelections, invalidSelections]); const button = (
@@ -115,7 +122,9 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub numActiveFilters={validSelectionsCount} hasActiveFilters={Boolean(validSelectionsCount)} > - {hasSelections ? selectionDisplayNode : OptionsListStrings.control.getPlaceholder()} + {hasSelections || existsSelected + ? selectionDisplayNode + : OptionsListStrings.control.getPlaceholder()}
); 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 596b315941cb8..bac208dbc7df4 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 @@ -196,7 +196,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop // console.log(existsSelected); }} > - {`Exists (*)`} + {`Exists (*)`}
{!existsSelected && availableOptions?.map((availableOption, index) => ( From f5dcc03601c6e6aaaf721fa1481c99bc1abcd2a9 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 20 Oct 2022 15:49:19 -0600 Subject: [PATCH 04/33] Overwrite selections instead of disabling them --- .../components/options_list_popover.tsx | 58 +++++++------- .../embeddable/options_list_embeddable.tsx | 78 ++++++++----------- .../options_list/options_list_reducers.ts | 15 +--- .../controls/public/options_list/types.ts | 1 - 4 files changed, 62 insertions(+), 90 deletions(-) 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 bac208dbc7df4..1ddec816e9779 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 @@ -68,7 +68,6 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); - const ignoredSelections = select((state) => state.componentState.ignoredSelections); const totalCardinality = select((state) => state.componentState.totalCardinality); const availableOptions = select((state) => state.componentState.availableOptions); const searchString = select((state) => state.componentState.searchString); @@ -93,10 +92,6 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const [showOnlySelected, setShowOnlySelected] = useState(false); const euiBackgroundColor = useEuiBackgroundColor('subdued'); - const combinedIgnoredSelections = useMemo(() => { - return (invalidSelections ?? []).concat(ignoredSelections ?? []); - }, [ignoredSelections, invalidSelections]); - return ( <> {title} @@ -127,14 +122,14 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop /> - {combinedIgnoredSelections.length > 0 && ( + {(invalidSelections?.length ?? 0) > 0 && ( - {combinedIgnoredSelections.length} + {invalidSelections?.length} )} @@ -198,27 +193,26 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop > {`Exists (*)`} - {!existsSelected && - availableOptions?.map((availableOption, index) => ( - { - if (singleSelect) { - dispatch(replaceSelection(availableOption)); - return; - } - if (selectedOptionsSet.has(availableOption)) { - dispatch(deselectOption(availableOption)); - return; - } - dispatch(selectOption(availableOption)); - }} - > - {`${availableOption}`} - - ))} + {availableOptions?.map((availableOption, index) => ( + { + if (singleSelect) { + dispatch(replaceSelection(availableOption)); + return; + } + if (selectedOptionsSet.has(availableOption)) { + dispatch(deselectOption(availableOption)); + return; + } + dispatch(selectOption(availableOption)); + }} + > + {`${availableOption}`} + + ))} {!loading && (!availableOptions || availableOptions.length === 0) && (
)} - {!isEmpty(combinedIgnoredSelections) && ( + {invalidSelections && !isEmpty(invalidSelections) && ( <> <> - {combinedIgnoredSelections?.map((ignoredSelection, index) => ( + {invalidSelections?.map((ignoredSelection, index) => ( - isEqual(a.selectedOptions, b.selectedOptions) && a.exclude === b.exclude && - a.existsSelected === b.existsSelected + a.existsSelected === b.existsSelected && + isEqual(a.selectedOptions, b.selectedOptions) ) ) - .subscribe(async ({ selectedOptions: newSelectedOptions }) => { - const { - actions: { - clearValidAndInvalidSelections, - setValidAndInvalidSelections, - setIgnoredSelections, - publishFilters, - }, - dispatch, - } = this.reduxEmbeddableTools; - // console.log('NEW SELECTIONS:', newSelectedOptions); - if (!newSelectedOptions || isEmpty(newSelectedOptions)) { - batch(() => { + .subscribe( + async ({ selectedOptions: newSelectedOptions, existsSelected: newExistsSelected }) => { + const { + actions: { + clearValidAndInvalidSelections, + setValidAndInvalidSelections, + publishFilters, + }, + dispatch, + } = this.reduxEmbeddableTools; + // console.log('NEW SELECTIONS:', newSelectedOptions); + + if (!newSelectedOptions || isEmpty(newSelectedOptions)) { dispatch(clearValidAndInvalidSelections({})); - dispatch(setIgnoredSelections([])); - }); - } else { - const { invalidSelections, ignoredSelections } = - this.reduxEmbeddableTools.getState().componentState ?? {}; - const newValidSelections: string[] = []; - const newInvalidSelections: string[] = []; - const newIgnoredSelections: string[] = []; - for (const selectedOption of newSelectedOptions) { - if (invalidSelections?.includes(selectedOption)) { - newInvalidSelections.push(selectedOption); - continue; + } else { + const { invalidSelections } = + this.reduxEmbeddableTools.getState().componentState ?? {}; + const newValidSelections: string[] = []; + const newInvalidSelections: string[] = []; + for (const selectedOption of newSelectedOptions) { + if (invalidSelections?.includes(selectedOption)) { + newInvalidSelections.push(selectedOption); + continue; + } + newValidSelections.push(selectedOption); } - if (ignoredSelections?.includes(selectedOption)) { - newIgnoredSelections.push(selectedOption); - continue; - } - newValidSelections.push(selectedOption); - } - batch(() => { + dispatch( setValidAndInvalidSelections({ validSelections: newValidSelections, invalidSelections: newInvalidSelections, }) ); - dispatch(setIgnoredSelections(newIgnoredSelections)); - }); + } + const newFilters = await this.buildFilter(); + // console.log('new filter 1:', newFilters); + dispatch(publishFilters(newFilters)); } - const newFilters = await this.buildFilter(); - // console.log('new filter 1:', newFilters); - - dispatch(publishFilters(newFilters)); - }) + ) ); }; @@ -369,7 +360,6 @@ export class OptionsListEmbeddable extends Embeddable { dispatch(setLoading(false)); dispatch(publishFilters(newFilters)); @@ -389,8 +379,8 @@ export class OptionsListEmbeddable extends Embeddable { const { getState } = this.reduxEmbeddableTools; const { validSelections } = getState().componentState ?? {}; - const { exclude } = this.getInput(); const { existsSelected } = getState().explicitInput ?? {}; + const { exclude } = this.getInput(); if ((!validSelections || isEmpty(validSelections)) && !existsSelected) { return []; @@ -408,10 +398,10 @@ export class OptionsListEmbeddable extends Embeddable, action: PayloadAction) => { state.explicitInput.existsSelected = action.payload; - if (state.explicitInput.existsSelected) { - state.componentState.ignoredSelections = state.componentState.validSelections; - state.componentState.validSelections = []; - } else { - state.componentState.validSelections = state.componentState.ignoredSelections; - state.componentState.ignoredSelections = []; - } - }, - setIgnoredSelections: ( - state: WritableDraft, - action: PayloadAction - ) => { - state.componentState.ignoredSelections = action.payload; + state.explicitInput.selectedOptions = []; }, selectOption: (state: WritableDraft, action: PayloadAction) => { if (!state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; + if (state.explicitInput.existsSelected) state.explicitInput.existsSelected = false; state.explicitInput.selectedOptions?.push(action.payload); }, replaceSelection: ( diff --git a/src/plugins/controls/public/options_list/types.ts b/src/plugins/controls/public/options_list/types.ts index a868448b1af11..4001299a9ab53 100644 --- a/src/plugins/controls/public/options_list/types.ts +++ b/src/plugins/controls/public/options_list/types.ts @@ -22,7 +22,6 @@ export interface OptionsListComponentState { availableOptions?: string[]; invalidSelections?: string[]; validSelections?: string[]; - ignoredSelections?: string[]; searchString: SearchString; } From 405434d91d0da22d8aac2b32a0c83145c42d90d6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 11:31:57 -0600 Subject: [PATCH 05/33] Add support for negate to exists --- .../components/options_list_control.tsx | 16 ++++++++++------ .../components/options_list_popover.tsx | 6 +++++- .../components/options_list_strings.ts | 10 ++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) 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 f1c6b2987ef11..0e6780120f662 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 @@ -83,15 +83,19 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub validSelectionsCount: validSelections?.length, selectionDisplayNode: ( <> - {exclude && ( - - {OptionsListStrings.control.getNegate()}{' '} - - )} {existsSelected ? ( - {'Exists (*)'} + + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + ) : ( <> + {exclude && ( + + {OptionsListStrings.control.getNegate()}{' '} + + )} {validSelections && ( {validSelections?.join(OptionsListStrings.control.getSeparator())} )} 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 1ddec816e9779..d90c4e2decbfb 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 @@ -191,7 +191,11 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop // console.log(existsSelected); }} > - {`Exists (*)`} + + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + {availableOptions?.map((availableOption, index) => ( + i18n.translate('controls.optionsList.controlAndPopover.exists', { + defaultMessage: 'Exists (*)', + }), + getNegateExists: () => + i18n.translate('controls.optionsList.controlAndPopover.negateExists', { + defaultMessage: 'Does not exist (!)', + }), + }, }; From 70a0e6f01dafc7bd26d8d4bfeb58541a403cd382 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 11:37:38 -0600 Subject: [PATCH 06/33] Add to diffing system --- .../common/control_group/control_group_panel_diff_system.ts | 3 +++ 1 file changed, 3 insertions(+) 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 0ef2438494d7e..4d841d39b1224 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 @@ -33,6 +33,7 @@ export const ControlPanelDiffSystems: { const { exclude: excludeA, + existsSelected: existsSelectedA, selectedOptions: selectedA, singleSelect: singleSelectA, hideExclude: hideExcludeA, @@ -41,6 +42,7 @@ export const ControlPanelDiffSystems: { }: Partial = initialInput.explicitInput; const { exclude: excludeB, + existsSelected: existsSelectedB, selectedOptions: selectedB, singleSelect: singleSelectB, hideExclude: hideExcludeB, @@ -50,6 +52,7 @@ export const ControlPanelDiffSystems: { return ( Boolean(excludeA) === Boolean(excludeB) && + Boolean(existsSelectedA) === Boolean(existsSelectedB) && Boolean(singleSelectA) === Boolean(singleSelectB) && Boolean(hideExcludeA) === Boolean(hideExcludeB) && Boolean(runPastTimeoutA) === Boolean(runPastTimeoutB) && From 721670d1fb9714604ea68d3661694d41409b738f Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 13:46:36 -0600 Subject: [PATCH 07/33] Add toggle to disable `exists` query --- packages/kbn-doc-links/src/get_doc_links.ts | 1 + packages/kbn-doc-links/src/types.ts | 1 + .../control_group_panel_diff_system.ts | 3 ++ .../controls/common/options_list/types.ts | 1 + .../options_list_editor_options.tsx | 38 ++++++++++++++++++- .../components/options_list_popover.tsx | 33 ++++++++-------- .../components/options_list_strings.ts | 4 ++ .../documentation_links_service.ts | 32 ++++++++++++++++ .../services/documentation_links/types.ts | 13 +++++++ .../public/services/plugin_services.ts | 12 +++--- src/plugins/controls/public/services/types.ts | 2 + 11 files changed, 119 insertions(+), 21 deletions(-) create mode 100644 src/plugins/controls/public/services/documentation_links/documentation_links_service.ts create mode 100644 src/plugins/controls/public/services/documentation_links/types.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index 796e46ecbf6e5..a19111dcfca90 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -384,6 +384,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, + exists: `${ELASTICSEARCH_DOCS}query-dsl-exists-query.html`, kueryQuerySyntax: `${KIBANA_DOCS}kuery-query.html`, luceneQuery: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html`, luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index 7affe129b8173..daecde18db75b 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -286,6 +286,7 @@ export interface DocLinks { }; readonly query: { readonly eql: string; + readonly exists: string; readonly kueryQuerySyntax: string; readonly luceneQuery: string; readonly luceneQuerySyntax: string; 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 4d841d39b1224..2009c244a6ec0 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 @@ -37,6 +37,7 @@ export const ControlPanelDiffSystems: { selectedOptions: selectedA, singleSelect: singleSelectA, hideExclude: hideExcludeA, + hideExists: hideExistsA, runPastTimeout: runPastTimeoutA, ...inputA }: Partial = initialInput.explicitInput; @@ -46,6 +47,7 @@ export const ControlPanelDiffSystems: { selectedOptions: selectedB, singleSelect: singleSelectB, hideExclude: hideExcludeB, + hideExists: hideExistsB, runPastTimeout: runPastTimeoutB, ...inputB }: Partial = newInput.explicitInput; @@ -55,6 +57,7 @@ export const ControlPanelDiffSystems: { Boolean(existsSelectedA) === Boolean(existsSelectedB) && Boolean(singleSelectA) === Boolean(singleSelectB) && Boolean(hideExcludeA) === Boolean(hideExcludeB) && + Boolean(hideExistsA) === Boolean(hideExistsB) && Boolean(runPastTimeoutA) === Boolean(runPastTimeoutB) && isEqual(selectedA ?? [], selectedB ?? []) && deepEqual(inputA, inputB) diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index 2b820fee14a5e..8c156efc99be3 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -20,6 +20,7 @@ export interface OptionsListEmbeddableInput extends DataControlInput { singleSelect?: boolean; hideExclude?: boolean; exclude?: boolean; + hideExists?: boolean; } export type OptionsListField = FieldSpec & { 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 0723fcaad6ba6..a47c86d34eb2a 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 @@ -8,16 +8,25 @@ import React, { useState } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip, EuiSwitch } from '@elastic/eui'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIconTip, + EuiSwitch, + EuiButtonIcon, +} from '@elastic/eui'; import { css } from '@emotion/react'; import { OptionsListStrings } from './options_list_strings'; import { ControlEditorProps, OptionsListEmbeddableInput } from '../..'; +import { pluginServices } from '../../services'; interface OptionsListEditorState { singleSelect?: boolean; runPastTimeout?: boolean; hideExclude?: boolean; + hideExists?: boolean; } export const OptionsListEditorOptions = ({ @@ -28,8 +37,13 @@ export const OptionsListEditorOptions = ({ singleSelect: initialInput?.singleSelect, runPastTimeout: initialInput?.runPastTimeout, hideExclude: initialInput?.hideExclude, + hideExists: initialInput?.hideExists, }); + const { + documentationLinks: { existsQueryDocLink }, + } = pluginServices.getServices(); + return ( <> @@ -53,6 +67,28 @@ export const OptionsListEditorOptions = ({ }} /> + + + + { + onChange({ hideExists: !state.hideExists }); + setState((s) => ({ ...s, hideExists: !s.hideExists })); + }} + /> + + + + + + 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 d90c4e2decbfb..d5ad92e630c1f 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 @@ -77,6 +77,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const hideExclude = select((state) => state.explicitInput.hideExclude); const existsSelected = select((state) => state.explicitInput.existsSelected); const singleSelect = select((state) => state.explicitInput.singleSelect); + const hideExists = select((state) => state.explicitInput.hideExists); const title = select((state) => state.explicitInput.title); const exclude = select((state) => state.explicitInput.exclude); @@ -182,21 +183,23 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop > {!showOnlySelected && ( <> - { - dispatch(selectExists(!Boolean(existsSelected))); - // console.log(existsSelected); - }} - > - - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} - - + {!hideExists && ( + { + dispatch(selectExists(!Boolean(existsSelected))); + // console.log(existsSelected); + }} + > + + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + + + )} {availableOptions?.map((availableOption, index) => ( + i18n.translate('controls.optionsList.editor.hideExistsQuery', { + defaultMessage: 'Allow exists query', + }), }, popover: { getLoadingMessage: () => diff --git a/src/plugins/controls/public/services/documentation_links/documentation_links_service.ts b/src/plugins/controls/public/services/documentation_links/documentation_links_service.ts new file mode 100644 index 0000000000000..4adbe95a44b3d --- /dev/null +++ b/src/plugins/controls/public/services/documentation_links/documentation_links_service.ts @@ -0,0 +1,32 @@ +/* + * 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 type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { ControlsPluginStartDeps } from '../../types'; +import { ControlsDocumentationLinksService } from './types'; + +export type DocumentationLinksServiceFactory = KibanaPluginServiceFactory< + ControlsDocumentationLinksService, + ControlsPluginStartDeps +>; + +export const documentationLinksServiceFactory: DocumentationLinksServiceFactory = ({ + coreStart, +}) => { + const { + docLinks: { + links: { + query: { exists }, + }, + }, + } = coreStart; + + return { + existsQueryDocLink: exists, + }; +}; diff --git a/src/plugins/controls/public/services/documentation_links/types.ts b/src/plugins/controls/public/services/documentation_links/types.ts new file mode 100644 index 0000000000000..7ce537895c7e6 --- /dev/null +++ b/src/plugins/controls/public/services/documentation_links/types.ts @@ -0,0 +1,13 @@ +/* + * 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 { CoreStart } from '@kbn/core/public'; + +export interface ControlsDocumentationLinksService { + existsQueryDocLink: CoreStart['docLinks']['links']['query']['exists']; +} diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts index 20950d42df516..6185549619d48 100644 --- a/src/plugins/controls/public/services/plugin_services.ts +++ b/src/plugins/controls/public/services/plugin_services.ts @@ -16,16 +16,17 @@ import { import { ControlsPluginStartDeps } from '../types'; import { ControlsServices } from './types'; -import { dataViewsServiceFactory } from './data_views/data_views_service'; import { controlsServiceFactory } from './controls/controls_service'; -import { overlaysServiceFactory } from './overlays/overlays_service'; import { dataServiceFactory } from './data/data_service'; +import { dataViewsServiceFactory } from './data_views/data_views_service'; +import { embeddableServiceFactory } from './embeddable/embeddable_service'; +import { documentationLinksServiceFactory } from './documentation_links/documentation_links_service'; import { httpServiceFactory } from './http/http_service'; import { optionsListServiceFactory } from './options_list/options_list_service'; +import { overlaysServiceFactory } from './overlays/overlays_service'; import { settingsServiceFactory } from './settings/settings_service'; -import { unifiedSearchServiceFactory } from './unified_search/unified_search_service'; import { themeServiceFactory } from './theme/theme_service'; -import { embeddableServiceFactory } from './embeddable/embeddable_service'; +import { unifiedSearchServiceFactory } from './unified_search/unified_search_service'; export const providers: PluginServiceProviders< ControlsServices, @@ -34,12 +35,13 @@ export const providers: PluginServiceProviders< controls: new PluginServiceProvider(controlsServiceFactory), data: new PluginServiceProvider(dataServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), + documentationLinks: new PluginServiceProvider(documentationLinksServiceFactory), + embeddable: new PluginServiceProvider(embeddableServiceFactory), http: new PluginServiceProvider(httpServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory, ['data', 'http']), overlays: new PluginServiceProvider(overlaysServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), theme: new PluginServiceProvider(themeServiceFactory), - embeddable: new PluginServiceProvider(embeddableServiceFactory), unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), }; diff --git a/src/plugins/controls/public/services/types.ts b/src/plugins/controls/public/services/types.ts index f0785f9991bed..73c20b317484a 100644 --- a/src/plugins/controls/public/services/types.ts +++ b/src/plugins/controls/public/services/types.ts @@ -9,6 +9,7 @@ import { ControlsDataViewsService } from './data_views/types'; import { ControlsOverlaysService } from './overlays/types'; import { ControlsDataService } from './data/types'; +import { ControlsDocumentationLinksService } from './documentation_links/types'; import { ControlsUnifiedSearchService } from './unified_search/types'; import { ControlsServiceType } from './controls/types'; import { ControlsHTTPService } from './http/types'; @@ -23,6 +24,7 @@ export interface ControlsServices { overlays: ControlsOverlaysService; embeddable: ControlsEmbeddableService; data: ControlsDataService; + documentationLinks: ControlsDocumentationLinksService; unifiedSearch: ControlsUnifiedSearchService; http: ControlsHTTPService; settings: ControlsSettingsService; From e11b760d342bda5b9a3e12e2d5a76dc9d6bb595f Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 14:32:10 -0600 Subject: [PATCH 08/33] Clear `exists` selection when toggle is disabled + fix mocks --- .../options_list_editor_options.tsx | 1 + .../documentation_links_service.stub.ts | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts 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 a47c86d34eb2a..5719e2c3eafba 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 @@ -76,6 +76,7 @@ export const OptionsListEditorOptions = ({ onChange={() => { onChange({ hideExists: !state.hideExists }); setState((s) => ({ ...s, hideExists: !s.hideExists })); + if (initialInput?.existsSelected) onChange({ existsSelected: false }); }} /> diff --git a/src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts b/src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts new file mode 100644 index 0000000000000..22128159faa00 --- /dev/null +++ b/src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts @@ -0,0 +1,19 @@ +/* + * 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 { coreMock } from '@kbn/core/public/mocks'; +import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; +import { ControlsDocumentationLinksService } from './types'; + +type DocumentationLinksServiceFactory = PluginServiceFactory; + +export const DocumentationLinksServiceFactory: DocumentationLinksServiceFactory = () => { + const corePluginMock = coreMock.createStart(); + + return { existsQueryDocLink: corePluginMock.docLinks.links.query.exists }; +}; From 92dab10eaa277727467dc75b52e28ca734a501d8 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 14:51:08 -0600 Subject: [PATCH 09/33] Switch to tooltip instead of docs link --- packages/kbn-doc-links/src/get_doc_links.ts | 1 - packages/kbn-doc-links/src/types.ts | 1 - .../options_list_editor_options.tsx | 102 +++++++++--------- .../components/options_list_strings.ts | 5 + .../documentation_links_service.stub.ts | 19 ---- .../documentation_links_service.ts | 32 ------ .../services/documentation_links/types.ts | 13 --- .../public/services/plugin_services.ts | 10 +- src/plugins/controls/public/services/types.ts | 2 - 9 files changed, 61 insertions(+), 124 deletions(-) delete mode 100644 src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts delete mode 100644 src/plugins/controls/public/services/documentation_links/documentation_links_service.ts delete mode 100644 src/plugins/controls/public/services/documentation_links/types.ts diff --git a/packages/kbn-doc-links/src/get_doc_links.ts b/packages/kbn-doc-links/src/get_doc_links.ts index a19111dcfca90..796e46ecbf6e5 100644 --- a/packages/kbn-doc-links/src/get_doc_links.ts +++ b/packages/kbn-doc-links/src/get_doc_links.ts @@ -384,7 +384,6 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => { }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, - exists: `${ELASTICSEARCH_DOCS}query-dsl-exists-query.html`, kueryQuerySyntax: `${KIBANA_DOCS}kuery-query.html`, luceneQuery: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html`, luceneQuerySyntax: `${ELASTICSEARCH_DOCS}query-dsl-query-string-query.html#query-string-syntax`, diff --git a/packages/kbn-doc-links/src/types.ts b/packages/kbn-doc-links/src/types.ts index daecde18db75b..7affe129b8173 100644 --- a/packages/kbn-doc-links/src/types.ts +++ b/packages/kbn-doc-links/src/types.ts @@ -286,7 +286,6 @@ export interface DocLinks { }; readonly query: { readonly eql: string; - readonly exists: string; readonly kueryQuerySyntax: string; readonly luceneQuery: string; readonly luceneQuerySyntax: string; 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 5719e2c3eafba..d19c907a09f4b 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 @@ -14,14 +14,12 @@ import { EuiFormRow, EuiIconTip, EuiSwitch, - EuiButtonIcon, + EuiSwitchEvent, } from '@elastic/eui'; import { css } from '@emotion/react'; import { OptionsListStrings } from './options_list_strings'; import { ControlEditorProps, OptionsListEmbeddableInput } from '../..'; -import { pluginServices } from '../../services'; - interface OptionsListEditorState { singleSelect?: boolean; runPastTimeout?: boolean; @@ -29,6 +27,11 @@ interface OptionsListEditorState { hideExists?: boolean; } +interface SwitchProps { + checked: boolean; + onChange: (event: EuiSwitchEvent) => void; +} + export const OptionsListEditorOptions = ({ initialInput, onChange, @@ -40,9 +43,29 @@ export const OptionsListEditorOptions = ({ hideExists: initialInput?.hideExists, }); - const { - documentationLinks: { existsQueryDocLink }, - } = pluginServices.getServices(); + const SwitchWithTooltip = ({ + switchProps, + label, + tooltip, + }: { + switchProps: SwitchProps; + label: string; + tooltip: string; + }) => ( + + + + + + + + + ); return ( <> @@ -68,52 +91,31 @@ export const OptionsListEditorOptions = ({ /> - - - { - onChange({ hideExists: !state.hideExists }); - setState((s) => ({ ...s, hideExists: !s.hideExists })); - if (initialInput?.existsSelected) onChange({ existsSelected: false }); - }} - /> - - - - - + { + onChange({ hideExists: !state.hideExists }); + setState((s) => ({ ...s, hideExists: !s.hideExists })); + if (initialInput?.existsSelected) onChange({ existsSelected: false }); + }, + }} + /> - - - { - onChange({ runPastTimeout: !state.runPastTimeout }); - setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout })); - }} - /> - - - - - + { + onChange({ runPastTimeout: !state.runPastTimeout }); + setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout })); + }, + }} + /> ); 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 21fb5aa59049c..6ccbd4dceb29f 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 @@ -45,6 +45,11 @@ export const OptionsListStrings = { i18n.translate('controls.optionsList.editor.hideExistsQuery', { defaultMessage: 'Allow exists query', }), + getHideExistsQueryTooltip: () => + i18n.translate('controls.optionsList.editor.hideExistsQueryTooltip', { + defaultMessage: + 'The exists query will only return documents that contain an indexed value for the given field.', + }), }, popover: { getLoadingMessage: () => diff --git a/src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts b/src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts deleted file mode 100644 index 22128159faa00..0000000000000 --- a/src/plugins/controls/public/services/documentation_links/documentation_links_service.stub.ts +++ /dev/null @@ -1,19 +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 { coreMock } from '@kbn/core/public/mocks'; -import { PluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsDocumentationLinksService } from './types'; - -type DocumentationLinksServiceFactory = PluginServiceFactory; - -export const DocumentationLinksServiceFactory: DocumentationLinksServiceFactory = () => { - const corePluginMock = coreMock.createStart(); - - return { existsQueryDocLink: corePluginMock.docLinks.links.query.exists }; -}; diff --git a/src/plugins/controls/public/services/documentation_links/documentation_links_service.ts b/src/plugins/controls/public/services/documentation_links/documentation_links_service.ts deleted file mode 100644 index 4adbe95a44b3d..0000000000000 --- a/src/plugins/controls/public/services/documentation_links/documentation_links_service.ts +++ /dev/null @@ -1,32 +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 type { KibanaPluginServiceFactory } from '@kbn/presentation-util-plugin/public'; -import { ControlsPluginStartDeps } from '../../types'; -import { ControlsDocumentationLinksService } from './types'; - -export type DocumentationLinksServiceFactory = KibanaPluginServiceFactory< - ControlsDocumentationLinksService, - ControlsPluginStartDeps ->; - -export const documentationLinksServiceFactory: DocumentationLinksServiceFactory = ({ - coreStart, -}) => { - const { - docLinks: { - links: { - query: { exists }, - }, - }, - } = coreStart; - - return { - existsQueryDocLink: exists, - }; -}; diff --git a/src/plugins/controls/public/services/documentation_links/types.ts b/src/plugins/controls/public/services/documentation_links/types.ts deleted file mode 100644 index 7ce537895c7e6..0000000000000 --- a/src/plugins/controls/public/services/documentation_links/types.ts +++ /dev/null @@ -1,13 +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 { CoreStart } from '@kbn/core/public'; - -export interface ControlsDocumentationLinksService { - existsQueryDocLink: CoreStart['docLinks']['links']['query']['exists']; -} diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts index 6185549619d48..805382254130a 100644 --- a/src/plugins/controls/public/services/plugin_services.ts +++ b/src/plugins/controls/public/services/plugin_services.ts @@ -16,17 +16,16 @@ import { import { ControlsPluginStartDeps } from '../types'; import { ControlsServices } from './types'; +import { dataViewsServiceFactory } from './data_views/data_views_service'; import { controlsServiceFactory } from './controls/controls_service'; +import { overlaysServiceFactory } from './overlays/overlays_service'; import { dataServiceFactory } from './data/data_service'; -import { dataViewsServiceFactory } from './data_views/data_views_service'; -import { embeddableServiceFactory } from './embeddable/embeddable_service'; -import { documentationLinksServiceFactory } from './documentation_links/documentation_links_service'; import { httpServiceFactory } from './http/http_service'; import { optionsListServiceFactory } from './options_list/options_list_service'; -import { overlaysServiceFactory } from './overlays/overlays_service'; import { settingsServiceFactory } from './settings/settings_service'; -import { themeServiceFactory } from './theme/theme_service'; import { unifiedSearchServiceFactory } from './unified_search/unified_search_service'; +import { themeServiceFactory } from './theme/theme_service'; +import { embeddableServiceFactory } from './embeddable/embeddable_service'; export const providers: PluginServiceProviders< ControlsServices, @@ -35,7 +34,6 @@ export const providers: PluginServiceProviders< controls: new PluginServiceProvider(controlsServiceFactory), data: new PluginServiceProvider(dataServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), - documentationLinks: new PluginServiceProvider(documentationLinksServiceFactory), embeddable: new PluginServiceProvider(embeddableServiceFactory), http: new PluginServiceProvider(httpServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory, ['data', 'http']), diff --git a/src/plugins/controls/public/services/types.ts b/src/plugins/controls/public/services/types.ts index 73c20b317484a..f0785f9991bed 100644 --- a/src/plugins/controls/public/services/types.ts +++ b/src/plugins/controls/public/services/types.ts @@ -9,7 +9,6 @@ import { ControlsDataViewsService } from './data_views/types'; import { ControlsOverlaysService } from './overlays/types'; import { ControlsDataService } from './data/types'; -import { ControlsDocumentationLinksService } from './documentation_links/types'; import { ControlsUnifiedSearchService } from './unified_search/types'; import { ControlsServiceType } from './controls/types'; import { ControlsHTTPService } from './http/types'; @@ -24,7 +23,6 @@ export interface ControlsServices { overlays: ControlsOverlaysService; embeddable: ControlsEmbeddableService; data: ControlsDataService; - documentationLinks: ControlsDocumentationLinksService; unifiedSearch: ControlsUnifiedSearchService; http: ControlsHTTPService; settings: ControlsSettingsService; From 865b066b705eaafc7ed877ddcfbf9ae46e8c5d23 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 16:28:55 -0600 Subject: [PATCH 10/33] Clean up popover logic --- .../components/options_list_popover.tsx | 416 +++++++++--------- 1 file changed, 208 insertions(+), 208 deletions(-) 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 d5ad92e630c1f..f94212b6803ee 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 @@ -93,225 +93,225 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const [showOnlySelected, setShowOnlySelected] = useState(false); const euiBackgroundColor = useEuiBackgroundColor('subdued'); + const TopActionBar = () => ( +
+ + + + updateSearchString(event.target.value)} + value={searchString.value} + data-test-subj="optionsList-control-search-input" + placeholder={ + totalCardinality + ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) + : undefined + } + /> + + + {(invalidSelections?.length ?? 0) > 0 && ( + + + {invalidSelections?.length} + + + )} + + + + dispatch(clearSelections({}))} + /> + + + + + setShowOnlySelected(!showOnlySelected)} + /> + + + + +
+ ); + + const BottomActionBar = () => ( + + dispatch(setExclude(optionId === 'optionsList__excludeResults'))} + buttonSize="compressed" + data-test-subj="optionsList__includeExcludeButtonGroup" + /> + + ); + + const SuggestionList = ({ + showOnlySelectedSuggestions, + }: { + showOnlySelectedSuggestions: boolean; + }) => { + const suggestions = showOnlySelected ? selectedOptions : availableOptions; + + if (!loading && (!suggestions || suggestions.length === 0) && !existsSelected) { + return ( +
+
+ + +

+ {showOnlySelectedSuggestions + ? OptionsListStrings.popover.getSelectionsEmptyMessage() + : OptionsListStrings.popover.getEmptyMessage()} +

+
+
+ ); + } + + return ( + <> + {!hideExists && !(showOnlySelectedSuggestions && !existsSelected) && ( + { + dispatch(selectExists(!Boolean(existsSelected))); + }} + > + + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + + + )} + {suggestions?.map((suggestion, index) => ( + { + if (showOnlySelected) { + dispatch(deselectOption(suggestion)); + return; + } + if (singleSelect) { + dispatch(replaceSelection(suggestion)); + return; + } + if (selectedOptionsSet.has(suggestion)) { + dispatch(deselectOption(suggestion)); + return; + } + dispatch(selectOption(suggestion)); + }} + className={ + showOnlySelectedSuggestions && invalidSelectionsSet.has(suggestion) + ? 'optionsList__selectionInvalid' + : undefined + } + > + {`${suggestion}`} + + ))} + + ); + }; + + const InvalidSelections = () => ( + <> + + + + + <> + {invalidSelections?.map((ignoredSelection, index) => ( + dispatch(deselectOption(ignoredSelection))} + > + {`${ignoredSelection}`} + + ))} + + + ); + return ( <> {title} - {field?.type !== 'boolean' && ( -
- - - - updateSearchString(event.target.value)} - value={searchString.value} - data-test-subj="optionsList-control-search-input" - placeholder={ - totalCardinality - ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) - : undefined - } - /> - - - {(invalidSelections?.length ?? 0) > 0 && ( - - - {invalidSelections?.length} - - - )} - - - - dispatch(clearSelections({}))} - /> - - - - - setShowOnlySelected(!showOnlySelected)} - /> - - - - -
- )} + {field?.type !== 'boolean' && }
300 ? width : undefined }} className="optionsList__items" data-option-count={availableOptions?.length ?? 0} data-test-subj={`optionsList-control-available-options`} > - {!showOnlySelected && ( - <> - {!hideExists && ( - { - dispatch(selectExists(!Boolean(existsSelected))); - // console.log(existsSelected); - }} - > - - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} - - - )} - {availableOptions?.map((availableOption, index) => ( - { - if (singleSelect) { - dispatch(replaceSelection(availableOption)); - return; - } - if (selectedOptionsSet.has(availableOption)) { - dispatch(deselectOption(availableOption)); - return; - } - dispatch(selectOption(availableOption)); - }} - > - {`${availableOption}`} - - ))} - - {!loading && (!availableOptions || availableOptions.length === 0) && ( -
-
- - -

{OptionsListStrings.popover.getEmptyMessage()}

-
-
- )} - - {invalidSelections && !isEmpty(invalidSelections) && ( - <> - - - - - <> - {invalidSelections?.map((ignoredSelection, index) => ( - dispatch(deselectOption(ignoredSelection))} - > - {`${ignoredSelection}`} - - ))} - - - )} - - )} - {showOnlySelected && ( - <> - {selectedOptions && - selectedOptions.map((availableOption, index) => ( - dispatch(deselectOption(availableOption))} - className={ - invalidSelectionsSet.has(availableOption) - ? 'optionsList__selectionInvalid' - : undefined - } - > - {`${availableOption}`} - - ))} - {(!selectedOptions || selectedOptions.length === 0) && ( -
-
- - -

{OptionsListStrings.popover.getSelectionsEmptyMessage()}

-
-
- )} - + + {!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && ( + )}
- {!hideExclude && ( - - - dispatch(setExclude(optionId === 'optionsList__excludeResults')) - } - buttonSize="compressed" - data-test-subj="optionsList__includeExcludeButtonGroup" - /> - - )} + {!hideExclude && } ); }; From a313b45064413a6be32376fdb19ac1f69aca6320 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Tue, 25 Oct 2022 16:48:08 -0600 Subject: [PATCH 11/33] Fix rendering through memoization --- .../components/options_list_popover.tsx | 290 ++++++++++-------- 1 file changed, 160 insertions(+), 130 deletions(-) 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 f94212b6803ee..c124895ea3981 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 @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { isEmpty } from 'lodash'; import { @@ -93,109 +93,119 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const [showOnlySelected, setShowOnlySelected] = useState(false); const euiBackgroundColor = useEuiBackgroundColor('subdued'); - const TopActionBar = () => ( -
- - - - updateSearchString(event.target.value)} - value={searchString.value} - data-test-subj="optionsList-control-search-input" - placeholder={ - totalCardinality - ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) - : undefined - } - /> - - - {(invalidSelections?.length ?? 0) > 0 && ( + const renderTopActionBar = useCallback( + () => ( +
+ + + + updateSearchString(event.target.value)} + value={searchString.value} + data-test-subj="optionsList-control-search-input" + placeholder={ + totalCardinality + ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) + : undefined + } + /> + + + {(invalidSelections?.length ?? 0) > 0 && ( + + + {invalidSelections?.length} + + + )} + + - - {invalidSelections?.length} - + dispatch(clearSelections({}))} + /> - )} - - - - dispatch(clearSelections({}))} - /> - - - - - setShowOnlySelected(!showOnlySelected)} - /> - - - - -
+
+ + + setShowOnlySelected(!showOnlySelected)} + /> + + +
+
+
+ ), + [ + clearSelections, + dispatch, + invalidSelections, + searchString, + showOnlySelected, + totalCardinality, + updateSearchString, + ] ); - const BottomActionBar = () => ( - - dispatch(setExclude(optionId === 'optionsList__excludeResults'))} - buttonSize="compressed" - data-test-subj="optionsList__includeExcludeButtonGroup" - /> - + const renderBottomActionBar = useCallback( + () => ( + + dispatch(setExclude(optionId === 'optionsList__excludeResults'))} + buttonSize="compressed" + data-test-subj="optionsList__includeExcludeButtonGroup" + /> + + ), + [dispatch, euiBackgroundColor, exclude, setExclude] ); - const SuggestionList = ({ - showOnlySelectedSuggestions, - }: { - showOnlySelectedSuggestions: boolean; - }) => { + const renderSuggestionList = useCallback(() => { const suggestions = showOnlySelected ? selectedOptions : availableOptions; if (!loading && (!suggestions || suggestions.length === 0) && !existsSelected) { @@ -203,14 +213,14 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop

- {showOnlySelectedSuggestions + {showOnlySelected ? OptionsListStrings.popover.getSelectionsEmptyMessage() : OptionsListStrings.popover.getEmptyMessage()}

@@ -221,7 +231,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop return ( <> - {!hideExists && !(showOnlySelectedSuggestions && !existsSelected) && ( + {!hideExists && !(showOnlySelected && !existsSelected) && ( ); - }; + }, [ + availableOptions, + deselectOption, + dispatch, + exclude, + existsSelected, + hideExists, + invalidSelectionsSet, + loading, + replaceSelection, + selectExists, + selectOption, + selectedOptions, + selectedOptionsSet, + showOnlySelected, + singleSelect, + ]); - const InvalidSelections = () => ( - <> - - - - + const renderInvalidSelections = useCallback( + () => ( <> - {invalidSelections?.map((ignoredSelection, index) => ( - dispatch(deselectOption(ignoredSelection))} - > - {`${ignoredSelection}`} - - ))} + + + + + <> + {invalidSelections?.map((ignoredSelection, index) => ( + dispatch(deselectOption(ignoredSelection))} + > + {`${ignoredSelection}`} + + ))} + - + ), + [deselectOption, dispatch, invalidSelections] ); return ( <> {title} - {field?.type !== 'boolean' && } + {field?.type !== 'boolean' && renderTopActionBar()}
300 ? width : undefined }} className="optionsList__items" data-option-count={availableOptions?.length ?? 0} data-test-subj={`optionsList-control-available-options`} > - - {!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && ( - - )} + {renderSuggestionList()} + {!showOnlySelected && + invalidSelections && + !isEmpty(invalidSelections) && + renderInvalidSelections()}
- {!hideExclude && } + {!hideExclude && renderBottomActionBar()} ); }; From 166c369cb80db2697bb9a7ffe90f8f057dc21ce9 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 26 Oct 2022 10:00:17 -0600 Subject: [PATCH 12/33] Auto focus on search when popover opens --- .../components/options_list_popover.tsx | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) 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 c124895ea3981..f1a8fecdd9d7e 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 @@ -55,12 +55,12 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop useEmbeddableDispatch, useEmbeddableSelector: select, actions: { - selectOption, - deselectOption, - clearSelections, replaceSelection, - setExclude, + clearSelections, + deselectOption, selectExists, + selectOption, + setExclude, }, } = useReduxEmbeddableContext(); @@ -74,12 +74,12 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const field = select((state) => state.componentState.field); const selectedOptions = select((state) => state.explicitInput.selectedOptions); - const hideExclude = select((state) => state.explicitInput.hideExclude); const existsSelected = select((state) => state.explicitInput.existsSelected); const singleSelect = select((state) => state.explicitInput.singleSelect); + const hideExclude = select((state) => state.explicitInput.hideExclude); const hideExists = select((state) => state.explicitInput.hideExists); - const title = select((state) => state.explicitInput.title); const exclude = select((state) => state.explicitInput.exclude); + const title = select((state) => state.explicitInput.title); const loading = select((state) => state.output.loading); @@ -118,6 +118,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) : undefined } + autoFocus={true} /> @@ -174,13 +175,13 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop
), [ - clearSelections, - dispatch, + updateSearchString, invalidSelections, - searchString, showOnlySelected, totalCardinality, - updateSearchString, + clearSelections, + searchString, + dispatch, ] ); @@ -279,21 +280,21 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop ); }, [ + invalidSelectionsSet, + selectedOptionsSet, availableOptions, + replaceSelection, + showOnlySelected, + selectedOptions, deselectOption, - dispatch, - exclude, existsSelected, - hideExists, - invalidSelectionsSet, - loading, - replaceSelection, selectExists, selectOption, - selectedOptions, - selectedOptionsSet, - showOnlySelected, singleSelect, + hideExists, + dispatch, + exclude, + loading, ]); const renderInvalidSelections = useCallback( From ed774656ffe94ab12ecca889ca9301ca2049dbb9 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 26 Oct 2022 11:12:37 -0600 Subject: [PATCH 13/33] Added Jest unit tests --- .../components/options_list_popover.test.tsx | 34 +++++++++++++++++++ .../components/options_list_popover.tsx | 2 +- 2 files changed, 35 insertions(+), 1 deletion(-) 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 eca6fe72376a1..d1199c884ef3a 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 @@ -115,4 +115,38 @@ describe('Options list popover', () => { expect(includeButton.prop('checked')).toBeFalsy(); expect(excludeButton.prop('checked')).toBe(true); }); + + test('if exclude = false and existsSelected = true, then the option should read "Exists"', () => { + const popover = mountComponent({ + explicitInput: { exclude: false, existsSelected: true }, + }); + const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); + expect(existsOption.text()).toBe('Exists (*)'); + }); + + test('if exclude = true and existsSelected = true, then the option should read "Does not exist"', () => { + const popover = mountComponent({ + explicitInput: { exclude: true, existsSelected: true }, + }); + const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); + expect(existsOption.text()).toBe('Does not exist (!)'); + }); + + test('if existsSelected = false and no suggestions, then "Exists" does not show up', () => { + const popover = mountComponent({ + componentState: { availableOptions: [] }, + explicitInput: { existsSelected: false }, + }); + const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); + expect(existsOption.exists()).toBeFalsy(); + }); + + test('if existsSelected = true, "Exists" is the only option when "Show only selected options" is toggled', () => { + const popover = mountComponent({ + explicitInput: { existsSelected: true }, + }); + clickShowOnlySelections(popover); + const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); + expect(availableOptionsDiv.children().at(0).text()).toBe('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 f1a8fecdd9d7e..8784c93fc4290 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 @@ -234,7 +234,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop <> {!hideExists && !(showOnlySelected && !existsSelected) && ( { From 4eefecb7e71fda6f309c5fec30dc13bf0ae07eff Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 26 Oct 2022 15:41:26 -0600 Subject: [PATCH 14/33] Beef up mock and add more Jest unit tests --- .../controls/common/options_list/mocks.tsx | 41 ++++----- .../components/options_list_popover.test.tsx | 87 +++++++++++++------ .../public/services/plugin_services.ts | 2 +- 3 files changed, 82 insertions(+), 48 deletions(-) diff --git a/src/plugins/controls/common/options_list/mocks.tsx b/src/plugins/controls/common/options_list/mocks.tsx index ccbe3a1132479..c6d15ec9fcdb6 100644 --- a/src/plugins/controls/common/options_list/mocks.tsx +++ b/src/plugins/controls/common/options_list/mocks.tsx @@ -6,10 +6,12 @@ * Side Public License, v 1. */ -import { ReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public/redux_embeddables/types'; -import { ControlOutput } from '../../public/types'; +import { createReduxEmbeddableTools } from '@kbn/presentation-util-plugin/public/redux_embeddables/create_redux_embeddable_tools'; + +import { OptionsListEmbeddable, OptionsListEmbeddableFactory } from '../../public'; import { OptionsListComponentState, OptionsListReduxState } from '../../public/options_list/types'; import { optionsListReducers } from '../../public/options_list/options_list_reducers'; +import { ControlFactory, ControlOutput } from '../../public/types'; import { OptionsListEmbeddableInput } from './types'; const mockOptionsListComponentState = { @@ -36,27 +38,26 @@ const mockOptionsListOutput = { loading: false, } as ControlOutput; -export const mockOptionsListContext = ( +export const mockOptionsListReduxEmbeddableTools = async ( partialState?: Partial -): ReduxEmbeddableContext => { - const mockReduxState = { - componentState: { +) => { + const optionsListFactoryStub = new OptionsListEmbeddableFactory(); + const optionsListControlFactory = optionsListFactoryStub as unknown as ControlFactory; + optionsListControlFactory.getDefaultInput = () => ({}); + const mockEmbeddable = (await optionsListControlFactory.create({ + ...mockOptionsListEmbeddableInput, + ...partialState?.explicitInput, + })) as OptionsListEmbeddable; + mockEmbeddable.getOutput = jest.fn().mockReturnValue(mockOptionsListOutput); + + const mockReduxEmbeddableTools = createReduxEmbeddableTools({ + embeddable: mockEmbeddable, + reducers: optionsListReducers, + initialComponentState: { ...mockOptionsListComponentState, ...partialState?.componentState, }, - explicitInput: { - ...mockOptionsListEmbeddableInput, - ...partialState?.explicitInput, - }, - output: { - ...mockOptionsListOutput, - ...partialState?.output, - }, - } as OptionsListReduxState; + }); - return { - actions: {}, - useEmbeddableDispatch: () => {}, - useEmbeddableSelector: (selector: any) => selector(mockReduxState), - } as unknown as ReduxEmbeddableContext; + return mockReduxEmbeddableTools; }; 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 d1199c884ef3a..ceb252cc708c3 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 @@ -11,12 +11,11 @@ import { ReactWrapper } from 'enzyme'; import { mountWithIntl } from '@kbn/test-jest-helpers'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { EmbeddableReduxContext } from '@kbn/presentation-util-plugin/public/redux_embeddables/use_redux_embeddable_context'; import { OptionsListPopover, OptionsListPopoverProps } from './options_list_popover'; import { OptionsListComponentState, OptionsListReduxState } from '../types'; import { ControlOutput, OptionsListEmbeddableInput } from '../..'; -import { mockOptionsListContext } from '../../../common/mocks'; +import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks'; describe('Options list popover', () => { const defaultProps = { @@ -31,18 +30,18 @@ describe('Options list popover', () => { popoverProps: Partial; } - function mountComponent(options?: Partial) { + async function mountComponent(options?: Partial) { const compProps = { ...defaultProps, ...(options?.popoverProps ?? {}) }; - const context = mockOptionsListContext({ + const mockReduxEmbeddableTools = await mockOptionsListReduxEmbeddableTools({ componentState: options?.componentState ?? {}, explicitInput: options?.explicitInput ?? {}, output: options?.output ?? {}, } as Partial); return mountWithIntl( - + - + ); } @@ -54,19 +53,19 @@ describe('Options list popover', () => { showOnlySelectedButton.simulate('click'); }; - test('available options list width responds to container size', () => { - let popover = mountComponent({ popoverProps: { width: 301 } }); + 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;'); // the div cannot be smaller than 301 pixels wide - popover = mountComponent({ popoverProps: { width: 300 } }); + popover = await mountComponent({ popoverProps: { width: 300 } }); availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); expect(availableOptionsDiv.getDOMNode().getAttribute('style')).toBe(null); }); - test('no available options', () => { - const popover = mountComponent({ componentState: { availableOptions: [] } }); + test('no available options', async () => { + const popover = await mountComponent({ componentState: { availableOptions: [] } }); const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); const noOptionsDiv = findTestSubject( availableOptionsDiv, @@ -75,8 +74,8 @@ describe('Options list popover', () => { expect(noOptionsDiv.exists()).toBeTruthy(); }); - test('display error message when the show only selected toggle is true but there are no selections', () => { - const popover = mountComponent(); + test('display error message when the show only selected toggle is true but there are no selections', async () => { + const popover = await mountComponent(); clickShowOnlySelections(popover); const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); const noSelectionsDiv = findTestSubject( @@ -86,9 +85,9 @@ describe('Options list popover', () => { expect(noSelectionsDiv.exists()).toBeTruthy(); }); - test('show only selected options', () => { + test('show only selected options', async () => { const selections = ['woof', 'bark']; - const popover = mountComponent({ + const popover = await mountComponent({ explicitInput: { selectedOptions: selections }, }); clickShowOnlySelections(popover); @@ -98,16 +97,16 @@ describe('Options list popover', () => { }); }); - test('should default to exclude = false', () => { - const popover = mountComponent(); + test('should default to exclude = false', async () => { + const popover = await mountComponent(); const includeButton = findTestSubject(popover, 'optionsList__includeResults'); const excludeButton = findTestSubject(popover, 'optionsList__excludeResults'); expect(includeButton.prop('checked')).toBe(true); expect(excludeButton.prop('checked')).toBeFalsy(); }); - test('if exclude = true, select appropriate button in button group', () => { - const popover = mountComponent({ + test('if exclude = true, select appropriate button in button group', async () => { + const popover = await mountComponent({ explicitInput: { exclude: true }, }); const includeButton = findTestSubject(popover, 'optionsList__includeResults'); @@ -116,24 +115,58 @@ describe('Options list popover', () => { expect(excludeButton.prop('checked')).toBe(true); }); - test('if exclude = false and existsSelected = true, then the option should read "Exists"', () => { - const popover = mountComponent({ + test('if exclude = false and existsSelected = true, then the option should read "Exists"', async () => { + const popover = await mountComponent({ explicitInput: { exclude: false, existsSelected: true }, }); const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); expect(existsOption.text()).toBe('Exists (*)'); }); - test('if exclude = true and existsSelected = true, then the option should read "Does not exist"', () => { - const popover = mountComponent({ + test('if exclude = true and existsSelected = true, then the option should read "Does not exist"', async () => { + const popover = await mountComponent({ explicitInput: { exclude: true, existsSelected: true }, }); const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); expect(existsOption.text()).toBe('Does not exist (!)'); }); - test('if existsSelected = false and no suggestions, then "Exists" does not show up', () => { - const popover = mountComponent({ + test('clicking another option unselects "Exists"', async () => { + const popover = await mountComponent({ + explicitInput: { existsSelected: true }, + }); + const woofOption = findTestSubject(popover, 'optionsList-control-selection-woof'); + woofOption.simulate('click'); + + const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); + availableOptionsDiv.children().forEach((child, i) => { + if (child.text() === 'woof') expect(child.prop('checked')).toBe('on'); + else expect(child.prop('checked')).toBeFalsy(); + }); + }); + + test('clicking "Exists" unselects all other selections', async () => { + const selections = ['woof', 'bark']; + const popover = await mountComponent({ + explicitInput: { existsSelected: false, selectedOptions: selections }, + }); + const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); + let availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); + availableOptionsDiv.children().forEach((child, i) => { + if (selections.includes(child.text())) expect(child.prop('checked')).toBe('on'); + else expect(child.prop('checked')).toBeFalsy(); + }); + + existsOption.simulate('click'); + availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); + availableOptionsDiv.children().forEach((child, i) => { + if (child.text() === 'Exists (*)') expect(child.prop('checked')).toBe('on'); + else expect(child.prop('checked')).toBeFalsy(); + }); + }); + + test('if existsSelected = false and no suggestions, then "Exists" does not show up', async () => { + const popover = await mountComponent({ componentState: { availableOptions: [] }, explicitInput: { existsSelected: false }, }); @@ -141,8 +174,8 @@ describe('Options list popover', () => { expect(existsOption.exists()).toBeFalsy(); }); - test('if existsSelected = true, "Exists" is the only option when "Show only selected options" is toggled', () => { - const popover = mountComponent({ + test('if existsSelected = true, "Exists" is the only option when "Show only selected options" is toggled', async () => { + const popover = await mountComponent({ explicitInput: { existsSelected: true }, }); clickShowOnlySelections(popover); diff --git a/src/plugins/controls/public/services/plugin_services.ts b/src/plugins/controls/public/services/plugin_services.ts index 805382254130a..20950d42df516 100644 --- a/src/plugins/controls/public/services/plugin_services.ts +++ b/src/plugins/controls/public/services/plugin_services.ts @@ -34,12 +34,12 @@ export const providers: PluginServiceProviders< controls: new PluginServiceProvider(controlsServiceFactory), data: new PluginServiceProvider(dataServiceFactory), dataViews: new PluginServiceProvider(dataViewsServiceFactory), - embeddable: new PluginServiceProvider(embeddableServiceFactory), http: new PluginServiceProvider(httpServiceFactory), optionsList: new PluginServiceProvider(optionsListServiceFactory, ['data', 'http']), overlays: new PluginServiceProvider(overlaysServiceFactory), settings: new PluginServiceProvider(settingsServiceFactory), theme: new PluginServiceProvider(themeServiceFactory), + embeddable: new PluginServiceProvider(embeddableServiceFactory), unifiedSearch: new PluginServiceProvider(unifiedSearchServiceFactory), }; From 72d595c52b44da04e623c4a11da53aa79136e715 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 27 Oct 2022 14:11:44 -0600 Subject: [PATCH 15/33] Add functional tests --- .../controls/options_list.ts | 108 ++++++++++++++---- .../services/visualizations/pie_chart.ts | 15 +++ 2 files changed, 98 insertions(+), 25 deletions(-) diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 091f893eec2cf..eedb904fed29f 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -21,10 +21,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const dashboardAddPanel = getService('dashboardAddPanel'); const dashboardPanelActions = getService('dashboardPanelActions'); - const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([ + + const { dashboardControls, timePicker, console, common, dashboard, header } = getPageObjects([ 'dashboardControls', 'timePicker', 'dashboard', + 'console', 'common', 'header', ]); @@ -34,6 +36,18 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Dashboard options list integration', () => { before(async () => { await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); + + /* start by adding some incomplete data so that we can test `exists` query */ + await common.navigateToApp('console'); + await console.collapseHelp(); + await console.clearTextArea(); + await console.enterRequest( + '\nPOST animals-dogs-2018-01-01/_doc/ \n{\n "@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Max", \n"sound": "bark' + ); + await console.clickPlay(); + await header.waitUntilLoadingHasFinished(); + + /* then, create our testing dashboard */ await common.navigateToApp('dashboard'); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); @@ -194,8 +208,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const ensureAvailableOptionsEql = async (expectation: string[], skipOpen?: boolean) => { if (!skipOpen) await dashboardControls.optionsListOpenPopover(controlId); await retry.try(async () => { - expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql( - expectation + expect((await dashboardControls.optionsListPopoverGetAvailableOptions()).sort()).to.eql( + expectation.sort() ); }); if (!skipOpen) await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); @@ -336,10 +350,75 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 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.optionsListPopoverClearSelections(); + 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(); + }); + + describe('test exists query', async () => { + before(async () => { + await dashboardControls.deleteAllControls(); + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, + dataViewTitle: 'animals-*', + fieldName: 'animal.keyword', + title: 'Animal', + }); + controlId = (await dashboardControls.getAllControlIds())[0]; + }); + + it('creating exists query has expected results', async () => { + expect((await pieChart.getPieChartValues())[2]).to.be(4); + 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())[2]).to.be(3); + }); + + 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 dashboardControls.deleteAllControls(); + + await dashboardControls.createControl({ + controlType: OPTIONS_LIST_CONTROL, + dataViewTitle: 'animals-*', + fieldName: 'sound.keyword', + title: 'Animal Sounds', + }); + controlId = (await dashboardControls.getAllControlIds())[0]; }); }); @@ -385,27 +464,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await pieChart.getPieSliceCount()).to.be(2); }); - it('excluding selections has expected results', async () => { - await dashboard.clickQuickSave(); - await dashboard.waitForRenderComplete(); - - await dashboardControls.optionsListOpenPopover(controlId); - await dashboardControls.optionsListPopoverSetIncludeSelections(false); - 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 dashboard.waitForRenderComplete(); - - expect(await pieChart.getPieSliceCount()).to.be(2); - await dashboard.clearUnsavedChanges(); - }); - it('Can mark multiple selections invalid with Filter', async () => { await filterBar.addFilter('sound.keyword', 'is', ['hiss']); await dashboard.waitForRenderComplete(); diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index 0669bb6e91e52..5caba713c5929 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -178,6 +178,21 @@ export class PieChartService extends FtrService { ); } + async getPieChartValues(isNewLibrary: boolean = true) { + if (isNewLibrary) { + const slices = + (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] + ?.partitions ?? []; + return slices.map((slice) => { + return slice.value; + }); + } + const chartTypes = await this.find.allByCssSelector('path.slice', this.defaultFindTimeout * 2); + return await Promise.all( + chartTypes.map(async (chart) => await chart.getAttribute('data-value')) + ); + } + async getPieSliceCount(isNewLibrary: boolean = true) { this.log.debug('PieChart.getPieSliceCount'); if (isNewLibrary) { From 4b875e349a07230fff72cc05cfeba7de6afbe928 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 27 Oct 2022 15:07:40 -0600 Subject: [PATCH 16/33] Split up popover in to smaller components --- .../components/options_list_popover.tsx | 329 ++---------------- .../options_list_popover_action_bar.tsx | 130 +++++++ .../options_list_popover_footer.tsx | 60 ++++ ...ptions_list_popover_invalid_selections.tsx | 55 +++ .../options_list_popover_suggestions.tsx | 123 +++++++ 5 files changed, 392 insertions(+), 305 deletions(-) create mode 100644 src/plugins/controls/public/options_list/components/options_list_popover_action_bar.tsx create mode 100644 src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx create mode 100644 src/plugins/controls/public/options_list/components/options_list_popover_invalid_selections.tsx create mode 100644 src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx 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 8784c93fc4290..2382fb1e019fd 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 @@ -6,343 +6,62 @@ * Side Public License, v 1. */ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { isEmpty } from 'lodash'; -import { - EuiFilterSelectItem, - EuiPopoverTitle, - EuiFieldSearch, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiToolTip, - EuiSpacer, - EuiBadge, - EuiIcon, - EuiTitle, - EuiPopoverFooter, - EuiButtonGroup, - useEuiBackgroundColor, -} from '@elastic/eui'; -import { css } from '@emotion/react'; +import { EuiPopoverTitle } from '@elastic/eui'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; -import { optionsListReducers } from '../options_list_reducers'; import { OptionsListReduxState } from '../types'; -import { OptionsListStrings } from './options_list_strings'; +import { optionsListReducers } from '../options_list_reducers'; +import { OptionsListPopoverFooter } from './options_list_popover_footer'; +import { OptionsListPopoverActionBar } from './options_list_popover_action_bar'; +import { OptionsListPopoverSuggestions } from './options_list_popover_suggestions'; +import { OptionsListPopoverInvalidSelections } from './options_list_popover_invalid_selections'; export interface OptionsListPopoverProps { width: number; updateSearchString: (newSearchString: string) => void; } -const aggregationToggleButtons = [ - { - id: 'optionsList__includeResults', - label: OptionsListStrings.popover.getIncludeLabel(), - }, - { - id: 'optionsList__excludeResults', - label: OptionsListStrings.popover.getExcludeLabel(), - }, -]; - export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPopoverProps) => { // Redux embeddable container Context - const { - useEmbeddableDispatch, - useEmbeddableSelector: select, - actions: { - replaceSelection, - clearSelections, - deselectOption, - selectExists, - selectOption, - setExclude, - }, - } = useReduxEmbeddableContext(); - - const dispatch = useEmbeddableDispatch(); + const { useEmbeddableSelector: select } = useReduxEmbeddableContext< + OptionsListReduxState, + typeof optionsListReducers + >(); // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); - const totalCardinality = select((state) => state.componentState.totalCardinality); const availableOptions = select((state) => state.componentState.availableOptions); - const searchString = select((state) => state.componentState.searchString); const field = select((state) => state.componentState.field); - - const selectedOptions = select((state) => state.explicitInput.selectedOptions); - const existsSelected = select((state) => state.explicitInput.existsSelected); - const singleSelect = select((state) => state.explicitInput.singleSelect); const hideExclude = select((state) => state.explicitInput.hideExclude); - const hideExists = select((state) => state.explicitInput.hideExists); - const exclude = select((state) => state.explicitInput.exclude); const title = select((state) => state.explicitInput.title); - 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 [showOnlySelected, setShowOnlySelected] = useState(false); - const euiBackgroundColor = useEuiBackgroundColor('subdued'); - - const renderTopActionBar = useCallback( - () => ( -
- - - - updateSearchString(event.target.value)} - value={searchString.value} - data-test-subj="optionsList-control-search-input" - placeholder={ - totalCardinality - ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) - : undefined - } - autoFocus={true} - /> - - - {(invalidSelections?.length ?? 0) > 0 && ( - - - {invalidSelections?.length} - - - )} - - - - dispatch(clearSelections({}))} - /> - - - - - setShowOnlySelected(!showOnlySelected)} - /> - - - - -
- ), - [ - updateSearchString, - invalidSelections, - showOnlySelected, - totalCardinality, - clearSelections, - searchString, - dispatch, - ] - ); - - const renderBottomActionBar = useCallback( - () => ( - - dispatch(setExclude(optionId === 'optionsList__excludeResults'))} - buttonSize="compressed" - data-test-subj="optionsList__includeExcludeButtonGroup" - /> - - ), - [dispatch, euiBackgroundColor, exclude, setExclude] - ); - - const renderSuggestionList = useCallback(() => { - const suggestions = showOnlySelected ? selectedOptions : availableOptions; - - if (!loading && (!suggestions || suggestions.length === 0) && !existsSelected) { - return ( -
-
- - -

- {showOnlySelected - ? OptionsListStrings.popover.getSelectionsEmptyMessage() - : OptionsListStrings.popover.getEmptyMessage()} -

-
-
- ); - } - - return ( - <> - {!hideExists && !(showOnlySelected && !existsSelected) && ( - { - dispatch(selectExists(!Boolean(existsSelected))); - }} - > - - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} - - - )} - {suggestions?.map((suggestion, index) => ( - { - if (showOnlySelected) { - dispatch(deselectOption(suggestion)); - return; - } - if (singleSelect) { - dispatch(replaceSelection(suggestion)); - return; - } - if (selectedOptionsSet.has(suggestion)) { - dispatch(deselectOption(suggestion)); - return; - } - dispatch(selectOption(suggestion)); - }} - className={ - showOnlySelected && invalidSelectionsSet.has(suggestion) - ? 'optionsList__selectionInvalid' - : undefined - } - > - {`${suggestion}`} - - ))} - - ); - }, [ - invalidSelectionsSet, - selectedOptionsSet, - availableOptions, - replaceSelection, - showOnlySelected, - selectedOptions, - deselectOption, - existsSelected, - selectExists, - selectOption, - singleSelect, - hideExists, - dispatch, - exclude, - loading, - ]); - - const renderInvalidSelections = useCallback( - () => ( - <> - - - - - <> - {invalidSelections?.map((ignoredSelection, index) => ( - dispatch(deselectOption(ignoredSelection))} - > - {`${ignoredSelection}`} - - ))} - - - ), - [deselectOption, dispatch, invalidSelections] - ); return ( <> {title} - {field?.type !== 'boolean' && renderTopActionBar()} + {field?.type !== 'boolean' && ( + + )}
300 ? width : undefined }} - className="optionsList__items" + className="optionsList __items" data-option-count={availableOptions?.length ?? 0} data-test-subj={`optionsList-control-available-options`} > - {renderSuggestionList()} - {!showOnlySelected && - invalidSelections && - !isEmpty(invalidSelections) && - renderInvalidSelections()} + + {!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && ( + + )}
- {!hideExclude && renderBottomActionBar()} + {!hideExclude && } ); }; 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 new file mode 100644 index 0000000000000..ad8e2eec26e43 --- /dev/null +++ b/src/plugins/controls/public/options_list/components/options_list_popover_action_bar.tsx @@ -0,0 +1,130 @@ +/* + * 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 { + EuiFieldSearch, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiToolTip, + EuiBadge, +} from '@elastic/eui'; +import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; + +import { OptionsListReduxState } from '../types'; +import { OptionsListStrings } from './options_list_strings'; +import { optionsListReducers } from '../options_list_reducers'; + +interface OptionsListPopoverProps { + showOnlySelected: boolean; + setShowOnlySelected: (value: boolean) => void; + updateSearchString: (newSearchString: string) => void; +} + +export const OptionsListPopoverActionBar = ({ + showOnlySelected, + setShowOnlySelected, + updateSearchString, +}: OptionsListPopoverProps) => { + // Redux embeddable container Context + const { + useEmbeddableDispatch, + useEmbeddableSelector: select, + actions: { clearSelections }, + } = useReduxEmbeddableContext(); + const dispatch = useEmbeddableDispatch(); + + // Select current state from Redux using multiple selectors to avoid rerenders. + const invalidSelections = select((state) => state.componentState.invalidSelections); + const totalCardinality = select((state) => state.componentState.totalCardinality); + const searchString = select((state) => state.componentState.searchString); + + return ( +
+ + + + updateSearchString(event.target.value)} + value={searchString.value} + data-test-subj="optionsList-control-search-input" + placeholder={ + totalCardinality + ? OptionsListStrings.popover.getTotalCardinalityPlaceholder(totalCardinality) + : undefined + } + autoFocus={true} + /> + + + {(invalidSelections?.length ?? 0) > 0 && ( + + + {invalidSelections?.length} + + + )} + + + + dispatch(clearSelections({}))} + /> + + + + + setShowOnlySelected(!showOnlySelected)} + /> + + + + +
+ ); +}; diff --git a/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx b/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx new file mode 100644 index 0000000000000..8a51a33a31ba0 --- /dev/null +++ b/src/plugins/controls/public/options_list/components/options_list_popover_footer.tsx @@ -0,0 +1,60 @@ +/* + * 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 { EuiPopoverFooter, EuiButtonGroup, useEuiBackgroundColor } from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; + +import { OptionsListReduxState } from '../types'; +import { OptionsListStrings } from './options_list_strings'; +import { optionsListReducers } from '../options_list_reducers'; + +const aggregationToggleButtons = [ + { + id: 'optionsList__includeResults', + label: OptionsListStrings.popover.getIncludeLabel(), + }, + { + id: 'optionsList__excludeResults', + label: OptionsListStrings.popover.getExcludeLabel(), + }, +]; + +export const OptionsListPopoverFooter = () => { + // Redux embeddable container Context + const { + useEmbeddableDispatch, + useEmbeddableSelector: select, + actions: { setExclude }, + } = useReduxEmbeddableContext(); + const dispatch = useEmbeddableDispatch(); + + // Select current state from Redux using multiple selectors to avoid rerenders. + const exclude = select((state) => state.explicitInput.exclude); + + return ( + <> + + dispatch(setExclude(optionId === 'optionsList__excludeResults'))} + buttonSize="compressed" + data-test-subj="optionsList__includeExcludeButtonGroup" + /> + + + ); +}; 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 new file mode 100644 index 0000000000000..283eabce53ebf --- /dev/null +++ b/src/plugins/controls/public/options_list/components/options_list_popover_invalid_selections.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiFilterSelectItem, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; + +import { OptionsListReduxState } from '../types'; +import { OptionsListStrings } from './options_list_strings'; +import { optionsListReducers } from '../options_list_reducers'; + +export const OptionsListPopoverInvalidSelections = () => { + // Redux embeddable container Context + const { + useEmbeddableDispatch, + useEmbeddableSelector: select, + actions: { deselectOption }, + } = useReduxEmbeddableContext(); + const dispatch = useEmbeddableDispatch(); + + // Select current state from Redux using multiple selectors to avoid rerenders. + const invalidSelections = select((state) => state.componentState.invalidSelections); + + return ( + <> + + + + + <> + {invalidSelections?.map((ignoredSelection, index) => ( + dispatch(deselectOption(ignoredSelection))} + > + {`${ignoredSelection}`} + + ))} + + + ); +}; 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 new file mode 100644 index 0000000000000..86be875160373 --- /dev/null +++ b/src/plugins/controls/public/options_list/components/options_list_popover_suggestions.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useMemo } from 'react'; + +import { EuiFilterSelectItem, EuiSpacer, EuiIcon } from '@elastic/eui'; +import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; + +import { OptionsListReduxState } from '../types'; +import { OptionsListStrings } from './options_list_strings'; +import { optionsListReducers } from '../options_list_reducers'; + +interface OptionsListPopoverSuggestionsProps { + showOnlySelected: boolean; +} + +export const OptionsListPopoverSuggestions = ({ + showOnlySelected, +}: OptionsListPopoverSuggestionsProps) => { + // Redux embeddable container Context + const { + useEmbeddableDispatch, + useEmbeddableSelector: select, + actions: { replaceSelection, deselectOption, selectExists, selectOption }, + } = useReduxEmbeddableContext(); + const dispatch = useEmbeddableDispatch(); + + // Select current state from Redux using multiple selectors to avoid rerenders. + const invalidSelections = select((state) => state.componentState.invalidSelections); + const availableOptions = select((state) => state.componentState.availableOptions); + + const selectedOptions = select((state) => state.explicitInput.selectedOptions); + const existsSelected = select((state) => state.explicitInput.existsSelected); + const singleSelect = select((state) => state.explicitInput.singleSelect); + const hideExists = select((state) => state.explicitInput.hideExists); + const exclude = select((state) => state.explicitInput.exclude); + + 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 : availableOptions; + + if (!loading && (!suggestions || suggestions.length === 0) && !existsSelected) { + return ( +
+
+ + +

+ {showOnlySelected + ? OptionsListStrings.popover.getSelectionsEmptyMessage() + : OptionsListStrings.popover.getEmptyMessage()} +

+
+
+ ); + } + + return ( + <> + {!hideExists && !(showOnlySelected && !existsSelected) && ( + { + dispatch(selectExists(!Boolean(existsSelected))); + }} + > + + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + + + )} + {suggestions?.map((suggestion, index) => ( + { + if (showOnlySelected) { + dispatch(deselectOption(suggestion)); + return; + } + if (singleSelect) { + dispatch(replaceSelection(suggestion)); + return; + } + if (selectedOptionsSet.has(suggestion)) { + dispatch(deselectOption(suggestion)); + return; + } + dispatch(selectOption(suggestion)); + }} + className={ + showOnlySelected && invalidSelectionsSet.has(suggestion) + ? 'optionsList__selectionInvalid' + : undefined + } + > + {`${suggestion}`} + + ))} + + ); +}; From afc0bcfeafe7d957e4d03e52eca67b6e407a6dc6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 28 Oct 2022 13:44:06 -0600 Subject: [PATCH 17/33] Fix unit tests + functional test flakiness --- .../components/options_list_popover.test.tsx | 9 ++++++--- .../apps/dashboard_elements/controls/options_list.ts | 10 +++++----- test/functional/services/visualizations/pie_chart.ts | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) 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 ceb252cc708c3..568bb1a46bd77 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 @@ -92,9 +92,12 @@ describe('Options list popover', () => { }); clickShowOnlySelections(popover); const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); - availableOptionsDiv.children().forEach((child, i) => { - expect(child.text()).toBe(selections[i]); - }); + availableOptionsDiv + .childAt(0) + .children() + .forEach((child, i) => { + expect(child.text()).toBe(selections[i]); + }); }); test('should default to exclude = false', async () => { diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index eedb904fed29f..01577dbedf05d 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -42,7 +42,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await console.collapseHelp(); await console.clearTextArea(); await console.enterRequest( - '\nPOST animals-dogs-2018-01-01/_doc/ \n{\n "@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Max", \n"sound": "bark' + '\nPOST animals-cats-2018-01-01/_doc/ \n{\n "@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss' ); await console.clickPlay(); await header.waitUntilLoadingHasFinished(); @@ -208,8 +208,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const ensureAvailableOptionsEql = async (expectation: string[], skipOpen?: boolean) => { if (!skipOpen) await dashboardControls.optionsListOpenPopover(controlId); await retry.try(async () => { - expect((await dashboardControls.optionsListPopoverGetAvailableOptions()).sort()).to.eql( - expectation.sort() + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql( + expectation ); }); if (!skipOpen) await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); @@ -388,14 +388,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('creating exists query has expected results', async () => { - expect((await pieChart.getPieChartValues())[2]).to.be(4); + 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())[2]).to.be(3); + expect((await pieChart.getPieChartValues())[0]).to.be(5); }); it('negating exists query has expected results', async () => { diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index 5caba713c5929..4067c2f1868c5 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -179,6 +179,7 @@ export class PieChartService extends FtrService { } async getPieChartValues(isNewLibrary: boolean = true) { + this.log.debug('PieChart.getPieChartValues'); if (isNewLibrary) { const slices = (await this.visChart.getEsChartDebugState(partitionVisChartSelector))?.partition?.[0] From 52961b439ebc1387c319a1ffe531a636a3765c03 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 28 Oct 2022 16:00:45 -0600 Subject: [PATCH 18/33] Fix flakiness a second time + add chaining tests --- .../controls/control_group_chaining.ts | 78 +++++++++++++++++-- .../controls/options_list.ts | 36 +++++++-- 2 files changed, 101 insertions(+), 13 deletions(-) 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 89a435430f9e9..8297d21403b85 100644 --- a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts +++ b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts @@ -14,14 +14,17 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const security = getService('security'); - const { dashboardControls, common, dashboard, timePicker } = getPageObjects([ + const { common, console, dashboard, dashboardControls, header, timePicker } = getPageObjects([ 'dashboardControls', 'timePicker', 'dashboard', + 'console', 'common', + 'header', ]); describe('Dashboard control group hierarchical chaining', () => { + const newDocuments: Array<{ index: string; id: string }> = []; let controlIds: string[]; const ensureAvailableOptionsEql = async (controlId: string, expectation: string[]) => { @@ -32,8 +35,31 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); }; + 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 security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); + + /* start by adding some incomplete data so that we can test `exists` query */ + 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"animal": "cat"' + ); + await addDocument( + 'animals-dogs-2018-01-01', + '"@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Max", \n"sound": "woof"' + ); + + /* then, create our testing dashboard */ await common.navigateToApp('dashboard'); await dashboard.gotoDashboardLandingPage(); await dashboard.clickNewDashboard(); @@ -65,6 +91,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await common.navigateToApp('console'); + await console.collapseHelp(); + await console.clearTextArea(); + for (const { index, id } of newDocuments) { + await console.enterRequest(`\nDELETE /${index}/_doc/${id}`); + await console.clickPlay(); + await header.waitUntilLoadingHasFinished(); + } await security.testUser.restoreDefaults(); }); @@ -128,7 +162,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListPopoverSetIncludeSelections(false); await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); - await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester']); + await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester', 'Max']); await ensureAvailableOptionsEql(controlIds[2], ['meow', 'hiss']); }); @@ -138,9 +172,42 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); await dashboardControls.optionsListOpenPopover(controlIds[1]); - expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0); + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1); + await dashboardControls.optionsListOpenPopover(controlIds[2]); + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]); + }); + + 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.optionsListEnsurePopoverIsClosed(controlIds[0]); + await dashboard.waitForRenderComplete(); + + await dashboardControls.optionsListOpenPopover(controlIds[1]); + await dashboardControls.optionsListPopoverClearSelections(); + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(['Max']); + + await dashboardControls.optionsListOpenPopover(controlIds[2]); + await dashboardControls.optionsListPopoverClearSelections(); + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(['woof']); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]); + }); + + it('Creating "exists" query from first control filters the second and third controls', async () => { + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await dashboardControls.optionsListPopoverSetIncludeSelections(true); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); + await dashboard.waitForRenderComplete(); + + await dashboardControls.optionsListOpenPopover(controlIds[1]); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.not.contain('Max'); await dashboardControls.optionsListOpenPopover(controlIds[2]); - expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.not.contain( + 'woof' + ); await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]); }); @@ -151,7 +218,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Selecting an option in the first Options List will not filter the second or third controls', async () => { await dashboardControls.optionsListOpenPopover(controlIds[0]); - await dashboardControls.optionsListPopoverSetIncludeSelections(true); await dashboardControls.optionsListPopoverSelectOption('cat'); await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); @@ -161,6 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'sylvester', 'Fee Fee', 'Rover', + 'Max', ]); await ensureAvailableOptionsEql(controlIds[2], [ 'hiss', @@ -171,6 +238,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'growl', 'grr', 'bow ow ow', + 'woof', ]); }); }); diff --git a/test/functional/apps/dashboard_elements/controls/options_list.ts b/test/functional/apps/dashboard_elements/controls/options_list.ts index 01577dbedf05d..6cfe2c31fa0c1 100644 --- a/test/functional/apps/dashboard_elements/controls/options_list.ts +++ b/test/functional/apps/dashboard_elements/controls/options_list.ts @@ -34,6 +34,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const DASHBOARD_NAME = 'Test Options List Control'; describe('Dashboard options list integration', () => { + 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 security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); @@ -41,11 +51,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await common.navigateToApp('console'); await console.collapseHelp(); await console.clearTextArea(); - await console.enterRequest( - '\nPOST animals-cats-2018-01-01/_doc/ \n{\n "@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss' + await addDocument( + 'animals-cats-2018-01-01', + '"@timestamp": "2018-01-01T16:00:00.000Z", \n"name": "Rosie", \n"sound": "hiss"' ); - await console.clickPlay(); - await header.waitUntilLoadingHasFinished(); /* then, create our testing dashboard */ await common.navigateToApp('dashboard'); @@ -229,7 +238,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Applies query settings to controls', async () => { it('Applies dashboard query to options list control', async () => { - await queryBar.setQuery('isDog : true '); + await queryBar.setQuery('animal.keyword : "dog" '); await queryBar.submitQuery(); await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); @@ -438,7 +447,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Can mark selections invalid with Query', async () => { - await queryBar.setQuery('isDog : false '); + await queryBar.setQuery('NOT animal.keyword : "dog" '); await queryBar.submitQuery(); await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); @@ -487,7 +496,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('Does not mark selections invalid with Query', async () => { - await queryBar.setQuery('isDog : false '); + await queryBar.setQuery('NOT animal.keyword : "dog" '); await queryBar.submitQuery(); await dashboard.waitForRenderComplete(); await header.waitUntilLoadingHasFinished(); @@ -506,8 +515,19 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await filterBar.removeAllFilters(); await queryBar.clickQuerySubmitButton(); await dashboardControls.clearAllControls(); - await security.testUser.restoreDefaults(); }); }); + + after(async () => { + await common.navigateToApp('console'); + await console.collapseHelp(); + await console.clearTextArea(); + for (const { index, id } of newDocuments) { + await console.enterRequest(`\nDELETE /${index}/_doc/${id}`); + await console.clickPlay(); + await header.waitUntilLoadingHasFinished(); + } + await security.testUser.restoreDefaults(); + }); }); } From 899a7f94b00da18f1cee266d61bc62aea4afabaf Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Mon, 31 Oct 2022 16:09:29 -0600 Subject: [PATCH 19/33] Clean up code --- .../control_group_panel_diff_system.ts | 18 +++++++++--------- .../controls/common/options_list/types.ts | 2 +- .../embeddable/options_list_embeddable.tsx | 2 -- 3 files changed, 10 insertions(+), 12 deletions(-) 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 2009c244a6ec0..dbe9c992460b3 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 @@ -33,31 +33,31 @@ export const ControlPanelDiffSystems: { const { exclude: excludeA, - existsSelected: existsSelectedA, + hideExists: hideExistsA, + hideExclude: hideExcludeA, selectedOptions: selectedA, singleSelect: singleSelectA, - hideExclude: hideExcludeA, - hideExists: hideExistsA, + existsSelected: existsSelectedA, runPastTimeout: runPastTimeoutA, ...inputA }: Partial = initialInput.explicitInput; const { exclude: excludeB, - existsSelected: existsSelectedB, + hideExists: hideExistsB, + hideExclude: hideExcludeB, selectedOptions: selectedB, singleSelect: singleSelectB, - hideExclude: hideExcludeB, - hideExists: hideExistsB, + existsSelected: existsSelectedB, runPastTimeout: runPastTimeoutB, ...inputB }: Partial = newInput.explicitInput; return ( Boolean(excludeA) === Boolean(excludeB) && - Boolean(existsSelectedA) === Boolean(existsSelectedB) && - Boolean(singleSelectA) === Boolean(singleSelectB) && - Boolean(hideExcludeA) === Boolean(hideExcludeB) && Boolean(hideExistsA) === Boolean(hideExistsB) && + Boolean(hideExcludeA) === Boolean(hideExcludeB) && + Boolean(singleSelectA) === Boolean(singleSelectB) && + Boolean(existsSelectedA) === Boolean(existsSelectedB) && Boolean(runPastTimeoutA) === Boolean(runPastTimeoutB) && isEqual(selectedA ?? [], selectedB ?? []) && deepEqual(inputA, inputB) diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index 8c156efc99be3..5a0080039e21a 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -19,8 +19,8 @@ export interface OptionsListEmbeddableInput extends DataControlInput { runPastTimeout?: boolean; singleSelect?: boolean; hideExclude?: boolean; - exclude?: boolean; hideExists?: boolean; + exclude?: boolean; } export type OptionsListField = FieldSpec & { 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 6e6c416701550..2824ae06cef73 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 @@ -173,7 +173,6 @@ export class OptionsListEmbeddable extends Embeddable Date: Wed, 2 Nov 2022 11:30:08 -0600 Subject: [PATCH 20/33] Add `exists` selection to validation --- .../controls/common/options_list/types.ts | 2 + .../components/options_list_control.tsx | 11 +- .../components/options_list_popover.tsx | 9 +- ...ptions_list_popover_invalid_selections.tsx | 45 ++++--- .../options_list_popover_suggestions.tsx | 5 +- .../embeddable/options_list_embeddable.tsx | 117 ++++++++++-------- .../options_list/options_list_reducers.ts | 10 ++ .../controls/public/options_list/types.ts | 1 + .../options_list/options_list_service.ts | 8 +- .../options_list/options_list_queries.ts | 28 ++++- .../options_list_suggestions_route.ts | 3 +- 11 files changed, 159 insertions(+), 80 deletions(-) diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index 5a0080039e21a..4869ff7ac7f47 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -59,9 +59,11 @@ export type OptionsListRequest = Omit< export interface OptionsListRequestBody { filters?: Array<{ bool: BoolQuery }>; selectedOptions?: string[]; + existsSelected?: boolean; runPastTimeout?: boolean; textFieldName?: string; searchString?: string; fieldSpec?: FieldSpec; + exclude?: boolean; fieldName: string; } 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 0e6780120f662..75df3b5bd5627 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 @@ -44,6 +44,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); const validSelections = select((state) => state.componentState.validSelections); + const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); const selectedOptions = select((state) => state.explicitInput.selectedOptions); const existsSelected = select((state) => state.explicitInput.existsSelected); @@ -84,14 +85,18 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub selectionDisplayNode: ( <> {existsSelected ? ( - + {exclude ? OptionsListStrings.controlAndPopover.getNegateExists() : OptionsListStrings.controlAndPopover.getExists()} ) : ( <> - {exclude && ( + {!existsSelected && exclude && ( {OptionsListStrings.control.getNegate()}{' '} @@ -109,7 +114,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub ), }; - }, [exclude, existsSelected, validSelections, invalidSelections]); + }, [exclude, existsSelected, existsSelectionInvalid, validSelections, invalidSelections]); const button = (
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 2382fb1e019fd..52ca3a25aefcf 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 @@ -32,9 +32,12 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop >(); // Select current state from Redux using multiple selectors to avoid rerenders. + const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); const invalidSelections = select((state) => state.componentState.invalidSelections); const availableOptions = select((state) => state.componentState.availableOptions); const field = select((state) => state.componentState.field); + + const existsSelected = select((state) => state.explicitInput.existsSelected); const hideExclude = select((state) => state.explicitInput.hideExclude); const title = select((state) => state.explicitInput.title); @@ -57,9 +60,9 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop data-test-subj={`optionsList-control-available-options`} > - {!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && ( - - )} + {!showOnlySelected && + ((invalidSelections && !isEmpty(invalidSelections)) || + (existsSelected && existsSelectionInvalid)) && }
{!hideExclude && } 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 283eabce53ebf..8dccfa6c1b827 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 @@ -20,12 +20,16 @@ export const OptionsListPopoverInvalidSelections = () => { const { useEmbeddableDispatch, useEmbeddableSelector: select, - actions: { deselectOption }, + actions: { selectExists, deselectOption }, } = useReduxEmbeddableContext(); const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); + const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); + + const existsSelected = select((state) => state.explicitInput.existsSelected); + const exclude = select((state) => state.explicitInput.exclude); return ( <> @@ -37,19 +41,32 @@ export const OptionsListPopoverInvalidSelections = () => { )} - <> - {invalidSelections?.map((ignoredSelection, index) => ( - dispatch(deselectOption(ignoredSelection))} - > - {`${ignoredSelection}`} - - ))} - + {existsSelected && existsSelectionInvalid ? ( + dispatch(selectExists(false))} + > + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + + ) : ( + <> + {invalidSelections?.map((ignoredSelection, index) => ( + dispatch(deselectOption(ignoredSelection))} + > + {`${ignoredSelection}`} + + ))} + + )} ); }; 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 86be875160373..3caf03cd9d7e5 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 @@ -26,11 +26,12 @@ export const OptionsListPopoverSuggestions = ({ const { useEmbeddableDispatch, useEmbeddableSelector: select, - actions: { replaceSelection, deselectOption, selectExists, selectOption }, + actions: { replaceSelection, deselectOption, selectOption, selectExists }, } = useReduxEmbeddableContext(); const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. + const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); const invalidSelections = select((state) => state.componentState.invalidSelections); const availableOptions = select((state) => state.componentState.availableOptions); @@ -73,7 +74,7 @@ export const OptionsListPopoverSuggestions = ({ return ( <> - {!hideExists && !(showOnlySelected && !existsSelected) && ( + {!hideExists && !existsSelectionInvalid && ( { - const { - actions: { - clearValidAndInvalidSelections, - setValidAndInvalidSelections, - publishFilters, - }, - dispatch, - } = this.reduxEmbeddableTools; - - if (!newSelectedOptions || isEmpty(newSelectedOptions)) { - dispatch(clearValidAndInvalidSelections({})); - } else { - const { invalidSelections } = - this.reduxEmbeddableTools.getState().componentState ?? {}; - const newValidSelections: string[] = []; - const newInvalidSelections: string[] = []; - for (const selectedOption of newSelectedOptions) { - if (invalidSelections?.includes(selectedOption)) { - newInvalidSelections.push(selectedOption); - continue; - } - newValidSelections.push(selectedOption); + .subscribe(async ({ selectedOptions: newSelectedOptions }) => { + const { + actions: { + clearValidAndInvalidSelections, + setValidAndInvalidSelections, + setExistsSelectionValidity, + publishFilters, + }, + dispatch, + } = this.reduxEmbeddableTools; + + if (!newSelectedOptions || isEmpty(newSelectedOptions)) { + dispatch(clearValidAndInvalidSelections({})); + } else { + const { invalidSelections } = this.reduxEmbeddableTools.getState().componentState ?? {}; + const { existsSelected } = this.reduxEmbeddableTools.getState().explicitInput ?? {}; + const newValidSelections: string[] = []; + const newInvalidSelections: string[] = []; + for (const selectedOption of newSelectedOptions) { + if (invalidSelections?.includes(selectedOption)) { + newInvalidSelections.push(selectedOption); + continue; + } + newValidSelections.push(selectedOption); + } + batch(() => { + if (existsSelected) { + dispatch(setExistsSelectionValidity(invalidSelections)); } - dispatch( setValidAndInvalidSelections({ validSelections: newValidSelections, invalidSelections: newInvalidSelections, }) ); - } - const newFilters = await this.buildFilter(); - dispatch(publishFilters(newFilters)); + }); } - ) + const newFilters = await this.buildFilter(); + dispatch(publishFilters(newFilters)); + }) ); }; @@ -274,7 +277,13 @@ export class OptionsListEmbeddable extends Embeddable { + if (existsSelected) { + dispatch(setExistsSelectionValidity(invalidSelections)); + } + dispatch( + updateQueryResults({ + availableOptions: suggestions, + invalidSelections: undefined, + validSelections: selectedOptions, + totalCardinality, + }) + ); + }); } else { const valid: string[] = []; const invalid: string[] = []; - for (const selectedOption of selectedOptions) { if (invalidSelections?.includes(selectedOption)) invalid.push(selectedOption); else valid.push(selectedOption); } - dispatch( - updateQueryResults({ - availableOptions: suggestions, - invalidSelections: invalid, - validSelections: valid, - totalCardinality, - }) - ); + batch(() => { + if (existsSelected) { + dispatch(setExistsSelectionValidity(invalidSelections)); + } + dispatch( + updateQueryResults({ + availableOptions: suggestions, + invalidSelections: invalid, + validSelections: valid, + totalCardinality, + }) + ); + }); } // publish filter diff --git a/src/plugins/controls/public/options_list/options_list_reducers.ts b/src/plugins/controls/public/options_list/options_list_reducers.ts index bf4ffa1969214..79e4bcad57636 100644 --- a/src/plugins/controls/public/options_list/options_list_reducers.ts +++ b/src/plugins/controls/public/options_list/options_list_reducers.ts @@ -85,6 +85,16 @@ export const optionsListReducers = { state.componentState.invalidSelections = invalidSelections; state.componentState.validSelections = validSelections; }, + setExistsSelectionValidity: ( + state: WritableDraft, + action: PayloadAction + ) => { + if (action.payload?.includes('existsQuery')) { + state.componentState.existsSelectionInvalid = true; + } else { + state.componentState.existsSelectionInvalid = false; + } + }, setLoading: (state: WritableDraft, action: PayloadAction) => { state.output.loading = action.payload; }, diff --git a/src/plugins/controls/public/options_list/types.ts b/src/plugins/controls/public/options_list/types.ts index 4001299a9ab53..18d9455eb8527 100644 --- a/src/plugins/controls/public/options_list/types.ts +++ b/src/plugins/controls/public/options_list/types.ts @@ -22,6 +22,7 @@ export interface OptionsListComponentState { availableOptions?: string[]; invalidSelections?: string[]; validSelections?: string[]; + existsSelectionInvalid?: boolean; searchString: SearchString; } 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 a537e7534a3b8..761d544fa82e7 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 @@ -39,11 +39,13 @@ class OptionsListService implements ControlsOptionsListService { private optionsListCacheResolver = (request: OptionsListRequest) => { const { query, + exclude, filters, timeRange, searchString, runPastTimeout, selectedOptions, + existsSelected, field: { name: fieldName }, dataView: { title: dataViewTitle }, } = request; @@ -51,12 +53,14 @@ class OptionsListService implements ControlsOptionsListService { ...(timeRange ? JSON.stringify(this.getRoundedTimeRange(timeRange)) : []), // round timeRange to the minute to avoid cache misses Math.floor(Date.now() / 1000 / 60), // Only cache results for a minute in case data changes in ES index selectedOptions?.join(','), + existsSelected, JSON.stringify(filters), JSON.stringify(query), runPastTimeout, dataViewTitle, searchString, fieldName, + exclude, ].join('|'); }; @@ -64,7 +68,7 @@ class OptionsListService implements ControlsOptionsListService { async (request: OptionsListRequest, abortSignal: AbortSignal) => { const index = request.dataView.title; const requestBody = this.getRequestBody(request); - return await this.http.fetch( + const response = await this.http.fetch( `/api/kibana/controls/optionsList/${index}`, { body: JSON.stringify(requestBody), @@ -72,6 +76,7 @@ class OptionsListService implements ControlsOptionsListService { method: 'POST', } ); + return response; }, this.optionsListCacheResolver ); @@ -82,6 +87,7 @@ class OptionsListService implements ControlsOptionsListService { const timeFilter = timeRange ? timeService.createFilter(dataView, timeRange) : undefined; const filtersToUse = [...(filters ?? []), ...(timeFilter ? [timeFilter] : [])]; const esFilters = [buildEsQuery(dataView, query ?? [], filtersToUse ?? [])]; + return { ...passThroughProps, filters: esFilters, diff --git a/src/plugins/controls/server/options_list/options_list_queries.ts b/src/plugins/controls/server/options_list/options_list_queries.ts index d1fa89bbc9358..91ed823716bd0 100644 --- a/src/plugins/controls/server/options_list/options_list_queries.ts +++ b/src/plugins/controls/server/options_list/options_list_queries.ts @@ -26,12 +26,27 @@ interface EsBucket { * Validation aggregations */ export const getValidationAggregationBuilder: () => OptionsListAggregationBuilder = () => ({ - buildAggregation: ({ selectedOptions, fieldName }: OptionsListRequestBody) => { - const selectedOptionsFilters = selectedOptions?.reduce((acc, currentOption) => { - acc[currentOption] = { match: { [fieldName]: currentOption } }; - return acc; - }, {} as { [key: string]: { match: { [key: string]: string } } }); - + buildAggregation: ({ + selectedOptions, + fieldName, + existsSelected, + exclude, + }: OptionsListRequestBody) => { + let selectedOptionsFilters; + if (existsSelected) { + if (exclude) { + selectedOptionsFilters = { + existsQuery: { bool: { must_not: { exists: { field: fieldName } } } }, + }; + } else { + selectedOptionsFilters = { existsQuery: { exists: { field: fieldName } } }; + } + } else if (selectedOptions) { + selectedOptionsFilters = selectedOptions.reduce((acc, currentOption) => { + acc[currentOption] = { match: { [fieldName]: currentOption } }; + return acc; + }, {} as { [key: string]: { match: { [key: string]: string } } }); + } return selectedOptionsFilters && !isEmpty(selectedOptionsFilters) ? { filters: { @@ -44,6 +59,7 @@ export const getValidationAggregationBuilder: () => OptionsListAggregationBuilde const rawInvalidSuggestions = get(rawEsResult, 'aggregations.validation.buckets') as { [key: string]: { doc_count: number }; }; + return rawInvalidSuggestions && !isEmpty(rawInvalidSuggestions) ? Object.entries(rawInvalidSuggestions) ?.filter(([, value]) => value?.doc_count === 0) 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 c9af30bb07b82..ad81e8dc1e4fd 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 @@ -43,6 +43,7 @@ export const setupOptionsListSuggestionsRoute = ( filters: schema.maybe(schema.any()), fieldSpec: schema.maybe(schema.any()), searchString: schema.maybe(schema.string()), + existsSelected: schema.maybe(schema.boolean()), selectedOptions: schema.maybe(schema.arrayOf(schema.string())), }, { unknowns: 'allow' } @@ -107,7 +108,6 @@ export const setupOptionsListSuggestionsRoute = ( validation: builtValidationAggregation, } : {}; - const body: SearchRequest['body'] = { size: 0, ...timeoutSettings, @@ -138,7 +138,6 @@ export const setupOptionsListSuggestionsRoute = ( const totalCardinality = get(rawEsResult, 'aggregations.unique_terms.value'); const suggestions = suggestionBuilder.parse(rawEsResult); const invalidSelections = validationBuilder.parse(rawEsResult); - return { suggestions, totalCardinality, From dcd5ca8cbef768b5bccef75ace9be68301ff40b0 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 2 Nov 2022 14:31:42 -0600 Subject: [PATCH 21/33] Fix invalid bug --- .../embeddable/options_list_embeddable.tsx | 70 ++++++++----------- .../options_list/options_list_reducers.ts | 25 ++++--- .../options_list/options_list_service.ts | 5 +- 3 files changed, 44 insertions(+), 56 deletions(-) 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 099cb4c230b7a..689692192b9df 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 @@ -129,13 +129,14 @@ export class OptionsListEmbeddable extends Embeddable ({ validate: !Boolean(newInput.ignoreParentSettings?.ignoreValidations), lastReloadRequestTime: newInput.lastReloadRequestTime, + existsSelected: newInput.existsSelected, dataViewId: newInput.dataViewId, fieldName: newInput.fieldName, timeRange: newInput.timeRange, timeslice: newInput.timeslice, + exclude: newInput.exclude, filters: newInput.filters, query: newInput.query, - exclude: newInput.exclude, })), distinctUntilChanged(diffDataFetchProps) ); @@ -168,7 +169,7 @@ export class OptionsListEmbeddable extends Embeddable { - if (existsSelected) { - dispatch(setExistsSelectionValidity(invalidSelections)); - } + dispatch( + updateQueryResults({ + existsSelectionInvalid: invalidSelections?.includes('existsQuery'), + }) + ); dispatch( setValidAndInvalidSelections({ validSelections: newValidSelections, @@ -277,13 +279,7 @@ export class OptionsListEmbeddable extends Embeddable { - if (existsSelected) { - dispatch(setExistsSelectionValidity(invalidSelections)); - } - dispatch( - updateQueryResults({ - availableOptions: suggestions, - invalidSelections: undefined, - validSelections: selectedOptions, - totalCardinality, - }) - ); - }); + dispatch( + updateQueryResults({ + existsSelectionInvalid: undefined, + availableOptions: suggestions, + invalidSelections: undefined, + validSelections: selectedOptions, + totalCardinality, + }) + ); } else { const valid: string[] = []; const invalid: string[] = []; - for (const selectedOption of selectedOptions) { + for (const selectedOption of selectedOptions ?? []) { if (invalidSelections?.includes(selectedOption)) invalid.push(selectedOption); else valid.push(selectedOption); } - batch(() => { - if (existsSelected) { - dispatch(setExistsSelectionValidity(invalidSelections)); - } - dispatch( - updateQueryResults({ - availableOptions: suggestions, - invalidSelections: invalid, - validSelections: valid, - totalCardinality, - }) - ); - }); + dispatch( + updateQueryResults({ + existsSelectionInvalid: invalidSelections?.includes('existsQuery'), + availableOptions: suggestions, + invalidSelections: invalid, + validSelections: valid, + totalCardinality, + }) + ); } // publish filter diff --git a/src/plugins/controls/public/options_list/options_list_reducers.ts b/src/plugins/controls/public/options_list/options_list_reducers.ts index 79e4bcad57636..d7ac617857840 100644 --- a/src/plugins/controls/public/options_list/options_list_reducers.ts +++ b/src/plugins/controls/public/options_list/options_list_reducers.ts @@ -52,8 +52,13 @@ export const optionsListReducers = { } }, selectExists: (state: WritableDraft, action: PayloadAction) => { - state.explicitInput.existsSelected = action.payload; - state.explicitInput.selectedOptions = []; + if (action.payload) { + state.explicitInput.existsSelected = true; + state.explicitInput.selectedOptions = []; + } else { + state.explicitInput.existsSelected = false; + state.componentState.existsSelectionInvalid = false; + } }, selectOption: (state: WritableDraft, action: PayloadAction) => { if (!state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = []; @@ -85,16 +90,6 @@ export const optionsListReducers = { state.componentState.invalidSelections = invalidSelections; state.componentState.validSelections = validSelections; }, - setExistsSelectionValidity: ( - state: WritableDraft, - action: PayloadAction - ) => { - if (action.payload?.includes('existsQuery')) { - state.componentState.existsSelectionInvalid = true; - } else { - state.componentState.existsSelectionInvalid = false; - } - }, setLoading: (state: WritableDraft, action: PayloadAction) => { state.output.loading = action.payload; }, @@ -109,7 +104,11 @@ export const optionsListReducers = { action: PayloadAction< Pick< OptionsListComponentState, - 'availableOptions' | 'invalidSelections' | 'validSelections' | 'totalCardinality' + | 'availableOptions' + | 'invalidSelections' + | 'validSelections' + | 'totalCardinality' + | 'existsSelectionInvalid' > > ) => { 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 761d544fa82e7..19369109a21f4 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 @@ -43,9 +43,9 @@ class OptionsListService implements ControlsOptionsListService { filters, timeRange, searchString, + existsSelected, runPastTimeout, selectedOptions, - existsSelected, field: { name: fieldName }, dataView: { title: dataViewTitle }, } = request; @@ -53,9 +53,9 @@ class OptionsListService implements ControlsOptionsListService { ...(timeRange ? JSON.stringify(this.getRoundedTimeRange(timeRange)) : []), // round timeRange to the minute to avoid cache misses Math.floor(Date.now() / 1000 / 60), // Only cache results for a minute in case data changes in ES index selectedOptions?.join(','), - existsSelected, JSON.stringify(filters), JSON.stringify(query), + existsSelected, runPastTimeout, dataViewTitle, searchString, @@ -76,6 +76,7 @@ class OptionsListService implements ControlsOptionsListService { method: 'POST', } ); + console.log('response', response); return response; }, this.optionsListCacheResolver From a45b451c39d519b192b3b9a856580335e7fac16a Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 2 Nov 2022 14:47:02 -0600 Subject: [PATCH 22/33] Fix failing unit test --- .../options_list_popover_suggestions.tsx | 34 ++++++++++--------- .../options_list/options_list_service.ts | 1 - 2 files changed, 18 insertions(+), 17 deletions(-) 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 3caf03cd9d7e5..79ff82ab7523f 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 @@ -74,22 +74,24 @@ export const OptionsListPopoverSuggestions = ({ return ( <> - {!hideExists && !existsSelectionInvalid && ( - { - dispatch(selectExists(!Boolean(existsSelected))); - }} - > - - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} - - - )} + {!hideExists && + ((showOnlySelected && existsSelected) || + (!showOnlySelected && !existsSelectionInvalid)) && ( + { + dispatch(selectExists(!Boolean(existsSelected))); + }} + > + + {exclude + ? OptionsListStrings.controlAndPopover.getNegateExists() + : OptionsListStrings.controlAndPopover.getExists()} + + + )} {suggestions?.map((suggestion, index) => ( Date: Wed, 2 Nov 2022 16:10:47 -0600 Subject: [PATCH 23/33] More code clean up --- .../options_list_popover_suggestions.tsx | 6 ++++- .../embeddable/options_list_embeddable.tsx | 27 +++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) 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 79ff82ab7523f..9210afc816d2e 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 @@ -51,7 +51,11 @@ export const OptionsListPopoverSuggestions = ({ ); const suggestions = showOnlySelected ? selectedOptions : availableOptions; - if (!loading && (!suggestions || suggestions.length === 0) && !existsSelected) { + if ( + !loading && + (!suggestions || suggestions.length === 0) && + !(showOnlySelected && existsSelected) + ) { return (
{ + return invalidSelections?.length === 1 && invalidSelections[0] === 'existsQuery'; +}; + export class OptionsListEmbeddable extends Embeddable { public readonly type = OPTIONS_LIST_CONTROL; public deferEmbeddableLoad = true; @@ -169,7 +173,6 @@ export class OptionsListEmbeddable extends Embeddable { - dispatch( - updateQueryResults({ - existsSelectionInvalid: invalidSelections?.includes('existsQuery'), - }) - ); - dispatch( - setValidAndInvalidSelections({ - validSelections: newValidSelections, - invalidSelections: newInvalidSelections, - }) - ); - }); + dispatch( + setValidAndInvalidSelections({ + validSelections: newValidSelections, + invalidSelections: newInvalidSelections, + }) + ); } const newFilters = await this.buildFilter(); dispatch(publishFilters(newFilters)); @@ -354,7 +350,7 @@ export class OptionsListEmbeddable extends Embeddable { dispatch( updateQueryResults({ + existsSelectionInvalid: undefined, availableOptions: [], }) ); From fd89c1e5a6f06e887666367027424dff8565302a Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Wed, 2 Nov 2022 16:59:03 -0600 Subject: [PATCH 24/33] Add another functional test --- ...ptions_list_popover_invalid_selections.tsx | 2 +- .../controls/control_group_chaining.ts | 27 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) 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 8dccfa6c1b827..8ab0a1a829b69 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 @@ -37,7 +37,7 @@ export const OptionsListPopoverInvalidSelections = () => { 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 8297d21403b85..1f07f183a4a19 100644 --- a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts +++ b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts @@ -27,10 +27,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const newDocuments: Array<{ index: string; id: string }> = []; let controlIds: string[]; - const ensureAvailableOptionsEql = async (controlId: string, expectation: string[]) => { + const ensureAvailableOptionsEql = async ( + controlId: string, + expectation: string[], + filterOutExists: boolean = true + ) => { await dashboardControls.optionsListOpenPopover(controlId); await retry.try(async () => { - expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(expectation); + expect( + await dashboardControls.optionsListPopoverGetAvailableOptions(filterOutExists) + ).to.eql(expectation); }); await dashboardControls.optionsListEnsurePopoverIsClosed(controlId); }; @@ -211,6 +217,23 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]); }); + it('Can make "does not exist" query invalid through previous controls', async () => { + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await dashboardControls.optionsListPopoverSetIncludeSelections(false); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); + + await dashboardControls.optionsListOpenPopover(controlIds[1]); + await dashboardControls.optionsListPopoverSelectOption('exists'); + await dashboardControls.optionsListPopoverSetIncludeSelections(false); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]); + + await ensureAvailableOptionsEql( + controlIds[1], + ['Max', 'Ignored selection', 'Does not exist (!)'], + false + ); + }); + describe('Hierarchical chaining off', async () => { before(async () => { await dashboardControls.updateChainingSystem('NONE'); From 8af6c5730857bf65cdafc1c9540c41e7c6c5d5f8 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 3 Nov 2022 09:40:41 -0600 Subject: [PATCH 25/33] Apply styling changes --- .../options_list/components/options_list.scss | 7 ++++++ .../components/options_list_control.tsx | 24 +++++++------------ ...ptions_list_popover_invalid_selections.tsx | 4 +--- .../options_list_popover_suggestions.tsx | 5 +--- .../components/options_list_strings.ts | 13 +++++----- 5 files changed, 25 insertions(+), 28 deletions(-) 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 1f1ea7f200a40..b48c3d5058972 100644 --- a/src/plugins/controls/public/options_list/components/options_list.scss +++ b/src/plugins/controls/public/options_list/components/options_list.scss @@ -39,6 +39,13 @@ font-style: italic; } +.optionsList__negateLabel { + font-weight: bold; + font-size: $euiSizeM; + color: $euiColorDanger; + padding-right: $euiSizeXS; +} + .optionsList__ignoredBadge { margin-left: $euiSizeS; } 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 75df3b5bd5627..a4b75c2989932 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 @@ -11,13 +11,7 @@ import classNames from 'classnames'; import { debounce, isEmpty } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; -import { - EuiFilterButton, - EuiFilterGroup, - EuiPopover, - EuiTextColor, - useResizeObserver, -} from '@elastic/eui'; +import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; import { OptionsListStrings } from './options_list_strings'; @@ -84,23 +78,23 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub validSelectionsCount: validSelections?.length, selectionDisplayNode: ( <> + {exclude && ( + + {existsSelected + ? OptionsListStrings.control.getExcludeExists() + : OptionsListStrings.control.getNegate()} + + )} {existsSelected ? ( - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} + {OptionsListStrings.controlAndPopover.getExists(+Boolean(exclude))} ) : ( <> - {!existsSelected && exclude && ( - - {OptionsListStrings.control.getNegate()}{' '} - - )} {validSelections && ( {validSelections?.join(OptionsListStrings.control.getSeparator())} )} 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 8ab0a1a829b69..2f8bdf3db4dde 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 @@ -48,9 +48,7 @@ export const OptionsListPopoverInvalidSelections = () => { className="optionsList__selectionInvalid" onClick={() => dispatch(selectExists(false))} > - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} + {OptionsListStrings.controlAndPopover.getExists(+Boolean(exclude))} ) : ( <> 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 9210afc816d2e..b1c45f0bc9e24 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 @@ -39,7 +39,6 @@ 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 exclude = select((state) => state.explicitInput.exclude); const loading = select((state) => state.output.loading); @@ -90,9 +89,7 @@ export const OptionsListPopoverSuggestions = ({ }} > - {exclude - ? OptionsListStrings.controlAndPopover.getNegateExists() - : OptionsListStrings.controlAndPopover.getExists()} + {OptionsListStrings.controlAndPopover.getExists()} )} 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 6ccbd4dceb29f..4ce114ebfcba2 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 @@ -22,6 +22,10 @@ export const OptionsListStrings = { i18n.translate('controls.optionsList.control.negate', { defaultMessage: 'NOT', }), + getExcludeExists: () => + i18n.translate('controls.optionsList.control.excludeExists', { + defaultMessage: 'DOES NOT', + }), }, editor: { getAllowMultiselectTitle: () => @@ -122,13 +126,10 @@ export const OptionsListStrings = { }), }, controlAndPopover: { - getExists: () => + getExists: (negate: number = +false) => i18n.translate('controls.optionsList.controlAndPopover.exists', { - defaultMessage: 'Exists (*)', - }), - getNegateExists: () => - i18n.translate('controls.optionsList.controlAndPopover.negateExists', { - defaultMessage: 'Does not exist (!)', + defaultMessage: '{negate, plural, one {Exist} other {Exists}}', + values: { negate }, }), }, }; From 592736987677a0a7e53854cf08edee120206a1a1 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 3 Nov 2022 11:06:56 -0600 Subject: [PATCH 26/33] Fix tests --- .../options_list/components/options_list.scss | 1 - .../components/options_list_control.test.tsx | 59 +++++++++++++++++++ .../components/options_list_control.tsx | 12 ++-- .../components/options_list_popover.test.tsx | 16 ----- ...ptions_list_popover_invalid_selections.tsx | 4 +- .../options_list_popover_suggestions.tsx | 5 +- .../controls/control_group_chaining.ts | 6 +- 7 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 src/plugins/controls/public/options_list/components/options_list_control.test.tsx 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 b48c3d5058972..928a10f3651b8 100644 --- a/src/plugins/controls/public/options_list/components/options_list.scss +++ b/src/plugins/controls/public/options_list/components/options_list.scss @@ -43,7 +43,6 @@ font-weight: bold; font-size: $euiSizeM; color: $euiColorDanger; - padding-right: $euiSizeXS; } .optionsList__ignoredBadge { diff --git a/src/plugins/controls/public/options_list/components/options_list_control.test.tsx b/src/plugins/controls/public/options_list/components/options_list_control.test.tsx new file mode 100644 index 0000000000000..cd840ecac5aec --- /dev/null +++ b/src/plugins/controls/public/options_list/components/options_list_control.test.tsx @@ -0,0 +1,59 @@ +/* + * 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import { findTestSubject } from '@elastic/eui/lib/test'; + +import { OptionsListComponentState, OptionsListReduxState } from '../types'; +import { ControlOutput, OptionsListEmbeddableInput } from '../..'; +import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks'; +import { OptionsListControl } from './options_list_control'; + +describe('Options list control', () => { + const defaultProps = { + typeaheadSubject: jest.fn(), + }; + + interface MountOptions { + componentState: Partial; + explicitInput: Partial; + output: Partial; + } + + async function mountComponent(options?: Partial) { + const mockReduxEmbeddableTools = await mockOptionsListReduxEmbeddableTools({ + componentState: options?.componentState ?? {}, + explicitInput: options?.explicitInput ?? {}, + output: options?.output ?? {}, + } as Partial); + + return mountWithIntl( + + + + ); + } + + test('if exclude = false and existsSelected = true, then the option should read "Exists"', async () => { + const control = await mountComponent({ + explicitInput: { id: 'testExists', exclude: false, existsSelected: true }, + }); + const existsOption = findTestSubject(control, 'optionsList-control-testExists'); + expect(existsOption.text()).toBe('Exists'); + }); + + test('if exclude = true and existsSelected = true, then the option should read "Does not exist"', async () => { + const control = await mountComponent({ + explicitInput: { id: 'testDoesNotExist', exclude: true, existsSelected: true }, + }); + const existsOption = findTestSubject(control, 'optionsList-control-testDoesNotExist'); + expect(existsOption.text()).toBe('DOES NOT Exist'); + }); +}); 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 a4b75c2989932..d884e0984b6f0 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 @@ -79,11 +79,13 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub selectionDisplayNode: ( <> {exclude && ( - - {existsSelected - ? OptionsListStrings.control.getExcludeExists() - : OptionsListStrings.control.getNegate()} - + <> + + {existsSelected + ? OptionsListStrings.control.getExcludeExists() + : OptionsListStrings.control.getNegate()} + {' '} + )} {existsSelected ? ( { expect(excludeButton.prop('checked')).toBe(true); }); - test('if exclude = false and existsSelected = true, then the option should read "Exists"', async () => { - const popover = await mountComponent({ - explicitInput: { exclude: false, existsSelected: true }, - }); - const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); - expect(existsOption.text()).toBe('Exists (*)'); - }); - - test('if exclude = true and existsSelected = true, then the option should read "Does not exist"', async () => { - const popover = await mountComponent({ - explicitInput: { exclude: true, existsSelected: true }, - }); - const existsOption = findTestSubject(popover, 'optionsList-control-selection-exists'); - expect(existsOption.text()).toBe('Does not exist (!)'); - }); - test('clicking another option unselects "Exists"', async () => { const popover = await mountComponent({ explicitInput: { existsSelected: true }, 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 2f8bdf3db4dde..f5486485019fc 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 @@ -27,9 +27,7 @@ export const OptionsListPopoverInvalidSelections = () => { // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); - const existsSelected = select((state) => state.explicitInput.existsSelected); - const exclude = select((state) => state.explicitInput.exclude); return ( <> @@ -48,7 +46,7 @@ export const OptionsListPopoverInvalidSelections = () => { className="optionsList__selectionInvalid" onClick={() => dispatch(selectExists(false))} > - {OptionsListStrings.controlAndPopover.getExists(+Boolean(exclude))} + {OptionsListStrings.controlAndPopover.getExists(+Boolean(false))} ) : ( <> 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 b1c45f0bc9e24..913197a31f106 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 @@ -87,10 +87,9 @@ export const OptionsListPopoverSuggestions = ({ onClick={() => { dispatch(selectExists(!Boolean(existsSelected))); }} + className="optionsList__existsFilter" > - - {OptionsListStrings.controlAndPopover.getExists()} - + {OptionsListStrings.controlAndPopover.getExists()} )} {suggestions?.map((suggestion, index) => ( 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 1f07f183a4a19..91e997a50ec5f 100644 --- a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts +++ b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts @@ -227,11 +227,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListPopoverSetIncludeSelections(false); await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]); - await ensureAvailableOptionsEql( - controlIds[1], - ['Max', 'Ignored selection', 'Does not exist (!)'], - false - ); + await ensureAvailableOptionsEql(controlIds[1], ['Max', 'Ignored selection', 'Exists'], false); }); describe('Hierarchical chaining off', async () => { From b1bbfc85a6fefee4b4796801a17c3101c391c8b1 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 3 Nov 2022 11:27:52 -0600 Subject: [PATCH 27/33] Fix a11y issues --- .../public/options_list/components/options_list_control.tsx | 4 +++- .../public/options_list/components/options_list_popover.tsx | 6 ++++-- .../public/options_list/components/options_list_strings.ts | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) 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 d884e0984b6f0..66df598cb4009 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 @@ -44,8 +44,9 @@ 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 id = select((state) => state.explicitInput.id); + 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); @@ -150,6 +151,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub className="optionsList__popoverOverride" closePopover={() => setIsPopoverOpen(false)} anchorClassName="optionsList__anchorOverride" + aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)} > 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 52ca3a25aefcf..813c5af12eb77 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 @@ -13,6 +13,7 @@ import { EuiPopoverTitle } from '@elastic/eui'; import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public'; import { OptionsListReduxState } from '../types'; +import { OptionsListStrings } from './options_list_strings'; import { optionsListReducers } from '../options_list_reducers'; import { OptionsListPopoverFooter } from './options_list_popover_footer'; import { OptionsListPopoverActionBar } from './options_list_popover_action_bar'; @@ -39,12 +40,13 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const existsSelected = select((state) => state.explicitInput.existsSelected); const hideExclude = select((state) => state.explicitInput.hideExclude); + const fieldName = select((state) => state.explicitInput.fieldName); const title = select((state) => state.explicitInput.title); const [showOnlySelected, setShowOnlySelected] = useState(false); return ( - <> + {title} {field?.type !== 'boolean' && ( }
{!hideExclude && } - +
); }; 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 4ce114ebfcba2..76b47fb8cbeb8 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 @@ -56,6 +56,11 @@ export const OptionsListStrings = { }), }, popover: { + getAriaLabel: (fieldName: string) => + i18n.translate('controls.optionsList.popover.ariaLabel', { + defaultMessage: 'Popover for {fieldName} control', + values: { fieldName }, + }), getLoadingMessage: () => i18n.translate('controls.optionsList.popover.loading', { defaultMessage: 'Loading options', From 20a1dd22ac6a6df378bfe8b9bf65751bb228b1ae Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 3 Nov 2022 12:02:09 -0600 Subject: [PATCH 28/33] Remove validation --- .../controls/common/options_list/types.ts | 2 -- .../components/options_list_control.tsx | 9 ++---- .../components/options_list_popover.tsx | 8 ++--- ...ptions_list_popover_invalid_selections.tsx | 31 +++++-------------- .../options_list_popover_suggestions.tsx | 29 ++++++++--------- .../embeddable/options_list_embeddable.tsx | 11 +------ .../options_list/options_list_reducers.ts | 7 +---- .../controls/public/options_list/types.ts | 1 - .../options_list/options_list_service.ts | 2 -- .../options_list/options_list_queries.ts | 17 ++-------- .../options_list_suggestions_route.ts | 1 - .../controls/control_group_chaining.ts | 13 -------- 12 files changed, 30 insertions(+), 101 deletions(-) diff --git a/src/plugins/controls/common/options_list/types.ts b/src/plugins/controls/common/options_list/types.ts index 4869ff7ac7f47..5a0080039e21a 100644 --- a/src/plugins/controls/common/options_list/types.ts +++ b/src/plugins/controls/common/options_list/types.ts @@ -59,11 +59,9 @@ export type OptionsListRequest = Omit< export interface OptionsListRequestBody { filters?: Array<{ bool: BoolQuery }>; selectedOptions?: string[]; - existsSelected?: boolean; runPastTimeout?: boolean; textFieldName?: string; searchString?: string; fieldSpec?: FieldSpec; - exclude?: boolean; fieldName: string; } 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 66df598cb4009..1c3b0ebb93113 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 @@ -38,7 +38,6 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); const validSelections = select((state) => state.componentState.validSelections); - const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); const selectedOptions = select((state) => state.explicitInput.selectedOptions); const existsSelected = select((state) => state.explicitInput.existsSelected); @@ -89,11 +88,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub )} {existsSelected ? ( - + {OptionsListStrings.controlAndPopover.getExists(+Boolean(exclude))} ) : ( @@ -111,7 +106,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub ), }; - }, [exclude, existsSelected, existsSelectionInvalid, validSelections, invalidSelections]); + }, [exclude, existsSelected, validSelections, invalidSelections]); const button = (
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 813c5af12eb77..89c7d43379aa1 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 @@ -33,12 +33,10 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop >(); // Select current state from Redux using multiple selectors to avoid rerenders. - const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); const invalidSelections = select((state) => state.componentState.invalidSelections); const availableOptions = select((state) => state.componentState.availableOptions); const field = select((state) => state.componentState.field); - const existsSelected = select((state) => state.explicitInput.existsSelected); const hideExclude = select((state) => state.explicitInput.hideExclude); const fieldName = select((state) => state.explicitInput.fieldName); const title = select((state) => state.explicitInput.title); @@ -62,9 +60,9 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop data-test-subj={`optionsList-control-available-options`} > - {!showOnlySelected && - ((invalidSelections && !isEmpty(invalidSelections)) || - (existsSelected && existsSelectionInvalid)) && } + {!showOnlySelected && invalidSelections && !isEmpty(invalidSelections) && ( + + )}
{!hideExclude && }
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 f5486485019fc..1a6ec2176dd42 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 @@ -20,14 +20,12 @@ export const OptionsListPopoverInvalidSelections = () => { const { useEmbeddableDispatch, useEmbeddableSelector: select, - actions: { selectExists, deselectOption }, + actions: { deselectOption }, } = useReduxEmbeddableContext(); const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. const invalidSelections = select((state) => state.componentState.invalidSelections); - const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); - const existsSelected = select((state) => state.explicitInput.existsSelected); return ( <> @@ -35,34 +33,21 @@ export const OptionsListPopoverInvalidSelections = () => { - {existsSelected && existsSelectionInvalid ? ( + {invalidSelections?.map((ignoredSelection, index) => ( dispatch(selectExists(false))} + key={index} + onClick={() => dispatch(deselectOption(ignoredSelection))} > - {OptionsListStrings.controlAndPopover.getExists(+Boolean(false))} + {`${ignoredSelection}`} - ) : ( - <> - {invalidSelections?.map((ignoredSelection, index) => ( - dispatch(deselectOption(ignoredSelection))} - > - {`${ignoredSelection}`} - - ))} - - )} + ))} ); }; 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 913197a31f106..5ca609d6ac64d 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 @@ -31,7 +31,6 @@ export const OptionsListPopoverSuggestions = ({ const dispatch = useEmbeddableDispatch(); // Select current state from Redux using multiple selectors to avoid rerenders. - const existsSelectionInvalid = select((state) => state.componentState.existsSelectionInvalid); const invalidSelections = select((state) => state.componentState.invalidSelections); const availableOptions = select((state) => state.componentState.availableOptions); @@ -77,21 +76,19 @@ export const OptionsListPopoverSuggestions = ({ return ( <> - {!hideExists && - ((showOnlySelected && existsSelected) || - (!showOnlySelected && !existsSelectionInvalid)) && ( - { - dispatch(selectExists(!Boolean(existsSelected))); - }} - className="optionsList__existsFilter" - > - {OptionsListStrings.controlAndPopover.getExists()} - - )} + {!hideExists && !(showOnlySelected && !existsSelected) && ( + { + dispatch(selectExists(!Boolean(existsSelected))); + }} + className="optionsList__existsFilter" + > + {OptionsListStrings.controlAndPopover.getExists()} + + )} {suggestions?.map((suggestion, index) => ( { - return invalidSelections?.length === 1 && invalidSelections[0] === 'existsQuery'; -}; - export class OptionsListEmbeddable extends Embeddable { public readonly type = OPTIONS_LIST_CONTROL; public deferEmbeddableLoad = true; @@ -288,7 +284,7 @@ export class OptionsListEmbeddable extends Embeddable { dispatch( updateQueryResults({ - existsSelectionInvalid: undefined, availableOptions: [], }) ); diff --git a/src/plugins/controls/public/options_list/options_list_reducers.ts b/src/plugins/controls/public/options_list/options_list_reducers.ts index d7ac617857840..731ae4c8eb507 100644 --- a/src/plugins/controls/public/options_list/options_list_reducers.ts +++ b/src/plugins/controls/public/options_list/options_list_reducers.ts @@ -57,7 +57,6 @@ export const optionsListReducers = { state.explicitInput.selectedOptions = []; } else { state.explicitInput.existsSelected = false; - state.componentState.existsSelectionInvalid = false; } }, selectOption: (state: WritableDraft, action: PayloadAction) => { @@ -104,11 +103,7 @@ export const optionsListReducers = { action: PayloadAction< Pick< OptionsListComponentState, - | 'availableOptions' - | 'invalidSelections' - | 'validSelections' - | 'totalCardinality' - | 'existsSelectionInvalid' + 'availableOptions' | 'invalidSelections' | 'validSelections' | 'totalCardinality' > > ) => { diff --git a/src/plugins/controls/public/options_list/types.ts b/src/plugins/controls/public/options_list/types.ts index 18d9455eb8527..4001299a9ab53 100644 --- a/src/plugins/controls/public/options_list/types.ts +++ b/src/plugins/controls/public/options_list/types.ts @@ -22,7 +22,6 @@ export interface OptionsListComponentState { availableOptions?: string[]; invalidSelections?: string[]; validSelections?: string[]; - existsSelectionInvalid?: boolean; searchString: SearchString; } 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 fde141166028d..caed6a3f4022b 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 @@ -39,7 +39,6 @@ class OptionsListService implements ControlsOptionsListService { private optionsListCacheResolver = (request: OptionsListRequest) => { const { query, - exclude, filters, timeRange, searchString, @@ -60,7 +59,6 @@ class OptionsListService implements ControlsOptionsListService { dataViewTitle, searchString, fieldName, - exclude, ].join('|'); }; diff --git a/src/plugins/controls/server/options_list/options_list_queries.ts b/src/plugins/controls/server/options_list/options_list_queries.ts index 91ed823716bd0..4a381aeac64c1 100644 --- a/src/plugins/controls/server/options_list/options_list_queries.ts +++ b/src/plugins/controls/server/options_list/options_list_queries.ts @@ -26,22 +26,9 @@ interface EsBucket { * Validation aggregations */ export const getValidationAggregationBuilder: () => OptionsListAggregationBuilder = () => ({ - buildAggregation: ({ - selectedOptions, - fieldName, - existsSelected, - exclude, - }: OptionsListRequestBody) => { + buildAggregation: ({ selectedOptions, fieldName }: OptionsListRequestBody) => { let selectedOptionsFilters; - if (existsSelected) { - if (exclude) { - selectedOptionsFilters = { - existsQuery: { bool: { must_not: { exists: { field: fieldName } } } }, - }; - } else { - selectedOptionsFilters = { existsQuery: { exists: { field: fieldName } } }; - } - } else if (selectedOptions) { + if (selectedOptions) { selectedOptionsFilters = selectedOptions.reduce((acc, currentOption) => { acc[currentOption] = { match: { [fieldName]: currentOption } }; return acc; 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 ad81e8dc1e4fd..fe2218c3f7135 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 @@ -43,7 +43,6 @@ export const setupOptionsListSuggestionsRoute = ( filters: schema.maybe(schema.any()), fieldSpec: schema.maybe(schema.any()), searchString: schema.maybe(schema.string()), - existsSelected: schema.maybe(schema.boolean()), selectedOptions: schema.maybe(schema.arrayOf(schema.string())), }, { unknowns: 'allow' } 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 91e997a50ec5f..652864471a04a 100644 --- a/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts +++ b/test/functional/apps/dashboard_elements/controls/control_group_chaining.ts @@ -217,19 +217,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]); }); - it('Can make "does not exist" query invalid through previous controls', async () => { - await dashboardControls.optionsListOpenPopover(controlIds[0]); - await dashboardControls.optionsListPopoverSetIncludeSelections(false); - await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); - - await dashboardControls.optionsListOpenPopover(controlIds[1]); - await dashboardControls.optionsListPopoverSelectOption('exists'); - await dashboardControls.optionsListPopoverSetIncludeSelections(false); - await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[1]); - - await ensureAvailableOptionsEql(controlIds[1], ['Max', 'Ignored selection', 'Exists'], false); - }); - describe('Hierarchical chaining off', async () => { before(async () => { await dashboardControls.updateChainingSystem('NONE'); From 1f3bd1b90d598c7ca25fe22aebb4c033ec2d0273 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 3 Nov 2022 12:35:32 -0600 Subject: [PATCH 29/33] Fix types --- .../options_list/components/options_list_control.test.tsx | 3 ++- .../public/services/options_list/options_list_service.ts | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/plugins/controls/public/options_list/components/options_list_control.test.tsx b/src/plugins/controls/public/options_list/components/options_list_control.test.tsx index cd840ecac5aec..a4d5028f0f7be 100644 --- a/src/plugins/controls/public/options_list/components/options_list_control.test.tsx +++ b/src/plugins/controls/public/options_list/components/options_list_control.test.tsx @@ -15,10 +15,11 @@ import { OptionsListComponentState, OptionsListReduxState } from '../types'; import { ControlOutput, OptionsListEmbeddableInput } from '../..'; import { mockOptionsListReduxEmbeddableTools } from '../../../common/mocks'; import { OptionsListControl } from './options_list_control'; +import { BehaviorSubject } from 'rxjs'; describe('Options list control', () => { const defaultProps = { - typeaheadSubject: jest.fn(), + typeaheadSubject: new BehaviorSubject(''), }; interface MountOptions { 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 caed6a3f4022b..18b4c8e327cfd 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 @@ -42,7 +42,6 @@ class OptionsListService implements ControlsOptionsListService { filters, timeRange, searchString, - existsSelected, runPastTimeout, selectedOptions, field: { name: fieldName }, @@ -54,7 +53,6 @@ class OptionsListService implements ControlsOptionsListService { selectedOptions?.join(','), JSON.stringify(filters), JSON.stringify(query), - existsSelected, runPastTimeout, dataViewTitle, searchString, From 0318b0a2476c7449d2745d8c031a59ea43ab6c82 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Thu, 3 Nov 2022 12:41:25 -0600 Subject: [PATCH 30/33] Clean up `a11y` fix --- .../options_list/components/options_list_control.tsx | 3 +-- .../options_list/components/options_list_popover.tsx | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) 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 1c3b0ebb93113..1f19382ab506b 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,7 +43,6 @@ 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); @@ -146,7 +145,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub className="optionsList__popoverOverride" closePopover={() => setIsPopoverOpen(false)} anchorClassName="optionsList__anchorOverride" - aria-label={OptionsListStrings.popover.getAriaLabel(fieldName)} + aria-labelledby={`control-popover-${id}`} > 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 89c7d43379aa1..bc1e62fccfda7 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 @@ -40,11 +40,16 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop const hideExclude = select((state) => state.explicitInput.hideExclude); const fieldName = select((state) => state.explicitInput.fieldName); const title = select((state) => state.explicitInput.title); + const id = select((state) => state.explicitInput.id); const [showOnlySelected, setShowOnlySelected] = useState(false); return ( - + {title} {field?.type !== 'boolean' && ( Date: Thu, 3 Nov 2022 15:12:14 -0600 Subject: [PATCH 31/33] Fix jest test --- .../options_list/components/options_list_popover.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 095e413e99592..1ee6de1c45763 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 @@ -167,6 +167,6 @@ describe('Options list popover', () => { }); clickShowOnlySelections(popover); const availableOptionsDiv = findTestSubject(popover, 'optionsList-control-available-options'); - expect(availableOptionsDiv.children().at(0).text()).toBe('Exists (*)'); + expect(availableOptionsDiv.children().at(0).text()).toBe('Exists'); }); }); From 7702df3db9789f7fbbb36e352de2fdcb8914b7fd Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 4 Nov 2022 13:49:54 -0600 Subject: [PATCH 32/33] Address feedback --- .../public/services/options_list/options_list_service.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 18b4c8e327cfd..27867b5724cec 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 @@ -64,7 +64,7 @@ class OptionsListService implements ControlsOptionsListService { async (request: OptionsListRequest, abortSignal: AbortSignal) => { const index = request.dataView.title; const requestBody = this.getRequestBody(request); - const response = await this.http.fetch( + return await this.http.fetch( `/api/kibana/controls/optionsList/${index}`, { body: JSON.stringify(requestBody), @@ -72,7 +72,6 @@ class OptionsListService implements ControlsOptionsListService { method: 'POST', } ); - return response; }, this.optionsListCacheResolver ); From a87f33d7aa2c8eac177d5fffdd632e98256bbdf6 Mon Sep 17 00:00:00 2001 From: Hannah Mudge Date: Fri, 4 Nov 2022 14:06:15 -0600 Subject: [PATCH 33/33] Fix wording of tooltip --- .../public/options_list/components/options_list_strings.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 76b47fb8cbeb8..5db1ddeae21b0 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 @@ -51,8 +51,7 @@ export const OptionsListStrings = { }), getHideExistsQueryTooltip: () => i18n.translate('controls.optionsList.editor.hideExistsQueryTooltip', { - defaultMessage: - 'The exists query will only return documents that contain an indexed value for the given field.', + defaultMessage: 'Returns the documents that contain an indexed value for the field.', }), }, popover: {