diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx index 2d7e3438d4c02..dfca497807718 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_nav.tsx @@ -211,8 +211,7 @@ export const EngineNav: React.FC = () => { )} {canManageEngineSynonyms && ( {SYNONYMS_TITLE} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx index 27ef42e72764c..d01958942e0a1 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.test.tsx @@ -22,6 +22,7 @@ import { CurationsRouter } from '../curations'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; import { ResultSettings } from '../result_settings'; +import { Synonyms } from '../synonyms'; import { EngineRouter } from './engine_router'; @@ -100,6 +101,13 @@ describe('EngineRouter', () => { expect(wrapper.find(AnalyticsRouter)).toHaveLength(1); }); + it('renders a synonyms view', () => { + setMockValues({ ...values, myRole: { canManageEngineSynonyms: true } }); + const wrapper = shallow(); + + expect(wrapper.find(Synonyms)).toHaveLength(1); + }); + it('renders a curations view', () => { setMockValues({ ...values, myRole: { canManageEngineCurations: true } }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx index 8c6f248e9ce8e..c246af3611563 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine/engine_router.tsx @@ -27,7 +27,7 @@ import { // ENGINE_CRAWLER_PATH, // META_ENGINE_SOURCE_ENGINES_PATH, ENGINE_RELEVANCE_TUNING_PATH, - // ENGINE_SYNONYMS_PATH, + ENGINE_SYNONYMS_PATH, ENGINE_CURATIONS_PATH, ENGINE_RESULT_SETTINGS_PATH, // ENGINE_SEARCH_UI_PATH, @@ -39,8 +39,8 @@ import { CurationsRouter } from '../curations'; import { DocumentDetail, Documents } from '../documents'; import { EngineOverview } from '../engine_overview'; import { RelevanceTuning } from '../relevance_tuning'; - import { ResultSettings } from '../result_settings'; +import { Synonyms } from '../synonyms'; import { EngineLogic, getEngineBreadcrumbs } from './'; @@ -53,7 +53,7 @@ export const EngineRouter: React.FC = () => { // canViewEngineCrawler, // canViewMetaEngineSourceEngines, canManageEngineRelevanceTuning, - // canManageEngineSynonyms, + canManageEngineSynonyms, canManageEngineCurations, canManageEngineResultSettings, // canManageEngineSearchUi, @@ -107,6 +107,11 @@ export const EngineRouter: React.FC = () => { )} + {canManageEngineSynonyms && ( + + + + )} {canManageEngineResultSettings && ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts index 5b0fde246ed44..177bc5eade0f6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/index.ts @@ -6,3 +6,4 @@ */ export { SYNONYMS_TITLE } from './constants'; +export { Synonyms } from './synonyms'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx new file mode 100644 index 0000000000000..e093442f77b77 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.test.tsx @@ -0,0 +1,21 @@ +/* + * 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 '../../__mocks__/engine_logic.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { Synonyms } from './'; + +describe('Synonyms', () => { + it('renders', () => { + shallow(); + // TODO: Check for Synonym cards, Synonym modal + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx new file mode 100644 index 0000000000000..0b18271660911 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/synonyms/synonyms.tsx @@ -0,0 +1,27 @@ +/* + * 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 { EuiPageHeader, EuiPageContentBody } from '@elastic/eui'; + +import { FlashMessages } from '../../../shared/flash_messages'; +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { getEngineBreadcrumbs } from '../engine'; + +import { SYNONYMS_TITLE } from './constants'; + +export const Synonyms: React.FC = () => { + return ( + <> + + + + TODO + + ); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts new file mode 100644 index 0000000000000..26b44b5ad8889 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.test.ts @@ -0,0 +1,207 @@ +/* + * 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 { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerSynonymsRoutes } from './synonyms'; + +describe('synonyms routes', () => { + describe('GET /api/app_search/engines/{engineName}/synonyms', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/api/app_search/engines/{engineName}/synonyms', + }); + + registerSynonymsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/synonyms/collection', + }); + }); + + describe('validates', () => { + it('with pagination query params', () => { + const request = { + query: { + 'page[current]': 1, + 'page[size]': 10, + }, + }; + mockRouter.shouldValidate(request); + }); + + it('missing query params', () => { + const request = { query: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('POST /api/app_search/engines/{engineName}/synonyms', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines/{engineName}/synonyms', + }); + + registerSynonymsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/synonyms/collection', + }); + }); + + describe('validates', () => { + it('with synonyms', () => { + const request = { + body: { + synonyms: ['a', 'b', 'c'], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('empty synonyms array', () => { + const request = { + body: { + queries: [], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('only one synonym', () => { + const request = { + body: { + queries: ['a'], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('empty synonym strings', () => { + const request = { + body: { + queries: ['', '', ''], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('missing synonyms', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('PUT /api/app_search/engines/{engineName}/synonyms/{synonymId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'put', + path: '/api/app_search/engines/{engineName}/synonyms/{synonymId}', + }); + + registerSynonymsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/synonyms/:synonymId', + }); + }); + + describe('validates', () => { + it('with synonyms', () => { + const request = { + body: { + synonyms: ['a', 'b', 'c'], + }, + }; + mockRouter.shouldValidate(request); + }); + + it('empty synonyms array', () => { + const request = { + body: { + queries: [], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('only one synonym', () => { + const request = { + body: { + queries: ['a'], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('empty synonym strings', () => { + const request = { + body: { + queries: ['', '', ''], + }, + }; + mockRouter.shouldThrow(request); + }); + + it('missing synonyms', () => { + const request = { body: {} }; + mockRouter.shouldThrow(request); + }); + }); + }); + + describe('DELETE /api/app_search/engines/{engineName}/synonyms/{synonymId}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/api/app_search/engines/{engineName}/synonyms/{synonymId}', + }); + + registerSynonymsRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/:engineName/synonyms/:synonymId', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts new file mode 100644 index 0000000000000..1be58f00c476a --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/synonyms.ts @@ -0,0 +1,85 @@ +/* + * 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'; + +const synonymsSchema = schema.arrayOf(schema.string({ minLength: 1 }), { minSize: 2 }); + +export function registerSynonymsRoutes({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/api/app_search/engines/{engineName}/synonyms', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + query: schema.object({ + 'page[current]': schema.number(), + 'page[size]': schema.number(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/synonyms/collection', + }) + ); + + router.post( + { + path: '/api/app_search/engines/{engineName}/synonyms', + validate: { + params: schema.object({ + engineName: schema.string(), + }), + body: schema.object({ + synonyms: synonymsSchema, + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/synonyms/collection', + }) + ); + + router.put( + { + path: '/api/app_search/engines/{engineName}/synonyms/{synonymId}', + validate: { + params: schema.object({ + engineName: schema.string(), + synonymId: schema.string(), + }), + body: schema.object({ + synonyms: synonymsSchema, + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/synonyms/:synonymId', + }) + ); + + router.delete( + { + path: '/api/app_search/engines/{engineName}/synonyms/{synonymId}', + validate: { + params: schema.object({ + engineName: schema.string(), + synonymId: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/as/engines/:engineName/synonyms/:synonymId', + }) + ); +}