[App Search] Migrate Delete Domain Panel (#107795)

* Add deleteDomain action to CrawlerSingleDomainLogic

* New DeleteDomainPanel component

* Added DeleteDomainPanel to CrawlerSingleDomain

* Missing tests for DeleteDomainPanel

* Abstract getDeleteDomainConfirmationMessage
This commit is contained in:
Byron Hulcher 2021-08-09 11:43:02 -04:00 committed by GitHub
parent 58054c3325
commit be5f538a1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 254 additions and 34 deletions

View file

@ -0,0 +1,66 @@
/*
* 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 { setMockActions, setMockValues } from '../../../../__mocks__/kea_logic';
import React from 'react';
import { shallow } from 'enzyme';
import { EuiButton } from '@elastic/eui';
import { DeleteDomainPanel } from './delete_domain_panel';
const MOCK_VALUES = {
domain: { id: '9876' },
};
const MOCK_ACTIONS = {
deleteDomain: jest.fn(),
};
describe('DeleteDomainPanel', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('contains a button to delete the domain', () => {
setMockValues(MOCK_VALUES);
setMockActions(MOCK_ACTIONS);
const confirmSpy = jest.spyOn(window, 'confirm');
confirmSpy.mockImplementation(jest.fn(() => true));
const wrapper = shallow(<DeleteDomainPanel />);
wrapper.find(EuiButton).simulate('click');
expect(MOCK_ACTIONS.deleteDomain).toHaveBeenCalledWith(MOCK_VALUES.domain);
});
it("doesn't throw if the users chooses not to confirm", () => {
setMockValues(MOCK_VALUES);
const confirmSpy = jest.spyOn(window, 'confirm');
confirmSpy.mockImplementation(jest.fn(() => false));
const wrapper = shallow(<DeleteDomainPanel />);
wrapper.find(EuiButton).simulate('click');
});
// The user should never encounter this state, the containing AppSearchTemplate should be loading until
// the relevant domain has been loaded. However we must account for the possibility in this component.
it('is empty if domain has not yet been set', () => {
setMockValues({
...MOCK_VALUES,
domain: null,
});
const wrapper = shallow(<DeleteDomainPanel />);
expect(wrapper.isEmptyRender()).toBe(true);
});
});

View file

@ -0,0 +1,70 @@
/*
* 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, EuiSpacer, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic';
import { getDeleteDomainSuccessMessage } from '../utils';
export const DeleteDomainPanel: React.FC = ({}) => {
const { domain } = useValues(CrawlerSingleDomainLogic);
const { deleteDomain } = useActions(CrawlerSingleDomainLogic);
if (!domain) {
return null;
}
return (
<>
<EuiText size="s">
<p>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.crawler.deleteDomainPanel.description"
defaultMessage="Remove this domain from your crawler. This will also delete all entry points and crawl
rules you have setup. {cannotUndoMessage}."
values={{
cannotUndoMessage: (
<strong>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.deleteDomainPanel.cannotUndoMessage',
{
defaultMessage: 'This cannot be undone',
}
)}
</strong>
),
}}
/>
</p>
</EuiText>
<EuiSpacer size="m" />
<EuiButton
color="danger"
iconType="trash"
onClick={() => {
if (confirm(getDeleteDomainSuccessMessage(domain.url))) {
deleteDomain(domain);
}
}}
>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.deleteDomainPanel.deleteDomainButtonLabel',
{
defaultMessage: 'Delete domain',
}
)}
</EuiButton>
</>
);
};

View file

@ -23,6 +23,8 @@ import { generateEnginePath } from '../../engine';
import { CrawlerOverviewLogic } from '../crawler_overview_logic';
import { CrawlerDomain } from '../types';
import { getDeleteDomainConfirmationMessage } from '../utils';
import { CustomFormattedTimestamp } from './custom_formatted_timestamp';
export const DomainsTable: React.FC = () => {
@ -101,20 +103,7 @@ export const DomainsTable: React.FC = () => {
icon: 'trash',
color: 'danger',
onClick: (domain) => {
if (
window.confirm(
i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.confirmationPopupMessage',
{
defaultMessage:
'Are you sure you want to remove the domain "{domainUrl}" and all of its settings?',
values: {
domainUrl: domain.url,
},
}
)
)
) {
if (window.confirm(getDeleteDomainConfirmationMessage(domain.url))) {
deleteDomain(domain);
}
},

View file

@ -7,8 +7,6 @@
import { kea, MakeLogicType } from 'kea';
import { i18n } from '@kbn/i18n';
import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
@ -21,18 +19,11 @@ import {
CrawlRequestFromServer,
CrawlerStatus,
} from './types';
import { crawlerDataServerToClient, crawlRequestServerToClient } from './utils';
export const DELETE_DOMAIN_MESSAGE = (domainUrl: string) =>
i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.delete.successMessage',
{
defaultMessage: "Domain '{domainUrl}' was deleted",
values: {
domainUrl,
},
}
);
import {
crawlerDataServerToClient,
crawlRequestServerToClient,
getDeleteDomainSuccessMessage,
} from './utils';
const POLLING_DURATION = 1000;
const POLLING_DURATION_ON_FAILURE = 5000;
@ -145,7 +136,7 @@ export const CrawlerOverviewLogic = kea<
);
const crawlerData = crawlerDataServerToClient(response);
actions.onReceiveCrawlerData(crawlerData);
flashSuccessToast(DELETE_DOMAIN_MESSAGE(domain.url));
flashSuccessToast(getDeleteDomainSuccessMessage(domain.url));
} catch (e) {
flashAPIErrors(e);
}

View file

@ -19,6 +19,7 @@ import { getPageHeaderActions } from '../../../test_helpers';
import { CrawlerStatusBanner } from './components/crawler_status_banner';
import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator';
import { DeleteDomainPanel } from './components/delete_domain_panel';
import { CrawlerOverview } from './crawler_overview';
import { CrawlerSingleDomain } from './crawler_single_domain';
@ -50,6 +51,7 @@ describe('CrawlerSingleDomain', () => {
it('renders', () => {
const wrapper = shallow(<CrawlerSingleDomain />);
expect(wrapper.find(DeleteDomainPanel)).toHaveLength(1);
expect(wrapper.find(EuiCode).render().text()).toContain('https://elastic.co');
expect(wrapper.prop('pageHeader').pageTitle).toEqual('https://elastic.co');
});

View file

@ -11,7 +11,7 @@ import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
import { EuiCode, EuiSpacer } from '@elastic/eui';
import { EuiCode, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
@ -20,6 +20,7 @@ import { AppSearchPageTemplate } from '../layout';
import { CrawlerStatusBanner } from './components/crawler_status_banner';
import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator';
import { DeleteDomainPanel } from './components/delete_domain_panel';
import { CRAWLER_TITLE } from './constants';
import { CrawlerSingleDomainLogic } from './crawler_single_domain_logic';
@ -48,6 +49,19 @@ export const CrawlerSingleDomain: React.FC = () => {
>
<CrawlerStatusBanner />
<EuiSpacer size="l" />
<EuiTitle size="s">
<h2>
{i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.singleDomain.deleteDomainTitle',
{
defaultMessage: 'Delete domain',
}
)}
</h2>
</EuiTitle>
<EuiSpacer size="m" />
<DeleteDomainPanel />
<EuiSpacer size="xl" />
<EuiCode>{JSON.stringify(domain, null, 2)}</EuiCode>
</AppSearchPageTemplate>
);

View file

@ -9,6 +9,7 @@ import {
LogicMounter,
mockHttpValues,
mockFlashMessageHelpers,
mockKibanaValues,
} from '../../../__mocks__/kea_logic';
import '../../__mocks__/engine_logic.mock';
@ -25,7 +26,7 @@ const DEFAULT_VALUES: CrawlerSingleDomainValues = {
describe('CrawlerSingleDomainLogic', () => {
const { mount } = new LogicMounter(CrawlerSingleDomainLogic);
const { http } = mockHttpValues;
const { flashAPIErrors } = mockFlashMessageHelpers;
const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
beforeEach(() => {
jest.clearAllMocks();
@ -53,6 +54,33 @@ describe('CrawlerSingleDomainLogic', () => {
});
describe('listeners', () => {
describe('deleteDomain', () => {
it('flashes a success toast and redirects the user to the crawler overview on success', async () => {
const { navigateToUrl } = mockKibanaValues;
http.delete.mockReturnValue(Promise.resolve());
CrawlerSingleDomainLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
await nextTick();
expect(http.delete).toHaveBeenCalledWith(
'/api/app_search/engines/some-engine/crawler/domains/1234'
);
expect(flashSuccessToast).toHaveBeenCalled();
expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/crawler');
});
it('calls flashApiErrors when there is an error', async () => {
http.delete.mockReturnValue(Promise.reject('error'));
CrawlerSingleDomainLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
await nextTick();
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
});
describe('fetchDomainData', () => {
it('updates logic with data that has been converted from server to client', async () => {
jest.spyOn(CrawlerSingleDomainLogic.actions, 'onReceiveDomainData');

View file

@ -7,13 +7,15 @@
import { kea, MakeLogicType } from 'kea';
import { flashAPIErrors } from '../../../shared/flash_messages';
import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_messages';
import { HttpLogic } from '../../../shared/http';
import { EngineLogic } from '../engine';
import { KibanaLogic } from '../../../shared/kibana';
import { ENGINE_CRAWLER_PATH } from '../../routes';
import { EngineLogic, generateEnginePath } from '../engine';
import { CrawlerDomain } from './types';
import { crawlerDomainServerToClient } from './utils';
import { crawlerDomainServerToClient, getDeleteDomainSuccessMessage } from './utils';
export interface CrawlerSingleDomainValues {
dataLoading: boolean;
@ -21,6 +23,7 @@ export interface CrawlerSingleDomainValues {
}
interface CrawlerSingleDomainActions {
deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain };
fetchDomainData(domainId: string): { domainId: string };
onReceiveDomainData(domain: CrawlerDomain): { domain: CrawlerDomain };
}
@ -30,6 +33,7 @@ export const CrawlerSingleDomainLogic = kea<
>({
path: ['enterprise_search', 'app_search', 'crawler', 'crawler_single_domain'],
actions: {
deleteDomain: (domain) => ({ domain }),
fetchDomainData: (domainId) => ({ domainId }),
onReceiveDomainData: (domain) => ({ domain }),
},
@ -48,6 +52,19 @@ export const CrawlerSingleDomainLogic = kea<
],
},
listeners: ({ actions }) => ({
deleteDomain: async ({ domain }) => {
const { http } = HttpLogic.values;
const { engineName } = EngineLogic.values;
try {
await http.delete(`/api/app_search/engines/${engineName}/crawler/domains/${domain.id}`);
flashSuccessToast(getDeleteDomainSuccessMessage(domain.url));
KibanaLogic.values.navigateToUrl(generateEnginePath(ENGINE_CRAWLER_PATH));
} catch (e) {
flashAPIErrors(e);
}
},
fetchDomainData: async ({ domainId }) => {
const { http } = HttpLogic.values;
const { engineName } = EngineLogic.values;

View file

@ -23,6 +23,8 @@ import {
crawlerDataServerToClient,
crawlDomainValidationToResult,
crawlRequestServerToClient,
getDeleteDomainConfirmationMessage,
getDeleteDomainSuccessMessage,
} from './utils';
const DEFAULT_CRAWL_RULE: CrawlRule = {
@ -223,3 +225,17 @@ describe('crawlDomainValidationToResult', () => {
} as CrawlerDomainValidationStep);
});
});
describe('getDeleteDomainConfirmationMessage', () => {
it('includes the url', () => {
expect(getDeleteDomainConfirmationMessage('https://elastic.co/')).toContain(
'https://elastic.co'
);
});
});
describe('getDeleteDomainSuccessMessage', () => {
it('includes the url', () => {
expect(getDeleteDomainSuccessMessage('https://elastic.co/')).toContain('https://elastic.co');
});
});

View file

@ -5,6 +5,8 @@
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import {
CrawlerDomain,
CrawlerDomainFromServer,
@ -101,3 +103,28 @@ export function crawlDomainValidationToResult(
state: 'valid',
};
}
export const getDeleteDomainConfirmationMessage = (domainUrl: string) => {
return i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.action.deleteDomain.confirmationPopupMessage',
{
defaultMessage:
'Are you sure you want to remove the domain "{domainUrl}" and all of its settings?',
values: {
domainUrl,
},
}
);
};
export const getDeleteDomainSuccessMessage = (domainUrl: string) => {
return i18n.translate(
'xpack.enterpriseSearch.appSearch.crawler.action.deleteDomain.successMessage',
{
defaultMessage: "Domain '{domainUrl}' was deleted",
values: {
domainUrl,
},
}
);
};