From de9a71acf79ad76aaae2868b60b73e9577557c24 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 11 Jan 2021 12:47:56 +0100 Subject: [PATCH] Fix UI glitch on SOM delete confirmation modal (#87623) (#87806) * extract delete confirm modal * extract the export modal * add data-test-subj to confirm modal * add comment on why we can't use EuiConfirmModal --- .../saved_objects_table.test.tsx.snap | 238 ++++------------- .../components/delete_confirm_modal.test.tsx | 97 +++++++ .../components/delete_confirm_modal.tsx | 153 +++++++++++ .../components/export_modal.test.tsx | 100 +++++++ .../objects_table/components/export_modal.tsx | 137 ++++++++++ .../objects_table/components/index.ts | 2 + .../saved_objects_table.test.tsx | 4 +- .../objects_table/saved_objects_table.tsx | 249 +++--------------- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 10 files changed, 582 insertions(+), 400 deletions(-) create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx create mode 100644 src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap index 2e262ce43731a..518b1831abda8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/__snapshots__/saved_objects_table.test.tsx.snap @@ -1,200 +1,62 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`SavedObjectsTable delete should show a confirm modal 1`] = ` - - } - confirmButtonText={ - - } - defaultFocusedButton="confirm" + + selectedObjects={ + Array [ + Object { + "id": "1", + "type": "index-pattern", + }, + Object { + "id": "3", + "type": "dashboard", + }, + ] } -> -

- -

- -
+/> `; exports[`SavedObjectsTable export should allow the user to choose when exporting all 1`] = ` - - - - - - - - - } - labelType="legend" - > - - - - - } - name="includeReferencesDeep" - onChange={[Function]} - /> - - - - - - - - - - - - - - - - - - - - + `; exports[`SavedObjectsTable should render normally 1`] = ` diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx new file mode 100644 index 0000000000000..db1f83759fad5 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.test.tsx @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { SavedObjectWithMetadata } from '../../../../common'; +import { DeleteConfirmModal } from './delete_confirm_modal'; + +const createObject = (): SavedObjectWithMetadata => ({ + id: 'foo', + type: 'bar', + attributes: {}, + references: [], + meta: {}, +}); + +describe('DeleteConfirmModal', () => { + let onConfirm: jest.Mock; + let onCancel: jest.Mock; + + beforeEach(() => { + onConfirm = jest.fn(); + onCancel = jest.fn(); + }); + + it('displays a loader if `isDeleting` is true', () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('EuiLoadingElastic')).toHaveLength(1); + expect(wrapper.find('EuiModal')).toHaveLength(0); + }); + + it('lists the objects to delete', () => { + const objs = [createObject(), createObject(), createObject()]; + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('.euiTableRow')).toHaveLength(3); + }); + + it('calls `onCancel` when clicking on the cancel button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButtonEmpty').simulate('click'); + + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onConfirm).not.toHaveBeenCalled(); + }); + + it('calls `onDelete` when clicking on the delete button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButton').simulate('click'); + + expect(onConfirm).toHaveBeenCalledTimes(1); + expect(onCancel).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx new file mode 100644 index 0000000000000..07564843e9745 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/delete_confirm_modal.tsx @@ -0,0 +1,153 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; +import { + EuiInMemoryTable, + EuiLoadingElastic, + EuiToolTip, + EuiIcon, + EuiOverlayMask, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SavedObjectWithMetadata } from '../../../../common'; +import { getSavedObjectLabel } from '../../../lib'; + +export interface DeleteConfirmModalProps { + isDeleting: boolean; + onConfirm: () => void; + onCancel: () => void; + selectedObjects: SavedObjectWithMetadata[]; +} + +export const DeleteConfirmModal: FC = ({ + isDeleting, + onConfirm, + onCancel, + selectedObjects, +}) => { + if (isDeleting) { + return ( + + + + ); + } + + // can't use `EuiConfirmModal` here as the confirm modal body is wrapped + // inside a `

` element, causing UI glitches with the table. + return ( + + + + + + + + +

+ +

+ + ( + + + + ), + }, + { + field: 'id', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', + { defaultMessage: 'Id' } + ), + }, + { + field: 'meta.title', + name: i18n.translate( + 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', + { defaultMessage: 'Title' } + ), + }, + ]} + pagination={true} + sorting={false} + /> + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx new file mode 100644 index 0000000000000..c76c5b68cd66f --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.test.tsx @@ -0,0 +1,100 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { ExportModal } from './export_modal'; + +describe('ExportModal', () => { + let onExport: jest.Mock; + let onCancel: jest.Mock; + let onSelectedOptionsChange: jest.Mock; + let onIncludeReferenceChange: jest.Mock; + + const options = [ + { id: '1', label: 'option 1' }, + { id: '2', label: 'option 2' }, + ]; + const selectedOptions = { + 1: true, + 2: false, + }; + + beforeEach(() => { + onExport = jest.fn(); + onCancel = jest.fn(); + onSelectedOptionsChange = jest.fn(); + onIncludeReferenceChange = jest.fn(); + }); + + it('Displays a checkbox for each option', () => { + const wrapper = mountWithIntl( + + ); + + expect(wrapper.find('EuiCheckbox')).toHaveLength(2); + }); + + it('calls `onCancel` when clicking on the cancel button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButtonEmpty').simulate('click'); + + expect(onCancel).toHaveBeenCalledTimes(1); + expect(onExport).not.toHaveBeenCalled(); + }); + + it('calls `onExport` when clicking on the export button', () => { + const wrapper = mountWithIntl( + + ); + wrapper.find('EuiButton').simulate('click'); + + expect(onExport).toHaveBeenCalledTimes(1); + expect(onCancel).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx new file mode 100644 index 0000000000000..01ef145bcd077 --- /dev/null +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/export_modal.tsx @@ -0,0 +1,137 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import React, { FC } from 'react'; +import { + EuiOverlayMask, + EuiModal, + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiSpacer, + EuiFormRow, + EuiCheckboxGroup, + EuiSwitch, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export interface ExportModalProps { + onExport: () => void; + onCancel: () => void; + onSelectedOptionsChange: (newSelectedOptions: Record) => void; + filteredItemCount: number; + options: Array<{ id: string; label: string }>; + selectedOptions: Record; + includeReferences: boolean; + onIncludeReferenceChange: (newIncludeReference: boolean) => void; +} + +export const ExportModal: FC = ({ + onCancel, + onExport, + onSelectedOptionsChange, + options, + filteredItemCount, + selectedOptions, + includeReferences, + onIncludeReferenceChange, +}) => { + return ( + + + + + + + + + + } + labelType="legend" + > + { + onSelectedOptionsChange({ + ...selectedOptions, + ...{ + [optionId]: !selectedOptions[optionId], + }, + }); + }} + /> + + + + } + checked={includeReferences} + onChange={() => onIncludeReferenceChange(!includeReferences)} + /> + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts index 9c8736a9011eb..23e681b92b269 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/index.ts @@ -21,3 +21,5 @@ export { Header } from './header'; export { Table } from './table'; export { Flyout } from './flyout'; export { Relationships } from './relationships'; +export { DeleteConfirmModal } from './delete_confirm_modal'; +export { ExportModal } from './export_modal'; diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx index 98fcb7fad3db6..ecf6802ea9d6a 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.test.tsx @@ -327,7 +327,7 @@ describe('SavedObjectsTable', () => { (component.find('Header') as any).prop('onExportAll')(); component.update(); - expect(component.find('EuiModal')).toMatchSnapshot(); + expect(component.find('ExportModal')).toMatchSnapshot(); }); it('should export all', async () => { @@ -506,7 +506,7 @@ describe('SavedObjectsTable', () => { await component.instance().onDelete(); component.update(); - expect(component.find('EuiConfirmModal')).toMatchSnapshot(); + expect(component.find('DeleteConfirmModal')).toMatchSnapshot(); }); it('should delete selected objects', async () => { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx index a5a4bcab364af..bb158b7621125 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/saved_objects_table.tsx @@ -21,32 +21,8 @@ import React, { Component } from 'react'; import { debounce } from 'lodash'; // @ts-expect-error import { saveAs } from '@elastic/filesaver'; -import { - EuiSpacer, - Query, - EuiInMemoryTable, - EuiIcon, - EuiConfirmModal, - EuiLoadingElastic, - EuiOverlayMask, - EUI_MODAL_CONFIRM_BUTTON, - EuiCheckboxGroup, - EuiToolTip, - EuiPageContent, - EuiSwitch, - EuiModal, - EuiModalHeader, - EuiModalBody, - EuiModalFooter, - EuiButtonEmpty, - EuiButton, - EuiModalHeaderTitle, - EuiFormRow, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiSpacer, Query, EuiPageContent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { SavedObjectsClientContract, SavedObjectsFindOptions, @@ -62,7 +38,6 @@ import { parseQuery, getSavedObjectCounts, getRelationships, - getSavedObjectLabel, fetchExportObjects, fetchExportByTypeAndSearch, findObjects, @@ -77,7 +52,14 @@ import { SavedObjectsManagementActionServiceStart, SavedObjectsManagementColumnServiceStart, } from '../../services'; -import { Header, Table, Flyout, Relationships } from './components'; +import { + Header, + Table, + Flyout, + Relationships, + DeleteConfirmModal, + ExportModal, +} from './components'; import { DataPublicPluginStart } from '../../../../../plugins/data/public'; interface ExportAllOption { @@ -554,114 +536,24 @@ export class SavedObjectsTable extends Component; - } else { - const onCancel = () => { - this.setState({ isShowingDeleteConfirmModal: false }); - }; - - const onConfirm = () => { - this.delete(); - }; - - modal = ( - - } - onCancel={onCancel} - onConfirm={onConfirm} - buttonColor="danger" - cancelButtonText={ - - } - confirmButtonText={ - isDeleting ? ( - - ) : ( - - ) - } - defaultFocusedButton={EUI_MODAL_CONFIRM_BUTTON} - > -

- -

- ( - - - - ), - }, - { - field: 'id', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName', - { defaultMessage: 'Id' } - ), - }, - { - field: 'meta.title', - name: i18n.translate( - 'savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName', - { defaultMessage: 'Title' } - ), - }, - ]} - pagination={true} - sorting={false} - /> -
- ); - } - - return {modal}; + return ( + { + this.delete(); + }} + onCancel={() => { + this.setState({ isShowingDeleteConfirmModal: false }); + }} + selectedObjects={selectedSavedObjects} + /> + ); } - changeIncludeReferencesDeep = () => { - this.setState((state) => ({ - isIncludeReferencesDeepChecked: !state.isIncludeReferencesDeepChecked, - })); - }; - - closeExportAllModal = () => { - this.setState({ isShowingExportAllOptionsModal: false }); - }; - renderExportAllOptionsModal() { const { isShowingExportAllOptionsModal, @@ -676,85 +568,26 @@ export class SavedObjectsTable extends Component - - - - - - - - - } - labelType="legend" - > - { - const newExportAllSelectedOptions = { - ...exportAllSelectedOptions, - ...{ - [optionId]: !exportAllSelectedOptions[optionId], - }, - }; - - this.setState({ - exportAllSelectedOptions: newExportAllSelectedOptions, - }); - }} - /> - - - - } - checked={isIncludeReferencesDeepChecked} - onChange={this.changeIncludeReferencesDeep} - /> - - - - - - - - - - - - - - - - - - - - - + { + this.setState({ isShowingExportAllOptionsModal: false }); + }} + onSelectedOptionsChange={(newOptions) => { + this.setState({ + exportAllSelectedOptions: newOptions, + }); + }} + filteredItemCount={filteredItemCount} + options={exportAllOptions} + selectedOptions={exportAllSelectedOptions} + includeReferences={isIncludeReferencesDeepChecked} + onIncludeReferenceChange={(newIncludeReferences) => { + this.setState({ + isIncludeReferencesDeepChecked: newIncludeReferences, + }); + }} + /> ); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 2a25bbc29f5af..19c6d01bcf4b9 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3228,7 +3228,6 @@ "savedObjectsManagement.objects.savedObjectsTitle": "保存されたオブジェクト", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "キャンセル", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "削除", - "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel": "削除中…", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "Id", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "タイトル", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "型", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 16130e594222f..bbea473876bde 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3232,7 +3232,6 @@ "savedObjectsManagement.objects.savedObjectsTitle": "已保存对象", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.cancelButtonLabel": "取消", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteButtonLabel": "删除", - "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.deleteProcessButtonLabel": "正在删除……", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.idColumnName": "ID", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.titleColumnName": "标题", "savedObjectsManagement.objectsTable.deleteSavedObjectsConfirmModal.typeColumnName": "类型",