Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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
);
Comment on lines +108 to +114
Copy link
Contributor Author

@Heenawter Heenawter Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes a bug where resetting the selections back to "undefined" / empty would cause unsaved changes:

Before:

Screen.Recording.2023-06-14.at.10.35.23.AM.mov

After:

Screen.Recording.2023-06-14.at.10.36.53.AM.mov

},
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 { ClearableControlEmbeddable, ControlEmbeddable, DataControlInput } 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) &&
embeddable instanceof ClearableControlEmbeddable
);
}

public async execute({ embeddable }: ClearControlActionContext) {
if (
!embeddable.parent ||
!isControlGroup(embeddable.parent) ||
!(embeddable instanceof ClearableControlEmbeddable)
) {
throw new IncompatibleActionError();
}
embeddable.clearSelections();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -60,7 +60,7 @@ export class DeleteControlAction implements Action<DeleteControlActionContext> {
if (!embeddable.parent || !isControlGroup(embeddable.parent)) {
throw new IncompatibleActionError();
}
return 'cross';
return 'trash';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As noted in the PR description, this change was made to be more consistent with the words + icons we use when deleting panels from the dashboard:

Delete control - Before Delete control - After Delete panel
Screenshot 2023-06-13 at 5 32 22 PM image

}

public async isCompatible({ embeddable }: DeleteControlActionContext) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@
*/

export const ACTION_EDIT_CONTROL = 'editControl';
export const ACTION_CLEAR_CONTROL = 'clearControl';
export const ACTION_DELETE_CONTROL = 'deleteControl';
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export const ControlGroup = () => {
});
}
}
(document.activeElement as HTMLElement)?.blur();
Copy link
Contributor Author

@Heenawter Heenawter Jun 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fixes this bug where the drag handler was retaining focus after it was dropped, which caused the floating actions to remain on the screen after moving a control until the user clicked somewhere to manually remove the focus:

Before:

Screen.Recording.2023-06-14.at.10.42.44.AM.mov

After:

Screen.Recording.2023-06-14.at.10.41.58.AM.mov

setDraggingId(null);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { DataView, FieldSpec } from '@kbn/data-views-plugin/public';
import { Embeddable, IContainer } from '@kbn/embeddable-plugin/public';
import { IContainer } from '@kbn/embeddable-plugin/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';

Expand All @@ -35,10 +35,11 @@ import {
OptionsListEmbeddableInput,
} from '../..';
import { pluginServices } from '../../services';
import { MIN_OPTIONS_LIST_REQUEST_SIZE, OptionsListReduxState } from '../types';
import { ClearableControlEmbeddable } 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 = (
Expand Down Expand Up @@ -76,7 +77,7 @@ type OptionsListReduxEmbeddableTools = ReduxEmbeddableTools<
typeof optionsListReducers
>;

export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput, ControlOutput> {
export class OptionsListEmbeddable extends ClearableControlEmbeddable<OptionsListEmbeddableInput> {
public readonly type = OPTIONS_LIST_CONTROL;
public deferEmbeddableLoad = true;

Expand Down Expand Up @@ -411,6 +412,10 @@ export class OptionsListEmbeddable extends Embeddable<OptionsListEmbeddableInput
return [newFilter];
};

public clearSelections() {
this.dispatch.clearSelections({});
}

reload = () => {
// clear cache when reload is requested
this.optionsListService.clearOptionsListCache();
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/controls/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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.',
Expand Down
Loading