diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/constants.ts new file mode 100644 index 0000000000000..5ac778c83bd0d --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/constants.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; + +export const CREATE_ENGINE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.title', + { + defaultMessage: 'Create an Engine', + } +); + +export const CREATE_ENGINE_FORM_TITLE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.title', + { + defaultMessage: 'Name your Engine', + } +); + +export const CREATE_ENGINE_FORM_ENGINE_NAME_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.engineName.label', + { + defaultMessage: 'Engine Name', + } +); + +export const ALLOWED_CHARS_NOTE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.engineName.allowedCharactersNote', + { + defaultMessage: 'Engine names can only contain lowercase letters, numbers, and hyphens', + } +); + +export const SANITIZED_NAME_NOTE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.engineName.sanitizedNameNote', + { + defaultMessage: 'Your engine will be named', + } +); + +export const CREATE_ENGINE_FORM_ENGINE_NAME_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.engineName.placeholder', + { + defaultMessage: 'i.e., my-search-engine', + } +); + +export const CREATE_ENGINE_FORM_ENGINE_LANGUAGE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.engineLanguage.label', + { + defaultMessage: 'Engine Language', + } +); + +export const CREATE_ENGINE_FORM_SUBMIT_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.form.submitButton.label', + { + defaultMessage: 'Create Engine', + } +); + +export const CREATE_ENGINE_SUCCESS_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.appSearch.createEngine.successMessage', + { + defaultMessage: 'Successfully created engine.', + } +); + +export const SUPPORTED_LANGUAGES = [ + { + value: 'Universal', + text: 'Universal', + }, + { + text: '—', + disabled: true, + }, + { + value: 'zh', + text: 'Chinese', + }, + { + value: 'da', + text: 'Danish', + }, + { + value: 'nl', + text: 'Dutch', + }, + { + value: 'en', + text: 'English', + }, + { + value: 'fr', + text: 'French', + }, + { + value: 'de', + text: 'German', + }, + { + value: 'it', + text: 'Italian', + }, + { + value: 'ja', + text: 'Japanese', + }, + { + value: 'ko', + text: 'Korean', + }, + { + value: 'pt', + text: 'Portuguese', + }, + { + value: 'pt-br', + text: 'Portuguese (Brazil)', + }, + { + value: 'ru', + text: 'Russian', + }, + { + value: 'es', + text: 'Spanish', + }, + { + value: 'th', + text: 'Thai', + }, +]; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine.test.tsx new file mode 100644 index 0000000000000..11cbedd632234 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import '../../../__mocks__/shallow_useeffect.mock'; +import { setMockValues } from '../../../__mocks__'; + +import { CreateEngine } from './'; +import { DefaultStringBooleanFalse } from '../../../../../../lists/common/schemas/types/default_string_boolean_false'; + +describe('CreateEngine', () => { + const DEFAULT_VALUES = { + name: '', + rawName: '', + language: 'Universal', + }; + + describe('default values', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders', () => { + setMockValues(DEFAULT_VALUES); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="CreateEngine"]')).toHaveLength(1); + }); + + it('contains a form', () => { + setMockValues(DEFAULT_VALUES); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="CreateEngineForm"]')).toHaveLength(1); + }); + + it('contains a name input', () => { + setMockValues(DEFAULT_VALUES); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="CreateEngineNameInput"]')).toHaveLength(1); + }); + + it('contains a language input', () => { + setMockValues(DEFAULT_VALUES); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="CreateEngineLanguageInput"]')).toHaveLength(1); + }); + + describe('NewEngineSubmitButton', () => { + it('renders', () => { + setMockValues(DEFAULT_VALUES); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="NewEngineSubmitButton"]')).toHaveLength(1); + }); + + it('is disabled when name is empty', () => { + setMockValues(DEFAULT_VALUES); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="NewEngineSubmitButton"]').prop('disabled')).toEqual( + true + ); + }); + + it('is enabled when name has a value', () => { + setMockValues({ ...DEFAULT_VALUES, name: 'test', rawName: 'test' }); + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="NewEngineSubmitButton"]').prop('disabled')).toEqual( + false + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine.tsx new file mode 100644 index 0000000000000..a5addc9687ccb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { + EuiForm, + EuiFlexGroup, + EuiFormRow, + EuiFlexItem, + EuiFieldText, + EuiSelect, + EuiPageHeader, + EuiPageHeaderSection, + EuiSpacer, + EuiText, + EuiTitle, + EuiButton, + EuiPanel, +} from '@elastic/eui'; +import { useActions, useValues } from 'kea'; + +import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome'; +import { FlashMessages } from '../../../shared/flash_messages'; +import { + ALLOWED_CHARS_NOTE, + CREATE_ENGINE_FORM_ENGINE_LANGUAGE_LABEL, + CREATE_ENGINE_FORM_ENGINE_NAME_LABEL, + CREATE_ENGINE_FORM_ENGINE_NAME_PLACEHOLDER, + CREATE_ENGINE_FORM_SUBMIT_BUTTON_LABEL, + CREATE_ENGINE_FORM_TITLE, + CREATE_ENGINE_TITLE, + SANITIZED_NAME_NOTE, + SUPPORTED_LANGUAGES, +} from './constants'; +import { CreateEngineLogic } from './create_engine_logic'; + +export const CreateEngine: React.FC = () => { + const { name, rawName, language } = useValues(CreateEngineLogic); + const { setLanguage, setRawName, submitEngine } = useActions(CreateEngineLogic); + + return ( +
+ + + + +

{CREATE_ENGINE_TITLE}

+
+
+
+ + + +
{ + e.preventDefault(); + submitEngine(); + }} + > + + {CREATE_ENGINE_FORM_TITLE} + + + + + 0 && rawName !== name ? ( + <> + {SANITIZED_NAME_NOTE} {name} + + ) : ( + ALLOWED_CHARS_NOTE + ) + } + fullWidth={true} + > + setRawName(event.currentTarget.value)} + autoComplete="off" + fullWidth={true} + data-test-subj="CreateEngineNameInput" + placeholder={CREATE_ENGINE_FORM_ENGINE_NAME_PLACEHOLDER} + autoFocus={true} + /> + + + + + setLanguage(event.target.value)} + /> + + + + + + {CREATE_ENGINE_FORM_SUBMIT_BUTTON_LABEL} + + +
+
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine_logic.test.ts new file mode 100644 index 0000000000000..e135b7912e3db --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine_logic.test.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { generatePath } from 'react-router-dom'; + +import { CREATE_ENGINE_SUCCESS_MESSAGE } from './constants'; +import { + LogicMounter, + mockHttpValues, + mockKibanaValues, + mockFlashMessageHelpers, +} from '../../../__mocks__'; +import { ENGINE_PATH } from '../../routes'; +import { formatApiName } from '../../utils/format_api_name'; +import { CreateEngineLogic, DEFAULT_LANGUAGE } from './create_engine_logic'; + +describe('CreateEngineLogic', () => { + const { mount } = new LogicMounter(CreateEngineLogic); + const { http } = mockHttpValues; + const { navigateToUrl } = mockKibanaValues; + const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + name: '', + rawName: '', + language: DEFAULT_LANGUAGE, + }; + + it('has expected default values', () => { + mount(); + expect(CreateEngineLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onCreateEngineSuccess', () => { + beforeAll(() => { + mount(); + // setup local state + CreateEngineLogic.actions.setLanguage('English'); + CreateEngineLogic.actions.setRawName('test'); + // call action + CreateEngineLogic.actions.onCreateEngineSuccess(); + }); + + it('should set a success message', () => { + expect(setQueuedSuccessMessage).toHaveBeenCalledWith(CREATE_ENGINE_SUCCESS_MESSAGE); + }); + + it('should navigate the user to the engine page', () => { + const enginePath = generatePath(ENGINE_PATH, { engineName: CreateEngineLogic.values.name }); + expect(navigateToUrl).toHaveBeenCalledWith(enginePath); + }); + }); + + describe('setLanguage', () => { + const newLanguage = 'English'; + + beforeAll(() => { + mount(); + CreateEngineLogic.actions.setLanguage(newLanguage); + }); + + describe('language', () => { + it('should be set to provided value', () => { + expect(CreateEngineLogic.values.language).toEqual(newLanguage); + }); + }); + }); + + describe('setRawName', () => { + const newName = 'Name__With#$&*%Special--Characters'; + const sanitizedNewName = formatApiName(newName); + + beforeAll(() => { + mount(); + CreateEngineLogic.actions.setRawName(newName); + }); + + describe('rawName', () => { + it('should be set to provided value', () => { + expect(CreateEngineLogic.values.rawName).toEqual(newName); + }); + }); + + describe('name', () => { + it('should be set to a sanitized value', () => { + expect(CreateEngineLogic.values.name).toEqual(sanitizedNewName); + }); + }); + }); + + describe('submitEngine', () => { + beforeAll(() => { + mount(); + // setup local state + CreateEngineLogic.actions.setLanguage('English'); + CreateEngineLogic.actions.setRawName('test'); + }); + + it('POSTS to /api/app_search/engines', () => { + const body = JSON.stringify({ + name: CreateEngineLogic.values.name, + language: CreateEngineLogic.values.language, + }); + CreateEngineLogic.actions.submitEngine(); + expect(http.post).toHaveBeenCalledWith('/api/app_search/engines', { body }); + }); + + it('calls onCreateEngineSuccess on valid submission', async () => { + jest.spyOn(CreateEngineLogic.actions, 'onCreateEngineSuccess'); // .mockImplementation(); + const promise = (http.post as jest.Mock).mockReturnValueOnce({}); + await CreateEngineLogic.actions.submitEngine(); + await promise; + expect(CreateEngineLogic.actions.onCreateEngineSuccess).toHaveBeenCalledTimes(1); + }); + + it('calls flashAPIErrors on API Error', async () => { + const promise = (http.post as jest.Mock).mockReturnValueOnce( + Promise.reject({ + body: { + statusCode: 400, + error: 'Bad Request', + message: 'Invalid request payload JSON format', + }, + }) + ); + await CreateEngineLogic.actions.submitEngine(); + await promise; + expect(flashAPIErrors).toHaveBeenCalledTimes(1); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine_logic.ts new file mode 100644 index 0000000000000..acdc0f2f8476c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/create_engine_logic.ts @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { kea, MakeLogicType } from 'kea'; +import { generatePath } from 'react-router-dom'; + +import { CREATE_ENGINE_SUCCESS_MESSAGE } from './constants'; +import { ENGINE_PATH } from '../../routes'; +import { formatApiName } from '../../utils/format_api_name'; +import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { KibanaLogic } from '../../../shared/kibana'; + +export const DEFAULT_LANGUAGE = 'Universal'; + +export interface CreateEngineActions { + onCreateEngineSuccess(): void; + setLanguage(language: string): { language: string }; + setRawName(rawName: string): { rawName: string }; + submitEngine(): void; +} + +export interface CreateEngineValues { + language: string; + name: string; + rawName: string; +} + +export const CreateEngineLogic = kea>({ + path: ['enterprise_search', 'app_search', 'create_engine_logic'], + actions: { + onCreateEngineSuccess: true, + setLanguage: (language) => ({ language }), + setRawName: (rawName) => ({ rawName }), + submitEngine: true, + }, + reducers: { + language: [ + DEFAULT_LANGUAGE, + { + setLanguage: (_, { language }) => language, + }, + ], + rawName: [ + '', + { + setRawName: (_, { rawName }) => rawName, + }, + ], + }, + selectors: ({ selectors }) => ({ + name: [() => [selectors.rawName], (rawName) => formatApiName(rawName)], + }), + listeners: ({ values, actions }) => ({ + submitEngine: async () => { + const { http } = HttpLogic.values; + const { name, language } = values; + + const body = JSON.stringify({ name, language }); + + try { + await http.post('/api/app_search/engines', { body }); + actions.onCreateEngineSuccess(); + } catch (e) { + flashAPIErrors(e); + } + }, + onCreateEngineSuccess: () => { + const { name } = values; + const { navigateToUrl } = KibanaLogic.values; + const enginePath = generatePath(ENGINE_PATH, { engineName: name }); + + setQueuedSuccessMessage(CREATE_ENGINE_SUCCESS_MESSAGE); + navigateToUrl(enginePath); + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/index.ts new file mode 100644 index 0000000000000..44fee51db0ba7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/create_engine/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { CreateEngine } from './create_engine'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx index 1b6acf341c08e..4705b62e676b4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.test.tsx @@ -9,7 +9,7 @@ import { shallow, ShallowWrapper } from 'enzyme'; import { EuiButton } from '@elastic/eui'; import { docLinks } from '../../../shared/doc_links'; - +import { FlashMessages } from '../../../shared/flash_messages'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; import { EmptyEngineOverview } from './engine_overview_empty'; @@ -26,6 +26,10 @@ describe('EmptyEngineOverview', () => { it('renders a documentation link', () => { expect(wrapper.find(EuiButton).prop('href')).toEqual(`${docLinks.appSearchBase}/index.html`); + + it('contains FlashMessages', () => { + expect(wrapper.find(FlashMessages)).toHaveLength(1); + }); }); it('renders document creation components', () => { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx index 83dd396e5e080..16a35ead9cbf3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_empty.tsx @@ -15,6 +15,7 @@ import { EuiButton, } from '@elastic/eui'; +import { FlashMessages } from '../../../shared/flash_messages'; import { DOCS_PREFIX } from '../../routes'; import { DocumentCreationButtons, DocumentCreationFlyout } from '../document_creation'; @@ -40,6 +41,7 @@ export const EmptyEngineOverview: React.FC = () => { + diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx index 8250446e231b3..e8590c51f9886 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.test.tsx @@ -9,6 +9,7 @@ import { setMockValues } from '../../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; +import { FlashMessages } from '../../../shared/flash_messages'; import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; import { EngineOverviewMetrics } from './engine_overview_metrics'; @@ -18,6 +19,11 @@ describe('EngineOverviewMetrics', () => { expect(wrapper.find('h1').text()).toEqual('Engine overview'); }); + it('contains FlashMessages', () => { + const wrapper = shallow(); + expect(wrapper.find(FlashMessages)).toHaveLength(1); + }); + it('renders an unavailable prompt if engine data is still indexing', () => { setMockValues({ apiLogsUnavailable: true }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx index 9630f6fa2f81d..99a044ee24f26 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engine_overview/engine_overview_metrics.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiPageHeader, EuiTitle, EuiSpacer } from '@elastic/eui'; import { EngineOverviewLogic } from './'; - +import { FlashMessages } from '../../../shared/flash_messages'; import { UnavailablePrompt, TotalStats, TotalCharts, RecentApiLogs } from './components'; export const EngineOverviewMetrics: React.FC = () => { @@ -28,6 +28,7 @@ export const EngineOverviewMetrics: React.FC = () => { + {apiLogsUnavailable ? ( ) : ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts index 12545c59b40a0..fe86e3f981a01 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts @@ -14,3 +14,10 @@ export const META_ENGINES_TITLE = i18n.translate( 'xpack.enterpriseSearch.appSearch.metaEngines.title', { defaultMessage: 'Meta Engines' } ); + +export const CREATE_AN_ENGINE_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.appSearch.engines.createAnEngineButton.label', + { + defaultMessage: 'Create an Engine', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx index e2d366e5e5817..ffabe5f435d1b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.test.tsx @@ -72,6 +72,7 @@ describe('EnginesOverview', () => { const wrapper = shallow(); expect(wrapper.find(EnginesTable)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="appSearchEnginesCreateEngineButton"]')).toHaveLength(1); expect(actions.loadEngines).toHaveBeenCalled(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx index 8a24ee746ed14..c6a3f2c75da5e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/engines_overview.tsx @@ -7,8 +7,10 @@ import React, { useEffect } from 'react'; import { useValues, useActions } from 'kea'; import { + EuiButton, EuiPageContent, EuiPageContentHeader, + EuiPageContentHeaderSection, EuiPageContentBody, EuiTitle, EuiSpacer, @@ -18,10 +20,12 @@ import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chro import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry'; import { FlashMessages } from '../../../shared/flash_messages'; import { LicensingLogic } from '../../../shared/licensing'; +import { CREATE_ENGINES_PATH } from '../../routes'; +import { KibanaLogic } from '../../../shared/kibana'; import { EngineIcon } from './assets/engine_icon'; import { MetaEngineIcon } from './assets/meta_engine_icon'; -import { ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; +import { CREATE_AN_ENGINE_BUTTON_LABEL, ENGINES_TITLE, META_ENGINES_TITLE } from './constants'; import { EnginesOverviewHeader, LoadingState, EmptyState } from './components'; import { EnginesTable } from './engines_table'; import { EnginesLogic } from './engines_logic'; @@ -63,11 +67,23 @@ export const EnginesOverview: React.FC = () => { - -

- {ENGINES_TITLE} -

-
+ + +

+ {ENGINES_TITLE} +

+
+
+ + KibanaLogic.values.navigateToUrl(CREATE_ENGINES_PATH)} + > + {CREATE_AN_ENGINE_BUTTON_LABEL} + +
{ }); describe('ability checks', () => { - // TODO: Use this section for routes wrapped in canViewX conditionals - // e.g., it('renders settings if a user can view settings') + describe('canManageEngines', () => { + it('renders CreateEngine when user canManageEngines is true', () => { + setMockValues({ myRole: { canManageEngines: true } }); + const wrapper = shallow(); + + expect(wrapper.find(CreateEngine)).toHaveLength(1); + }); + + it('does not render CreateEngine when user canManageEngines is false', () => { + setMockValues({ myRole: { canManageEngines: false } }); + const wrapper = shallow(); + + expect(wrapper.find(CreateEngine)).toHaveLength(0); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx index 769230ccffd22..2e9cf552f2fce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/index.tsx @@ -19,6 +19,7 @@ import { Layout, SideNav, SideNavLink } from '../shared/layout'; import { EngineNav, EngineRouter } from './components/engine'; import { + CREATE_ENGINES_PATH, ROOT_PATH, SETUP_GUIDE_PATH, SETTINGS_PATH, @@ -29,6 +30,7 @@ import { LIBRARY_PATH, } from './routes'; +import { CreateEngine } from './components/create_engine'; import { SetupGuide } from './components/setup_guide'; import { ErrorConnecting } from './components/error_connecting'; import { NotFound } from '../shared/not_found'; @@ -56,7 +58,10 @@ export const AppSearchUnconfigured: React.FC = () => ( export const AppSearchConfigured: React.FC = (props) => { const { initializeAppData } = useActions(AppLogic); - const { hasInitialized } = useValues(AppLogic); + const { + hasInitialized, + myRole: { canManageEngines }, + } = useValues(AppLogic); const { errorConnecting, readOnlyMode } = useValues(HttpLogic); useEffect(() => { @@ -73,6 +78,13 @@ export const AppSearchConfigured: React.FC = (props) => { )} + {canManageEngines && ( + + } readOnlyMode={readOnlyMode}> + + + + )} } />} readOnlyMode={readOnlyMode}> diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts index 7f12f7d29671a..f72b179a27c7e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/routes.ts @@ -16,7 +16,7 @@ export const CREDENTIALS_PATH = '/credentials'; export const ROLE_MAPPINGS_PATH = '#/role-mappings'; // This page seems to 404 if the # isn't included export const ENGINES_PATH = '/engines'; -export const CREATE_ENGINES_PATH = `${ENGINES_PATH}/new`; +export const CREATE_ENGINES_PATH = `/create_engine`; export const ENGINE_PATH = `${ENGINES_PATH}/:engineName`; export const SAMPLE_ENGINE_PATH = `${ENGINES_PATH}/national-parks-demo`; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts index 9755fff02f738..6eccbad67638b 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.test.ts @@ -110,6 +110,48 @@ describe('engine routes', () => { }); }); + describe('POST /api/app_search/engines', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/api/app_search/engines', + payload: 'body', + }); + + registerEnginesRoutes({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + mockRouter.callRoute({ body: { name: 'some-engine', language: 'en' } }); + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/as/engines/collection', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { body: { name: 'some-engine', language: 'en' } }; + mockRouter.shouldValidate(request); + }); + + it('missing name', () => { + const request = { body: { language: 'en' } }; + mockRouter.shouldThrow(request); + }); + + it('optional language', () => { + const request = { body: { name: 'some-engine' } }; + mockRouter.shouldValidate(request); + }); + }); + }); + describe('GET /api/app_search/engines/{name}', () => { let mockRouter: MockRouter; diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts index c0bbc40ff8d2d..30af2f130eaa0 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/engines.ts @@ -44,6 +44,21 @@ export function registerEnginesRoutes({ } ); + router.post( + { + path: '/api/app_search/engines', + validate: { + body: schema.object({ + name: schema.string(), + language: schema.maybe(schema.string()), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: `/as/engines/collection`, + }) + ); + // Single engine endpoints router.get( {