[Controls] Move "clear selections" to hover action (#159526)

Closes https://github.com/elastic/kibana/issues/159395
Closes https://github.com/elastic/kibana/issues/153383

## Summary

This PR moves the "clear selections" button for all controls (options
list, range slider, and time slider) from inside their respective
popovers to a general hover action - this not only saves users a click
for this common interaction (which has actually been brought in user
feedback up as a downside of the current controls compared to the legacy
controls), it also allows us to fully move forward with migrating the
range slider control to the `EuiDualRange` component. This will be done
in a follow up PR, which should both (1) clean up our range slider code
significantly and (2) fix the [bug discussed
here](https://github.com/elastic/kibana/pull/159271#pullrequestreview-1477930356).
The related issue can be tracked
[here](https://github.com/elastic/kibana/issues/159724), since we might
not be able to get to it right away.

This "clear selections" action is available in both view and edit mode,
like so:

|  | Edit mode | View mode |
|--------|--------|--------|
| **Range slider** |
![image](83cb1e1a-0b20-43aa-a37b-14484b5f4945)
|
![image](0d28ce03-5242-4f3a-8a05-d447bca50ddb)
|
| **Options list** |
![image](066257f6-c0ce-4e33-a193-5bbc62e341a6)
|
![image](d1ec124c-f5ee-4137-9eb9-33e06d522435)
|
| **Time slider** |
![image](33b8bb80-fa0c-4281-ae81-f1e1b44086f3)
|
![image](bd7c41ae-706c-45f3-8b49-9bd4d259e5cf)
|

You may notice in the above screenshots that the "delete" action is now
represented with a red trash icon rather than a red cross, and the
tooltip text was also changed to use the word "Delete" rather than the
word "Remove" - these changes were both made to be more consistent with
the "Delete panel" action available on dashboards:

| Delete control - Before | Delete control - After | Delete panel |
|--------|--------|--------|
| ![Screenshot 2023-06-13 at 5 32 22
PM](2600b197-653b-43ea-a043-a50be7e6a796)
|
![image](5ef80380-2575-45fc-ba11-c59f3f252ac3)
| <img
src="a7f65777-45cf-44f2-96a7-f1042cb25e02"/>
|

Beyond these changes, I also made a few quick changes to the time slider
control, including:
1. Fixing the appearance so that the background is once again white, as
described
[here](https://github.com/elastic/kibana/pull/159526#discussion_r1229792071)
2. Adding comparison logic so that clearing selections no longer causes
unsaved changes unnecessarily, as described
[here](https://github.com/elastic/kibana/pull/159526#discussion_r1229789753)

### Videos

**Before**


96365c85-748e-4fd7-ae5d-589aa11a23ef


**After**


68352559-e71b-4b5e-8709-587016f0b35a



### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Hannah Mudge 2023-06-20 16:53:10 -06:00 committed by GitHub
parent 69849ee03b
commit f1dc1e1869
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 207 additions and 148 deletions

View file

@ -11,6 +11,7 @@ import { omit, isEqual } from 'lodash';
import { OPTIONS_LIST_DEFAULT_SORT } from '../options_list/suggestions_sorting';
import { OptionsListEmbeddableInput, OPTIONS_LIST_CONTROL } from '../options_list/types';
import { RangeSliderEmbeddableInput, RANGE_SLIDER_CONTROL } from '../range_slider/types';
import { TimeSliderControlEmbeddableInput, TIME_SLIDER_CONTROL } from '../time_slider/types';
import { ControlPanelState } from './types';
@ -88,4 +89,29 @@ export const ControlPanelDiffSystems: {
);
},
},
[TIME_SLIDER_CONTROL]: {
getPanelIsEqual: (initialInput, newInput) => {
if (!deepEqual(omit(initialInput, 'explicitInput'), omit(newInput, 'explicitInput'))) {
return false;
}
const {
isAnchored: isAnchoredA,
timesliceStartAsPercentageOfTimeRange: startA,
timesliceEndAsPercentageOfTimeRange: endA,
}: Partial<TimeSliderControlEmbeddableInput> = initialInput.explicitInput;
const {
isAnchored: isAnchoredB,
timesliceStartAsPercentageOfTimeRange: startB,
timesliceEndAsPercentageOfTimeRange: endB,
}: Partial<TimeSliderControlEmbeddableInput> = newInput.explicitInput;
return (
Boolean(isAnchoredA) === Boolean(isAnchoredB) &&
Boolean(startA) === Boolean(startB) &&
startA === startB &&
Boolean(endA) === Boolean(endB) &&
endA === endB
);
},
},
};

View file

@ -0,0 +1,78 @@
/*
* 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, { SyntheticEvent } from 'react';
import { EuiButtonIcon, EuiToolTip } from '@elastic/eui';
import { isErrorEmbeddable } from '@kbn/embeddable-plugin/public';
import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';
import { ACTION_CLEAR_CONTROL } from '.';
import { ControlGroupStrings } from '../control_group_strings';
import { ControlEmbeddable, DataControlInput, isClearableControl } from '../../types';
import { isControlGroup } from '../embeddable/control_group_helpers';
export interface ClearControlActionContext {
embeddable: ControlEmbeddable<DataControlInput>;
}
export class ClearControlAction implements Action<ClearControlActionContext> {
public readonly type = ACTION_CLEAR_CONTROL;
public readonly id = ACTION_CLEAR_CONTROL;
public order = 1;
constructor() {}
public readonly MenuItem = ({ context }: { context: ClearControlActionContext }) => {
return (
<EuiToolTip content={this.getDisplayName(context)}>
<EuiButtonIcon
data-test-subj={`control-action-${context.embeddable.id}-erase`}
aria-label={this.getDisplayName(context)}
iconType={this.getIconType(context)}
onClick={(event: SyntheticEvent<HTMLButtonElement>) => {
(event.target as HTMLButtonElement).blur();
this.execute(context);
}}
color="text"
/>
</EuiToolTip>
);
};
public getDisplayName({ embeddable }: ClearControlActionContext) {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
throw new IncompatibleActionError();
}
return ControlGroupStrings.floatingActions.getClearButtonTitle();
}
public getIconType({ embeddable }: ClearControlActionContext) {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
throw new IncompatibleActionError();
}
return 'eraser';
}
public async isCompatible({ embeddable }: ClearControlActionContext) {
if (isErrorEmbeddable(embeddable)) return false;
const controlGroup = embeddable.parent;
return Boolean(controlGroup && isControlGroup(controlGroup)) && isClearableControl(embeddable);
}
public async execute({ embeddable }: ClearControlActionContext) {
if (
!embeddable.parent ||
!isControlGroup(embeddable.parent) ||
!isClearableControl(embeddable)
) {
throw new IncompatibleActionError();
}
embeddable.clearSelections();
}
}

View file

@ -25,7 +25,7 @@ export interface DeleteControlActionContext {
export class DeleteControlAction implements Action<DeleteControlActionContext> {
public readonly type = ACTION_DELETE_CONTROL;
public readonly id = ACTION_DELETE_CONTROL;
public order = 2;
public order = 100; // should always be last
private openConfirm;
@ -60,7 +60,7 @@ export class DeleteControlAction implements Action<DeleteControlActionContext> {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
throw new IncompatibleActionError();
}
return 'cross';
return 'trash';
}
public async isCompatible({ embeddable }: DeleteControlActionContext) {

View file

@ -29,7 +29,7 @@ export interface EditControlActionContext {
export class EditControlAction implements Action<EditControlActionContext> {
public readonly type = ACTION_EDIT_CONTROL;
public readonly id = ACTION_EDIT_CONTROL;
public order = 1;
public order = 2;
private getEmbeddableFactory;
private openFlyout;

View file

@ -7,4 +7,5 @@
*/
export const ACTION_EDIT_CONTROL = 'editControl';
export const ACTION_CLEAR_CONTROL = 'clearControl';
export const ACTION_DELETE_CONTROL = 'deleteControl';

View file

@ -82,6 +82,7 @@ export const ControlGroup = () => {
});
}
}
(document.activeElement as HTMLElement)?.blur();
setDraggingId(null);
};

View file

@ -285,11 +285,16 @@ export const ControlGroupStrings = {
floatingActions: {
getEditButtonTitle: () =>
i18n.translate('controls.controlGroup.floatingActions.editTitle', {
defaultMessage: 'Edit control',
defaultMessage: 'Edit',
}),
getRemoveButtonTitle: () =>
i18n.translate('controls.controlGroup.floatingActions.removeTitle', {
defaultMessage: 'Remove control',
defaultMessage: 'Delete',
}),
getClearButtonTitle: () =>
i18n.translate('controls.controlGroup.floatingActions.clearTitle', {
defaultMessage: 'Clear',
}),
},
ariaActions: {

View file

@ -134,21 +134,6 @@ export const OptionsListPopoverActionBar = ({
/>
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
position="top"
content={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()}
>
<EuiButtonIcon
size="xs"
color="danger"
iconType="eraser"
onClick={() => optionsList.dispatch.clearSelections({})}
data-test-subj="optionsList-control-clear-all-selections"
aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()}
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -129,10 +129,6 @@ export const OptionsListStrings = {
i18n.translate('controls.optionsList.popover.selectedOptionsTitle', {
defaultMessage: 'Show only selected options',
}),
getClearAllSelectionsButtonTitle: () =>
i18n.translate('controls.optionsList.popover.clearAllSelectionsTitle', {
defaultMessage: 'Clear selections',
}),
searchPlaceholder: {
prefix: {
getPlaceholderText: () =>

View file

@ -35,10 +35,11 @@ import {
OptionsListEmbeddableInput,
} from '../..';
import { pluginServices } from '../../services';
import { MIN_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
import { IClearableControl } from '../../types';
import { OptionsListControl } from '../components/options_list_control';
import { ControlsDataViewsService } from '../../services/data_views/types';
import { ControlsOptionsListService } from '../../services/options_list/types';
import { MIN_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
import { getDefaultComponentState, optionsListReducers } from '../options_list_reducers';
const diffDataFetchProps = (
@ -76,7 +77,10 @@ type OptionsListReduxEmbeddableTools = ReduxEmbeddableTools<
typeof optionsListReducers
>;
export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput, ControlOutput> {
export class OptionsListEmbeddable
extends Embeddable<OptionsListEmbeddableInput, ControlOutput>
implements IClearableControl
{
public readonly type = OPTIONS_LIST_CONTROL;
public deferEmbeddableLoad = true;
@ -411,6 +415,10 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
return [newFilter];
};
public clearSelections() {
this.dispatch.clearSelections({});
}
reload = () => {
// clear cache when reload is requested
this.optionsListService.clearOptionsListCache();

View file

@ -124,6 +124,11 @@ export class ControlsPlugin
const editControlAction = new EditControlAction(deleteControlAction);
uiActions.registerAction(editControlAction);
uiActions.attachAction(PANEL_HOVER_TRIGGER, editControlAction.id);
const { ClearControlAction } = await import('./control_group/actions/clear_control_action');
const clearControlAction = new ClearControlAction();
uiActions.registerAction(clearControlAction);
uiActions.attachAction(PANEL_HOVER_TRIGGER, clearControlAction.id);
});
const { getControlFactory, getControlTypes } = controlsService;

View file

@ -9,15 +9,7 @@
import React, { FC, ComponentProps, Ref, useEffect, useState, useMemo } from 'react';
import useMount from 'react-use/lib/useMount';
import {
EuiPopoverTitle,
EuiFlexGroup,
EuiFlexItem,
EuiDualRange,
EuiToolTip,
EuiButtonIcon,
EuiText,
} from '@elastic/eui';
import { EuiPopoverTitle, EuiDualRange, EuiText } from '@elastic/eui';
import type { EuiDualRangeClass } from '@elastic/eui/src/components/form/range/dual_range';
import { pluginServices } from '../../services';
@ -101,55 +93,34 @@ export const RangeSliderPopover: FC<{
}, [min, max]);
return (
<>
<div data-test-subj="rangeSlider__popover">
<EuiPopoverTitle paddingSize="s">{title}</EuiPopoverTitle>
<EuiFlexGroup
className="rangeSlider__actions"
gutterSize="none"
data-test-subj="rangeSlider-control-actions"
responsive={false}
>
<EuiFlexItem>
{min !== -Infinity && max !== Infinity ? (
<EuiDualRange
id={id}
min={rangeSliderMin}
max={rangeSliderMax}
onChange={([minSelection, maxSelection]) => {
onChange([String(minSelection), String(maxSelection)]);
}}
value={value}
ticks={ticks}
levels={levels}
showTicks
fullWidth
ref={rangeRef}
data-test-subj="rangeSlider__slider"
/>
) : isInvalid ? (
<EuiText size="s" data-test-subj="rangeSlider__helpText">
{RangeSliderStrings.popover.getNoDataHelpText()}
</EuiText>
) : (
<EuiText size="s" data-test-subj="rangeSlider__helpText">
{RangeSliderStrings.popover.getNoAvailableDataHelpText()}
</EuiText>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={RangeSliderStrings.popover.getClearRangeButtonTitle()}>
<EuiButtonIcon
iconType="eraser"
color="danger"
onClick={() => {
rangeSlider.dispatch.setSelectedRange(['', '']);
}}
aria-label={RangeSliderStrings.popover.getClearRangeButtonTitle()}
data-test-subj="rangeSlider__clearRangeButton"
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
</>
{min !== -Infinity && max !== Infinity ? (
<EuiDualRange
id={id}
min={rangeSliderMin}
max={rangeSliderMax}
onChange={([minSelection, maxSelection]) => {
onChange([String(minSelection), String(maxSelection)]);
}}
value={value}
ticks={ticks}
levels={levels}
showTicks
fullWidth
ref={rangeRef}
data-test-subj="rangeSlider__slider"
/>
) : isInvalid ? (
<EuiText size="s" data-test-subj="rangeSlider__helpText">
{RangeSliderStrings.popover.getNoDataHelpText()}
</EuiText>
) : (
<EuiText size="s" data-test-subj="rangeSlider__helpText">
{RangeSliderStrings.popover.getNoAvailableDataHelpText()}
</EuiText>
)}
</div>
);
};

View file

@ -10,10 +10,6 @@ import { i18n } from '@kbn/i18n';
export const RangeSliderStrings = {
popover: {
getClearRangeButtonTitle: () =>
i18n.translate('controls.rangeSlider.popover.clearRangeTitle', {
defaultMessage: 'Clear range',
}),
getNoDataHelpText: () =>
i18n.translate('controls.rangeSlider.popover.noDataHelpText', {
defaultMessage: 'Selected range resulted in no data. No filter was applied.',

View file

@ -23,8 +23,8 @@ import {
Filter,
} from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { DataView, DataViewField } from '@kbn/data-views-plugin/public';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';
@ -36,6 +36,7 @@ import {
} from '../..';
import { pluginServices } from '../../services';
import { RangeSliderReduxState } from '../types';
import { IClearableControl } from '../../types';
import { ControlsDataService } from '../../services/data/types';
import { RangeSliderControl } from '../components/range_slider_control';
import { ControlsDataViewsService } from '../../services/data_views/types';
@ -83,7 +84,10 @@ type RangeSliderReduxEmbeddableTools = ReduxEmbeddableTools<
typeof rangeSliderReducers
>;
export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput, ControlOutput> {
export class RangeSliderEmbeddable
extends Embeddable<RangeSliderEmbeddableInput, ControlOutput>
implements IClearableControl
{
public readonly type = RANGE_SLIDER_CONTROL;
public deferEmbeddableLoad = true;
@ -421,6 +425,10 @@ export class RangeSliderEmbeddable extends Embeddable<RangeSliderEmbeddableInput
});
};
public clearSelections() {
this.dispatch.setSelectedRange(['', '']);
}
public reload = async () => {
try {
await this.runRangeSliderQuery();

View file

@ -33,7 +33,7 @@
}
.euiText {
background-color: $euiFormBackgroundColor;
background-color: $euiFormBackgroundColor !important;
}
.timeSlider__anchorText {

View file

@ -77,9 +77,6 @@ export const TimeSlider: FC<Props> = (props: Props) => {
rangeRef={rangeRef}
value={[from, to]}
onChange={props.onChange}
onClear={() => {
props.onChange([timeRangeMin, timeRangeMax]);
}}
stepSize={stepSize}
ticks={ticks}
timeRangeMin={timeRangeMin}

View file

@ -18,7 +18,6 @@ import { EuiDualRangeRef, TimeSliderSlidingWindowRange } from './time_slider_sli
interface Props {
value: [number, number];
onChange: (value?: [number, number]) => void;
onClear: () => void;
stepSize: number;
ticks: EuiRangeTick[];
timeRangeMin: number;
@ -90,17 +89,6 @@ export function TimeSliderPopoverContent(props: Props) {
</EuiToolTip>
</EuiFlexItem>
<EuiFlexItem>{rangeInput}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip content={TimeSliderStrings.control.getClearSelection()}>
<EuiButtonIcon
iconType="eraser"
color="danger"
onClick={props.onClear}
aria-label={TimeSliderStrings.control.getClearSelection()}
data-test-subj="timeSlider__clearTimeButton"
/>
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>
);
}

View file

@ -10,10 +10,6 @@ import { i18n } from '@kbn/i18n';
export const TimeSliderStrings = {
control: {
getClearSelection: () =>
i18n.translate('controls.timeSlider.popover.clearTimeTitle', {
defaultMessage: 'Clear time selection',
}),
getPinStart: () =>
i18n.translate('controls.timeSlider.settings.pinStart', {
defaultMessage: 'Pin start',

View file

@ -21,7 +21,7 @@ import { TimeSliderControlEmbeddableInput } from '../../../common/time_slider/ty
import { pluginServices } from '../../services';
import { ControlsSettingsService } from '../../services/settings/types';
import { ControlsDataService } from '../../services/data/types';
import { ControlOutput } from '../../types';
import { ControlOutput, IClearableControl } from '../../types';
import { ControlGroupContainer } from '../../control_group/embeddable/control_group_container';
import { TimeSlider, TimeSliderPrepend } from '../components';
import { timeSliderReducers } from '../time_slider_reducers';
@ -51,10 +51,10 @@ type TimeSliderReduxEmbeddableTools = ReduxEmbeddableTools<
typeof timeSliderReducers
>;
export class TimeSliderControlEmbeddable extends Embeddable<
TimeSliderControlEmbeddableInput,
ControlOutput
> {
export class TimeSliderControlEmbeddable
extends Embeddable<TimeSliderControlEmbeddableInput, ControlOutput>
implements IClearableControl
{
public readonly type = TIME_SLIDER_CONTROL;
public deferEmbeddedLoad = true;
@ -249,6 +249,7 @@ export class TimeSliderControlEmbeddable extends Embeddable<
private onTimesliceChange = (value?: [number, number]) => {
const { timesliceStartAsPercentageOfTimeRange, timesliceEndAsPercentageOfTimeRange } =
this.getTimeSliceAsPercentageOfTimeRange(value);
this.dispatch.setValueAsPercentageOfTimeRange({
timesliceStartAsPercentageOfTimeRange,
timesliceEndAsPercentageOfTimeRange,
@ -354,6 +355,10 @@ export class TimeSliderControlEmbeddable extends Embeddable<
.format(this.getState().componentState.format);
};
public clearSelections() {
this.onTimesliceChange();
}
public render = (node: HTMLElement) => {
if (this.node) {
ReactDOM.unmountComponentAtNode(this.node);

View file

@ -46,6 +46,14 @@ export type ControlEmbeddable<
renderPrepend?: () => ReactNode | undefined;
};
export interface IClearableControl extends ControlEmbeddable {
clearSelections: () => void;
}
export const isClearableControl = (control: ControlEmbeddable): control is IClearableControl => {
return Boolean((control as IClearableControl).clearSelections);
};
/**
* Control embeddable editor types
*/

View file

@ -144,8 +144,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
it('Selecting a conflicting option in the first control will validate the second and third controls', async () => {
await dashboardControls.clearControlSelections(controlIds[0]);
await dashboardControls.optionsListOpenPopover(controlIds[0]);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListPopoverSelectOption('dog');
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
@ -200,8 +201,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]);
await dashboard.waitForRenderComplete();
await dashboardControls.clearControlSelections(controlIds[1]);
await dashboardControls.optionsListOpenPopover(controlIds[1]);
await dashboardControls.optionsListPopoverClearSelections();
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
await dashboardControls.ensureAvailableOptionsEqual(
controlIds[1],
@ -212,8 +213,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
true
);
await dashboardControls.clearControlSelections(controlIds[2]);
await dashboardControls.optionsListOpenPopover(controlIds[2]);
await dashboardControls.optionsListPopoverClearSelections();
expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1);
await dashboardControls.ensureAvailableOptionsEqual(
controlIds[2],

View file

@ -64,9 +64,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
});
after(async () => {
await dashboardControls.optionsListOpenPopover(controlId);
await dashboardControls.optionsListPopoverClearSelections();
await dashboardControls.optionsListEnsurePopoverIsClosed(controlId);
await dashboardControls.clearControlSelections(controlId);
await filterBar.removeAllFilters();
await queryBar.clickQuerySubmitButton();
});

View file

@ -176,7 +176,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
it('can clear out selections by clicking the reset button', async () => {
const firstId = (await dashboardControls.getAllControlIds())[0];
await dashboardControls.rangeSliderClearSelection(firstId);
await dashboardControls.clearControlSelections(firstId);
await dashboardControls.rangeSliderOpenPopover(firstId);
await dashboardControls.validateRange('value', firstId, '', '');
await dashboardControls.rangeSliderEnsurePopoverIsClosed(firstId);
await dashboard.clearUnsavedChanges();

View file

@ -335,6 +335,12 @@ export class DashboardPageControls extends FtrService {
await this.common.clickConfirmOnModal();
}
public async clearControlSelections(controlId: string) {
this.log.debug(`clearing all selections from control ${controlId}`);
await this.hoverOverExistingControl(controlId);
await this.testSubjects.click(`control-action-${controlId}-erase`);
}
public async verifyControlType(controlId: string, expectedType: string) {
let controlButton;
switch (expectedType) {
@ -523,12 +529,6 @@ export class DashboardPageControls extends FtrService {
await this.optionsListPopoverClearSearch();
}
public async optionsListPopoverClearSelections() {
this.log.debug(`clearing all selections from options list`);
await this.optionsListPopoverAssertOpen();
await this.testSubjects.click(`optionsList-control-clear-all-selections`);
}
public async optionsListPopoverSetIncludeSelections(include: boolean) {
this.log.debug(`exclude selections`);
await this.optionsListPopoverAssertOpen();
@ -673,7 +673,7 @@ export class DashboardPageControls extends FtrService {
this.log.debug(`Opening popover for Range Slider: ${controlId}`);
await this.testSubjects.click(`range-slider-control-${controlId}`);
await this.retry.try(async () => {
await this.testSubjects.existOrFail(`rangeSlider-control-actions`);
await this.testSubjects.existOrFail(`rangeSlider__popover`);
});
}
@ -681,13 +681,13 @@ export class DashboardPageControls extends FtrService {
this.log.debug(`Opening popover for Range Slider: ${controlId}`);
const controlLabel = await this.find.byXPath(`//div[@data-control-id='${controlId}']//label`);
await controlLabel.click();
await this.testSubjects.waitForDeleted(`rangeSlider-control-actions`);
await this.testSubjects.waitForDeleted(`rangeSlider__popover`);
}
public async rangeSliderPopoverAssertOpen() {
await this.retry.try(async () => {
if (!(await this.testSubjects.exists(`rangeSlider-control-actions`))) {
throw new Error('options list popover must be open before calling selectOption');
if (!(await this.testSubjects.exists(`rangeSlider__popover`))) {
throw new Error('range slider popover must be open before calling selectOption');
}
});
}
@ -698,13 +698,6 @@ export class DashboardPageControls extends FtrService {
);
}
public async rangeSliderClearSelection(controlId: string) {
this.log.debug(`Clearing range slider selection from control: ${controlId}`);
await this.rangeSliderOpenPopover(controlId);
await this.rangeSliderPopoverAssertOpen();
await this.testSubjects.click('rangeSlider__clearRangeButton');
}
public async validateRange(
compare: 'value' | 'placeholder', // if 'value', compare actual selections; otherwise, compare the default range
controlId: string,

View file

@ -470,6 +470,7 @@ export class MapEmbeddable
timeslice: this.input.timeslice
? { from: this.input.timeslice[0], to: this.input.timeslice[1] }
: undefined,
clearTimeslice: this.input.timeslice === undefined,
forceRefresh,
searchSessionId: this._getSearchSessionId(),
searchSessionMapBuffer: getIsRestore(this._getSearchSessionId())

View file

@ -474,7 +474,6 @@
"controls.optionsList.editor.runPastTimeout.tooltip": "Attendre que la liste soit complète pour afficher les résultats. Ce paramètre est utile pour les ensembles de données volumineux, mais le remplissage des résultats peut prendre plus de temps.",
"controls.optionsList.popover.allOptionsTitle": "Afficher toutes les options",
"controls.optionsList.popover.allowExpensiveQueriesWarning": "Le paramètre de cluster permettant d'autoriser les requêtes lourdes est désactivé, impliquant la désactivation d'autres fonctionnalités.",
"controls.optionsList.popover.clearAllSelectionsTitle": "Effacer les sélections",
"controls.optionsList.popover.empty": "Aucune option trouvée",
"controls.optionsList.popover.endOfOptions": "Les 1 000 premières options disponibles sont affichées. Affichez davantage d'options en recherchant le nom.",
"controls.optionsList.popover.excludeLabel": "Exclure",
@ -494,7 +493,6 @@
"controls.optionsList.popover.sortTitle": "Trier",
"controls.rangeSlider.description": "Ajoutez un contrôle pour la sélection d'une plage de valeurs de champ.",
"controls.rangeSlider.displayName": "Curseur de plage",
"controls.rangeSlider.popover.clearRangeTitle": "Effacer la plage",
"controls.rangeSlider.popover.noAvailableDataHelpText": "Il n'y a aucune donnée à afficher. Ajustez la plage temporelle et les filtres.",
"controls.rangeSlider.popover.noDataHelpText": "La plage sélectionnée n'a généré aucune donnée. Aucun filtre n'a été appliqué.",
"controls.timeSlider.description": "Ajouter un curseur pour la sélection d'une plage temporelle",
@ -502,7 +500,6 @@
"controls.timeSlider.nextLabel": "Fenêtre temporelle suivante",
"controls.timeSlider.pauseLabel": "Pause",
"controls.timeSlider.playLabel": "Lecture",
"controls.timeSlider.popover.clearTimeTitle": "Effacer la sélection de temps",
"controls.timeSlider.previousLabel": "Fenêtre temporelle précédente",
"controls.timeSlider.settings.pinStart": "Épingler le début",
"controls.timeSlider.settings.unpinStart": "Désépingler le début",

View file

@ -474,7 +474,6 @@
"controls.optionsList.editor.runPastTimeout.tooltip": "リストが入力されるまで待機してから、結果を表示します。この設定は大きいデータセットで有用です。ただし、結果の入力に時間がかかる場合があります。",
"controls.optionsList.popover.allOptionsTitle": "すべてのオプションを表示",
"controls.optionsList.popover.allowExpensiveQueriesWarning": "コストがかかるクエリを許可するクラスター設定がオフであるため、一部の機能が無効です。",
"controls.optionsList.popover.clearAllSelectionsTitle": "選択した項目をクリア",
"controls.optionsList.popover.empty": "オプションが見つかりません",
"controls.optionsList.popover.endOfOptions": "上位1,000個の使用可能なオプションが表示されます。その他のオプションを表示するには、名前を検索します。",
"controls.optionsList.popover.excludeLabel": "除外",
@ -494,7 +493,6 @@
"controls.optionsList.popover.sortTitle": "並べ替え",
"controls.rangeSlider.description": "フィールド値の範囲を選択するためのコントロールを追加",
"controls.rangeSlider.displayName": "範囲スライダー",
"controls.rangeSlider.popover.clearRangeTitle": "範囲を消去",
"controls.rangeSlider.popover.noAvailableDataHelpText": "表示するデータがありません。時間範囲とフィルターを調整します。",
"controls.rangeSlider.popover.noDataHelpText": "選択された範囲にはデータがありません。フィルターが適用されませんでした。",
"controls.timeSlider.description": "時間範囲を選択するためのスライダーを追加",
@ -502,7 +500,6 @@
"controls.timeSlider.nextLabel": "次の時間ウィンドウ",
"controls.timeSlider.pauseLabel": "一時停止",
"controls.timeSlider.playLabel": "再生",
"controls.timeSlider.popover.clearTimeTitle": "時間選択のクリア",
"controls.timeSlider.previousLabel": "前の時間ウィンドウ",
"controls.timeSlider.settings.pinStart": "開始をピン留め",
"controls.timeSlider.settings.unpinStart": "開始をピン留め解除",

View file

@ -474,7 +474,6 @@
"controls.optionsList.editor.runPastTimeout.tooltip": "等待显示结果,直到列表完成。此设置用于大型数据集,但可能需要更长时间来填充结果。",
"controls.optionsList.popover.allOptionsTitle": "显示所有选项",
"controls.optionsList.popover.allowExpensiveQueriesWarning": "允许资源密集型查询的集群设置已关闭,因此会禁用某些功能。",
"controls.optionsList.popover.clearAllSelectionsTitle": "清除所选内容",
"controls.optionsList.popover.empty": "找不到选项",
"controls.optionsList.popover.endOfOptions": "显示了前 1,000 个可用选项。通过搜索名称查看更多选项。",
"controls.optionsList.popover.excludeLabel": "排除",
@ -494,7 +493,6 @@
"controls.optionsList.popover.sortTitle": "排序",
"controls.rangeSlider.description": "添加用于选择字段值范围的控件。",
"controls.rangeSlider.displayName": "范围滑块",
"controls.rangeSlider.popover.clearRangeTitle": "清除范围",
"controls.rangeSlider.popover.noAvailableDataHelpText": "没有可显示的数据。调整时间范围和筛选。",
"controls.rangeSlider.popover.noDataHelpText": "选定范围未生成任何数据。未应用任何筛选。",
"controls.timeSlider.description": "添加用于选择时间范围的滑块",
@ -502,7 +500,6 @@
"controls.timeSlider.nextLabel": "下一时间窗口",
"controls.timeSlider.pauseLabel": "暂停",
"controls.timeSlider.playLabel": "播放",
"controls.timeSlider.popover.clearTimeTitle": "清除时间选择",
"controls.timeSlider.previousLabel": "上一时间窗口",
"controls.timeSlider.settings.pinStart": "固定开始屏幕",
"controls.timeSlider.settings.unpinStart": "取消固定开始屏幕",