[Enterprise Search] Confirmation modal for deleting Crawler domains in Kibana Content app (#136481)

This commit is contained in:
Byron Hulcher 2022-07-18 16:16:04 -04:00 committed by GitHub
parent 7f55a1a9e0
commit fef8e72286
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 276 additions and 59 deletions

View file

@ -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<DeleteCrawlerDomainResponse> => {
await HttpLogic.values.http.delete(
`/internal/enterprise_search/indices/${indexName}/crawler/domains/${domain.id}`
);

View file

@ -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: [
<CrawlerStatusIndicator />,
<EuiButton
isLoading={getLoading || deleteLoading}
isLoading={getLoading}
color="danger"
onClick={() => {
if (window.confirm(getDeleteDomainConfirmationMessage(domainUrl))) {
deleteDomain();
if (domain) {
showModal(domain);
}
}}
>
@ -110,6 +112,7 @@ export const CrawlerDomainDetail: React.FC = () => {
<DeduplicationPanel />
</>
)}
<DeleteDomainModal />
<CrawlCustomSettingsFlyout />
</EnterpriseSearchContentPageTemplate>
);

View file

@ -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<CrawlerDomainDetailValues, CrawlerDomainDetailActions>
>({
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;

View file

@ -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 (
<EuiModal
onClose={hideModal}
aria-label={i18n.translate('xpack.enterpriseSearch.crawler.deleteDomainModal.title', {
defaultMessage: 'Delete domain',
})}
>
<EuiModalHeader>
<EuiModalHeaderTitle>
{i18n.translate('xpack.enterpriseSearch.crawler.deleteDomainModal.title', {
defaultMessage: 'Delete domain',
})}
</EuiModalHeaderTitle>
</EuiModalHeader>
<EuiModalBody>
<EuiText>
<FormattedMessage
id="xpack.enterpriseSearch.crawler.deleteDomainModal.description"
defaultMessage="Remove the domain {domainUrl} from your crawler. This will also delete all entry points and crawl rules you have set up. Any documents related to this domain will be removed on the next crawl. {thisCannotBeUndoneMessage}"
values={{
domainUrl: <strong>{domain?.url}</strong>,
thisCannotBeUndoneMessage: (
<strong>
{i18n.translate(
'xpack.enterpriseSearch.crawler.deleteDomainModal.thisCannotBeUndoneMessage',
{
defaultMessage: 'This cannot be undone.',
}
)}
</strong>
),
}}
/>
</EuiText>
</EuiModalBody>
<EuiModalFooter>
<EuiButtonEmpty onClick={hideModal}>{CANCEL_BUTTON_LABEL}</EuiButtonEmpty>
<EuiButton onClick={deleteDomain} isLoading={isLoading} color="danger" fill>
{i18n.translate(
'xpack.enterpriseSearch.crawler.deleteDomainModal.deleteDomainButtonLabel',
{
defaultMessage: 'Delete domain',
}
)}
</EuiButton>
</EuiModalFooter>
</EuiModal>
);
};

View file

@ -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<DeleteCrawlerDomainArgs, DeleteCrawlerDomainResponse>,
'apiError' | 'apiSuccess' | 'makeRequest'
> & {
deleteDomain(): void;
hideModal(): void;
showModal(domain: CrawlerDomain): { domain: CrawlerDomain };
};
export const DeleteDomainModalLogic = kea<
MakeLogicType<DeleteDomainModalValues, DeleteDomainModalActions>
>({
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,
],
}),
});

View file

@ -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 = () => {
<EuiSpacer />
<CrawlerStatusBanner />
{domains.length > 0 ? <DomainsPanel /> : <EmptyStatePanel />}
<DeleteDomainModal />
<AddDomainFlyout />
</>
);

View file

@ -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();
});
});
});

View file

@ -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<EuiBasicTableColumn<CrawlerDomain>> = [
{
@ -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);
},
},
],