[App Search] Migrate Create Meta Engine View (#92127)
* New empty MetaEngineCreation component * Added MetaEngineCreation to AppSearchConfigured router * Empty MetaEngineCreationLogic * Add rawName value and setRawName action to MetaEngineCreationLogic * Add indexedEngineNames value and setIndexedEngineNames action to MEtaEngineCreationLogic * Add selectedIndexedEngineNames value and setIndexedEngineNames action to MetaEngineCreationLogic * Add description to MetaEngineCreation * Add name selector to MetaEngineCreationLogic * Added MetaEngineCreationNameInput to MetaEngineCreation * Add fetchIndexedEngineNames listener to MetaEngineCreationLogic * Call fetchIndexedEngineNames when MetaEngineCreation first renders * Add EuiComboBox for selectedEngineNames to MetaEngineCreation * WIP Add meta engine source engine limit warning to MetaEngineCreation * Add submitEngine listener to MetaEngineCreationLogic * Add onEngineCreationSuccess to MetaEngineCreationLogic * Fixing tests for MetaEngineCreationLogic * Fix tests for MetaEngineCreation * Add Create a meta engine button to EnginesOverview * Use DEFAULT_META for fetching indexed engine names in MetaEngineCreationLogic * Copy fixes * Updating POST /api/engines tests * Add noItemsMessage prop to EnginesTable * Add empty prompt to Meta Engines table in EnginesOverview * Apply suggestions from code review Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Better form functionality in MetaEngineCreation * Fix errors from github * More MetaEngineCreation coverage * Meta MetaEngineCreationLogic coverage * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/meta_engine_creation/meta_engine_creation_logic.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> * Update x-pack/plugins/enterprise_search/public/applications/app_search/components/engines/constants.ts Co-authored-by: Constance <constancecchen@users.noreply.github.com> Co-authored-by: Constance <constancecchen@users.noreply.github.com>
This commit is contained in:
parent
41b81a1011
commit
3d065739ea
16 changed files with 978 additions and 27 deletions
|
@ -22,3 +22,25 @@ export const CREATE_AN_ENGINE_BUTTON_LABEL = i18n.translate(
|
|||
defaultMessage: 'Create an engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const CREATE_A_META_ENGINE_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engines.createAMetaEngineButton.ButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Create a meta engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_EMPTY_PROMPT_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPrompTitle',
|
||||
{
|
||||
defaultMessage: 'No meta engines yet',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_EMPTY_PROMPT_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.engines.metaEngines.emptyPromptDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Meta engines allow you to combine multiple engines into one searchable engine.',
|
||||
}
|
||||
);
|
||||
|
|
|
@ -12,6 +12,8 @@ import React from 'react';
|
|||
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { EuiEmptyPrompt } from '@elastic/eui';
|
||||
|
||||
import { LoadingState, EmptyState } from './components';
|
||||
import { EnginesTable } from './engines_table';
|
||||
|
||||
|
@ -19,7 +21,6 @@ import { EnginesOverview } from './';
|
|||
|
||||
describe('EnginesOverview', () => {
|
||||
const values = {
|
||||
hasPlatinumLicense: false,
|
||||
dataLoading: false,
|
||||
engines: [],
|
||||
enginesMeta: {
|
||||
|
@ -39,6 +40,7 @@ describe('EnginesOverview', () => {
|
|||
},
|
||||
},
|
||||
metaEnginesLoading: false,
|
||||
hasPlatinumLicense: false,
|
||||
};
|
||||
const actions = {
|
||||
loadEngines: jest.fn(),
|
||||
|
@ -73,7 +75,7 @@ describe('EnginesOverview', () => {
|
|||
const valuesWithEngines = {
|
||||
...values,
|
||||
dataLoading: false,
|
||||
engines: ['dummy-engine'],
|
||||
engines: ['test-engine'],
|
||||
enginesMeta: {
|
||||
page: {
|
||||
current: 1,
|
||||
|
@ -84,6 +86,7 @@ describe('EnginesOverview', () => {
|
|||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(valuesWithEngines);
|
||||
});
|
||||
|
||||
|
@ -102,18 +105,47 @@ describe('EnginesOverview', () => {
|
|||
).toEqual('/engine_creation');
|
||||
});
|
||||
|
||||
describe('when on a platinum license', () => {
|
||||
it('renders a 2nd meta engines table & makes a 2nd meta engines API call', async () => {
|
||||
describe('when user has a platinum license', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
setMockValues({
|
||||
...valuesWithEngines,
|
||||
hasPlatinumLicense: true,
|
||||
metaEngines: ['dummy-meta-engine'],
|
||||
});
|
||||
const wrapper = shallow(<EnginesOverview />);
|
||||
wrapper = shallow(<EnginesOverview />);
|
||||
});
|
||||
|
||||
it('renders a 2nd meta engines table ', async () => {
|
||||
expect(wrapper.find(EnginesTable)).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('makes a 2nd meta engines call', () => {
|
||||
expect(actions.loadMetaEngines).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders a create engine button which takes users to the create meta engine page', () => {
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="appSearchEnginesMetaEngineCreationButton"]').prop('to')
|
||||
).toEqual('/meta_engine_creation');
|
||||
});
|
||||
|
||||
it('contains an EuiEmptyPrompt that takes users to the create meta when metaEngines is empty', () => {
|
||||
setMockValues({
|
||||
...valuesWithEngines,
|
||||
hasPlatinumLicense: true,
|
||||
metaEngines: [],
|
||||
});
|
||||
wrapper = shallow(<EnginesOverview />);
|
||||
const metaEnginesTable = wrapper.find(EnginesTable).last().dive();
|
||||
const emptyPrompt = metaEnginesTable.dive().find(EuiEmptyPrompt).dive();
|
||||
|
||||
expect(
|
||||
emptyPrompt
|
||||
.find('[data-test-subj="appSearchMetaEnginesEmptyStateCreationButton"]')
|
||||
.prop('to')
|
||||
).toEqual('/meta_engine_creation');
|
||||
});
|
||||
});
|
||||
|
||||
describe('pagination', () => {
|
||||
|
@ -150,7 +182,7 @@ describe('EnginesOverview', () => {
|
|||
setMockValues({
|
||||
...valuesWithEngines,
|
||||
hasPlatinumLicense: true,
|
||||
metaEngines: ['dummy-meta-engine'],
|
||||
metaEngines: ['test-meta-engine'],
|
||||
});
|
||||
const wrapper = shallow(<EnginesOverview />);
|
||||
const pageEvent = { page: { index: 0 } };
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
EuiPageContentBody,
|
||||
EuiTitle,
|
||||
EuiSpacer,
|
||||
EuiEmptyPrompt,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
|
@ -24,12 +25,19 @@ import { LicensingLogic } from '../../../shared/licensing';
|
|||
import { EuiButtonTo } from '../../../shared/react_router_helpers';
|
||||
import { convertMetaToPagination, handlePageChange } from '../../../shared/table_pagination';
|
||||
import { SendAppSearchTelemetry as SendTelemetry } from '../../../shared/telemetry';
|
||||
import { ENGINE_CREATION_PATH } from '../../routes';
|
||||
import { ENGINE_CREATION_PATH, META_ENGINE_CREATION_PATH } from '../../routes';
|
||||
|
||||
import { EngineIcon } from './assets/engine_icon';
|
||||
import { MetaEngineIcon } from './assets/meta_engine_icon';
|
||||
import { EnginesOverviewHeader, LoadingState, EmptyState } from './components';
|
||||
import { CREATE_AN_ENGINE_BUTTON_LABEL, ENGINES_TITLE, META_ENGINES_TITLE } from './constants';
|
||||
import {
|
||||
CREATE_AN_ENGINE_BUTTON_LABEL,
|
||||
CREATE_A_META_ENGINE_BUTTON_LABEL,
|
||||
ENGINES_TITLE,
|
||||
META_ENGINE_EMPTY_PROMPT_DESCRIPTION,
|
||||
META_ENGINE_EMPTY_PROMPT_TITLE,
|
||||
META_ENGINES_TITLE,
|
||||
} from './constants';
|
||||
import { EnginesLogic } from './engines_logic';
|
||||
import { EnginesTable } from './engines_table';
|
||||
|
||||
|
@ -37,6 +45,7 @@ import './engines_overview.scss';
|
|||
|
||||
export const EnginesOverview: React.FC = () => {
|
||||
const { hasPlatinumLicense } = useValues(LicensingLogic);
|
||||
|
||||
const {
|
||||
dataLoading,
|
||||
engines,
|
||||
|
@ -46,6 +55,7 @@ export const EnginesOverview: React.FC = () => {
|
|||
metaEnginesMeta,
|
||||
metaEnginesLoading,
|
||||
} = useValues(EnginesLogic);
|
||||
|
||||
const { loadEngines, loadMetaEngines, onEnginesPagination, onMetaEnginesPagination } = useActions(
|
||||
EnginesLogic
|
||||
);
|
||||
|
@ -100,15 +110,27 @@ export const EnginesOverview: React.FC = () => {
|
|||
/>
|
||||
</EuiPageContentBody>
|
||||
|
||||
{metaEngines.length > 0 && (
|
||||
{hasPlatinumLicense && (
|
||||
<>
|
||||
<EuiSpacer size="xl" />
|
||||
<EuiPageContentHeader>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
<MetaEngineIcon /> {META_ENGINES_TITLE}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiTitle size="s">
|
||||
<h2>
|
||||
<MetaEngineIcon /> {META_ENGINES_TITLE}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeaderSection>
|
||||
<EuiPageContentHeaderSection>
|
||||
<EuiButtonTo
|
||||
color="primary"
|
||||
fill
|
||||
data-test-subj="appSearchEnginesMetaEngineCreationButton"
|
||||
to={META_ENGINE_CREATION_PATH}
|
||||
>
|
||||
{CREATE_A_META_ENGINE_BUTTON_LABEL}
|
||||
</EuiButtonTo>
|
||||
</EuiPageContentHeaderSection>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody data-test-subj="appSearchMetaEngines">
|
||||
<EnginesTable
|
||||
|
@ -118,6 +140,21 @@ export const EnginesOverview: React.FC = () => {
|
|||
...convertMetaToPagination(metaEnginesMeta),
|
||||
hidePerPageOptions: true,
|
||||
}}
|
||||
noItemsMessage={
|
||||
<EuiEmptyPrompt
|
||||
title={<h2>{META_ENGINE_EMPTY_PROMPT_TITLE}</h2>}
|
||||
body={<p>{META_ENGINE_EMPTY_PROMPT_DESCRIPTION}</p>}
|
||||
actions={
|
||||
<EuiButtonTo
|
||||
data-test-subj="appSearchMetaEnginesEmptyStateCreationButton"
|
||||
fill
|
||||
to={META_ENGINE_CREATION_PATH}
|
||||
>
|
||||
{CREATE_A_META_ENGINE_BUTTON_LABEL}
|
||||
</EuiButtonTo>
|
||||
}
|
||||
/>
|
||||
}
|
||||
onChange={handlePageChange(onMetaEnginesPagination)}
|
||||
/>
|
||||
</EuiPageContentBody>
|
||||
|
|
|
@ -88,6 +88,13 @@ describe('EnginesTable', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('noItemsMessage', () => {
|
||||
it('passes the noItemsMessage prop', () => {
|
||||
const wrapper = mountWithIntl(<EnginesTable {...props} noItemsMessage={'No items.'} />);
|
||||
expect(wrapper.find(EuiBasicTable).prop('noItemsMessage')).toEqual('No items.');
|
||||
});
|
||||
});
|
||||
|
||||
describe('language field', () => {
|
||||
it('renders language when available', () => {
|
||||
const wrapper = mountWithIntl(
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import { useActions } from 'kea';
|
||||
|
||||
|
@ -24,6 +24,7 @@ import { EngineDetails } from '../engine/types';
|
|||
interface EnginesTableProps {
|
||||
items: EngineDetails[];
|
||||
loading: boolean;
|
||||
noItemsMessage?: ReactNode;
|
||||
pagination: {
|
||||
pageIndex: number;
|
||||
pageSize: number;
|
||||
|
@ -36,6 +37,7 @@ interface EnginesTableProps {
|
|||
export const EnginesTable: React.FC<EnginesTableProps> = ({
|
||||
items,
|
||||
loading,
|
||||
noItemsMessage,
|
||||
pagination,
|
||||
onChange,
|
||||
}) => {
|
||||
|
@ -148,6 +150,7 @@ export const EnginesTable: React.FC<EnginesTableProps> = ({
|
|||
loading={loading}
|
||||
pagination={pagination}
|
||||
onChange={onChange}
|
||||
noItemsMessage={noItemsMessage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 { EuiLink } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { DOCS_PREFIX } from '../../routes';
|
||||
|
||||
export const DEFAULT_LANGUAGE = 'Universal';
|
||||
|
||||
export const META_ENGINE_CREATION_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.title',
|
||||
{
|
||||
defaultMessage: 'Create a meta engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_TITLE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.title',
|
||||
{
|
||||
defaultMessage: 'Name your meta engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.submitButton.buttonLabel',
|
||||
{
|
||||
defaultMessage: 'Create meta engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.metaEngineDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'Meta engines allow you to combine multiple engines into one searchable engine.',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.documentationLink',
|
||||
{
|
||||
defaultMessage: 'Read the documentation',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION = (
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.appSearch.metaEngineCreation.form.documentationDescription"
|
||||
defaultMessage="{documentationLink} for information about how to get started."
|
||||
values={{
|
||||
documentationLink: (
|
||||
<EuiLink href={`${DOCS_PREFIX}/meta-engines-guide.html`} target="_blank">
|
||||
{META_ENGINE_CREATION_FORM_DOCUMENTATION_LINK}
|
||||
</EuiLink>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_ENGINE_NAME_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.engineName.label',
|
||||
{
|
||||
defaultMessage: 'Meta engine name',
|
||||
}
|
||||
);
|
||||
|
||||
export const ALLOWED_CHARS_NOTE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.engineName.allowedCharactersHelpText',
|
||||
{
|
||||
defaultMessage: 'Meta engine names can only contain lowercase letters, numbers, and hyphens',
|
||||
}
|
||||
);
|
||||
|
||||
export const SANITIZED_NAME_NOTE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.engineName.sanitizedNameHelpText',
|
||||
{
|
||||
defaultMessage: 'Your meta engine will be named',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.engineName.placeholder',
|
||||
{
|
||||
defaultMessage: 'i.e., my-meta-engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_ENGINE_SOURCE_ENGINES_LABEL = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.sourceEngines.label',
|
||||
{
|
||||
defaultMessage: 'Add source engines to this meta engine',
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_FORM_MAX_SOURCE_ENGINES_WARNING_TITLE = (
|
||||
maxEnginesPerMetaEngine: number
|
||||
) =>
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.form.sourceEngines.maxSourceEnginesWarningTitle',
|
||||
{
|
||||
defaultMessage: 'Meta engines have a limit of {maxEnginesPerMetaEngine} source engines',
|
||||
values: { maxEnginesPerMetaEngine },
|
||||
}
|
||||
);
|
||||
|
||||
export const META_ENGINE_CREATION_SUCCESS_MESSAGE = i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.metaEngineCreation.successMessage',
|
||||
{
|
||||
defaultMessage: 'Successfully created meta engine.',
|
||||
}
|
||||
);
|
|
@ -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 { MetaEngineCreation } from './meta_engine_creation';
|
|
@ -0,0 +1,187 @@
|
|||
/*
|
||||
* 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__/shallow_useeffect.mock';
|
||||
import { setMockActions, setMockValues } from '../../../__mocks__';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
|
||||
import { MetaEngineCreation } from './';
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
// MetaEngineLogic
|
||||
name: 'test-meta-engine',
|
||||
rawName: 'test-meta-engine',
|
||||
indexedEngineNames: [],
|
||||
selectedIndexedEngineNames: ['one'],
|
||||
// AppLogic
|
||||
configuredLimits: { engine: { maxEnginesPerMetaEngine: 10 } },
|
||||
};
|
||||
|
||||
const MOCK_ACTIONS = {
|
||||
setRawName: jest.fn(),
|
||||
setSelectedIndexedEngineNames: jest.fn(),
|
||||
fetchIndexedEngineNames: jest.fn(),
|
||||
submitEngine: jest.fn(),
|
||||
};
|
||||
|
||||
describe('MetaEngineCreation', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockValues(DEFAULT_VALUES);
|
||||
setMockActions(MOCK_ACTIONS);
|
||||
});
|
||||
|
||||
it('renders and calls fetchIndexedEngineNames', () => {
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
expect(wrapper.find('[data-test-subj="MetaEngineCreation"]')).toHaveLength(1);
|
||||
expect(MOCK_ACTIONS.fetchIndexedEngineNames).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('MetaEngineCreationNameInput', () => {
|
||||
it('uses rawName as its value', () => {
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
expect(
|
||||
wrapper
|
||||
.find('[data-test-subj="MetaEngineCreationNameInput"]')
|
||||
.render()
|
||||
.find('input') // as far as I can tell I can't include this input in the .find() two lines above
|
||||
.attr('value')
|
||||
).toEqual('test-meta-engine');
|
||||
});
|
||||
|
||||
it('EngineCreationForm calls submitEngine on form submit', () => {
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const simulatedEvent = {
|
||||
preventDefault: jest.fn(),
|
||||
};
|
||||
wrapper.find('[data-test-subj="MetaEngineCreationForm"]').simulate('submit', simulatedEvent);
|
||||
|
||||
expect(MOCK_ACTIONS.submitEngine).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('MetaEngineCreationNameInput calls setRawName on change', () => {
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const simulatedEvent = {
|
||||
currentTarget: { value: 'new-raw-name' },
|
||||
};
|
||||
wrapper
|
||||
.find('[data-test-subj="MetaEngineCreationNameInput"]')
|
||||
.simulate('change', simulatedEvent);
|
||||
expect(MOCK_ACTIONS.setRawName).toHaveBeenCalledWith('new-raw-name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('EngineCreationNameFormRow', () => {
|
||||
it('renders sanitized name helptext when the raw name is being sanitized', () => {
|
||||
setMockValues({
|
||||
...DEFAULT_VALUES,
|
||||
name: 'name-with-special-characters',
|
||||
rawName: 'Name__With#$&*%Special--Characters',
|
||||
});
|
||||
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const formRow = wrapper.find('[data-test-subj="MetaEngineCreationNameFormRow"]').dive();
|
||||
|
||||
expect(formRow.contains('Your meta engine will be named')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders allowed character helptext when rawName and sanitizedName match', () => {
|
||||
setMockValues({
|
||||
...DEFAULT_VALUES,
|
||||
name: 'name-without-special-characters',
|
||||
rawName: 'name-without-special-characters',
|
||||
});
|
||||
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const formRow = wrapper.find('[data-test-subj="MetaEngineCreationNameFormRow"]').dive();
|
||||
|
||||
expect(
|
||||
formRow.contains(
|
||||
'Meta engine names can only contain lowercase letters, numbers, and hyphens'
|
||||
)
|
||||
).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
it('MetaEngineCreationSourceEnginesInput calls calls setSelectedIndexedEngines on change', () => {
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
|
||||
wrapper
|
||||
.find('[data-test-subj="MetaEngineCreationSourceEnginesInput"]')
|
||||
.simulate('change', [{ label: 'foo', value: 'foo' }]);
|
||||
|
||||
expect(MOCK_ACTIONS.setSelectedIndexedEngineNames).toHaveBeenCalledWith(['foo']);
|
||||
});
|
||||
|
||||
it('renders a warning callout when user has selected too many engines', () => {
|
||||
setMockValues({
|
||||
...DEFAULT_VALUES,
|
||||
...{
|
||||
selectedIndexedEngineNames: ['one', 'two', 'three'],
|
||||
configuredLimits: { engine: { maxEnginesPerMetaEngine: 2 } },
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
|
||||
expect(wrapper.find(EuiCallOut).prop('title')).toContain('Meta engines have a limit of');
|
||||
});
|
||||
|
||||
describe('NewMetaEngineSubmitButton', () => {
|
||||
it('is enabled for a valid submission', () => {
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const submitButton = wrapper.find('[data-test-subj="NewMetaEngineSubmitButton"]');
|
||||
|
||||
expect(submitButton.prop('disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('is disabled when name is empty', () => {
|
||||
setMockValues({
|
||||
...DEFAULT_VALUES,
|
||||
...{
|
||||
name: '',
|
||||
rawName: '',
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const submitButton = wrapper.find('[data-test-subj="NewMetaEngineSubmitButton"]');
|
||||
|
||||
expect(submitButton.prop('disabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('is disabled when user has selected no engines', () => {
|
||||
setMockValues({
|
||||
...DEFAULT_VALUES,
|
||||
...{
|
||||
selectedIndexedEngineNames: [],
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const submitButton = wrapper.find('[data-test-subj="NewMetaEngineSubmitButton"]');
|
||||
|
||||
expect(submitButton.prop('disabled')).toEqual(true);
|
||||
});
|
||||
|
||||
it('is disabled when user has selected too many engines', () => {
|
||||
setMockValues({
|
||||
...DEFAULT_VALUES,
|
||||
...{
|
||||
selectedIndexedEngineNames: ['one', 'two', 'three'],
|
||||
configuredLimits: { engine: { maxEnginesPerMetaEngine: 2 } },
|
||||
},
|
||||
});
|
||||
const wrapper = shallow(<MetaEngineCreation />);
|
||||
const submitButton = wrapper.find('[data-test-subj="NewMetaEngineSubmitButton"]');
|
||||
|
||||
expect(submitButton.prop('disabled')).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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, { useEffect } from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiCallOut,
|
||||
EuiComboBox,
|
||||
EuiComboBoxOptionOption,
|
||||
EuiForm,
|
||||
EuiFlexGroup,
|
||||
EuiFormRow,
|
||||
EuiFlexItem,
|
||||
EuiFieldText,
|
||||
EuiPageContent,
|
||||
EuiPageHeader,
|
||||
EuiPageHeaderSection,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
EuiButton,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { FlashMessages } from '../../../shared/flash_messages';
|
||||
import { SetAppSearchChrome as SetPageChrome } from '../../../shared/kibana_chrome';
|
||||
import { AppLogic } from '../../app_logic';
|
||||
|
||||
import {
|
||||
ALLOWED_CHARS_NOTE,
|
||||
META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION,
|
||||
META_ENGINE_CREATION_FORM_ENGINE_NAME_LABEL,
|
||||
META_ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER,
|
||||
META_ENGINE_CREATION_FORM_ENGINE_SOURCE_ENGINES_LABEL,
|
||||
META_ENGINE_CREATION_FORM_MAX_SOURCE_ENGINES_WARNING_TITLE,
|
||||
META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION,
|
||||
META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL,
|
||||
META_ENGINE_CREATION_FORM_TITLE,
|
||||
META_ENGINE_CREATION_TITLE,
|
||||
SANITIZED_NAME_NOTE,
|
||||
} from './constants';
|
||||
import { MetaEngineCreationLogic } from './meta_engine_creation_logic';
|
||||
|
||||
const engineNameToComboBoxOption = (engineName: string): EuiComboBoxOptionOption<string> => ({
|
||||
label: engineName,
|
||||
});
|
||||
|
||||
const comboBoxOptionToEngineName = (option: EuiComboBoxOptionOption<string>): string =>
|
||||
option.label;
|
||||
|
||||
export const MetaEngineCreation: React.FC = () => {
|
||||
const {
|
||||
configuredLimits: {
|
||||
engine: { maxEnginesPerMetaEngine } = { maxEnginesPerMetaEngine: Infinity },
|
||||
},
|
||||
} = useValues(AppLogic);
|
||||
|
||||
const {
|
||||
fetchIndexedEngineNames,
|
||||
setRawName,
|
||||
setSelectedIndexedEngineNames,
|
||||
submitEngine,
|
||||
} = useActions(MetaEngineCreationLogic);
|
||||
|
||||
const { rawName, name, indexedEngineNames, selectedIndexedEngineNames } = useValues(
|
||||
MetaEngineCreationLogic
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
fetchIndexedEngineNames();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div data-test-subj="MetaEngineCreation">
|
||||
<SetPageChrome trail={[META_ENGINE_CREATION_TITLE]} />
|
||||
<EuiPageHeader>
|
||||
<EuiPageHeaderSection>
|
||||
<EuiTitle size="l">
|
||||
<h1>{META_ENGINE_CREATION_TITLE}</h1>
|
||||
</EuiTitle>
|
||||
<EuiText>{META_ENGINE_CREATION_FORM_META_ENGINE_DESCRIPTION}</EuiText>
|
||||
<EuiText>{META_ENGINE_CREATION_FORM_DOCUMENTATION_DESCRIPTION}</EuiText>
|
||||
</EuiPageHeaderSection>
|
||||
</EuiPageHeader>
|
||||
<FlashMessages />
|
||||
<EuiPageContent>
|
||||
<EuiForm
|
||||
component="form"
|
||||
data-test-subj="MetaEngineCreationForm"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submitEngine();
|
||||
}}
|
||||
>
|
||||
<EuiTitle>
|
||||
<EuiText>{META_ENGINE_CREATION_FORM_TITLE}</EuiText>
|
||||
</EuiTitle>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
<EuiFormRow
|
||||
data-test-subj="MetaEngineCreationNameFormRow"
|
||||
label={META_ENGINE_CREATION_FORM_ENGINE_NAME_LABEL}
|
||||
helpText={
|
||||
name.length > 0 && rawName !== name ? (
|
||||
<>
|
||||
{SANITIZED_NAME_NOTE} <strong>{name}</strong>
|
||||
</>
|
||||
) : (
|
||||
ALLOWED_CHARS_NOTE
|
||||
)
|
||||
}
|
||||
fullWidth
|
||||
>
|
||||
<EuiFieldText
|
||||
name="engine-name"
|
||||
value={rawName}
|
||||
onChange={(event) => setRawName(event.currentTarget.value)}
|
||||
fullWidth
|
||||
data-test-subj="MetaEngineCreationNameInput"
|
||||
placeholder={META_ENGINE_CREATION_FORM_ENGINE_NAME_PLACEHOLDER}
|
||||
autoFocus
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer />
|
||||
<EuiFormRow label={META_ENGINE_CREATION_FORM_ENGINE_SOURCE_ENGINES_LABEL} fullWidth>
|
||||
<EuiComboBox
|
||||
data-test-subj="MetaEngineCreationSourceEnginesInput"
|
||||
options={indexedEngineNames.map(engineNameToComboBoxOption)}
|
||||
selectedOptions={selectedIndexedEngineNames.map(engineNameToComboBoxOption)}
|
||||
onChange={(options) => {
|
||||
setSelectedIndexedEngineNames(options.map(comboBoxOptionToEngineName));
|
||||
}}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
<EuiSpacer />
|
||||
{selectedIndexedEngineNames.length > maxEnginesPerMetaEngine && (
|
||||
<EuiCallOut
|
||||
color="warning"
|
||||
title={META_ENGINE_CREATION_FORM_MAX_SOURCE_ENGINES_WARNING_TITLE(
|
||||
maxEnginesPerMetaEngine
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<EuiSpacer />
|
||||
<EuiButton
|
||||
disabled={
|
||||
name.length === 0 ||
|
||||
selectedIndexedEngineNames.length === 0 ||
|
||||
selectedIndexedEngineNames.length > maxEnginesPerMetaEngine
|
||||
}
|
||||
type="submit"
|
||||
data-test-subj="NewMetaEngineSubmitButton"
|
||||
fill
|
||||
color="secondary"
|
||||
>
|
||||
{META_ENGINE_CREATION_FORM_SUBMIT_BUTTON_LABEL}
|
||||
</EuiButton>
|
||||
</EuiForm>
|
||||
</EuiPageContent>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* 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 {
|
||||
LogicMounter,
|
||||
mockHttpValues,
|
||||
mockFlashMessageHelpers,
|
||||
mockKibanaValues,
|
||||
} from '../../../__mocks__';
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { MetaEngineCreationLogic } from './meta_engine_creation_logic';
|
||||
|
||||
describe('MetaEngineCreationLogic', () => {
|
||||
const { mount } = new LogicMounter(MetaEngineCreationLogic);
|
||||
const { http } = mockHttpValues;
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
const { setQueuedSuccessMessage, flashAPIErrors } = mockFlashMessageHelpers;
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
indexedEngineNames: [],
|
||||
name: '',
|
||||
rawName: '',
|
||||
selectedIndexedEngineNames: [],
|
||||
};
|
||||
|
||||
it('has expected default values', () => {
|
||||
mount();
|
||||
expect(MetaEngineCreationLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('setRawName', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
mount();
|
||||
MetaEngineCreationLogic.actions.setRawName('Name__With#$&*%Special--Characters');
|
||||
});
|
||||
|
||||
it('should set rawName to provided value', () => {
|
||||
expect(MetaEngineCreationLogic.values.rawName).toEqual(
|
||||
'Name__With#$&*%Special--Characters'
|
||||
);
|
||||
});
|
||||
|
||||
it('should set name to a sanitized value', () => {
|
||||
expect(MetaEngineCreationLogic.values.name).toEqual('name-with-special-characters');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setIndexedEngineNames', () => {
|
||||
it('should set indexedEngineNames to provided value', () => {
|
||||
mount();
|
||||
MetaEngineCreationLogic.actions.setIndexedEngineNames(['first', 'middle', 'last']);
|
||||
expect(MetaEngineCreationLogic.values.indexedEngineNames).toEqual([
|
||||
'first',
|
||||
'middle',
|
||||
'last',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSelectedIndexedEngineNames', () => {
|
||||
it('should set selectedIndexedEngineNames to provided value', () => {
|
||||
mount();
|
||||
MetaEngineCreationLogic.actions.setSelectedIndexedEngineNames(['one', 'two', 'three']);
|
||||
expect(MetaEngineCreationLogic.values.selectedIndexedEngineNames).toEqual([
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('fetchIndexedEngineNames', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls flashAPIErrors on API Error', async () => {
|
||||
http.get.mockReturnValueOnce(Promise.reject());
|
||||
MetaEngineCreationLogic.actions.fetchIndexedEngineNames();
|
||||
await nextTick();
|
||||
expect(flashAPIErrors).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls onEngineCreationSuccess on valid submission', async () => {
|
||||
jest.spyOn(MetaEngineCreationLogic.actions, 'setIndexedEngineNames');
|
||||
http.get.mockReturnValueOnce(
|
||||
Promise.resolve({ results: [{ name: 'foo' }], meta: { page: { total_pages: 1 } } })
|
||||
);
|
||||
MetaEngineCreationLogic.actions.fetchIndexedEngineNames();
|
||||
await nextTick();
|
||||
expect(MetaEngineCreationLogic.actions.setIndexedEngineNames).toHaveBeenCalledWith(['foo']);
|
||||
});
|
||||
|
||||
it('if there are remaining pages it should call fetchIndexedEngineNames recursively with an incremented page', async () => {
|
||||
jest.spyOn(MetaEngineCreationLogic.actions, 'fetchIndexedEngineNames');
|
||||
http.get.mockReturnValueOnce(
|
||||
Promise.resolve({ results: [{ name: 'foo' }], meta: { page: { total_pages: 2 } } })
|
||||
);
|
||||
MetaEngineCreationLogic.actions.fetchIndexedEngineNames();
|
||||
await nextTick();
|
||||
expect(MetaEngineCreationLogic.actions.fetchIndexedEngineNames).toHaveBeenCalledWith(2);
|
||||
});
|
||||
|
||||
it('if there are no remaining pages it should end without calling recursively', async () => {
|
||||
jest.spyOn(MetaEngineCreationLogic.actions, 'fetchIndexedEngineNames');
|
||||
http.get.mockReturnValueOnce(
|
||||
Promise.resolve({ results: [{ name: 'foo' }], meta: { page: { total_pages: 1 } } })
|
||||
);
|
||||
MetaEngineCreationLogic.actions.fetchIndexedEngineNames();
|
||||
await nextTick();
|
||||
expect(MetaEngineCreationLogic.actions.fetchIndexedEngineNames).toHaveBeenCalledTimes(1); // it's one time cause we called it two lines above
|
||||
});
|
||||
});
|
||||
|
||||
describe('onEngineCreationSuccess', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
mount({ language: 'English', rawName: 'test' });
|
||||
MetaEngineCreationLogic.actions.onEngineCreationSuccess();
|
||||
});
|
||||
|
||||
it('should set a success message', () => {
|
||||
expect(setQueuedSuccessMessage).toHaveBeenCalledWith('Successfully created meta engine.');
|
||||
});
|
||||
|
||||
it('should navigate the user to the engine page', () => {
|
||||
expect(navigateToUrl).toHaveBeenCalledWith('/engines/test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitEngine', () => {
|
||||
beforeAll(() => {
|
||||
jest.clearAllMocks();
|
||||
mount({ rawName: 'test', selectedIndexedEngineNames: ['foo'] });
|
||||
});
|
||||
|
||||
it('POSTS to /api/app_search/engines', () => {
|
||||
const body = JSON.stringify({
|
||||
name: 'test',
|
||||
type: 'meta',
|
||||
source_engines: ['foo'],
|
||||
});
|
||||
MetaEngineCreationLogic.actions.submitEngine();
|
||||
expect(http.post).toHaveBeenCalledWith('/api/app_search/engines', { body });
|
||||
});
|
||||
|
||||
it('calls onEngineCreationSuccess on valid submission', async () => {
|
||||
jest.spyOn(MetaEngineCreationLogic.actions, 'onEngineCreationSuccess');
|
||||
http.post.mockReturnValueOnce(Promise.resolve({}));
|
||||
MetaEngineCreationLogic.actions.submitEngine();
|
||||
await nextTick();
|
||||
expect(MetaEngineCreationLogic.actions.onEngineCreationSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('calls flashAPIErrors on API Error', async () => {
|
||||
jest.spyOn(MetaEngineCreationLogic.actions, 'setIndexedEngineNames');
|
||||
http.post.mockReturnValueOnce(Promise.reject());
|
||||
MetaEngineCreationLogic.actions.submitEngine();
|
||||
await nextTick();
|
||||
expect(flashAPIErrors).toHaveBeenCalledTimes(1);
|
||||
expect(MetaEngineCreationLogic.actions.setIndexedEngineNames).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* 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 { generatePath } from 'react-router-dom';
|
||||
|
||||
import { kea, MakeLogicType } from 'kea';
|
||||
|
||||
import { Meta } from '../../../../../common/types';
|
||||
import { DEFAULT_META } from '../../../shared/constants';
|
||||
import { flashAPIErrors, setQueuedSuccessMessage } from '../../../shared/flash_messages';
|
||||
import { HttpLogic } from '../../../shared/http';
|
||||
import { KibanaLogic } from '../../../shared/kibana';
|
||||
import { ENGINE_PATH } from '../../routes';
|
||||
import { formatApiName } from '../../utils/format_api_name';
|
||||
import { EngineDetails } from '../engine/types';
|
||||
|
||||
import { META_ENGINE_CREATION_SUCCESS_MESSAGE } from './constants';
|
||||
|
||||
interface MetaEngineCreationValues {
|
||||
indexedEngineNames: string[];
|
||||
name: string;
|
||||
rawName: string;
|
||||
selectedIndexedEngineNames: string[];
|
||||
}
|
||||
|
||||
interface MetaEngineCreationActions {
|
||||
fetchIndexedEngineNames(page?: number): { page: number };
|
||||
onEngineCreationSuccess(): void;
|
||||
setIndexedEngineNames(
|
||||
indexedEngineNames: MetaEngineCreationValues['indexedEngineNames']
|
||||
): { indexedEngineNames: MetaEngineCreationValues['indexedEngineNames'] };
|
||||
setRawName(rawName: string): { rawName: string };
|
||||
setSelectedIndexedEngineNames(
|
||||
selectedIndexedEngineNames: MetaEngineCreationValues['selectedIndexedEngineNames']
|
||||
): { selectedIndexedEngineNames: MetaEngineCreationValues['selectedIndexedEngineNames'] };
|
||||
submitEngine(): void;
|
||||
}
|
||||
|
||||
export const MetaEngineCreationLogic = kea<
|
||||
MakeLogicType<MetaEngineCreationValues, MetaEngineCreationActions>
|
||||
>({
|
||||
path: ['enterprise_search', 'app_search', 'meta_engine_creation_logic'],
|
||||
actions: {
|
||||
fetchIndexedEngineNames: (page = DEFAULT_META.page.current) => ({ page }),
|
||||
onEngineCreationSuccess: true,
|
||||
setIndexedEngineNames: (indexedEngineNames) => ({ indexedEngineNames }),
|
||||
setRawName: (rawName) => ({ rawName }),
|
||||
setSelectedIndexedEngineNames: (selectedIndexedEngineNames) => ({ selectedIndexedEngineNames }),
|
||||
submitEngine: () => null,
|
||||
},
|
||||
reducers: {
|
||||
indexedEngineNames: [
|
||||
[],
|
||||
{
|
||||
setIndexedEngineNames: (_, { indexedEngineNames }) => indexedEngineNames,
|
||||
},
|
||||
],
|
||||
rawName: [
|
||||
'',
|
||||
{
|
||||
setRawName: (_, { rawName }) => rawName,
|
||||
},
|
||||
],
|
||||
selectedIndexedEngineNames: [
|
||||
[],
|
||||
{
|
||||
setSelectedIndexedEngineNames: (_, { selectedIndexedEngineNames }) =>
|
||||
selectedIndexedEngineNames,
|
||||
},
|
||||
],
|
||||
},
|
||||
selectors: ({ selectors }) => ({
|
||||
name: [() => [selectors.rawName], (rawName: string) => formatApiName(rawName)],
|
||||
}),
|
||||
listeners: ({ values, actions }) => ({
|
||||
fetchIndexedEngineNames: async ({ page }) => {
|
||||
const { http } = HttpLogic.values;
|
||||
let response: { results: EngineDetails[]; meta: Meta } | undefined;
|
||||
|
||||
try {
|
||||
response = await http.get('/api/app_search/engines', {
|
||||
query: { type: 'indexed', 'page[current]': page, 'page[size]': DEFAULT_META.page.size },
|
||||
});
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
|
||||
if (response) {
|
||||
const engineNames = response.results.map((result) => result.name);
|
||||
actions.setIndexedEngineNames([...values.indexedEngineNames, ...engineNames]);
|
||||
|
||||
if (page < response.meta.page.total_pages) {
|
||||
actions.fetchIndexedEngineNames(page + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
onEngineCreationSuccess: () => {
|
||||
const { name } = values;
|
||||
const { navigateToUrl } = KibanaLogic.values;
|
||||
const enginePath = generatePath(ENGINE_PATH, { engineName: name });
|
||||
|
||||
setQueuedSuccessMessage(META_ENGINE_CREATION_SUCCESS_MESSAGE);
|
||||
navigateToUrl(enginePath);
|
||||
},
|
||||
submitEngine: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { name, selectedIndexedEngineNames } = values;
|
||||
|
||||
const body = JSON.stringify({
|
||||
name,
|
||||
type: 'meta',
|
||||
source_engines: selectedIndexedEngineNames,
|
||||
});
|
||||
|
||||
try {
|
||||
await http.post('/api/app_search/engines', { body });
|
||||
actions.onEngineCreationSuccess();
|
||||
} catch (e) {
|
||||
flashAPIErrors(e);
|
||||
}
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -21,6 +21,7 @@ import { EngineRouter } from './components/engine';
|
|||
import { EngineCreation } from './components/engine_creation';
|
||||
import { EnginesOverview } from './components/engines';
|
||||
import { ErrorConnecting } from './components/error_connecting';
|
||||
import { MetaEngineCreation } from './components/meta_engine_creation';
|
||||
import { SetupGuide } from './components/setup_guide';
|
||||
|
||||
import { AppSearch, AppSearchUnconfigured, AppSearchConfigured, AppSearchNav } from './';
|
||||
|
@ -117,6 +118,22 @@ describe('AppSearchConfigured', () => {
|
|||
expect(wrapper.find(EngineCreation)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('canManageMetaEngines', () => {
|
||||
it('renders MetaEngineCreation when user canManageMetaEngines is true', () => {
|
||||
setMockValues({ myRole: { canManageMetaEngines: true } });
|
||||
const wrapper = shallow(<AppSearchConfigured />);
|
||||
|
||||
expect(wrapper.find(MetaEngineCreation)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('does not render MetaEngineCreation when user canManageMetaEngines is false', () => {
|
||||
setMockValues({ myRole: { canManageMetaEngines: false } });
|
||||
const wrapper = shallow(<AppSearchConfigured />);
|
||||
|
||||
expect(wrapper.find(MetaEngineCreation)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@ import { EngineCreation } from './components/engine_creation';
|
|||
import { EnginesOverview, ENGINES_TITLE } from './components/engines';
|
||||
import { ErrorConnecting } from './components/error_connecting';
|
||||
import { Library } from './components/library';
|
||||
import { MetaEngineCreation } from './components/meta_engine_creation';
|
||||
import { ROLE_MAPPINGS_TITLE } from './components/role_mappings';
|
||||
import { Settings, SETTINGS_TITLE } from './components/settings';
|
||||
import { SetupGuide } from './components/setup_guide';
|
||||
|
@ -38,6 +39,7 @@ import {
|
|||
ENGINES_PATH,
|
||||
ENGINE_PATH,
|
||||
LIBRARY_PATH,
|
||||
META_ENGINE_CREATION_PATH,
|
||||
} from './routes';
|
||||
|
||||
export const AppSearch: React.FC<InitialAppData> = (props) => {
|
||||
|
@ -60,7 +62,7 @@ export const AppSearchConfigured: React.FC<InitialAppData> = (props) => {
|
|||
const { initializeAppData } = useActions(AppLogic);
|
||||
const {
|
||||
hasInitialized,
|
||||
myRole: { canManageEngines },
|
||||
myRole: { canManageEngines, canManageMetaEngines },
|
||||
} = useValues(AppLogic);
|
||||
const { errorConnecting, readOnlyMode } = useValues(HttpLogic);
|
||||
|
||||
|
@ -106,6 +108,11 @@ export const AppSearchConfigured: React.FC<InitialAppData> = (props) => {
|
|||
<EngineCreation />
|
||||
</Route>
|
||||
)}
|
||||
{canManageMetaEngines && (
|
||||
<Route exact path={META_ENGINE_CREATION_PATH}>
|
||||
<MetaEngineCreation />
|
||||
</Route>
|
||||
)}
|
||||
<Route>
|
||||
<NotFound product={APP_SEARCH_PLUGIN} />
|
||||
</Route>
|
||||
|
|
|
@ -40,6 +40,7 @@ export const ENGINE_REINDEX_JOB_PATH = `${ENGINE_PATH}/reindex-job/:activeReinde
|
|||
export const ENGINE_CRAWLER_PATH = `${ENGINE_PATH}/crawler`;
|
||||
// TODO: Crawler sub-pages
|
||||
|
||||
export const META_ENGINE_CREATION_PATH = '/meta_engine_creation';
|
||||
export const META_ENGINE_SOURCE_ENGINES_PATH = `${ENGINE_PATH}/engines`;
|
||||
|
||||
export const ENGINE_RELEVANCE_TUNING_PATH = `${ENGINE_PATH}/relevance_tuning`;
|
||||
|
|
|
@ -122,19 +122,56 @@ describe('engine routes', () => {
|
|||
});
|
||||
|
||||
describe('validates', () => {
|
||||
it('correctly', () => {
|
||||
const request = { body: { name: 'some-engine', language: 'en' } };
|
||||
mockRouter.shouldValidate(request);
|
||||
describe('indexed engines', () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('missing name', () => {
|
||||
const request = { body: { language: 'en' } };
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
describe('meta engines', () => {
|
||||
it('all properties', () => {
|
||||
const request = {
|
||||
body: { name: 'some-meta-engine', type: 'any', language: 'en', source_engines: [] },
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('optional language', () => {
|
||||
const request = { body: { name: 'some-engine' } };
|
||||
mockRouter.shouldValidate(request);
|
||||
it('missing name', () => {
|
||||
const request = {
|
||||
body: { type: 'any', language: 'en', source_engines: [] },
|
||||
};
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
|
||||
it('optional language', () => {
|
||||
const request = {
|
||||
body: { name: 'some-meta-engine', type: 'any', source_engines: [] },
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('optional source_engines', () => {
|
||||
const request = {
|
||||
body: { name: 'some-meta-engine', type: 'any', language: 'en' },
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('optional type', () => {
|
||||
const request = { body: { name: 'some-engine' } };
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -45,6 +45,8 @@ export function registerEnginesRoutes({
|
|||
body: schema.object({
|
||||
name: schema.string(),
|
||||
language: schema.maybe(schema.string()),
|
||||
source_engines: schema.maybe(schema.arrayOf(schema.string())),
|
||||
type: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Reference in a new issue