diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_banner.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_banner.test.tsx
new file mode 100644
index 0000000000000..1be4c27037d1f
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_banner.test.tsx
@@ -0,0 +1,59 @@
+/*
+ * 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 { setMockValues } from '../../../../__mocks__/kea_logic';
+
+import React from 'react';
+
+import { shallow } from 'enzyme';
+
+import { EuiCallOut } from '@elastic/eui';
+
+import { CrawlerStatus } from '../types';
+
+import { CrawlerStatusBanner } from './crawler_status_banner';
+
+describe('CrawlerStatusBanner', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ [(CrawlerStatus.Starting, CrawlerStatus.Running, CrawlerStatus.Canceling)].forEach((status) => {
+ describe(`when the status is ${status}`, () => {
+ it('renders a callout', () => {
+ setMockValues({
+ mostRecentCrawlRequestStatus: status,
+ });
+
+ const wrapper = shallow();
+
+ expect(wrapper.find(EuiCallOut)).toHaveLength(1);
+ });
+ });
+ });
+
+ [
+ CrawlerStatus.Success,
+ CrawlerStatus.Failed,
+ CrawlerStatus.Canceled,
+ CrawlerStatus.Pending,
+ CrawlerStatus.Suspended,
+ CrawlerStatus.Suspending,
+ ].forEach((status) => {
+ describe(`when the status is ${status}`, () => {
+ it('does not render a banner/callout', () => {
+ setMockValues({
+ mostRecentCrawlRequestStatus: status,
+ });
+
+ const wrapper = shallow();
+
+ expect(wrapper.isEmptyRender()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_banner.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_banner.tsx
new file mode 100644
index 0000000000000..833db286fc2e5
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_banner.tsx
@@ -0,0 +1,40 @@
+/*
+ * 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 { useValues } from 'kea';
+
+import { EuiCallOut } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { CrawlerOverviewLogic } from '../crawler_overview_logic';
+import { CrawlerStatus } from '../types';
+
+export const CrawlerStatusBanner: React.FC = () => {
+ const { mostRecentCrawlRequestStatus } = useValues(CrawlerOverviewLogic);
+ if (
+ mostRecentCrawlRequestStatus === CrawlerStatus.Running ||
+ mostRecentCrawlRequestStatus === CrawlerStatus.Starting ||
+ mostRecentCrawlRequestStatus === CrawlerStatus.Canceling
+ ) {
+ return (
+
+ );
+ }
+ return null;
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx
new file mode 100644
index 0000000000000..9d585789d8e50
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx
@@ -0,0 +1,156 @@
+/*
+ * 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 { CrawlerDomain, CrawlerStatus } from '../../types';
+
+import { CrawlerStatusIndicator } from './crawler_status_indicator';
+import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu';
+
+const MOCK_VALUES = {
+ domains: [{}, {}] as CrawlerDomain[],
+ mostRecentCrawlRequestStatus: CrawlerStatus.Success,
+};
+
+const MOCK_ACTIONS = {
+ startCrawl: jest.fn(),
+ stopCrawl: jest.fn(),
+};
+
+describe('CrawlerStatusIndicator', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ setMockActions(MOCK_ACTIONS);
+ });
+
+ describe('when status is not a valid status', () => {
+ it('is disabled', () => {
+ // this tests a codepath that should be impossible to reach, status should always be a CrawlerStatus
+ // but we use a switch statement and need to test the default case for this to recieve 100% coverage
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: null,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Start a crawl');
+ expect(wrapper.prop('disabled')).toEqual(true);
+ });
+ });
+
+ describe('when there are no domains', () => {
+ it('is disabled', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ domains: [],
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Start a crawl');
+ expect(wrapper.prop('disabled')).toEqual(true);
+ });
+ });
+
+ describe('when the status is success', () => {
+ it('renders an CrawlerStatusIndicator with a start crawl button', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: CrawlerStatus.Success,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Start a crawl');
+ expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl);
+ });
+ });
+
+ [CrawlerStatus.Failed, CrawlerStatus.Canceled].forEach((status) => {
+ describe(`when the status is ready for retry: ${status}`, () => {
+ it('renders an CrawlerStatusIndicator with a retry crawl button', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: status,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Retry crawl');
+ expect(wrapper.prop('onClick')).toEqual(MOCK_ACTIONS.startCrawl);
+ });
+ });
+ });
+
+ [CrawlerStatus.Pending, CrawlerStatus.Suspended].forEach((status) => {
+ describe(`when the status is ${status}`, () => {
+ it('renders an CrawlerStatusIndicator with a pending indicator', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: status,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Pending...');
+ expect(wrapper.prop('disabled')).toEqual(true);
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+ });
+ });
+
+ describe('when the status is Starting', () => {
+ it('renders an appropriate CrawlerStatusIndicator', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: CrawlerStatus.Starting,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Starting...');
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+ });
+
+ describe('when the status is Running', () => {
+ it('renders a stop crawl popover menu', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: CrawlerStatus.Running,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(StopCrawlPopoverContextMenu)).toEqual(true);
+ expect(wrapper.prop('stopCrawl')).toEqual(MOCK_ACTIONS.stopCrawl);
+ });
+ });
+
+ [CrawlerStatus.Canceling, CrawlerStatus.Suspending].forEach((status) => {
+ describe(`when the status is ${status}`, () => {
+ it('renders an CrawlerStatusIndicator with a stopping indicator', () => {
+ setMockValues({
+ ...MOCK_VALUES,
+ mostRecentCrawlRequestStatus: status,
+ });
+
+ const wrapper = shallow();
+ expect(wrapper.is(EuiButton)).toEqual(true);
+ expect(wrapper.render().text()).toContain('Stopping...');
+ expect(wrapper.prop('isLoading')).toEqual(true);
+ });
+ });
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx
new file mode 100644
index 0000000000000..c1b8ad2073444
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.tsx
@@ -0,0 +1,106 @@
+/*
+ * 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 } from '@elastic/eui';
+
+import { i18n } from '@kbn/i18n';
+
+import { CrawlerOverviewLogic } from '../../crawler_overview_logic';
+import { CrawlerStatus } from '../../types';
+
+import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu';
+
+export const CrawlerStatusIndicator: React.FC = () => {
+ const { domains, mostRecentCrawlRequestStatus } = useValues(CrawlerOverviewLogic);
+ const { startCrawl, stopCrawl } = useActions(CrawlerOverviewLogic);
+
+ const disabledButton = (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel',
+ {
+ defaultMessage: 'Start a crawl',
+ }
+ )}
+
+ );
+
+ if (domains.length === 0) {
+ return disabledButton;
+ }
+
+ switch (mostRecentCrawlRequestStatus) {
+ case CrawlerStatus.Success:
+ return (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startACrawlButtonLabel',
+ {
+ defaultMessage: 'Start a crawl',
+ }
+ )}
+
+ );
+ case CrawlerStatus.Failed:
+ case CrawlerStatus.Canceled:
+ return (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.retryCrawlButtonLabel',
+ {
+ defaultMessage: 'Retry crawl',
+ }
+ )}
+
+ );
+ case CrawlerStatus.Pending:
+ case CrawlerStatus.Suspended:
+ return (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.pendingButtonLabel',
+ {
+ defaultMessage: 'Pending...',
+ }
+ )}
+
+ );
+ case CrawlerStatus.Starting:
+ return (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.startingButtonLabel',
+ {
+ defaultMessage: 'Starting...',
+ }
+ )}
+
+ );
+ case CrawlerStatus.Running:
+ return ;
+ case CrawlerStatus.Canceling:
+ case CrawlerStatus.Suspending:
+ return (
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.stoppingButtonLabel',
+ {
+ defaultMessage: 'Stopping...',
+ }
+ )}
+
+ );
+ default:
+ // We should never get here, you would have to pass a CrawlerStatus option not covered
+ // in the switch cases above
+ return disabledButton;
+ }
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/stop_crawl_popover_context_menu.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/stop_crawl_popover_context_menu.test.tsx
new file mode 100644
index 0000000000000..096238faff9a0
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/stop_crawl_popover_context_menu.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * 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 { shallow } from 'enzyme';
+
+import {
+ EuiButton,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiPopover,
+ EuiResizeObserver,
+} from '@elastic/eui';
+
+import { mountWithIntl } from '../../../../../test_helpers';
+
+import { StopCrawlPopoverContextMenu } from './stop_crawl_popover_context_menu';
+
+const stopCrawl = jest.fn();
+
+describe('StopCrawlsPopoverContextMenu', () => {
+ it('is initially closed', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.is(EuiPopover)).toBe(true);
+ expect(wrapper.prop('isOpen')).toEqual(false);
+ });
+
+ it('can be opened to stop crawls', () => {
+ const wrapper = mountWithIntl();
+
+ wrapper.find(EuiButton).simulate('click');
+
+ expect(wrapper.find(EuiPopover).prop('isOpen')).toEqual(true);
+
+ const menuItem = wrapper
+ .find(EuiContextMenuPanel)
+ .find(EuiResizeObserver)
+ .find(EuiContextMenuItem);
+
+ expect(menuItem).toHaveLength(1);
+
+ menuItem.simulate('click');
+
+ expect(stopCrawl).toHaveBeenCalled();
+ });
+});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/stop_crawl_popover_context_menu.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/stop_crawl_popover_context_menu.tsx
new file mode 100644
index 0000000000000..6c0e91995e281
--- /dev/null
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/stop_crawl_popover_context_menu.tsx
@@ -0,0 +1,86 @@
+/*
+ * 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, { useState } from 'react';
+
+import {
+ EuiButton,
+ EuiContextMenuItem,
+ EuiContextMenuPanel,
+ EuiFlexGroup,
+ EuiFlexItem,
+ EuiLoadingSpinner,
+ EuiPopover,
+} from '@elastic/eui';
+import { i18n } from '@kbn/i18n';
+
+interface StopCrawlPopoverContextMenuProps {
+ stopCrawl(): void;
+}
+
+export const StopCrawlPopoverContextMenu: React.FC = ({
+ stopCrawl,
+ ...rest
+}) => {
+ const [isPopoverOpen, setPopover] = useState(false);
+
+ const togglePopover = () => setPopover(!isPopoverOpen);
+
+ const closePopover = () => setPopover(false);
+
+ return (
+
+
+
+
+
+
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.crawlingButtonLabel',
+ {
+ defaultMessage: 'Crawling...',
+ }
+ )}
+
+
+
+ }
+ isOpen={isPopoverOpen}
+ closePopover={closePopover}
+ panelPaddingSize="none"
+ anchorPosition="downLeft"
+ >
+ {
+ closePopover();
+ stopCrawl();
+ }}
+ >
+ {i18n.translate(
+ 'xpack.enterpriseSearch.appSearch.crawler.crawlerStatusIndicator.cancelCrawlMenuItemLabel',
+ {
+ defaultMessage: 'Cancel Crawl',
+ }
+ )}
+ ,
+ ]}
+ />
+
+ );
+};
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
index e46804a658803..ae4fc6b04b002 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.test.tsx
@@ -13,10 +13,14 @@ import React from 'react';
import { shallow } from 'enzyme';
+import { getPageHeaderActions } from '../../../test_helpers';
+
import { AddDomainFlyout } from './components/add_domain/add_domain_flyout';
import { AddDomainForm } from './components/add_domain/add_domain_form';
import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_form_submit_button';
import { CrawlRequestsTable } from './components/crawl_requests_table';
+import { CrawlerStatusBanner } from './components/crawler_status_banner';
+import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator';
import { DomainsTable } from './components/domains_table';
import { CrawlerOverview } from './crawler_overview';
import {
@@ -75,6 +79,7 @@ const crawlRequests: CrawlRequestFromServer[] = [
describe('CrawlerOverview', () => {
const mockActions = {
fetchCrawlerData: jest.fn(),
+ getLatestCrawlRequests: jest.fn(),
};
const mockValues = {
@@ -88,12 +93,26 @@ describe('CrawlerOverview', () => {
setMockActions(mockActions);
});
- it('calls fetchCrawlerData on page load', () => {
+ it('calls fetchCrawlerData and starts polling on page load', () => {
setMockValues(mockValues);
shallow();
expect(mockActions.fetchCrawlerData).toHaveBeenCalledTimes(1);
+ expect(mockActions.getLatestCrawlRequests).toHaveBeenCalledWith(false);
+ });
+
+ it('contains a crawler status banner', () => {
+ setMockValues(mockValues);
+ const wrapper = shallow();
+
+ expect(wrapper.find(CrawlerStatusBanner)).toHaveLength(1);
+ });
+
+ it('contains a crawler status indicator', () => {
+ const wrapper = shallow();
+
+ expect(getPageHeaderActions(wrapper).find(CrawlerStatusIndicator)).toHaveLength(1);
});
it('hides the domain and crawl request tables when there are no domains, and no crawl requests', () => {
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
index 1f676467a5503..c18c1a753d247 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview.tsx
@@ -21,6 +21,8 @@ import { AddDomainFlyout } from './components/add_domain/add_domain_flyout';
import { AddDomainForm } from './components/add_domain/add_domain_form';
import { AddDomainFormSubmitButton } from './components/add_domain/add_domain_form_submit_button';
import { CrawlRequestsTable } from './components/crawl_requests_table';
+import { CrawlerStatusBanner } from './components/crawler_status_banner';
+import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator';
import { DomainsTable } from './components/domains_table';
import { CRAWLER_TITLE } from './constants';
import { CrawlerOverviewLogic } from './crawler_overview_logic';
@@ -28,18 +30,24 @@ import { CrawlerOverviewLogic } from './crawler_overview_logic';
export const CrawlerOverview: React.FC = () => {
const { crawlRequests, dataLoading, domains } = useValues(CrawlerOverviewLogic);
- const { fetchCrawlerData } = useActions(CrawlerOverviewLogic);
+ const { fetchCrawlerData, getLatestCrawlRequests } = useActions(CrawlerOverviewLogic);
useEffect(() => {
fetchCrawlerData();
+ getLatestCrawlRequests(false);
}, []);
return (
],
+ }}
isLoading={dataLoading}
>
+
+
{domains.length > 0 ? (
<>
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
index 6ec0cb0f26a78..86f6e14631329 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.test.ts
@@ -23,15 +23,16 @@ import {
CrawlerRules,
CrawlerStatus,
CrawlRequest,
- CrawlRequestFromServer,
CrawlRule,
} from './types';
-import { crawlerDataServerToClient, crawlRequestServerToClient } from './utils';
+import { crawlerDataServerToClient } from './utils';
const DEFAULT_VALUES: CrawlerOverviewValues = {
crawlRequests: [],
dataLoading: true,
domains: [],
+ mostRecentCrawlRequestStatus: CrawlerStatus.Success,
+ timeoutId: null,
};
const DEFAULT_CRAWL_RULE: CrawlRule = {
@@ -55,36 +56,50 @@ const MOCK_SERVER_CRAWLER_DATA: CrawlerDataFromServer = {
],
};
-const MOCK_SERVER_CRAWL_REQUESTS_DATA: CrawlRequestFromServer[] = [
- {
- id: '618d0e66abe97bc688328900',
- status: CrawlerStatus.Pending,
- created_at: 'Mon, 31 Aug 2020 17:00:00 +0000',
- began_at: null,
- completed_at: null,
- },
-];
-
const MOCK_CLIENT_CRAWLER_DATA = crawlerDataServerToClient(MOCK_SERVER_CRAWLER_DATA);
-const MOCK_CLIENT_CRAWL_REQUESTS_DATA = MOCK_SERVER_CRAWL_REQUESTS_DATA.map(
- crawlRequestServerToClient
-);
describe('CrawlerOverviewLogic', () => {
- const { mount } = new LogicMounter(CrawlerOverviewLogic);
+ const { mount, unmount } = new LogicMounter(CrawlerOverviewLogic);
const { http } = mockHttpValues;
const { flashAPIErrors, flashSuccessToast } = mockFlashMessageHelpers;
beforeEach(() => {
jest.clearAllMocks();
+ jest.useFakeTimers(); // this should be run before every test to reset these mocks
mount();
});
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
it('has expected default values', () => {
expect(CrawlerOverviewLogic.values).toEqual(DEFAULT_VALUES);
});
describe('actions', () => {
+ describe('clearTimeoutId', () => {
+ it('clears the timeout in the logic', () => {
+ mount({
+ timeoutId: setTimeout(() => {}, 1),
+ });
+
+ CrawlerOverviewLogic.actions.clearTimeoutId();
+
+ expect(CrawlerOverviewLogic.values.timeoutId).toEqual(null);
+ });
+ });
+
+ describe('onCreateNewTimeout', () => {
+ it('sets the timeout in the logic', () => {
+ const timeout = setTimeout(() => {}, 1);
+
+ CrawlerOverviewLogic.actions.onCreateNewTimeout(timeout);
+
+ expect(CrawlerOverviewLogic.values.timeoutId).toEqual(timeout);
+ });
+ });
+
describe('onReceiveCrawlerData', () => {
const crawlerData: CrawlerData = {
domains: [
@@ -139,42 +154,20 @@ describe('CrawlerOverviewLogic', () => {
describe('fetchCrawlerData', () => {
it('updates logic with data that has been converted from server to client', async () => {
jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData');
- // TODO this spyOn should be removed when crawl request polling is added
- jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlRequests');
-
- // TODO this first mock for MOCK_SERVER_CRAWL_REQUESTS_DATA should be removed when crawl request polling is added
- http.get.mockReturnValueOnce(Promise.resolve(MOCK_SERVER_CRAWL_REQUESTS_DATA));
http.get.mockReturnValueOnce(Promise.resolve(MOCK_SERVER_CRAWLER_DATA));
+
CrawlerOverviewLogic.actions.fetchCrawlerData();
await nextTick();
- expect(http.get).toHaveBeenNthCalledWith(
- 1,
- '/api/app_search/engines/some-engine/crawler/crawl_requests'
- );
- expect(CrawlerOverviewLogic.actions.onReceiveCrawlRequests).toHaveBeenCalledWith(
- MOCK_CLIENT_CRAWL_REQUESTS_DATA
- );
-
- expect(http.get).toHaveBeenNthCalledWith(2, '/api/app_search/engines/some-engine/crawler');
+ expect(http.get).toHaveBeenCalledWith('/api/app_search/engines/some-engine/crawler');
expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith(
MOCK_CLIENT_CRAWLER_DATA
);
});
- // TODO this test should be removed when crawl request polling is added
- it('calls flashApiErrors when there is an error on the request for crawl results', async () => {
- http.get.mockReturnValueOnce(Promise.reject('error'));
- CrawlerOverviewLogic.actions.fetchCrawlerData();
- await nextTick();
-
- expect(flashAPIErrors).toHaveBeenCalledWith('error');
- });
-
it('calls flashApiErrors when there is an error on the request for crawler data', async () => {
- // TODO this first mock for MOCK_SERVER_CRAWL_REQUESTS_DATA should be removed when crawl request polling is added
- http.get.mockReturnValueOnce(Promise.resolve(MOCK_SERVER_CRAWL_REQUESTS_DATA));
http.get.mockReturnValueOnce(Promise.reject('error'));
+
CrawlerOverviewLogic.actions.fetchCrawlerData();
await nextTick();
@@ -185,8 +178,8 @@ describe('CrawlerOverviewLogic', () => {
describe('deleteDomain', () => {
it('calls onReceiveCrawlerData with retrieved data that has been converted from server to client', async () => {
jest.spyOn(CrawlerOverviewLogic.actions, 'onReceiveCrawlerData');
-
http.delete.mockReturnValue(Promise.resolve(MOCK_SERVER_CRAWLER_DATA));
+
CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
await nextTick();
@@ -204,11 +197,248 @@ describe('CrawlerOverviewLogic', () => {
it('calls flashApiErrors when there is an error', async () => {
http.delete.mockReturnValue(Promise.reject('error'));
+
CrawlerOverviewLogic.actions.deleteDomain({ id: '1234' } as CrawlerDomain);
await nextTick();
expect(flashAPIErrors).toHaveBeenCalledWith('error');
});
});
+
+ describe('startCrawl', () => {
+ describe('success path', () => {
+ it('creates a new crawl request and then fetches the latest crawl requests', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests');
+ http.post.mockReturnValueOnce(Promise.resolve());
+
+ CrawlerOverviewLogic.actions.startCrawl();
+ await nextTick();
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/crawler/crawl_requests'
+ );
+ expect(CrawlerOverviewLogic.actions.getLatestCrawlRequests).toHaveBeenCalled();
+ });
+ });
+
+ describe('on failure', () => {
+ it('flashes an error message', async () => {
+ http.post.mockReturnValueOnce(Promise.reject('error'));
+
+ CrawlerOverviewLogic.actions.startCrawl();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
+ });
+
+ describe('stopCrawl', () => {
+ describe('success path', () => {
+ it('stops the crawl starts and then fetches the latest crawl requests', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests');
+ http.post.mockReturnValueOnce(Promise.resolve());
+
+ CrawlerOverviewLogic.actions.stopCrawl();
+ await nextTick();
+
+ expect(http.post).toHaveBeenCalledWith(
+ '/api/app_search/engines/some-engine/crawler/crawl_requests/cancel'
+ );
+ expect(CrawlerOverviewLogic.actions.getLatestCrawlRequests).toHaveBeenCalled();
+ });
+ });
+
+ describe('on failure', () => {
+ it('flashes an error message', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests');
+ http.post.mockReturnValueOnce(Promise.reject('error'));
+
+ CrawlerOverviewLogic.actions.stopCrawl();
+ await nextTick();
+
+ expect(flashAPIErrors).toHaveBeenCalledWith('error');
+ });
+ });
+ });
+
+ describe('createNewTimeoutForCrawlRequests', () => {
+ it('saves the timeout ID in the logic', () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'onCreateNewTimeout');
+ jest.spyOn(CrawlerOverviewLogic.actions, 'getLatestCrawlRequests');
+
+ CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests(2000);
+
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 2000);
+ expect(CrawlerOverviewLogic.actions.onCreateNewTimeout).toHaveBeenCalled();
+
+ jest.runAllTimers();
+
+ expect(CrawlerOverviewLogic.actions.getLatestCrawlRequests).toHaveBeenCalled();
+ });
+
+ it('clears a timeout if one already exists', () => {
+ const timeoutId = setTimeout(() => {}, 1);
+ mount({
+ timeoutId,
+ });
+
+ CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests(2000);
+
+ expect(clearTimeout).toHaveBeenCalledWith(timeoutId);
+ });
+ });
+
+ describe('getLatestCrawlRequests', () => {
+ describe('on success', () => {
+ [
+ CrawlerStatus.Pending,
+ CrawlerStatus.Starting,
+ CrawlerStatus.Running,
+ CrawlerStatus.Canceling,
+ ].forEach((status) => {
+ it(`creates a new timeout for status ${status}`, async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'createNewTimeoutForCrawlRequests');
+ http.get.mockReturnValueOnce(Promise.resolve([{ status }]));
+
+ CrawlerOverviewLogic.actions.getLatestCrawlRequests();
+ await nextTick();
+
+ expect(
+ CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests
+ ).toHaveBeenCalled();
+ });
+ });
+
+ [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].forEach((status) => {
+ it(`clears the timeout and fetches data for status ${status}`, async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'clearTimeoutId');
+ jest.spyOn(CrawlerOverviewLogic.actions, 'fetchCrawlerData');
+ http.get.mockReturnValueOnce(Promise.resolve([{ status }]));
+
+ CrawlerOverviewLogic.actions.getLatestCrawlRequests();
+ await nextTick();
+
+ expect(CrawlerOverviewLogic.actions.clearTimeoutId).toHaveBeenCalled();
+ expect(CrawlerOverviewLogic.actions.fetchCrawlerData).toHaveBeenCalled();
+ });
+
+ it(`optionally supresses fetching data for status ${status}`, async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'clearTimeoutId');
+ jest.spyOn(CrawlerOverviewLogic.actions, 'fetchCrawlerData');
+ http.get.mockReturnValueOnce(Promise.resolve([{ status }]));
+
+ CrawlerOverviewLogic.actions.getLatestCrawlRequests(false);
+ await nextTick();
+
+ expect(CrawlerOverviewLogic.actions.clearTimeoutId).toHaveBeenCalled();
+ expect(CrawlerOverviewLogic.actions.fetchCrawlerData).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+
+ describe('on failure', () => {
+ it('creates a new timeout', async () => {
+ jest.spyOn(CrawlerOverviewLogic.actions, 'createNewTimeoutForCrawlRequests');
+ http.get.mockReturnValueOnce(Promise.reject());
+
+ CrawlerOverviewLogic.actions.getLatestCrawlRequests();
+ await nextTick();
+
+ expect(CrawlerOverviewLogic.actions.createNewTimeoutForCrawlRequests).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('selectors', () => {
+ describe('mostRecentCrawlRequestStatus', () => {
+ it('is Success when there are no crawl requests', () => {
+ mount({
+ crawlRequests: [],
+ });
+
+ expect(CrawlerOverviewLogic.values.mostRecentCrawlRequestStatus).toEqual(
+ CrawlerStatus.Success
+ );
+ });
+
+ it('is Success when there are only crawl requests', () => {
+ mount({
+ crawlRequests: [
+ {
+ id: '2',
+ status: CrawlerStatus.Skipped,
+ createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000',
+ beganAt: null,
+ completedAt: null,
+ },
+ {
+ id: '1',
+ status: CrawlerStatus.Skipped,
+ createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000',
+ beganAt: null,
+ completedAt: null,
+ },
+ ],
+ });
+
+ expect(CrawlerOverviewLogic.values.mostRecentCrawlRequestStatus).toEqual(
+ CrawlerStatus.Success
+ );
+ });
+
+ it('is the first non-skipped crawl request status', () => {
+ mount({
+ crawlRequests: [
+ {
+ id: '3',
+ status: CrawlerStatus.Skipped,
+ createdAt: 'Mon, 31 Aug 2020 17:00:00 +0000',
+ beganAt: null,
+ completedAt: null,
+ },
+ {
+ id: '2',
+ status: CrawlerStatus.Failed,
+ createdAt: 'Mon, 30 Aug 2020 17:00:00 +0000',
+ beganAt: null,
+ completedAt: null,
+ },
+ {
+ id: '1',
+ status: CrawlerStatus.Success,
+ createdAt: 'Mon, 29 Aug 2020 17:00:00 +0000',
+ beganAt: null,
+ completedAt: null,
+ },
+ ],
+ });
+
+ expect(CrawlerOverviewLogic.values.mostRecentCrawlRequestStatus).toEqual(
+ CrawlerStatus.Failed
+ );
+ });
+ });
+ });
+
+ describe('events', () => {
+ describe('beforeUnmount', () => {
+ it('clears the timeout if there is one', () => {
+ jest.spyOn(global, 'setTimeout');
+
+ mount({
+ timeoutId: setTimeout(() => {}, 1),
+ });
+ unmount();
+
+ expect(setTimeout).toHaveBeenCalled();
+ });
+
+ it('does not crash if no timeout exists', () => {
+ mount({ timeoutId: null });
+ unmount();
+ });
+ });
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
index 5d6d1db2bd8a3..35dd866c0453e 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_overview_logic.ts
@@ -14,7 +14,13 @@ import { flashAPIErrors, flashSuccessToast } from '../../../shared/flash_message
import { HttpLogic } from '../../../shared/http';
import { EngineLogic } from '../engine';
-import { CrawlerData, CrawlerDomain, CrawlRequest, CrawlRequestFromServer } from './types';
+import {
+ CrawlerData,
+ CrawlerDomain,
+ CrawlRequest,
+ CrawlRequestFromServer,
+ CrawlerStatus,
+} from './types';
import { crawlerDataServerToClient, crawlRequestServerToClient } from './utils';
export const DELETE_DOMAIN_MESSAGE = (domainUrl: string) =>
@@ -28,17 +34,28 @@ export const DELETE_DOMAIN_MESSAGE = (domainUrl: string) =>
}
);
+const POLLING_DURATION = 1000;
+const POLLING_DURATION_ON_FAILURE = 5000;
+
export interface CrawlerOverviewValues {
crawlRequests: CrawlRequest[];
dataLoading: boolean;
domains: CrawlerDomain[];
+ mostRecentCrawlRequestStatus: CrawlerStatus;
+ timeoutId: NodeJS.Timeout | null;
}
interface CrawlerOverviewActions {
+ clearTimeoutId(): void;
+ createNewTimeoutForCrawlRequests(duration: number): { duration: number };
deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain };
fetchCrawlerData(): void;
+ getLatestCrawlRequests(refreshData?: boolean): { refreshData?: boolean };
+ onCreateNewTimeout(timeoutId: NodeJS.Timeout): { timeoutId: NodeJS.Timeout };
onReceiveCrawlerData(data: CrawlerData): { data: CrawlerData };
onReceiveCrawlRequests(crawlRequests: CrawlRequest[]): { crawlRequests: CrawlRequest[] };
+ startCrawl(): void;
+ stopCrawl(): void;
}
export const CrawlerOverviewLogic = kea<
@@ -46,10 +63,16 @@ export const CrawlerOverviewLogic = kea<
>({
path: ['enterprise_search', 'app_search', 'crawler', 'crawler_overview'],
actions: {
+ clearTimeoutId: true,
+ createNewTimeoutForCrawlRequests: (duration) => ({ duration }),
deleteDomain: (domain) => ({ domain }),
fetchCrawlerData: true,
+ getLatestCrawlRequests: (refreshData) => ({ refreshData }),
+ onCreateNewTimeout: (timeoutId) => ({ timeoutId }),
onReceiveCrawlerData: (data) => ({ data }),
onReceiveCrawlRequests: (crawlRequests) => ({ crawlRequests }),
+ startCrawl: () => null,
+ stopCrawl: () => null,
},
reducers: {
dataLoading: [
@@ -70,24 +93,33 @@ export const CrawlerOverviewLogic = kea<
onReceiveCrawlRequests: (_, { crawlRequests }) => crawlRequests,
},
],
+ timeoutId: [
+ null,
+ {
+ clearTimeoutId: () => null,
+ onCreateNewTimeout: (_, { timeoutId }) => timeoutId,
+ },
+ ],
},
- listeners: ({ actions }) => ({
+ selectors: ({ selectors }) => ({
+ mostRecentCrawlRequestStatus: [
+ () => [selectors.crawlRequests],
+ (crawlRequests: CrawlerOverviewValues['crawlRequests']) => {
+ const eligibleCrawlRequests = crawlRequests.filter(
+ (req) => req.status !== CrawlerStatus.Skipped
+ );
+ if (eligibleCrawlRequests.length === 0) {
+ return CrawlerStatus.Success;
+ }
+ return eligibleCrawlRequests[0].status;
+ },
+ ],
+ }),
+ listeners: ({ actions, values }) => ({
fetchCrawlerData: async () => {
const { http } = HttpLogic.values;
const { engineName } = EngineLogic.values;
- // TODO Remove fetching crawl requests here once Crawl Request Polling is implemented
- try {
- const crawlResultsResponse: CrawlRequestFromServer[] = await http.get(
- `/api/app_search/engines/${engineName}/crawler/crawl_requests`
- );
-
- const crawlRequests = crawlResultsResponse.map(crawlRequestServerToClient);
- actions.onReceiveCrawlRequests(crawlRequests);
- } catch (e) {
- flashAPIErrors(e);
- }
-
try {
const response = await http.get(`/api/app_search/engines/${engineName}/crawler`);
@@ -118,5 +150,78 @@ export const CrawlerOverviewLogic = kea<
flashAPIErrors(e);
}
},
+ startCrawl: async () => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ await http.post(`/api/app_search/engines/${engineName}/crawler/crawl_requests`);
+ actions.getLatestCrawlRequests();
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ stopCrawl: async () => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ await http.post(`/api/app_search/engines/${engineName}/crawler/crawl_requests/cancel`);
+ actions.getLatestCrawlRequests();
+ } catch (e) {
+ flashAPIErrors(e);
+ }
+ },
+ createNewTimeoutForCrawlRequests: ({ duration }) => {
+ if (values.timeoutId) {
+ clearTimeout(values.timeoutId);
+ }
+
+ const timeoutIdId = setTimeout(() => {
+ actions.getLatestCrawlRequests();
+ }, duration);
+
+ actions.onCreateNewTimeout(timeoutIdId);
+ },
+ getLatestCrawlRequests: async ({ refreshData = true }) => {
+ const { http } = HttpLogic.values;
+ const { engineName } = EngineLogic.values;
+
+ try {
+ const crawlRequestsFromServer: CrawlRequestFromServer[] = await http.get(
+ `/api/app_search/engines/${engineName}/crawler/crawl_requests`
+ );
+ const crawlRequests = crawlRequestsFromServer.map(crawlRequestServerToClient);
+ actions.onReceiveCrawlRequests(crawlRequests);
+ if (
+ [
+ CrawlerStatus.Pending,
+ CrawlerStatus.Starting,
+ CrawlerStatus.Running,
+ CrawlerStatus.Canceling,
+ ].includes(crawlRequests[0]?.status)
+ ) {
+ actions.createNewTimeoutForCrawlRequests(POLLING_DURATION);
+ } else if (
+ [CrawlerStatus.Success, CrawlerStatus.Failed, CrawlerStatus.Canceled].includes(
+ crawlRequests[0]?.status
+ )
+ ) {
+ actions.clearTimeoutId();
+ if (refreshData) {
+ actions.fetchCrawlerData();
+ }
+ }
+ } catch (e) {
+ actions.createNewTimeoutForCrawlRequests(POLLING_DURATION_ON_FAILURE);
+ }
+ },
+ }),
+ events: ({ values }) => ({
+ beforeUnmount: () => {
+ if (values.timeoutId) {
+ clearTimeout(values.timeoutId);
+ }
+ },
}),
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx
index dd0966276dd68..e2b230041c810 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.test.tsx
@@ -15,17 +15,28 @@ import { shallow } from 'enzyme';
import { EuiCode } from '@elastic/eui';
+import { getPageHeaderActions } from '../../../test_helpers';
+
+import { CrawlerStatusBanner } from './components/crawler_status_banner';
+import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator';
+import { CrawlerOverview } from './crawler_overview';
import { CrawlerSingleDomain } from './crawler_single_domain';
const MOCK_VALUES = {
+ // CrawlerSingleDomainLogic
dataLoading: false,
domain: {
url: 'https://elastic.co',
},
+ // CrawlerOverviewLogic
+ domains: [],
+ crawlRequests: [],
};
const MOCK_ACTIONS = {
+ fetchCrawlerData: jest.fn(),
fetchDomainData: jest.fn(),
+ getLatestCrawlRequests: jest.fn(),
};
describe('CrawlerSingleDomain', () => {
@@ -40,10 +51,10 @@ describe('CrawlerSingleDomain', () => {
const wrapper = shallow();
expect(wrapper.find(EuiCode).render().text()).toContain('https://elastic.co');
- expect(wrapper.prop('pageHeader')).toEqual({ pageTitle: 'https://elastic.co' });
+ expect(wrapper.prop('pageHeader').pageTitle).toEqual('https://elastic.co');
});
- it('uses a placeholder for the page title and page chrome if a domain has not been', () => {
+ it('uses a placeholder for the page title and page chrome if a domain has not been set', () => {
setMockValues({
...MOCK_VALUES,
domain: null,
@@ -51,6 +62,18 @@ describe('CrawlerSingleDomain', () => {
const wrapper = shallow();
- expect(wrapper.prop('pageHeader')).toEqual({ pageTitle: 'Loading...' });
+ expect(wrapper.prop('pageHeader').pageTitle).toEqual('Loading...');
+ });
+
+ it('contains a crawler status banner', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find(CrawlerStatusBanner)).toHaveLength(1);
+ });
+
+ it('contains a crawler status indicator', () => {
+ const wrapper = shallow();
+
+ expect(getPageHeaderActions(wrapper).find(CrawlerStatusIndicator)).toHaveLength(1);
});
});
diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx
index bdcfa465c8c32..9f9ec288a690c 100644
--- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx
+++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain.tsx
@@ -11,13 +11,15 @@ import { useParams } from 'react-router-dom';
import { useActions, useValues } from 'kea';
-import { EuiCode } from '@elastic/eui';
+import { EuiCode, EuiSpacer } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { getEngineBreadcrumbs } from '../engine';
import { AppSearchPageTemplate } from '../layout';
+import { CrawlerStatusBanner } from './components/crawler_status_banner';
+import { CrawlerStatusIndicator } from './components/crawler_status_indicator/crawler_status_indicator';
import { CRAWLER_TITLE } from './constants';
import { CrawlerSingleDomainLogic } from './crawler_single_domain_logic';
@@ -41,9 +43,11 @@ export const CrawlerSingleDomain: React.FC = () => {
return (
] }}
isLoading={dataLoading}
>
+
+
{JSON.stringify(domain, null, 2)}
);
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
index ee41dce661451..3b790bf2696cb 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.test.ts
@@ -76,6 +76,72 @@ describe('crawler routes', () => {
});
});
+ describe('POST /api/app_search/engines/{name}/crawler/crawl_requests', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{name}/crawler/crawl_requests',
+ });
+
+ registerCrawlerRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:name/crawler/crawl_requests',
+ });
+ });
+
+ it('validates correctly with name', () => {
+ const request = { params: { name: 'some-engine' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: {} };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
+ describe('POST /api/app_search/engines/{name}/crawler/crawl_requests/cancel', () => {
+ let mockRouter: MockRouter;
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ mockRouter = new MockRouter({
+ method: 'post',
+ path: '/api/app_search/engines/{name}/crawler/crawl_requests/cancel',
+ });
+
+ registerCrawlerRoutes({
+ ...mockDependencies,
+ router: mockRouter.router,
+ });
+ });
+
+ it('creates a request to enterprise search', () => {
+ expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
+ path: '/api/as/v0/engines/:name/crawler/crawl_requests/active/cancel',
+ });
+ });
+
+ it('validates correctly with name', () => {
+ const request = { params: { name: 'some-engine' } };
+ mockRouter.shouldValidate(request);
+ });
+
+ it('fails validation without name', () => {
+ const request = { params: {} };
+ mockRouter.shouldThrow(request);
+ });
+ });
+
describe('POST /api/app_search/engines/{name}/crawler/domains', () => {
let mockRouter: MockRouter;
diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
index 29c1dd74582a3..26a755a6d2b6c 100644
--- a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
+++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler.ts
@@ -41,6 +41,34 @@ export function registerCrawlerRoutes({
})
);
+ router.post(
+ {
+ path: '/api/app_search/engines/{name}/crawler/crawl_requests',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:name/crawler/crawl_requests',
+ })
+ );
+
+ router.post(
+ {
+ path: '/api/app_search/engines/{name}/crawler/crawl_requests/cancel',
+ validate: {
+ params: schema.object({
+ name: schema.string(),
+ }),
+ },
+ },
+ enterpriseSearchRequestHandler.createRequest({
+ path: '/api/as/v0/engines/:name/crawler/crawl_requests/active/cancel',
+ })
+ );
+
router.post(
{
path: '/api/app_search/engines/{name}/crawler/domains',