mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[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** |  |  | | **Options list** |  |  | | **Time slider** |  |  | 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 | |--------|--------|--------| |  |  | <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:
parent
69849ee03b
commit
f1dc1e1869
28 changed files with 207 additions and 148 deletions
|
@ -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
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -7,4 +7,5 @@
|
|||
*/
|
||||
|
||||
export const ACTION_EDIT_CONTROL = 'editControl';
|
||||
export const ACTION_CLEAR_CONTROL = 'clearControl';
|
||||
export const ACTION_DELETE_CONTROL = 'deleteControl';
|
||||
|
|
|
@ -82,6 +82,7 @@ export const ControlGroup = () => {
|
|||
});
|
||||
}
|
||||
}
|
||||
(document.activeElement as HTMLElement)?.blur();
|
||||
setDraggingId(null);
|
||||
};
|
||||
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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: () =>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
}
|
||||
|
||||
.euiText {
|
||||
background-color: $euiFormBackgroundColor;
|
||||
background-color: $euiFormBackgroundColor !important;
|
||||
}
|
||||
|
||||
.timeSlider__anchorText {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "開始をピン留め解除",
|
||||
|
|
|
@ -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": "取消固定开始屏幕",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue