mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
5e6aec2517
commit
b64b9c39c7
6 changed files with 266 additions and 39 deletions
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -315,6 +315,7 @@ export function LayerPanel(
|
|||
layerIndex={layerIndex}
|
||||
isOnlyLayer={isOnlyLayer}
|
||||
activeVisualization={activeVisualization}
|
||||
layerType={activeVisualization.getLayerType(layerId, visualizationState)}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue