From 4d7fd0a0ad8b33ba943a5958e86ca49b60ac2e8b Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Thu, 12 Aug 2021 12:39:58 -0400 Subject: [PATCH] [App Search] Added an EntryPointsTable (#108316) --- .../components/entry_points_table.test.tsx | 119 ++++++++++++++ .../crawler/components/entry_points_table.tsx | 150 ++++++++++++++++++ .../entry_points_table_logic.test.ts | 77 +++++++++ .../components/entry_points_table_logic.ts | 59 +++++++ .../crawler/crawler_single_domain.tsx | 14 +- .../crawler_single_domain_logic.test.ts | 30 ++++ .../crawler/crawler_single_domain_logic.ts | 6 +- ...generic_endpoint_inline_editable_table.tsx | 1 + .../index.ts | 8 + .../inline_editable_table.tsx | 2 + .../applications/shared/tables/types.ts | 2 +- .../app_search/crawler_entry_points.test.ts | 134 ++++++++++++++++ .../routes/app_search/crawler_entry_points.ts | 79 +++++++++ .../server/routes/app_search/index.ts | 2 + 14 files changed, 679 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts create mode 100644 x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx new file mode 100644 index 0000000000000..09f1523f8e3be --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.test.tsx @@ -0,0 +1,119 @@ +/* + * 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 { EuiFieldText } from '@elastic/eui'; + +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; + +import { mountWithIntl } from '../../../../test_helpers'; + +import { EntryPointsTable } from './entry_points_table'; + +describe('EntryPointsTable', () => { + const engineName = 'my-engine'; + const entryPoints = [ + { id: '1', value: '/whatever' }, + { id: '2', value: '/foo' }, + ]; + const domain = { + createdOn: '2018-01-01T00:00:00.000Z', + documentCount: 10, + id: '6113e1407a2f2e6f42489794', + url: 'https://www.elastic.co', + crawlRules: [], + entryPoints, + sitemaps: [], + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(GenericEndpointInlineEditableTable).exists()).toBe(true); + }); + + describe('the first and only column in the table', () => { + it('shows the value of an entry point', () => { + const entryPoint = { id: '1', value: '/whatever' }; + + const wrapper = shallow( + + ); + + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + const column = shallow(
{columns[0].render(entryPoint)}
); + expect(column.html()).toContain('/whatever'); + }); + + it('can show the value of an entry point as editable', () => { + const entryPoint = { id: '1', value: '/whatever' }; + const onChange = jest.fn(); + + const wrapper = shallow( + + ); + + const columns = wrapper.find(GenericEndpointInlineEditableTable).prop('columns'); + const column = shallow( +
+ {columns[0].editingRender(entryPoint, onChange, { isInvalid: false, isLoading: false })} +
+ ); + + const textField = column.find(EuiFieldText); + expect(textField.props()).toEqual( + expect.objectContaining({ + value: '/whatever', + disabled: false, // It would be disabled if isLoading is true + isInvalid: false, + prepend: 'https://www.elastic.co', + }) + ); + + textField.simulate('change', { target: { value: '/foo' } }); + expect(onChange).toHaveBeenCalledWith('/foo'); + }); + }); + + describe('routes', () => { + it('can calculate an update and delete route correctly', () => { + const wrapper = shallow( + + ); + + const table = wrapper.find(GenericEndpointInlineEditableTable); + + const entryPoint = { id: '1', value: '/whatever' }; + expect(table.prop('deleteRoute')(entryPoint)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/entry_points/1' + ); + expect(table.prop('updateRoute')(entryPoint)).toEqual( + '/api/app_search/engines/my-engine/crawler/domains/6113e1407a2f2e6f42489794/entry_points/1' + ); + }); + }); + + it('shows a no items message whem there are no entry points to show', () => { + const wrapper = shallow( + + ); + + const editNewItems = jest.fn(); + const table = wrapper.find(GenericEndpointInlineEditableTable); + const message = mountWithIntl(
{table.prop('noItemsMessage')!(editNewItems)}
); + expect(message.html()).toContain('There are no existing entry points.'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx new file mode 100644 index 0000000000000..7657a23ce4789 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table.tsx @@ -0,0 +1,150 @@ +/* + * 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 } from 'kea'; + +import { EuiFieldText, EuiLink, EuiSpacer, EuiText, EuiTitle } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { GenericEndpointInlineEditableTable } from '../../../../shared/tables/generic_endpoint_inline_editable_table'; +import { InlineEditableTableColumn } from '../../../../shared/tables/inline_editable_table/types'; +import { ItemWithAnID } from '../../../../shared/tables/types'; +import { DOCS_PREFIX } from '../../../routes'; +import { CrawlerDomain, EntryPoint } from '../types'; + +import { EntryPointsTableLogic } from './entry_points_table_logic'; + +interface EntryPointsTableProps { + domain: CrawlerDomain; + engineName: string; + items: EntryPoint[]; +} + +export const EntryPointsTable: React.FC = ({ + domain, + engineName, + items, +}) => { + const { onAdd, onDelete, onUpdate } = useActions(EntryPointsTableLogic); + const field = 'value'; + + const columns: Array> = [ + { + editingRender: (entryPoint, onChange, { isInvalid, isLoading }) => ( + onChange(e.target.value)} + disabled={isLoading} + isInvalid={isInvalid} + prepend={domain.url} + /> + ), + render: (entryPoint) => ( + + {domain.url} + {(entryPoint as EntryPoint)[field]} + + ), + name: i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.urlTableHead', + { defaultMessage: 'URL' } + ), + field, + }, + ]; + + const entryPointsRoute = `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/entry_points`; + + const getEntryPointRoute = (entryPoint: EntryPoint) => + `/api/app_search/engines/${engineName}/crawler/domains/${domain.id}/entry_points/${entryPoint.id}`; + + return ( + + {i18n.translate('xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.description', { + defaultMessage: + 'Include the most important URLs for your website here. Entry point URLs will be the first pages to be indexed and processed for links to other pages.', + })}{' '} + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.learnMoreLinkText', + { defaultMessage: 'Learn more about entry points.' } + )} + +

+ } + instanceId="EntryPointsTable" + items={items} + lastItemWarning={i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.lastItemMessage', + { defaultMessage: 'The crawler requires at least one entry point.' } + )} + // Since canRemoveLastItem is false, the only time noItemsMessage would be displayed is if the last entry point was deleted via the API. + noItemsMessage={(editNewItem) => ( + <> + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageTitle', + { + defaultMessage: 'There are no existing entry points.', + } + )} +

+
+ + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.emptyMessageLinkText', + { defaultMessage: 'Add an entry point' } + )} + + ), + }} + /> + + + + )} + addRoute={entryPointsRoute} + canRemoveLastItem={false} + deleteRoute={getEntryPointRoute} + updateRoute={getEntryPointRoute} + dataProperty="entry_points" + onAdd={onAdd} + onDelete={onDelete} + onUpdate={onUpdate} + title={i18n.translate('xpack.enterpriseSearch.appSearch.crawler.entryPointsTable.title', { + defaultMessage: 'Entry points', + })} + disableReordering + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts new file mode 100644 index 0000000000000..7d6704b9abdb3 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.test.ts @@ -0,0 +1,77 @@ +/* + * 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. + */ + +jest.mock('../crawler_single_domain_logic', () => ({ + CrawlerSingleDomainLogic: { + actions: { + updateEntryPoints: jest.fn(), + }, + }, +})); + +import { LogicMounter, mockFlashMessageHelpers } from '../../../../__mocks__/kea_logic'; + +import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; + +import { EntryPointsTableLogic } from './entry_points_table_logic'; + +describe('EntryPointsTableLogic', () => { + const { mount } = new LogicMounter(EntryPointsTableLogic); + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('listeners', () => { + describe('onAdd', () => { + it('should update the entry points for the current domain, and clear flash messages', () => { + const entryThatWasAdded = { id: '2', value: 'bar' }; + const updatedEntries = [ + { id: '1', value: 'foo' }, + { id: '2', value: 'bar' }, + ]; + mount(); + EntryPointsTableLogic.actions.onAdd(entryThatWasAdded, updatedEntries); + expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith( + updatedEntries + ); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + + describe('onDelete', () => { + it('should update the entry points for the current domain, clear flash messages, and show a success toast', () => { + const entryThatWasDeleted = { id: '2', value: 'bar' }; + const updatedEntries = [{ id: '1', value: 'foo' }]; + mount(); + EntryPointsTableLogic.actions.onDelete(entryThatWasDeleted, updatedEntries); + expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith( + updatedEntries + ); + expect(clearFlashMessages).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + }); + + describe('onUpdate', () => { + it('should update the entry points for the current domain, clear flash messages, and show a success toast', () => { + const entryThatWasUpdated = { id: '2', value: 'baz' }; + const updatedEntries = [ + { id: '1', value: 'foo' }, + { id: '2', value: 'baz' }, + ]; + mount(); + EntryPointsTableLogic.actions.onUpdate(entryThatWasUpdated, updatedEntries); + expect(CrawlerSingleDomainLogic.actions.updateEntryPoints).toHaveBeenCalledWith( + updatedEntries + ); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts new file mode 100644 index 0000000000000..2332a24ea8b74 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/entry_points_table_logic.ts @@ -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 { kea, MakeLogicType } from 'kea'; + +import { clearFlashMessages, flashSuccessToast } from '../../../../shared/flash_messages'; + +import { CrawlerSingleDomainLogic } from '../crawler_single_domain_logic'; + +import { EntryPoint } from '../types'; + +interface EntryPointsTableValues { + dataLoading: boolean; +} + +interface EntryPointsTableActions { + onAdd( + entryPoint: EntryPoint, + entryPoints: EntryPoint[] + ): { entryPoint: EntryPoint; entryPoints: EntryPoint[] }; + onDelete( + entryPoint: EntryPoint, + entryPoints: EntryPoint[] + ): { entryPoint: EntryPoint; entryPoints: EntryPoint[] }; + onUpdate( + entryPoint: EntryPoint, + entryPoints: EntryPoint[] + ): { entryPoint: EntryPoint; entryPoints: EntryPoint[] }; +} + +export const EntryPointsTableLogic = kea< + MakeLogicType +>({ + path: ['enterprise_search', 'app_search', 'crawler', 'entry_points_table'], + actions: () => ({ + onAdd: (entryPoint, entryPoints) => ({ entryPoint, entryPoints }), + onDelete: (entryPoint, entryPoints) => ({ entryPoint, entryPoints }), + onUpdate: (entryPoint, entryPoints) => ({ entryPoint, entryPoints }), + }), + listeners: () => ({ + onAdd: ({ entryPoints }) => { + CrawlerSingleDomainLogic.actions.updateEntryPoints(entryPoints); + clearFlashMessages(); + }, + onDelete: ({ entryPoint, entryPoints }) => { + CrawlerSingleDomainLogic.actions.updateEntryPoints(entryPoints); + clearFlashMessages(); + flashSuccessToast(`Entry point "${entryPoint.value}" was removed.`); + }, + onUpdate: ({ entryPoints }) => { + CrawlerSingleDomainLogic.actions.updateEntryPoints(entryPoints); + clearFlashMessages(); + }, + }), +}); 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 6419c31cc16ca..da910ebc30726 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,22 +11,24 @@ import { useParams } from 'react-router-dom'; import { useActions, useValues } from 'kea'; -import { EuiCode, EuiSpacer, EuiTitle } from '@elastic/eui'; +import { EuiCode, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { getEngineBreadcrumbs } from '../engine'; +import { EngineLogic, 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 { DeleteDomainPanel } from './components/delete_domain_panel'; +import { EntryPointsTable } from './components/entry_points_table'; import { ManageCrawlsPopover } from './components/manage_crawls_popover/manage_crawls_popover'; import { CRAWLER_TITLE } from './constants'; import { CrawlerSingleDomainLogic } from './crawler_single_domain_logic'; export const CrawlerSingleDomain: React.FC = () => { const { domainId } = useParams() as { domainId: string }; + const { engineName } = EngineLogic.values; const { dataLoading, domain } = useValues(CrawlerSingleDomainLogic); @@ -51,6 +53,14 @@ export const CrawlerSingleDomain: React.FC = () => { > + {domain && ( + <> + + + + + + )}

{i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts index f6c3b2c87ab8c..ead0c0ad91ced 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.test.ts @@ -51,6 +51,36 @@ describe('CrawlerSingleDomainLogic', () => { expect(CrawlerSingleDomainLogic.values.domain).toEqual(domain); }); }); + + describe('updateEntryPoints', () => { + beforeEach(() => { + mount({ + domain: { + id: '507f1f77bcf86cd799439011', + entryPoints: [], + }, + }); + + CrawlerSingleDomainLogic.actions.updateEntryPoints([ + { + id: '1234', + value: '/', + }, + ]); + }); + + it('should update the entry points on the domain', () => { + expect(CrawlerSingleDomainLogic.values.domain).toEqual({ + id: '507f1f77bcf86cd799439011', + entryPoints: [ + { + id: '1234', + value: '/', + }, + ], + }); + }); + }); }); describe('listeners', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts index 7b5ba6984f106..780cab45564bb 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/crawler_single_domain_logic.ts @@ -14,7 +14,7 @@ import { KibanaLogic } from '../../../shared/kibana'; import { ENGINE_CRAWLER_PATH } from '../../routes'; import { EngineLogic, generateEnginePath } from '../engine'; -import { CrawlerDomain } from './types'; +import { CrawlerDomain, EntryPoint } from './types'; import { crawlerDomainServerToClient, getDeleteDomainSuccessMessage } from './utils'; export interface CrawlerSingleDomainValues { @@ -26,6 +26,7 @@ interface CrawlerSingleDomainActions { deleteDomain(domain: CrawlerDomain): { domain: CrawlerDomain }; fetchDomainData(domainId: string): { domainId: string }; onReceiveDomainData(domain: CrawlerDomain): { domain: CrawlerDomain }; + updateEntryPoints(entryPoints: EntryPoint[]): { entryPoints: EntryPoint[] }; } export const CrawlerSingleDomainLogic = kea< @@ -36,6 +37,7 @@ export const CrawlerSingleDomainLogic = kea< deleteDomain: (domain) => ({ domain }), fetchDomainData: (domainId) => ({ domainId }), onReceiveDomainData: (domain) => ({ domain }), + updateEntryPoints: (entryPoints) => ({ entryPoints }), }, reducers: { dataLoading: [ @@ -48,6 +50,8 @@ export const CrawlerSingleDomainLogic = kea< null, { onReceiveDomainData: (_, { domain }) => domain, + updateEntryPoints: (currentDomain, { entryPoints }) => + ({ ...currentDomain, entryPoints } as CrawlerDomain), }, ], }, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx index 63b3d4b407667..4db5e81653a9a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/generic_endpoint_inline_editable_table.tsx @@ -32,6 +32,7 @@ export interface GenericEndpointInlineEditableTableProps onDelete(item: ItemWithAnID, items: ItemWithAnID[]): void; onUpdate(item: ItemWithAnID, items: ItemWithAnID[]): void; onReorder?(items: ItemWithAnID[]): void; + disableReordering?: boolean; } export const GenericEndpointInlineEditableTable = ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts new file mode 100644 index 0000000000000..ce766171a85c6 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/generic_endpoint_inline_editable_table/index.ts @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { GenericEndpointInlineEditableTable } from './generic_endpoint_inline_editable_table'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx index 3e351f5826abc..093692dfde335 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/inline_editable_table/inline_editable_table.tsx @@ -33,6 +33,7 @@ export interface InlineEditableTableProps { canRemoveLastItem?: boolean; className?: string; description?: React.ReactNode; + disableReordering?: boolean; isLoading?: boolean; lastItemWarning?: string; noItemsMessage?: (editNewItem: () => void) => React.ReactNode; @@ -170,6 +171,7 @@ export const InlineEditableTableContents = ({ noItemsMessage={noItemsMessage(editNewItem)} onReorder={reorderItems} disableDragging={isEditing} + {...rest} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts index f2a2531726b33..8407a14eae207 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/tables/types.ts @@ -6,6 +6,6 @@ */ export type ItemWithAnID = { - id: number | null; + id: number | string | null; created_at?: string; } & object; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts new file mode 100644 index 0000000000000..cdfb397b47c7e --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.test.ts @@ -0,0 +1,134 @@ +/* + * 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 { mockDependencies, mockRequestHandler, MockRouter } from '../../__mocks__'; + +import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; + +describe('crawler entry point routes', () => { + describe('POST /api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', + }); + + registerCrawlerEntryPointRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234' }, + body: { + value: 'test', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('PUT /api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + }); + + registerCrawlerEntryPointRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', entryPointId: '5678' }, + body: { + value: 'test', + }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {}, body: {} }; + mockRouter.shouldThrow(request); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + }); + + registerCrawlerEntryPointRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request to enterprise search', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }); + }); + + it('validates correctly with required params', () => { + const request = { + params: { engineName: 'some-engine', domainId: '1234', entryPointId: '5678' }, + }; + mockRouter.shouldValidate(request); + }); + + it('fails otherwise', () => { + const request = { params: {} }; + mockRouter.shouldThrow(request); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts new file mode 100644 index 0000000000000..88cb58f70953c --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/crawler_entry_points.ts @@ -0,0 +1,79 @@ +/* + * 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 { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerCrawlerEntryPointRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.post( + { + path: '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + }), + body: schema.object({ + value: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points', + params: { + respond_with: 'index', + }, + }) + ); + + router.put( + { + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + entryPointId: schema.string(), + }), + body: schema.object({ + value: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }) + ); + + router.delete( + { + path: + '/api/app_search/engines/{engineName}/crawler/domains/{domainId}/entry_points/{entryPointId}', + validate: { + params: schema.object({ + engineName: schema.string(), + domainId: schema.string(), + entryPointId: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/api/as/v0/engines/:engineName/crawler/domains/:domainId/entry_points/:entryPointId', + params: { + respond_with: 'index', + }, + }) + ); +} diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts index 2442b61c632c1..af5cc78f01e78 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/index.ts @@ -10,6 +10,7 @@ import { RouteDependencies } from '../../plugin'; import { registerAnalyticsRoutes } from './analytics'; import { registerApiLogsRoutes } from './api_logs'; import { registerCrawlerRoutes } from './crawler'; +import { registerCrawlerEntryPointRoutes } from './crawler_entry_points'; import { registerCredentialsRoutes } from './credentials'; import { registerCurationsRoutes } from './curations'; import { registerDocumentsRoutes, registerDocumentRoutes } from './documents'; @@ -44,4 +45,5 @@ export const registerAppSearchRoutes = (dependencies: RouteDependencies) => { registerApiLogsRoutes(dependencies); registerOnboardingRoutes(dependencies); registerCrawlerRoutes(dependencies); + registerCrawlerEntryPointRoutes(dependencies); };