[Lens] Add confirmation modal when removing the layer (#135707)

* [Lens] Add confirmation modal when removing the layer

* copy updates

* Update x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* Update x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/remove_layer_button.tsx

Co-authored-by: Michael Marcialis <michael@marcial.is>

* fix tests

Co-authored-by: Michael Marcialis <michael@marcial.is>
This commit is contained in:
Marta Bondyra 2022-07-11 10:18:21 +02:00 committed by GitHub
parent 5e6aec2517
commit b64b9c39c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 266 additions and 39 deletions

View file

@ -156,6 +156,10 @@ describe('ConfigPanel', () => {
act(() => {
instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
});
instance.update();
act(() => {
instance.find('[data-test-subj="lnsLayerRemoveConfirmButton"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(firstLayerFocusable);
});
@ -179,6 +183,10 @@ describe('ConfigPanel', () => {
act(() => {
instance.find('[data-test-subj="lnsLayerRemove"]').at(0).simulate('click');
});
instance.update();
act(() => {
instance.find('[data-test-subj="lnsLayerRemoveConfirmButton"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(secondLayerFocusable);
});
@ -201,6 +209,10 @@ describe('ConfigPanel', () => {
act(() => {
instance.find('[data-test-subj="lnsLayerRemove"]').at(2).simulate('click');
});
instance.update();
act(() => {
instance.find('[data-test-subj="lnsLayerRemoveConfirmButton"]').first().simulate('click');
});
const focusedEl = document.activeElement;
expect(focusedEl).toEqual(firstLayerFocusable);
});

View file

@ -164,6 +164,10 @@ describe('LayerPanel', () => {
act(() => {
instance.find('[data-test-subj="lnsLayerRemove"]').first().simulate('click');
});
instance.update();
act(() => {
instance.find('[data-test-subj="lnsLayerRemoveConfirmButton"]').first().simulate('click');
});
expect(cb).toHaveBeenCalled();
});
});

View file

@ -315,6 +315,7 @@ export function LayerPanel(
layerIndex={layerIndex}
isOnlyLayer={isOnlyLayer}
activeVisualization={activeVisualization}
layerType={activeVisualization.getLayerType(layerId, visualizationState)}
/>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -5,25 +5,82 @@
* 2.0.
*/
import React from 'react';
import { EuiButtonIcon } from '@elastic/eui';
import React, { useState } from 'react';
import {
EuiButton,
EuiButtonEmpty,
EuiButtonIcon,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
EuiModal,
EuiModalBody,
EuiModalFooter,
EuiModalHeader,
EuiModalHeaderTitle,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import useLocalStorage from 'react-use/lib/useLocalStorage';
import { Visualization } from '../../../types';
import { LocalStorageLens, LOCAL_STORAGE_LENS_KEY } from '../../../settings_storage';
import { LayerType, layerTypes } from '../../..';
export function RemoveLayerButton({
onRemoveLayer,
layerIndex,
isOnlyLayer,
activeVisualization,
}: {
onRemoveLayer: () => void;
layerIndex: number;
isOnlyLayer: boolean;
activeVisualization: Visualization;
}) {
const modalDescClear = i18n.translate('xpack.lens.layer.confirmModal.clearVis', {
defaultMessage: `Clearing this layer removes the visualization and its configurations. `,
});
const modalDescVis = i18n.translate('xpack.lens.layer.confirmModal.deleteVis', {
defaultMessage: `Deleting this layer removes the visualization and its configurations. `,
});
const modalDescRefLine = i18n.translate('xpack.lens.layer.confirmModal.deleteRefLine', {
defaultMessage: `Deleting this layer removes the reference lines and their configurations. `,
});
const modalDescAnnotation = i18n.translate('xpack.lens.layer.confirmModal.deleteAnnotation', {
defaultMessage: `Deleting this layer removes the annotations and their configurations. `,
});
const getButtonCopy = (
layerIndex: number,
layerType: LayerType,
canBeRemoved?: boolean,
isOnlyLayer?: boolean
) => {
let ariaLabel;
if (!activeVisualization.removeLayer) {
const layerTypeCopy =
layerType === layerTypes.DATA
? i18n.translate('xpack.lens.modalTitle.layerType.data', {
defaultMessage: 'visualization',
})
: layerType === layerTypes.ANNOTATIONS
? i18n.translate('xpack.lens.modalTitle.layerType.annotation', {
defaultMessage: 'annotations',
})
: i18n.translate('xpack.lens.modalTitle.layerType.refLines', {
defaultMessage: 'reference lines',
});
let modalTitle = i18n.translate('xpack.lens.modalTitle.title.delete', {
defaultMessage: 'Delete {layerType} layer?',
values: { layerType: layerTypeCopy },
});
let modalDesc = modalDescVis;
if (!canBeRemoved || isOnlyLayer) {
modalTitle = i18n.translate('xpack.lens.modalTitle.title.clear', {
defaultMessage: 'Clear {layerType} layer?',
values: { layerType: layerTypeCopy },
});
modalDesc = modalDescClear;
}
if (layerType === layerTypes.ANNOTATIONS) {
modalDesc = modalDescAnnotation;
} else if (layerType === layerTypes.REFERENCELINE) {
modalDesc = modalDescRefLine;
}
if (!canBeRemoved) {
ariaLabel = i18n.translate('xpack.lens.resetVisualizationAriaLabel', {
defaultMessage: 'Reset visualization',
});
@ -39,27 +96,169 @@ export function RemoveLayerButton({
});
}
return {
ariaLabel,
modalTitle,
modalDesc,
};
};
export function RemoveLayerButton({
onRemoveLayer,
layerIndex,
isOnlyLayer,
activeVisualization,
layerType,
}: {
onRemoveLayer: () => void;
layerIndex: number;
isOnlyLayer: boolean;
activeVisualization: Visualization;
layerType?: LayerType;
}) {
const { ariaLabel, modalTitle, modalDesc } = getButtonCopy(
layerIndex,
layerType || layerTypes.DATA,
!!activeVisualization.removeLayer,
isOnlyLayer
);
const [isModalVisible, setIsModalVisible] = useState(false);
const [lensLocalStorage, setLensLocalStorage] = useLocalStorage<LocalStorageLens>(
LOCAL_STORAGE_LENS_KEY,
{}
);
const onChangeShouldShowModal = () =>
setLensLocalStorage({
...lensLocalStorage,
skipDeleteModal: !lensLocalStorage?.skipDeleteModal,
});
const closeModal = () => setIsModalVisible(false);
const showModal = () => setIsModalVisible(true);
const removeLayer = () => {
// If we don't blur the remove / clear button, it remains focused
// which is a strange UX in this case. e.target.blur doesn't work
// due to who knows what, but probably event re-writing. Additionally,
// activeElement does not have blur so, we need to do some casting + safeguards.
const el = document.activeElement as unknown as { blur: () => void };
if (el?.blur) {
el.blur();
}
onRemoveLayer();
};
return (
<EuiButtonIcon
size="xs"
iconType={isOnlyLayer ? 'eraser' : 'trash'}
color="danger"
data-test-subj="lnsLayerRemove"
aria-label={ariaLabel}
title={ariaLabel}
onClick={() => {
// If we don't blur the remove / clear button, it remains focused
// which is a strange UX in this case. e.target.blur doesn't work
// due to who knows what, but probably event re-writing. Additionally,
// activeElement does not have blur so, we need to do some casting + safeguards.
const el = document.activeElement as unknown as { blur: () => void };
if (el?.blur) {
el.blur();
}
onRemoveLayer();
}}
/>
<>
<EuiButtonIcon
size="xs"
iconType={isOnlyLayer ? 'eraser' : 'trash'}
color="danger"
data-test-subj="lnsLayerRemove"
aria-label={ariaLabel}
title={ariaLabel}
onClick={() => {
if (lensLocalStorage?.skipDeleteModal) {
return removeLayer();
}
return showModal();
}}
/>
{isModalVisible ? (
<RemoveConfirmModal
modalTitle={modalTitle}
isDeletable={!!activeVisualization.removeLayer && !isOnlyLayer}
modalDesc={modalDesc}
closeModal={closeModal}
skipDeleteModal={lensLocalStorage?.skipDeleteModal}
onChangeShouldShowModal={onChangeShouldShowModal}
removeLayer={removeLayer}
/>
) : null}
</>
);
}
const RemoveConfirmModal = ({
modalTitle,
modalDesc,
closeModal,
skipDeleteModal,
onChangeShouldShowModal,
isDeletable,
removeLayer,
}: {
modalTitle: string;
modalDesc: string;
closeModal: () => void;
skipDeleteModal?: boolean;
isDeletable?: boolean;
onChangeShouldShowModal: () => void;
removeLayer: () => void;
}) => {
return (
<EuiModal data-test-subj="lnsLayerRemoveModal" onClose={closeModal}>
<EuiModalHeader>
<EuiModalHeaderTitle>{modalTitle}</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<p>
{modalDesc}
{i18n.translate('xpack.lens.layer.confirmModal.cannotUndo', {
defaultMessage: `You can't undo this action.`,
})}
</p>
</EuiModalBody>
<EuiModalFooter>
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem>
<EuiCheckbox
id={'lnsLayerRemoveModalCheckbox'}
label={i18n.translate('xpack.lens.layer.confirmModal.dontAskAgain', {
defaultMessage: `Don't ask me again`,
})}
indeterminate={skipDeleteModal}
onChange={onChangeShouldShowModal}
/>
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup alignItems="center" justifyContent="flexEnd" responsive={false}>
<EuiFlexItem grow={false}>
<EuiButtonEmpty onClick={closeModal}>
{i18n.translate('xpack.lens.layer.cancelDelete', {
defaultMessage: `Cancel`,
})}
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
data-test-subj="lnsLayerRemoveConfirmButton"
onClick={() => {
closeModal();
removeLayer();
}}
fill
color="danger"
iconType={isDeletable ? 'trash' : 'eraser'}
>
{isDeletable
? i18n.translate('xpack.lens.layer.confirmDelete', {
defaultMessage: `Delete layer`,
})
: i18n.translate('xpack.lens.layer.confirmClear', {
defaultMessage: `Clear layer`,
})}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -7,12 +7,17 @@
import { IStorageWrapper } from '@kbn/kibana-utils-plugin/public';
const STORAGE_KEY = 'lens-settings';
export interface LocalStorageLens {
indexPatternId?: string;
skipDeleteModal?: boolean;
}
export const LOCAL_STORAGE_LENS_KEY = 'lens-settings';
export const readFromStorage = (storage: IStorageWrapper, key: string) => {
const data = storage.get(STORAGE_KEY);
const data = storage.get(LOCAL_STORAGE_LENS_KEY);
return data && data[key];
};
export const writeToStorage = (storage: IStorageWrapper, key: string, value: string) => {
storage.set(STORAGE_KEY, { ...storage.get(STORAGE_KEY), [key]: value });
storage.set(LOCAL_STORAGE_LENS_KEY, { ...storage.get(LOCAL_STORAGE_LENS_KEY), [key]: value });
};

View file

@ -1215,7 +1215,13 @@ export function LensPageProvider({ getService, getPageObjects }: FtrProviderCont
/** resets visualization/layer or removes a layer */
async removeLayer() {
await testSubjects.click('lnsLayerRemove');
await retry.try(async () => {
await testSubjects.click('lnsLayerRemove');
if (await testSubjects.exists('lnsLayerRemoveModal')) {
await testSubjects.exists('lnsLayerRemoveConfirmButton');
await testSubjects.click('lnsLayerRemoveConfirmButton');
}
});
},
/**