From fef8e7228634eeaeb70ab8efc12798b78310107a Mon Sep 17 00:00:00 2001 From: Byron Hulcher Date: Mon, 18 Jul 2022 16:16:04 -0400 Subject: [PATCH] [Enterprise Search] Confirmation modal for deleting Crawler domains in Kibana Content app (#136481) --- .../delete_crawler_domain_api_logic.ts | 11 +- .../crawler_domain_detail.tsx | 15 ++- .../crawler_domain_detail_logic.ts | 86 ++++++++------ .../domain_management/delete_domain_modal.tsx | 89 +++++++++++++++ .../delete_domain_modal_logic.ts | 105 ++++++++++++++++++ .../domain_management/domain_management.tsx | 2 + .../domain_management/domains_table.test.tsx | 17 +-- .../domain_management/domains_table.tsx | 10 +- 8 files changed, 276 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal_logic.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/delete_crawler_domain_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/delete_crawler_domain_api_logic.ts index e7a959cedff47..8857ef95a32fa 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/delete_crawler_domain_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/crawler/delete_crawler_domain_api_logic.ts @@ -10,12 +10,19 @@ import { HttpLogic } from '../../../shared/http'; import { CrawlerDomain } from './types'; -export interface GetCrawlerDomainsArgs { +export interface DeleteCrawlerDomainArgs { domain: CrawlerDomain; indexName: string; } -export const deleteCrawlerDomain = async ({ domain, indexName }: GetCrawlerDomainsArgs) => { +export interface DeleteCrawlerDomainResponse { + domain: CrawlerDomain; +} + +export const deleteCrawlerDomain = async ({ + domain, + indexName, +}: DeleteCrawlerDomainArgs): Promise => { await HttpLogic.values.http.delete( `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domain.id}` ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail.tsx index c4303be878e9c..b4c913db555ec 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail.tsx @@ -22,7 +22,8 @@ import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; import { CrawlCustomSettingsFlyout } from '../search_index/crawler/crawl_custom_settings_flyout/crawl_custom_settings_flyout'; import { CrawlerStatusIndicator } from '../search_index/crawler/crawler_status_indicator/crawler_status_indicator'; import { CrawlerStatusBanner } from '../search_index/crawler/domain_management/crawler_status_banner'; -import { getDeleteDomainConfirmationMessage } from '../search_index/crawler/utils'; +import { DeleteDomainModal } from '../search_index/crawler/domain_management/delete_domain_modal'; +import { DeleteDomainModalLogic } from '../search_index/crawler/domain_management/delete_domain_modal_logic'; import { IndexNameLogic } from '../search_index/index_name_logic'; import { SearchIndexTabId } from '../search_index/search_index'; import { baseBreadcrumbs } from '../search_indices'; @@ -40,8 +41,9 @@ export const CrawlerDomainDetail: React.FC = () => { const { indexName } = useValues(IndexNameLogic); const crawlerDomainDetailLogic = CrawlerDomainDetailLogic({ domainId }); - const { deleteLoading, domain, getLoading } = useValues(crawlerDomainDetailLogic); - const { fetchDomainData, deleteDomain } = useActions(crawlerDomainDetailLogic); + const { domain, getLoading } = useValues(crawlerDomainDetailLogic); + const { fetchDomainData } = useActions(crawlerDomainDetailLogic); + const { showModal } = useActions(DeleteDomainModalLogic); useEffect(() => { fetchDomainData(domainId); @@ -58,11 +60,11 @@ export const CrawlerDomainDetail: React.FC = () => { rightSideItems: [ , { - if (window.confirm(getDeleteDomainConfirmationMessage(domainUrl))) { - deleteDomain(); + if (domain) { + showModal(domain); } }} > @@ -110,6 +112,7 @@ export const CrawlerDomainDetail: React.FC = () => { )} + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail_logic.ts index 5beb863ce579d..96e2c4a3411d3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/crawler_domain_detail/crawler_domain_detail_logic.ts @@ -9,12 +9,19 @@ import { kea, MakeLogicType } from 'kea'; import { i18n } from '@kbn/i18n'; +import { HttpError, Status } from '../../../../../common/types/api'; + import { generateEncodedPath } from '../../../shared/encode_path_params'; import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages'; import { HttpLogic } from '../../../shared/http'; import { KibanaLogic } from '../../../shared/kibana'; +import { + DeleteCrawlerDomainApiLogic, + DeleteCrawlerDomainArgs, + DeleteCrawlerDomainResponse, +} from '../../api/crawler/delete_crawler_domain_api_logic'; import { CrawlerDomain, CrawlerDomainFromServer, @@ -33,14 +40,17 @@ export interface CrawlerDomainDetailProps { export interface CrawlerDomainDetailValues { deleteLoading: boolean; + deleteStatus: Status; domain: CrawlerDomain | null; domainId: string; getLoading: boolean; } interface CrawlerDomainDetailActions { + deleteApiError(error: HttpError): HttpError; + deleteApiSuccess(response: DeleteCrawlerDomainResponse): DeleteCrawlerDomainResponse; deleteDomain(): void; - deleteDomainComplete(): void; + deleteMakeRequest(args: DeleteCrawlerDomainArgs): DeleteCrawlerDomainArgs; fetchDomainData(domainId: string): { domainId: string }; receiveDomainData(domain: CrawlerDomain): { domain: CrawlerDomain }; submitDeduplicationUpdate(payload: { enabled?: boolean; fields?: string[] }): { @@ -56,6 +66,17 @@ export const CrawlerDomainDetailLogic = kea< MakeLogicType >({ path: ['enterprise_search', 'crawler', 'crawler_domain_detail_logic'], + connect: { + actions: [ + DeleteCrawlerDomainApiLogic, + [ + 'apiError as deleteApiError', + 'apiSuccess as deleteApiSuccess', + 'makeRequest as deleteMakeRequest', + ], + ], + values: [DeleteCrawlerDomainApiLogic, ['status as deleteStatus']], + }, actions: { deleteDomain: () => true, deleteDomainComplete: () => true, @@ -67,13 +88,6 @@ export const CrawlerDomainDetailLogic = kea< updateSitemaps: (sitemaps) => ({ sitemaps }), }, reducers: ({ props }) => ({ - deleteLoading: [ - false, - { - deleteDomain: () => true, - deleteDomainComplete: () => false, - }, - ], domain: [ null, { @@ -94,34 +108,44 @@ export const CrawlerDomainDetailLogic = kea< }, ], }), + selectors: ({ selectors }) => ({ + deleteLoading: [ + () => [selectors.deleteStatus], + (deleteStatus: Status) => deleteStatus === Status.LOADING, + ], + }), listeners: ({ actions, values }) => ({ deleteDomain: async () => { - const { http } = HttpLogic.values; - const { domain, domainId } = values; + const { domain } = values; const { indexName } = IndexNameLogic.values; - try { - await http.delete( - `/internal/enterprise_search/indices/${indexName}/crawler/domains/${domainId}` - ); - flashSuccessToast( - i18n.translate('xpack.enterpriseSearch.crawler.action.deleteDomain.successMessage', { - defaultMessage: "Domain '{domainUrl}' was deleted", - values: { - domainUrl: domain?.url, - }, - }) - ); - KibanaLogic.values.navigateToUrl( - generateEncodedPath(SEARCH_INDEX_TAB_PATH, { - indexName, - tabId: SearchIndexTabId.DOMAIN_MANAGEMENT, - }) - ); - } catch (e) { - flashAPIErrors(e); + if (domain) { + actions.deleteMakeRequest({ + domain, + indexName, + }); } - actions.deleteDomainComplete(); }, + deleteApiSuccess: ({ domain }) => { + const { indexName } = IndexNameLogic.values; + flashSuccessToast( + i18n.translate('xpack.enterpriseSearch.crawler.action.deleteDomain.successMessage', { + defaultMessage: "Domain '{domainUrl}' was deleted", + values: { + domainUrl: domain?.url, + }, + }) + ); + KibanaLogic.values.navigateToUrl( + generateEncodedPath(SEARCH_INDEX_TAB_PATH, { + indexName, + tabId: SearchIndexTabId.DOMAIN_MANAGEMENT, + }) + ); + }, + deleteApiError: (error) => { + flashAPIErrors(error); + }, + fetchDomainData: async ({ domainId }) => { const { http } = HttpLogic.values; const { indexName } = IndexNameLogic.values; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal.tsx new file mode 100644 index 0000000000000..76a736277a002 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal.tsx @@ -0,0 +1,89 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiButtonEmpty, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiText, +} from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants'; + +import { DeleteCrawlerDomainApiLogic } from '../../../../api/crawler/delete_crawler_domain_api_logic'; + +import { DeleteDomainModalLogic } from './delete_domain_modal_logic'; + +export const DeleteDomainModal: React.FC = () => { + DeleteCrawlerDomainApiLogic.mount(); + const { deleteDomain, hideModal } = useActions(DeleteDomainModalLogic); + const { domain, isLoading, isHidden } = useValues(DeleteDomainModalLogic); + + if (isHidden) { + return null; + } + + return ( + + + + {i18n.translate('xpack.enterpriseSearch.crawler.deleteDomainModal.title', { + defaultMessage: 'Delete domain', + })} + + + + + {domain?.url}, + thisCannotBeUndoneMessage: ( + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.deleteDomainModal.thisCannotBeUndoneMessage', + { + defaultMessage: 'This cannot be undone.', + } + )} + + ), + }} + /> + + + + {CANCEL_BUTTON_LABEL} + + {i18n.translate( + 'xpack.enterpriseSearch.crawler.deleteDomainModal.deleteDomainButtonLabel', + { + defaultMessage: 'Delete domain', + } + )} + + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal_logic.ts new file mode 100644 index 0000000000000..96294170d6b77 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/delete_domain_modal_logic.ts @@ -0,0 +1,105 @@ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * 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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { i18n } from '@kbn/i18n'; + +import { Status } from '../../../../../../../common/types/api'; +import { Actions } from '../../../../../shared/api_logic/create_api_logic'; +import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; +import { + DeleteCrawlerDomainApiLogic, + DeleteCrawlerDomainResponse, + DeleteCrawlerDomainArgs, +} from '../../../../api/crawler/delete_crawler_domain_api_logic'; +import { CrawlerDomain } from '../../../../api/crawler/types'; +import { IndexNameLogic } from '../../index_name_logic'; +import { CrawlerLogic } from '../crawler_logic'; + +interface DeleteDomainModalValues { + domain: CrawlerDomain | null; + isHidden: boolean; + isLoading: boolean; + status: Status; +} + +type DeleteDomainModalActions = Pick< + Actions, + 'apiError' | 'apiSuccess' | 'makeRequest' +> & { + deleteDomain(): void; + hideModal(): void; + showModal(domain: CrawlerDomain): { domain: CrawlerDomain }; +}; + +export const DeleteDomainModalLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'delete_domain_modal'], + connect: { + actions: [DeleteCrawlerDomainApiLogic, ['apiError', 'apiSuccess']], + values: [DeleteCrawlerDomainApiLogic, ['status']], + }, + actions: { + deleteDomain: () => true, + hideModal: () => true, + showModal: (domain) => ({ domain }), + }, + reducers: { + domain: [ + null, + { + showModal: (_, { domain }) => domain, + }, + ], + isHidden: [ + true, + { + apiError: () => true, + apiSuccess: () => true, + hideModal: () => true, + showModal: () => false, + }, + ], + }, + listeners: ({ values }) => ({ + apiError: (error) => { + flashAPIErrors(error); + }, + apiSuccess: ({ domain }) => { + flashSuccessToast( + i18n.translate('xpack.enterpriseSearch.crawler.domainsTable.action.delete.successMessage', { + defaultMessage: "Successfully deleted domain '{domainUrl}'", + values: { + domainUrl: domain.url, + }, + }) + ); + CrawlerLogic.actions.fetchCrawlerData(); + }, + deleteDomain: () => { + const { domain } = values; + const { indexName } = IndexNameLogic.values; + if (domain) { + DeleteCrawlerDomainApiLogic.actions.makeRequest({ domain, indexName }); + } + }, + }), + selectors: ({ selectors }) => ({ + isLoading: [ + () => [selectors.status], + (status: DeleteDomainModalValues['status']) => status === Status.LOADING, + ], + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domain_management.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domain_management.tsx index 0f6211e07945a..3400dd19347d2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domain_management.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domain_management.tsx @@ -18,6 +18,7 @@ import { GetCrawlerDomainsApiLogic } from '../../../../api/crawler/get_crawler_d import { AddDomainFlyout } from './add_domain/add_domain_flyout'; import { CrawlerStatusBanner } from './crawler_status_banner'; +import { DeleteDomainModal } from './delete_domain_modal'; import { DomainManagementLogic } from './domain_management_logic'; import { DomainsPanel } from './domains_panel'; import { EmptyStatePanel } from './empty_state_panel'; @@ -36,6 +37,7 @@ export const SearchIndexDomainManagement: React.FC = () => { {domains.length > 0 ? : } + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx index b5d66f3078858..1edac9de49f53 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.test.tsx @@ -66,9 +66,10 @@ const values = { const actions = { // CrawlerDomainsLogic - deleteDomain: jest.fn(), fetchCrawlerDomainsData: jest.fn(), onPaginate: jest.fn(), + // DeleteDomainModalLogic + showModal: jest.fn(), }; describe('DomainsTable', () => { @@ -161,21 +162,9 @@ describe('DomainsTable', () => { describe('delete action', () => { it('clicking the action and confirming deletes the domain', () => { - jest.spyOn(global, 'confirm').mockReturnValueOnce(true); - - getDeleteAction().simulate('click'); - - expect(actions.deleteDomain).toHaveBeenCalledWith( - expect.objectContaining({ id: '1234' }) - ); - }); - - it('clicking the action and not confirming does not delete the engine', () => { - jest.spyOn(global, 'confirm').mockReturnValueOnce(false); - getDeleteAction().simulate('click'); - expect(actions.deleteDomain).not.toHaveBeenCalled(); + expect(actions.showModal).toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.tsx index 94c7c3ed32cf6..44a22f258b506 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_index/crawler/domain_management/domains_table.tsx @@ -26,14 +26,14 @@ import { CrawlerDomain } from '../../../../api/crawler/types'; import { SEARCH_INDEX_CRAWLER_DOMAIN_DETAIL_PATH } from '../../../../routes'; import { IndexNameLogic } from '../../index_name_logic'; -import { getDeleteDomainConfirmationMessage } from '../utils'; - +import { DeleteDomainModalLogic } from './delete_domain_modal_logic'; import { DomainManagementLogic } from './domain_management_logic'; export const DomainsTable: React.FC = () => { const { indexName } = useValues(IndexNameLogic); const { domains, meta, isLoading } = useValues(DomainManagementLogic); - const { deleteDomain, onPaginate } = useActions(DomainManagementLogic); + const { onPaginate } = useActions(DomainManagementLogic); + const { showModal } = useActions(DeleteDomainModalLogic); const columns: Array> = [ { @@ -106,9 +106,7 @@ export const DomainsTable: React.FC = () => { icon: 'trash', color: 'danger', onClick: (domain) => { - if (window.confirm(getDeleteDomainConfirmationMessage(domain.url))) { - deleteDomain(domain); - } + showModal(domain); }, }, ],