mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[Dashboard] [Controls] Add excludes
toggle to options list (#142780)
* Add buttons with no functionality * Added basic negate functionality * Add `NOT` text when negated * Clean up * Add jest and functional tests * Fix merge conflicts * Rename `negate` to `exclude` * Fix `unsaved changes` bug * Move erase button back to beside search * Clean up * Add chaining functional tests * Fix other unsaved changes bug * Fix mobile view of popover * Add option to disable exclude/include toggle * Prevent unsaved changes bug for options list settings * Add tooltip to run past timeout setting * Address review comments * Rename variable * Set `exclude` to `false` when footer is hidden
This commit is contained in:
parent
a0d237b62c
commit
7dd7a74820
14 changed files with 299 additions and 12 deletions
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* 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 deepEqual from 'fast-deep-equal';
|
||||
import { omit, isEqual } from 'lodash';
|
||||
import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from '../options_list/types';
|
||||
|
||||
import { ControlPanelState } from './types';
|
||||
|
||||
interface DiffSystem {
|
||||
getPanelIsEqual: (initialInput: ControlPanelState, newInput: ControlPanelState) => boolean;
|
||||
}
|
||||
|
||||
export const genericControlPanelDiffSystem: DiffSystem = {
|
||||
getPanelIsEqual: (initialInput, newInput) => {
|
||||
return deepEqual(initialInput, newInput);
|
||||
},
|
||||
};
|
||||
|
||||
export const ControlPanelDiffSystems: {
|
||||
[key: string]: DiffSystem;
|
||||
} = {
|
||||
[OPTIONS_LIST_CONTROL]: {
|
||||
getPanelIsEqual: (initialInput, newInput) => {
|
||||
if (!deepEqual(omit(initialInput, 'explicitInput'), omit(newInput, 'explicitInput'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const {
|
||||
exclude: excludeA,
|
||||
selectedOptions: selectedA,
|
||||
singleSelect: singleSelectA,
|
||||
hideExclude: hideExcludeA,
|
||||
runPastTimeout: runPastTimeoutA,
|
||||
...inputA
|
||||
}: Partial<OptionsListEmbeddableInput> = initialInput.explicitInput;
|
||||
const {
|
||||
exclude: excludeB,
|
||||
selectedOptions: selectedB,
|
||||
singleSelect: singleSelectB,
|
||||
hideExclude: hideExcludeB,
|
||||
runPastTimeout: runPastTimeoutB,
|
||||
...inputB
|
||||
}: Partial<OptionsListEmbeddableInput> = newInput.explicitInput;
|
||||
|
||||
return (
|
||||
Boolean(excludeA) === Boolean(excludeB) &&
|
||||
Boolean(singleSelectA) === Boolean(singleSelectB) &&
|
||||
Boolean(hideExcludeA) === Boolean(hideExcludeB) &&
|
||||
Boolean(runPastTimeoutA) === Boolean(runPastTimeoutB) &&
|
||||
isEqual(selectedA ?? [], selectedB ?? []) &&
|
||||
deepEqual(inputA, inputB)
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
|
@ -9,7 +9,7 @@
|
|||
import { SerializableRecord } from '@kbn/utility-types';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
|
||||
import { pick } from 'lodash';
|
||||
import { pick, omit, xor } from 'lodash';
|
||||
import { ControlGroupInput } from '..';
|
||||
import {
|
||||
DEFAULT_CONTROL_GROW,
|
||||
|
@ -17,6 +17,10 @@ import {
|
|||
DEFAULT_CONTROL_WIDTH,
|
||||
} from './control_group_constants';
|
||||
import { PersistableControlGroupInput, RawControlGroupAttributes } from './types';
|
||||
import {
|
||||
ControlPanelDiffSystems,
|
||||
genericControlPanelDiffSystem,
|
||||
} from './control_group_panel_diff_system';
|
||||
|
||||
const safeJSONParse = <OutType>(jsonString?: string): OutType | undefined => {
|
||||
if (!jsonString && typeof jsonString !== 'string') return;
|
||||
|
@ -54,10 +58,40 @@ export const persistableControlGroupInputIsEqual = (
|
|||
...defaultInput,
|
||||
...pick(b, ['panels', 'chainingSystem', 'controlStyle', 'ignoreParentSettings']),
|
||||
};
|
||||
if (deepEqual(inputA, inputB)) return true;
|
||||
|
||||
if (
|
||||
getPanelsAreEqual(inputA.panels, inputB.panels) &&
|
||||
deepEqual(omit(inputA, 'panels'), omit(inputB, 'panels'))
|
||||
)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getPanelsAreEqual = (
|
||||
originalPanels: PersistableControlGroupInput['panels'],
|
||||
newPanels: PersistableControlGroupInput['panels']
|
||||
) => {
|
||||
const originalPanelIds = Object.keys(originalPanels);
|
||||
const newPanelIds = Object.keys(newPanels);
|
||||
const panelIdDiff = xor(originalPanelIds, newPanelIds);
|
||||
if (panelIdDiff.length > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const panelId of newPanelIds) {
|
||||
const newPanelType = newPanels[panelId].type;
|
||||
const panelIsEqual = ControlPanelDiffSystems[newPanelType]
|
||||
? ControlPanelDiffSystems[newPanelType].getPanelIsEqual(
|
||||
originalPanels[panelId],
|
||||
newPanels[panelId]
|
||||
)
|
||||
: genericControlPanelDiffSystem.getPanelIsEqual(originalPanels[panelId], newPanels[panelId]);
|
||||
if (!panelIsEqual) return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const controlGroupInputToRawControlGroupAttributes = (
|
||||
controlGroupInput: Omit<ControlGroupInput, 'id'>
|
||||
): RawControlGroupAttributes => {
|
||||
|
|
|
@ -28,6 +28,8 @@ const mockOptionsListEmbeddableInput = {
|
|||
selectedOptions: [],
|
||||
runPastTimeout: false,
|
||||
singleSelect: false,
|
||||
allowExclude: false,
|
||||
exclude: false,
|
||||
} as OptionsListEmbeddableInput;
|
||||
|
||||
const mockOptionsListOutput = {
|
||||
|
|
|
@ -17,6 +17,8 @@ export interface OptionsListEmbeddableInput extends DataControlInput {
|
|||
selectedOptions?: string[];
|
||||
runPastTimeout?: boolean;
|
||||
singleSelect?: boolean;
|
||||
hideExclude?: boolean;
|
||||
exclude?: boolean;
|
||||
}
|
||||
|
||||
export type OptionsListField = FieldSpec & {
|
||||
|
|
|
@ -11,7 +11,13 @@ import classNames from 'classnames';
|
|||
import { debounce, isEmpty } from 'lodash';
|
||||
import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react';
|
||||
|
||||
import { EuiFilterButton, EuiFilterGroup, EuiPopover, useResizeObserver } from '@elastic/eui';
|
||||
import {
|
||||
EuiFilterButton,
|
||||
EuiFilterGroup,
|
||||
EuiPopover,
|
||||
EuiTextColor,
|
||||
useResizeObserver,
|
||||
} from '@elastic/eui';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
|
@ -43,6 +49,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub
|
|||
const controlStyle = select((state) => state.explicitInput.controlStyle);
|
||||
const singleSelect = select((state) => state.explicitInput.singleSelect);
|
||||
const id = select((state) => state.explicitInput.id);
|
||||
const exclude = select((state) => state.explicitInput.exclude);
|
||||
|
||||
const loading = select((state) => state.output.loading);
|
||||
|
||||
|
@ -75,6 +82,11 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub
|
|||
validSelectionsCount: validSelections?.length,
|
||||
selectionDisplayNode: (
|
||||
<>
|
||||
{exclude && (
|
||||
<EuiTextColor color="danger">
|
||||
<b>{OptionsListStrings.control.getNegate()}</b>{' '}
|
||||
</EuiTextColor>
|
||||
)}
|
||||
{validSelections && (
|
||||
<span>{validSelections?.join(OptionsListStrings.control.getSeparator())}</span>
|
||||
)}
|
||||
|
@ -86,7 +98,7 @@ export const OptionsListControl = ({ typeaheadSubject }: { typeaheadSubject: Sub
|
|||
</>
|
||||
),
|
||||
};
|
||||
}, [validSelections, invalidSelections]);
|
||||
}, [exclude, validSelections, invalidSelections]);
|
||||
|
||||
const button = (
|
||||
<div className="optionsList--filterBtnWrapper" ref={resizeRef}>
|
||||
|
|
|
@ -8,7 +8,8 @@
|
|||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { EuiFormRow, EuiSwitch } from '@elastic/eui';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIconTip, EuiSwitch } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
|
||||
import { OptionsListStrings } from './options_list_strings';
|
||||
import { ControlEditorProps, OptionsListEmbeddableInput } from '../..';
|
||||
|
@ -16,6 +17,7 @@ import { ControlEditorProps, OptionsListEmbeddableInput } from '../..';
|
|||
interface OptionsListEditorState {
|
||||
singleSelect?: boolean;
|
||||
runPastTimeout?: boolean;
|
||||
hideExclude?: boolean;
|
||||
}
|
||||
|
||||
export const OptionsListEditorOptions = ({
|
||||
|
@ -25,6 +27,7 @@ export const OptionsListEditorOptions = ({
|
|||
const [state, setState] = useState<OptionsListEditorState>({
|
||||
singleSelect: initialInput?.singleSelect,
|
||||
runPastTimeout: initialInput?.runPastTimeout,
|
||||
hideExclude: initialInput?.hideExclude,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -41,14 +44,40 @@ export const OptionsListEditorOptions = ({
|
|||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<EuiSwitch
|
||||
label={OptionsListStrings.editor.getRunPastTimeoutTitle()}
|
||||
checked={Boolean(state.runPastTimeout)}
|
||||
label={OptionsListStrings.editor.getHideExcludeTitle()}
|
||||
checked={!state.hideExclude}
|
||||
onChange={() => {
|
||||
onChange({ runPastTimeout: !state.runPastTimeout });
|
||||
setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout }));
|
||||
onChange({ hideExclude: !state.hideExclude });
|
||||
setState((s) => ({ ...s, hideExclude: !s.hideExclude }));
|
||||
if (initialInput?.exclude) onChange({ exclude: false });
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiFormRow>
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSwitch
|
||||
label={OptionsListStrings.editor.getRunPastTimeoutTitle()}
|
||||
checked={Boolean(state.runPastTimeout)}
|
||||
onChange={() => {
|
||||
onChange({ runPastTimeout: !state.runPastTimeout });
|
||||
setState((s) => ({ ...s, runPastTimeout: !s.runPastTimeout }));
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem
|
||||
grow={false}
|
||||
css={css`
|
||||
margin-top: 0px !important;
|
||||
`}
|
||||
>
|
||||
<EuiIconTip
|
||||
content={OptionsListStrings.editor.getRunPastTimeoutTooltip()}
|
||||
position="right"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -97,4 +97,22 @@ describe('Options list popover', () => {
|
|||
expect(child.text()).toBe(selections[i]);
|
||||
});
|
||||
});
|
||||
|
||||
test('should default to exclude = false', () => {
|
||||
const popover = 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({
|
||||
explicitInput: { exclude: true },
|
||||
});
|
||||
const includeButton = findTestSubject(popover, 'optionsList__includeResults');
|
||||
const excludeButton = findTestSubject(popover, 'optionsList__excludeResults');
|
||||
expect(includeButton.prop('checked')).toBeFalsy();
|
||||
expect(excludeButton.prop('checked')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -22,7 +22,11 @@ import {
|
|||
EuiBadge,
|
||||
EuiIcon,
|
||||
EuiTitle,
|
||||
EuiPopoverFooter,
|
||||
EuiButtonGroup,
|
||||
useEuiBackgroundColor,
|
||||
} from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { useReduxEmbeddableContext } from '@kbn/presentation-util-plugin/public';
|
||||
|
||||
import { optionsListReducers } from '../options_list_reducers';
|
||||
|
@ -34,12 +38,23 @@ export interface OptionsListPopoverProps {
|
|||
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: { selectOption, deselectOption, clearSelections, replaceSelection },
|
||||
actions: { selectOption, deselectOption, clearSelections, replaceSelection, setExclude },
|
||||
} = useReduxEmbeddableContext<OptionsListReduxState, typeof optionsListReducers>();
|
||||
|
||||
const dispatch = useEmbeddableDispatch();
|
||||
|
@ -52,8 +67,10 @@ 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 singleSelect = select((state) => state.explicitInput.singleSelect);
|
||||
const title = select((state) => state.explicitInput.title);
|
||||
const exclude = select((state) => state.explicitInput.exclude);
|
||||
|
||||
const loading = select((state) => state.output.loading);
|
||||
|
||||
|
@ -65,6 +82,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop
|
|||
);
|
||||
|
||||
const [showOnlySelected, setShowOnlySelected] = useState(false);
|
||||
const euiBackgroundColor = useEuiBackgroundColor('subdued');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -77,6 +95,7 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop
|
|||
direction="row"
|
||||
justifyContent="spaceBetween"
|
||||
alignItems="center"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem>
|
||||
<EuiFieldSearch
|
||||
|
@ -248,6 +267,25 @@ export const OptionsListPopover = ({ width, updateSearchString }: OptionsListPop
|
|||
</>
|
||||
)}
|
||||
</div>
|
||||
{!hideExclude && (
|
||||
<EuiPopoverFooter
|
||||
paddingSize="s"
|
||||
css={css`
|
||||
background-color: ${euiBackgroundColor};
|
||||
`}
|
||||
>
|
||||
<EuiButtonGroup
|
||||
legend={OptionsListStrings.popover.getIncludeExcludeLegend()}
|
||||
options={aggregationToggleButtons}
|
||||
idSelected={exclude ? 'optionsList__excludeResults' : 'optionsList__includeResults'}
|
||||
onChange={(optionId) =>
|
||||
dispatch(setExclude(optionId === 'optionsList__excludeResults'))
|
||||
}
|
||||
buttonSize="compressed"
|
||||
data-test-subj="optionsList__includeExcludeButtonGroup"
|
||||
/>
|
||||
</EuiPopoverFooter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -18,6 +18,10 @@ export const OptionsListStrings = {
|
|||
i18n.translate('controls.optionsList.control.placeholder', {
|
||||
defaultMessage: 'Any',
|
||||
}),
|
||||
getNegate: () =>
|
||||
i18n.translate('controls.optionsList.control.negate', {
|
||||
defaultMessage: 'NOT',
|
||||
}),
|
||||
},
|
||||
editor: {
|
||||
getAllowMultiselectTitle: () =>
|
||||
|
@ -26,7 +30,16 @@ export const OptionsListStrings = {
|
|||
}),
|
||||
getRunPastTimeoutTitle: () =>
|
||||
i18n.translate('controls.optionsList.editor.runPastTimeout', {
|
||||
defaultMessage: 'Run past timeout',
|
||||
defaultMessage: 'Ignore timeout for results',
|
||||
}),
|
||||
getRunPastTimeoutTooltip: () =>
|
||||
i18n.translate('controls.optionsList.editor.runPastTimeout.tooltip', {
|
||||
defaultMessage:
|
||||
'Wait to display results until the list is complete. This setting is useful for large data sets, but the results might take longer to populate.',
|
||||
}),
|
||||
getHideExcludeTitle: () =>
|
||||
i18n.translate('controls.optionsList.editor.hideExclude', {
|
||||
defaultMessage: 'Allow selections to be excluded',
|
||||
}),
|
||||
},
|
||||
popover: {
|
||||
|
@ -86,5 +99,17 @@ export const OptionsListStrings = {
|
|||
'{selectedOptions} selected {selectedOptions, plural, one {option} other {options}} {selectedOptions, plural, one {is} other {are}} ignored because {selectedOptions, plural, one {it is} other {they are}} no longer in the data.',
|
||||
values: { selectedOptions },
|
||||
}),
|
||||
getIncludeLabel: () =>
|
||||
i18n.translate('controls.optionsList.popover.includeLabel', {
|
||||
defaultMessage: 'Include',
|
||||
}),
|
||||
getExcludeLabel: () =>
|
||||
i18n.translate('controls.optionsList.popover.excludeLabel', {
|
||||
defaultMessage: 'Exclude',
|
||||
}),
|
||||
getIncludeExcludeLegend: () =>
|
||||
i18n.translate('controls.optionsList.popover.excludeOptionsLegend', {
|
||||
defaultMessage: 'Include or exclude selections',
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
|
|
@ -134,6 +134,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
timeslice: newInput.timeslice,
|
||||
filters: newInput.filters,
|
||||
query: newInput.query,
|
||||
exclude: newInput.exclude,
|
||||
})),
|
||||
distinctUntilChanged(diffDataFetchProps)
|
||||
);
|
||||
|
@ -153,7 +154,11 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
**/
|
||||
this.subscriptions.add(
|
||||
this.getInput$()
|
||||
.pipe(distinctUntilChanged((a, b) => isEqual(a.selectedOptions, b.selectedOptions)))
|
||||
.pipe(
|
||||
distinctUntilChanged(
|
||||
(a, b) => isEqual(a.selectedOptions, b.selectedOptions) && a.exclude === b.exclude
|
||||
)
|
||||
)
|
||||
.subscribe(async ({ selectedOptions: newSelectedOptions }) => {
|
||||
const {
|
||||
actions: {
|
||||
|
@ -364,6 +369,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
private buildFilter = async () => {
|
||||
const { getState } = this.reduxEmbeddableTools;
|
||||
const { validSelections } = getState().componentState ?? {};
|
||||
const { exclude } = this.getInput();
|
||||
|
||||
if (!validSelections || isEmpty(validSelections)) {
|
||||
return [];
|
||||
|
@ -379,6 +385,7 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
|
|||
}
|
||||
|
||||
newFilter.meta.key = field?.name;
|
||||
if (exclude) newFilter.meta.negate = true;
|
||||
return [newFilter];
|
||||
};
|
||||
|
||||
|
|
|
@ -64,6 +64,9 @@ export const optionsListReducers = {
|
|||
clearSelections: (state: WritableDraft<OptionsListReduxState>) => {
|
||||
if (state.explicitInput.selectedOptions) state.explicitInput.selectedOptions = [];
|
||||
},
|
||||
setExclude: (state: WritableDraft<OptionsListReduxState>, action: PayloadAction<boolean>) => {
|
||||
state.explicitInput.exclude = action.payload;
|
||||
},
|
||||
clearValidAndInvalidSelections: (state: WritableDraft<OptionsListReduxState>) => {
|
||||
state.componentState.invalidSelections = [];
|
||||
state.componentState.validSelections = [];
|
||||
|
|
|
@ -123,6 +123,27 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
]);
|
||||
});
|
||||
|
||||
it('Excluding selections in the first control will validate the second and third controls', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[0]);
|
||||
await dashboardControls.optionsListPopoverSetIncludeSelections(false);
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
|
||||
|
||||
await ensureAvailableOptionsEql(controlIds[1], ['Tiger', 'sylvester']);
|
||||
await ensureAvailableOptionsEql(controlIds[2], ['meow', 'hiss']);
|
||||
});
|
||||
|
||||
it('Excluding all options of first control removes all options in second and third controls', async () => {
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[0]);
|
||||
await dashboardControls.optionsListPopoverSelectOption('cat');
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
|
||||
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[1]);
|
||||
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0);
|
||||
await dashboardControls.optionsListOpenPopover(controlIds[2]);
|
||||
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(0);
|
||||
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[2]);
|
||||
});
|
||||
|
||||
describe('Hierarchical chaining off', async () => {
|
||||
before(async () => {
|
||||
await dashboardControls.updateChainingSystem('NONE');
|
||||
|
@ -130,6 +151,7 @@ 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]);
|
||||
|
||||
|
|
|
@ -385,6 +385,27 @@ 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();
|
||||
|
|
|
@ -376,6 +376,19 @@ export class DashboardPageControls extends FtrService {
|
|||
await this.testSubjects.click(`optionsList-control-clear-all-selections`);
|
||||
}
|
||||
|
||||
public async optionsListPopoverSetIncludeSelections(include: boolean) {
|
||||
this.log.debug(`exclude selections`);
|
||||
await this.optionsListPopoverAssertOpen();
|
||||
|
||||
const buttonGroup = await this.testSubjects.find('optionsList__includeExcludeButtonGroup');
|
||||
await (
|
||||
await this.find.descendantDisplayedByCssSelector(
|
||||
include ? '[data-text="Include"]' : '[data-text="Exclude"]',
|
||||
buttonGroup
|
||||
)
|
||||
).click();
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------
|
||||
Control editor flyout
|
||||
----------------------------------------------------------- */
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue