mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[App Search] New Add Domain Flyout for Crawler (#106102)
* Added /api/app_search/engines/{name}/crawler/domains to Crawler routes * New AddDomainLogic * New AddDomainFormSubmitButton component * New AddDomainFormErrors component * New AddDomainForm component * New AddDomainFlyout component * Add AddDomainFlyout to CrawlerOverview * Use exact path for CrawlerOverview in CrawlerRouter * Clean-up AddDomainFlyout * Clean-up AddDomainForm * Clean-up AddDomainFormSubmitButton * Extract getErrorsFromHttpResponse from flashAPIErrors * Clean-up AddDomainLogic * Remove unused imports Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
e45d25dde0
commit
f7a308859f
19 changed files with 1217 additions and 11 deletions
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* 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, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { EuiButton, EuiButtonEmpty, EuiFlyout, EuiFlyoutBody } from '@elastic/eui';
|
||||
|
||||
import { AddDomainFlyout } from './add_domain_flyout';
|
||||
import { AddDomainForm } from './add_domain_form';
|
||||
import { AddDomainFormErrors } from './add_domain_form_errors';
|
||||
import { AddDomainFormSubmitButton } from './add_domain_form_submit_button';
|
||||
|
||||
describe('AddDomainFlyout', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('is hidden by default', () => {
|
||||
const wrapper = shallow(<AddDomainFlyout />);
|
||||
|
||||
expect(wrapper.find(EuiFlyout)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('displays the flyout when the button is pressed', () => {
|
||||
const wrapper = shallow(<AddDomainFlyout />);
|
||||
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
expect(wrapper.find(EuiFlyout)).toHaveLength(1);
|
||||
});
|
||||
|
||||
describe('flyout', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<AddDomainFlyout />);
|
||||
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
});
|
||||
|
||||
it('displays form errors', () => {
|
||||
expect(wrapper.find(EuiFlyoutBody).dive().find(AddDomainFormErrors)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('contains a form to add domains', () => {
|
||||
expect(wrapper.find(AddDomainForm)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('contains a cancel buttonn', () => {
|
||||
wrapper.find(EuiButtonEmpty).simulate('click');
|
||||
expect(wrapper.find(EuiFlyout)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('contains a submit button', () => {
|
||||
expect(wrapper.find(AddDomainFormSubmitButton)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('hides the flyout on close', () => {
|
||||
wrapper.find(EuiFlyout).simulate('close');
|
||||
|
||||
expect(wrapper.find(EuiFlyout)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiFlyout,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutFooter,
|
||||
EuiFlyoutHeader,
|
||||
EuiPortal,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { CANCEL_BUTTON_LABEL } from '../../../../../shared/constants';
|
||||
|
||||
import { AddDomainForm } from './add_domain_form';
|
||||
import { AddDomainFormErrors } from './add_domain_form_errors';
|
||||
import { AddDomainFormSubmitButton } from './add_domain_form_submit_button';
|
||||
|
||||
export const AddDomainFlyout: React.FC = () => {
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="success"
|
||||
iconType="plusInCircle"
|
||||
onClick={() => setIsFlyoutVisible(true)}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.openButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Add domain',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
|
||||
{isFlyoutVisible && (
|
||||
<EuiPortal>
|
||||
<EuiFlyout onClose={() => setIsFlyoutVisible(false)}>
|
||||
<EuiFlyoutHeader>
|
||||
<EuiTitle size="m">
|
||||
<h2>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.title',
|
||||
{
|
||||
defaultMessage: 'Add a new domain',
|
||||
}
|
||||
)}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiFlyoutHeader>
|
||||
<EuiFlyoutBody banner={<AddDomainFormErrors />}>
|
||||
<EuiText>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.addDomainFlyout.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'You can add multiple domains to this engine\'s web crawler. Add another domain here and modify the entry points and crawl rules from the "Manage" page.',
|
||||
}
|
||||
)}
|
||||
<p />
|
||||
</EuiText>
|
||||
<EuiSpacer size="l" />
|
||||
<AddDomainForm />
|
||||
</EuiFlyoutBody>
|
||||
<EuiFlyoutFooter>
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty onClick={() => setIsFlyoutVisible(false)}>
|
||||
{CANCEL_BUTTON_LABEL}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddDomainFormSubmitButton />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlyoutFooter>
|
||||
</EuiFlyout>
|
||||
</EuiPortal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { EuiButton, EuiFieldText, EuiForm } from '@elastic/eui';
|
||||
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { rerender } from '../../../../../test_helpers';
|
||||
|
||||
import { AddDomainForm } from './add_domain_form';
|
||||
|
||||
const MOCK_VALUES = {
|
||||
addDomainFormInputValue: 'https://',
|
||||
entryPointValue: '/',
|
||||
};
|
||||
|
||||
const MOCK_ACTIONS = {
|
||||
setAddDomainFormInputValue: jest.fn(),
|
||||
validateDomain: jest.fn(),
|
||||
};
|
||||
|
||||
describe('AddDomainForm', () => {
|
||||
let wrapper: ShallowWrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
setMockActions(MOCK_ACTIONS);
|
||||
setMockValues(MOCK_VALUES);
|
||||
wrapper = shallow(<AddDomainForm />);
|
||||
});
|
||||
|
||||
it('renders', () => {
|
||||
expect(wrapper.find(EuiForm)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('contains a submit button', () => {
|
||||
expect(wrapper.find(EuiButton).prop('type')).toEqual('submit');
|
||||
});
|
||||
|
||||
it('validates domain on submit', () => {
|
||||
wrapper.find(EuiForm).simulate('submit', { preventDefault: jest.fn() });
|
||||
|
||||
expect(MOCK_ACTIONS.validateDomain).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('url field', () => {
|
||||
it('uses the value from the logic', () => {
|
||||
setMockValues({
|
||||
...MOCK_VALUES,
|
||||
addDomainFormInputValue: 'test value',
|
||||
});
|
||||
|
||||
rerender(wrapper);
|
||||
|
||||
expect(wrapper.find(EuiFieldText).prop('value')).toEqual('test value');
|
||||
});
|
||||
|
||||
it('sets the value in the logic on change', () => {
|
||||
wrapper.find(EuiFieldText).simulate('change', { target: { value: 'test value' } });
|
||||
|
||||
expect(MOCK_ACTIONS.setAddDomainFormInputValue).toHaveBeenCalledWith('test value');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validate domain button', () => {
|
||||
it('is enabled when the input has a value', () => {
|
||||
setMockValues({
|
||||
...MOCK_VALUES,
|
||||
addDomainFormInputValue: 'https://elastic.co',
|
||||
});
|
||||
|
||||
rerender(wrapper);
|
||||
|
||||
expect(wrapper.find(EuiButton).prop('disabled')).toEqual(false);
|
||||
});
|
||||
|
||||
it('is disabled when the input value is empty', () => {
|
||||
setMockValues({
|
||||
...MOCK_VALUES,
|
||||
addDomainFormInputValue: '',
|
||||
});
|
||||
|
||||
rerender(wrapper);
|
||||
|
||||
expect(wrapper.find(EuiButton).prop('disabled')).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('entry point indicator', () => {
|
||||
it('is hidden when the entry point is /', () => {
|
||||
setMockValues({
|
||||
...MOCK_VALUES,
|
||||
entryPointValue: '/',
|
||||
});
|
||||
|
||||
rerender(wrapper);
|
||||
|
||||
expect(wrapper.find(FormattedMessage)).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('displays the entry point otherwise', () => {
|
||||
setMockValues({
|
||||
...MOCK_VALUES,
|
||||
entryPointValue: '/guide',
|
||||
});
|
||||
|
||||
rerender(wrapper);
|
||||
|
||||
expect(wrapper.find(FormattedMessage)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiCode,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiForm,
|
||||
EuiFormRow,
|
||||
EuiFieldText,
|
||||
EuiSpacer,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { AddDomainLogic } from './add_domain_logic';
|
||||
|
||||
export const AddDomainForm: React.FC = () => {
|
||||
const { setAddDomainFormInputValue, validateDomain } = useActions(AddDomainLogic);
|
||||
|
||||
const { addDomainFormInputValue, entryPointValue } = useValues(AddDomainLogic);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiForm
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
validateDomain();
|
||||
}}
|
||||
component="form"
|
||||
>
|
||||
<EuiFormRow
|
||||
fullWidth
|
||||
label={
|
||||
i18n.translate('xpack.enterpriseSearch.appSearch.crawler.addDomainForm.urlLabel', {
|
||||
defaultMessage: 'Domain URL',
|
||||
}) /* "Domain URL"*/
|
||||
}
|
||||
helpText={
|
||||
<EuiText size="s">
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.urlHelpText',
|
||||
{
|
||||
defaultMessage: 'Domain URLs require a protocol and cannot contain any paths.',
|
||||
}
|
||||
)}
|
||||
</EuiText>
|
||||
}
|
||||
>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow>
|
||||
<EuiFieldText
|
||||
autoFocus
|
||||
placeholder="https://"
|
||||
value={addDomainFormInputValue}
|
||||
onChange={(e) => setAddDomainFormInputValue(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton type="submit" fill disabled={addDomainFormInputValue.length === 0}>
|
||||
{i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.validateButtonLabel',
|
||||
{
|
||||
defaultMessage: 'Validate Domain',
|
||||
}
|
||||
)}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFormRow>
|
||||
|
||||
{entryPointValue !== '/' && (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiText size="s">
|
||||
<p>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id="xpack.enterpriseSearch.appSearch.crawler.addDomainForm.entryPointLabel"
|
||||
defaultMessage="Web Crawler entry point has been set as {entryPointValue}"
|
||||
values={{
|
||||
entryPointValue: <EuiCode>{entryPointValue}</EuiCode>,
|
||||
}}
|
||||
/>
|
||||
</strong>
|
||||
</p>
|
||||
</EuiText>
|
||||
</>
|
||||
)}
|
||||
</EuiForm>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { setMockValues } from '../../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { AddDomainFormErrors } from './add_domain_form_errors';
|
||||
|
||||
describe('AddDomainFormErrors', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('is empty when there are no errors', () => {
|
||||
setMockValues({
|
||||
errors: [],
|
||||
});
|
||||
|
||||
const wrapper = shallow(<AddDomainFormErrors />);
|
||||
|
||||
expect(wrapper.isEmptyRender()).toBe(true);
|
||||
});
|
||||
|
||||
it('displays all the errors from the logic', () => {
|
||||
setMockValues({
|
||||
errors: ['first error', 'second error'],
|
||||
});
|
||||
|
||||
const wrapper = shallow(<AddDomainFormErrors />);
|
||||
|
||||
expect(wrapper.find('p')).toHaveLength(2);
|
||||
expect(wrapper.find('p').first().text()).toContain('first error');
|
||||
expect(wrapper.find('p').last().text()).toContain('second error');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useValues } from 'kea';
|
||||
|
||||
import { EuiCallOut } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AddDomainLogic } from './add_domain_logic';
|
||||
|
||||
export const AddDomainFormErrors: React.FC = () => {
|
||||
const { errors } = useValues(AddDomainLogic);
|
||||
|
||||
if (errors.length > 0) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
color="danger"
|
||||
iconType="alert"
|
||||
title={i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.addDomainForm.errorsTitle',
|
||||
{
|
||||
defaultMessage: 'Something went wrong. Please address the errors and try again.',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{errors.map((message, index) => (
|
||||
<p key={index}>{message}</p>
|
||||
))}
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
|
@ -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 { setMockActions, setMockValues } from '../../../../../__mocks__/kea_logic';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
import { AddDomainFormSubmitButton } from './add_domain_form_submit_button';
|
||||
|
||||
describe('AddDomainFormSubmitButton', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('is disabled when the domain has not been validated', () => {
|
||||
setMockValues({
|
||||
hasValidationCompleted: false,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<AddDomainFormSubmitButton />);
|
||||
|
||||
expect(wrapper.prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('is enabled when the domain has been validated', () => {
|
||||
setMockValues({
|
||||
hasValidationCompleted: true,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<AddDomainFormSubmitButton />);
|
||||
|
||||
expect(wrapper.prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('submits the domain on click', () => {
|
||||
const submitNewDomain = jest.fn();
|
||||
|
||||
setMockActions({
|
||||
submitNewDomain,
|
||||
});
|
||||
setMockValues({
|
||||
hasValidationCompleted: true,
|
||||
});
|
||||
|
||||
const wrapper = shallow(<AddDomainFormSubmitButton />);
|
||||
|
||||
wrapper.find(EuiButton).simulate('click');
|
||||
|
||||
expect(submitNewDomain).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { AddDomainLogic } from './add_domain_logic';
|
||||
|
||||
export const AddDomainFormSubmitButton: React.FC = () => {
|
||||
const { submitNewDomain } = useActions(AddDomainLogic);
|
||||
|
||||
const { hasValidationCompleted } = useValues(AddDomainLogic);
|
||||
|
||||
return (
|
||||
<EuiButton fill type="button" disabled={!hasValidationCompleted} onClick={submitNewDomain}>
|
||||
{i18n.translate('xpack.enterpriseSearch.appSearch.crawler.addDomainForm.submitButtonLabel', {
|
||||
defaultMessage: 'Add domain',
|
||||
})}
|
||||
</EuiButton>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* 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,
|
||||
mockFlashMessageHelpers,
|
||||
mockHttpValues,
|
||||
mockKibanaValues,
|
||||
} from '../../../../../__mocks__/kea_logic';
|
||||
import '../../../../__mocks__/engine_logic.mock';
|
||||
|
||||
jest.mock('../../crawler_overview_logic', () => ({
|
||||
CrawlerOverviewLogic: {
|
||||
actions: {
|
||||
onReceiveCrawlerData: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
import { nextTick } from '@kbn/test/jest';
|
||||
|
||||
import { CrawlerOverviewLogic } from '../../crawler_overview_logic';
|
||||
import { CrawlerDomain } from '../../types';
|
||||
|
||||
import { AddDomainLogic, AddDomainLogicValues } from './add_domain_logic';
|
||||
|
||||
const DEFAULT_VALUES: AddDomainLogicValues = {
|
||||
addDomainFormInputValue: 'https://',
|
||||
allowSubmit: false,
|
||||
entryPointValue: '/',
|
||||
hasValidationCompleted: false,
|
||||
errors: [],
|
||||
};
|
||||
|
||||
describe('AddDomainLogic', () => {
|
||||
const { mount } = new LogicMounter(AddDomainLogic);
|
||||
const { flashSuccessToast } = mockFlashMessageHelpers;
|
||||
const { http } = mockHttpValues;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('has default values', () => {
|
||||
mount();
|
||||
expect(AddDomainLogic.values).toEqual(DEFAULT_VALUES);
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('clearDomainFormInputValue', () => {
|
||||
beforeAll(() => {
|
||||
mount({
|
||||
addDomainFormInputValue: 'http://elastic.co',
|
||||
entryPointValue: '/foo',
|
||||
hasValidationCompleted: true,
|
||||
errors: ['first error', 'second error'],
|
||||
});
|
||||
|
||||
AddDomainLogic.actions.clearDomainFormInputValue();
|
||||
});
|
||||
|
||||
it('should clear the input value', () => {
|
||||
expect(AddDomainLogic.values.addDomainFormInputValue).toEqual('https://');
|
||||
});
|
||||
|
||||
it('should clear the entry point value', () => {
|
||||
expect(AddDomainLogic.values.entryPointValue).toEqual('/');
|
||||
});
|
||||
|
||||
it('should reset validation completion', () => {
|
||||
expect(AddDomainLogic.values.hasValidationCompleted).toEqual(false);
|
||||
});
|
||||
|
||||
it('should clear errors', () => {
|
||||
expect(AddDomainLogic.values.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmitNewDomainError', () => {
|
||||
it('should set errors', () => {
|
||||
mount();
|
||||
|
||||
AddDomainLogic.actions.onSubmitNewDomainError(['first error', 'second error']);
|
||||
|
||||
expect(AddDomainLogic.values.errors).toEqual(['first error', 'second error']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onValidateDomain', () => {
|
||||
beforeAll(() => {
|
||||
mount({
|
||||
addDomainFormInputValue: 'https://elastic.co',
|
||||
entryPointValue: '/customers',
|
||||
hasValidationCompleted: true,
|
||||
errors: ['first error', 'second error'],
|
||||
});
|
||||
|
||||
AddDomainLogic.actions.onValidateDomain('https://swiftype.com', '/site-search');
|
||||
});
|
||||
|
||||
it('should set the input value', () => {
|
||||
expect(AddDomainLogic.values.addDomainFormInputValue).toEqual('https://swiftype.com');
|
||||
});
|
||||
|
||||
it('should set the entry point value', () => {
|
||||
expect(AddDomainLogic.values.entryPointValue).toEqual('/site-search');
|
||||
});
|
||||
|
||||
it('should flag validation as being completed', () => {
|
||||
expect(AddDomainLogic.values.hasValidationCompleted).toEqual(true);
|
||||
});
|
||||
|
||||
it('should clear errors', () => {
|
||||
expect(AddDomainLogic.values.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setAddDomainFormInputValue', () => {
|
||||
beforeAll(() => {
|
||||
mount({
|
||||
addDomainFormInputValue: 'https://elastic.co',
|
||||
entryPointValue: '/customers',
|
||||
hasValidationCompleted: true,
|
||||
errors: ['first error', 'second error'],
|
||||
});
|
||||
|
||||
AddDomainLogic.actions.setAddDomainFormInputValue('https://swiftype.com/site-search');
|
||||
});
|
||||
|
||||
it('should set the input value', () => {
|
||||
expect(AddDomainLogic.values.addDomainFormInputValue).toEqual(
|
||||
'https://swiftype.com/site-search'
|
||||
);
|
||||
});
|
||||
|
||||
it('should clear the entry point value', () => {
|
||||
expect(AddDomainLogic.values.entryPointValue).toEqual('/');
|
||||
});
|
||||
|
||||
it('should reset validation completion', () => {
|
||||
expect(AddDomainLogic.values.hasValidationCompleted).toEqual(false);
|
||||
});
|
||||
|
||||
it('should clear errors', () => {
|
||||
expect(AddDomainLogic.values.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitNewDomain', () => {
|
||||
it('should clear errors', () => {
|
||||
expect(AddDomainLogic.values.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('listeners', () => {
|
||||
describe('onSubmitNewDomainSuccess', () => {
|
||||
it('should flash a success toast', () => {
|
||||
const { navigateToUrl } = mockKibanaValues;
|
||||
mount();
|
||||
|
||||
AddDomainLogic.actions.onSubmitNewDomainSuccess({ id: 'test-domain' } as CrawlerDomain);
|
||||
|
||||
expect(flashSuccessToast).toHaveBeenCalled();
|
||||
expect(navigateToUrl).toHaveBeenCalledWith(
|
||||
'/engines/some-engine/crawler/domains/test-domain'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitNewDomain', () => {
|
||||
it('calls the domains endpoint with a JSON formatted body', async () => {
|
||||
mount({
|
||||
addDomainFormInputValue: 'https://elastic.co',
|
||||
entryPointValue: '/guide',
|
||||
});
|
||||
http.post.mockReturnValueOnce(Promise.resolve({}));
|
||||
|
||||
AddDomainLogic.actions.submitNewDomain();
|
||||
await nextTick();
|
||||
|
||||
expect(http.post).toHaveBeenCalledWith(
|
||||
'/api/app_search/engines/some-engine/crawler/domains',
|
||||
{
|
||||
query: {
|
||||
respond_with: 'crawler_details',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: 'https://elastic.co',
|
||||
entry_points: [{ value: '/guide' }],
|
||||
}),
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('on success', () => {
|
||||
beforeEach(() => {
|
||||
mount();
|
||||
});
|
||||
|
||||
it('sets crawler data', async () => {
|
||||
http.post.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
domains: [],
|
||||
})
|
||||
);
|
||||
|
||||
AddDomainLogic.actions.submitNewDomain();
|
||||
await nextTick();
|
||||
|
||||
expect(CrawlerOverviewLogic.actions.onReceiveCrawlerData).toHaveBeenCalledWith({
|
||||
domains: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the success callback with the most recent domain', async () => {
|
||||
http.post.mockReturnValueOnce(
|
||||
Promise.resolve({
|
||||
domains: [
|
||||
{
|
||||
id: '1',
|
||||
name: 'https://elastic.co/guide',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'https://swiftype.co/site-search',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainSuccess');
|
||||
AddDomainLogic.actions.submitNewDomain();
|
||||
await nextTick();
|
||||
|
||||
expect(AddDomainLogic.actions.onSubmitNewDomainSuccess).toHaveBeenCalledWith({
|
||||
id: '2',
|
||||
url: 'https://swiftype.co/site-search',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('on error', () => {
|
||||
beforeEach(() => {
|
||||
mount();
|
||||
jest.spyOn(AddDomainLogic.actions, 'onSubmitNewDomainError');
|
||||
});
|
||||
|
||||
it('passes error messages to the error callback', async () => {
|
||||
http.post.mockReturnValueOnce(
|
||||
Promise.reject({
|
||||
body: {
|
||||
attributes: {
|
||||
errors: ['first error', 'second error'],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
AddDomainLogic.actions.submitNewDomain();
|
||||
await nextTick();
|
||||
|
||||
expect(AddDomainLogic.actions.onSubmitNewDomainError).toHaveBeenCalledWith([
|
||||
'first error',
|
||||
'second error',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateDomain', () => {
|
||||
it('extracts the domain and entrypoint and passes them to the callback ', () => {
|
||||
mount({ addDomainFormInputValue: 'https://swiftype.com/site-search' });
|
||||
jest.spyOn(AddDomainLogic.actions, 'onValidateDomain');
|
||||
|
||||
AddDomainLogic.actions.validateDomain();
|
||||
|
||||
expect(AddDomainLogic.actions.onValidateDomain).toHaveBeenCalledWith(
|
||||
'https://swiftype.com',
|
||||
'/site-search'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectors', () => {
|
||||
describe('allowSubmit', () => {
|
||||
it('gets set true when validation is completed', () => {
|
||||
mount({ hasValidationCompleted: false });
|
||||
expect(AddDomainLogic.values.allowSubmit).toEqual(false);
|
||||
|
||||
mount({ hasValidationCompleted: true });
|
||||
expect(AddDomainLogic.values.allowSubmit).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
import { flashSuccessToast } from '../../../../../shared/flash_messages';
|
||||
import { getErrorsFromHttpResponse } from '../../../../../shared/flash_messages/handle_api_errors';
|
||||
|
||||
import { HttpLogic } from '../../../../../shared/http';
|
||||
import { KibanaLogic } from '../../../../../shared/kibana';
|
||||
import { ENGINE_CRAWLER_DOMAIN_PATH } from '../../../../routes';
|
||||
import { EngineLogic, generateEnginePath } from '../../../engine';
|
||||
|
||||
import { CrawlerOverviewLogic } from '../../crawler_overview_logic';
|
||||
import { CrawlerDataFromServer, CrawlerDomain } from '../../types';
|
||||
import { crawlerDataServerToClient } from '../../utils';
|
||||
|
||||
import { extractDomainAndEntryPointFromUrl } from './utils';
|
||||
|
||||
export interface AddDomainLogicValues {
|
||||
addDomainFormInputValue: string;
|
||||
allowSubmit: boolean;
|
||||
hasValidationCompleted: boolean;
|
||||
entryPointValue: string;
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
export interface AddDomainLogicActions {
|
||||
clearDomainFormInputValue(): void;
|
||||
setAddDomainFormInputValue(newValue: string): string;
|
||||
onSubmitNewDomainError(errors: string[]): { errors: string[] };
|
||||
onSubmitNewDomainSuccess(domain: CrawlerDomain): { domain: CrawlerDomain };
|
||||
onValidateDomain(
|
||||
newValue: string,
|
||||
newEntryPointValue: string
|
||||
): { newValue: string; newEntryPointValue: string };
|
||||
submitNewDomain(): void;
|
||||
validateDomain(): void;
|
||||
}
|
||||
|
||||
const DEFAULT_SELECTOR_VALUES = {
|
||||
addDomainFormInputValue: 'https://',
|
||||
entryPointValue: '/',
|
||||
};
|
||||
|
||||
export const AddDomainLogic = kea<MakeLogicType<AddDomainLogicValues, AddDomainLogicActions>>({
|
||||
path: ['enterprise_search', 'app_search', 'crawler', 'add_domain'],
|
||||
actions: () => ({
|
||||
clearDomainFormInputValue: true,
|
||||
setAddDomainFormInputValue: (newValue) => newValue,
|
||||
onSubmitNewDomainSuccess: (domain) => ({ domain }),
|
||||
onSubmitNewDomainError: (errors) => ({ errors }),
|
||||
onValidateDomain: (newValue, newEntryPointValue) => ({
|
||||
newValue,
|
||||
newEntryPointValue,
|
||||
}),
|
||||
submitNewDomain: true,
|
||||
validateDomain: true,
|
||||
}),
|
||||
reducers: () => ({
|
||||
addDomainFormInputValue: [
|
||||
DEFAULT_SELECTOR_VALUES.addDomainFormInputValue,
|
||||
{
|
||||
clearDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.addDomainFormInputValue,
|
||||
setAddDomainFormInputValue: (_, newValue: string) => newValue,
|
||||
onValidateDomain: (_, { newValue }: { newValue: string }) => newValue,
|
||||
},
|
||||
],
|
||||
entryPointValue: [
|
||||
DEFAULT_SELECTOR_VALUES.entryPointValue,
|
||||
{
|
||||
clearDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.entryPointValue,
|
||||
setAddDomainFormInputValue: () => DEFAULT_SELECTOR_VALUES.entryPointValue,
|
||||
onValidateDomain: (_, { newEntryPointValue }) => newEntryPointValue,
|
||||
},
|
||||
],
|
||||
// TODO When 4-step validation is added this will become a selector as
|
||||
// we'll use individual step results to determine whether this is true/false
|
||||
hasValidationCompleted: [
|
||||
false,
|
||||
{
|
||||
clearDomainFormInputValue: () => false,
|
||||
setAddDomainFormInputValue: () => false,
|
||||
onValidateDomain: () => true,
|
||||
},
|
||||
],
|
||||
errors: [
|
||||
[],
|
||||
{
|
||||
clearDomainFormInputValue: () => [],
|
||||
setAddDomainFormInputValue: () => [],
|
||||
onValidateDomain: () => [],
|
||||
submitNewDomain: () => [],
|
||||
onSubmitNewDomainError: (_, { errors }) => errors,
|
||||
},
|
||||
],
|
||||
}),
|
||||
selectors: ({ selectors }) => ({
|
||||
// TODO include selectors.blockingFailures once 4-step validation is migrated
|
||||
allowSubmit: [
|
||||
() => [selectors.hasValidationCompleted], // should eventually also contain selectors.hasBlockingFailures when that is added
|
||||
(hasValidationCompleted: boolean) => hasValidationCompleted, // && !hasBlockingFailures
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions, values }) => ({
|
||||
onSubmitNewDomainSuccess: ({ domain }) => {
|
||||
flashSuccessToast(
|
||||
i18n.translate(
|
||||
'xpack.enterpriseSearch.appSearch.crawler.domainsTable.action.add.successMessage',
|
||||
{
|
||||
defaultMessage: "Successfully added domain '{domainUrl}'",
|
||||
values: {
|
||||
domainUrl: domain.url,
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
||||
KibanaLogic.values.navigateToUrl(
|
||||
generateEnginePath(ENGINE_CRAWLER_DOMAIN_PATH, { domainId: domain.id })
|
||||
);
|
||||
},
|
||||
submitNewDomain: async () => {
|
||||
const { http } = HttpLogic.values;
|
||||
const { engineName } = EngineLogic.values;
|
||||
|
||||
const requestBody = JSON.stringify({
|
||||
name: values.addDomainFormInputValue.trim(),
|
||||
entry_points: [{ value: values.entryPointValue }],
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await http.post(`/api/app_search/engines/${engineName}/crawler/domains`, {
|
||||
query: {
|
||||
respond_with: 'crawler_details',
|
||||
},
|
||||
body: requestBody,
|
||||
});
|
||||
|
||||
const crawlerData = crawlerDataServerToClient(response as CrawlerDataFromServer);
|
||||
CrawlerOverviewLogic.actions.onReceiveCrawlerData(crawlerData);
|
||||
const newDomain = crawlerData.domains[crawlerData.domains.length - 1];
|
||||
if (newDomain) {
|
||||
actions.onSubmitNewDomainSuccess(newDomain);
|
||||
}
|
||||
// If there is not a new domain, that means the server responded with a 200 but
|
||||
// didn't actually persist the new domain to our BE, and we take no action
|
||||
} catch (e) {
|
||||
// we surface errors inside the form instead of in flash messages
|
||||
const errorMessages = getErrorsFromHttpResponse(e);
|
||||
actions.onSubmitNewDomainError(errorMessages);
|
||||
}
|
||||
},
|
||||
validateDomain: () => {
|
||||
const { domain, entryPoint } = extractDomainAndEntryPointFromUrl(
|
||||
values.addDomainFormInputValue.trim()
|
||||
);
|
||||
actions.onValidateDomain(domain, entryPoint);
|
||||
},
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* 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 { extractDomainAndEntryPointFromUrl } from './utils';
|
||||
|
||||
describe('extractDomainAndEntryPointFromUrl', () => {
|
||||
it('extracts a provided entry point and domain', () => {
|
||||
expect(extractDomainAndEntryPointFromUrl('https://elastic.co/guide')).toEqual({
|
||||
domain: 'https://elastic.co',
|
||||
entryPoint: '/guide',
|
||||
});
|
||||
});
|
||||
|
||||
it('provides a default entry point if there is only a domain', () => {
|
||||
expect(extractDomainAndEntryPointFromUrl('https://elastic.co')).toEqual({
|
||||
domain: 'https://elastic.co',
|
||||
entryPoint: '/',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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.
|
||||
*/
|
||||
|
||||
export const extractDomainAndEntryPointFromUrl = (
|
||||
url: string
|
||||
): { domain: string; entryPoint: string } => {
|
||||
let domain = url;
|
||||
let entryPoint = '/';
|
||||
|
||||
const pathSlashIndex = url.search(/[^\:\/]\//);
|
||||
if (pathSlashIndex !== -1) {
|
||||
domain = url.substring(0, pathSlashIndex + 1);
|
||||
entryPoint = url.substring(pathSlashIndex + 1);
|
||||
}
|
||||
|
||||
return { domain, entryPoint };
|
||||
};
|
|
@ -13,6 +13,7 @@ import React from 'react';
|
|||
|
||||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
|
||||
import { AddDomainFlyout } from './components/add_domain/add_domain_flyout';
|
||||
import { DomainsTable } from './components/domains_table';
|
||||
import { CrawlerOverview } from './crawler_overview';
|
||||
|
||||
|
@ -44,7 +45,7 @@ describe('CrawlerOverview', () => {
|
|||
|
||||
// TODO test for CrawlRequestsTable after it is built in a future PR
|
||||
|
||||
// TODO test for AddDomainForm after it is built in a future PR
|
||||
expect(wrapper.find(AddDomainFlyout)).toHaveLength(1);
|
||||
|
||||
// TODO test for empty state after it is built in a future PR
|
||||
});
|
||||
|
|
|
@ -9,9 +9,14 @@ import React, { useEffect } from 'react';
|
|||
|
||||
import { useActions, useValues } from 'kea';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle } from '@elastic/eui';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import { getEngineBreadcrumbs } from '../engine';
|
||||
import { AppSearchPageTemplate } from '../layout';
|
||||
|
||||
import { AddDomainFlyout } from './components/add_domain/add_domain_flyout';
|
||||
import { DomainsTable } from './components/domains_table';
|
||||
import { CRAWLER_TITLE } from './constants';
|
||||
import { CrawlerOverviewLogic } from './crawler_overview_logic';
|
||||
|
@ -31,6 +36,21 @@ export const CrawlerOverview: React.FC = () => {
|
|||
pageHeader={{ pageTitle: CRAWLER_TITLE }}
|
||||
isLoading={dataLoading}
|
||||
>
|
||||
<EuiFlexGroup direction="row" alignItems="stretch">
|
||||
<EuiFlexItem>
|
||||
<EuiTitle size="s">
|
||||
<h3>
|
||||
{i18n.translate('xpack.enterpriseSearch.appSearch.crawler.domainsTitle', {
|
||||
defaultMessage: 'Domains',
|
||||
})}
|
||||
</h3>
|
||||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddDomainFlyout />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
<EuiSpacer size="m" />
|
||||
<DomainsTable />
|
||||
</AppSearchPageTemplate>
|
||||
);
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { ENGINE_CRAWLER_PATH } from '../../routes';
|
||||
|
||||
import { CrawlerLanding } from './crawler_landing';
|
||||
import { CrawlerOverview } from './crawler_overview';
|
||||
|
||||
export const CrawlerRouter: React.FC = () => {
|
||||
return (
|
||||
<Switch>
|
||||
<Route>
|
||||
<Route exact path={ENGINE_CRAWLER_PATH}>
|
||||
{process.env.NODE_ENV === 'development' ? <CrawlerOverview /> : <CrawlerLanding />}
|
||||
</Route>
|
||||
</Switch>
|
||||
|
|
|
@ -9,7 +9,7 @@ import '../../__mocks__/kea_logic/kibana_logic.mock';
|
|||
|
||||
import { FlashMessagesLogic } from './flash_messages_logic';
|
||||
|
||||
import { flashAPIErrors } from './handle_api_errors';
|
||||
import { flashAPIErrors, getErrorsFromHttpResponse } from './handle_api_errors';
|
||||
|
||||
describe('flashAPIErrors', () => {
|
||||
const mockHttpError = {
|
||||
|
@ -68,10 +68,29 @@ describe('flashAPIErrors', () => {
|
|||
try {
|
||||
flashAPIErrors(Error('whatever') as any);
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual('whatever');
|
||||
expect(FlashMessagesLogic.actions.setFlashMessages).toHaveBeenCalledWith([
|
||||
{ type: 'error', message: 'An unexpected error occurred' },
|
||||
{ type: 'error', message: expect.any(String) },
|
||||
]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('getErrorsFromHttpResponse', () => {
|
||||
it('should return errors from the response if present', () => {
|
||||
expect(
|
||||
getErrorsFromHttpResponse({
|
||||
body: { attributes: { errors: ['first error', 'second error'] } },
|
||||
} as any)
|
||||
).toEqual(['first error', 'second error']);
|
||||
});
|
||||
|
||||
it('should return a message from the responnse if no errors', () => {
|
||||
expect(getErrorsFromHttpResponse({ body: { message: 'test message' } } as any)).toEqual([
|
||||
'test message',
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return the a default message otherwise', () => {
|
||||
expect(getErrorsFromHttpResponse({} as any)).toEqual([expect.any(String)]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,13 +40,22 @@ export const defaultErrorMessage = i18n.translate(
|
|||
}
|
||||
);
|
||||
|
||||
export const getErrorsFromHttpResponse = (response: HttpResponse<ErrorResponse>) => {
|
||||
return Array.isArray(response?.body?.attributes?.errors)
|
||||
? response.body!.attributes.errors
|
||||
: [response?.body?.message || defaultErrorMessage];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts API/HTTP errors into user-facing Flash Messages
|
||||
*/
|
||||
export const flashAPIErrors = (error: HttpResponse<ErrorResponse>, { isQueued }: Options = {}) => {
|
||||
const errorFlashMessages: IFlashMessage[] = Array.isArray(error?.body?.attributes?.errors)
|
||||
? error.body!.attributes.errors.map((message) => ({ type: 'error', message }))
|
||||
: [{ type: 'error', message: error?.body?.message || defaultErrorMessage }];
|
||||
export const flashAPIErrors = (
|
||||
response: HttpResponse<ErrorResponse>,
|
||||
{ isQueued }: Options = {}
|
||||
) => {
|
||||
const errorFlashMessages: IFlashMessage[] = getErrorsFromHttpResponse(
|
||||
response
|
||||
).map((message) => ({ type: 'error', message }));
|
||||
|
||||
if (isQueued) {
|
||||
FlashMessagesLogic.actions.setQueuedMessages(errorFlashMessages);
|
||||
|
@ -56,7 +65,7 @@ export const flashAPIErrors = (error: HttpResponse<ErrorResponse>, { isQueued }:
|
|||
|
||||
// If this was a programming error or a failed request (such as a CORS) error,
|
||||
// we rethrow the error so it shows up in the developer console
|
||||
if (!error?.body?.message) {
|
||||
throw error;
|
||||
if (!response?.body?.message) {
|
||||
throw response;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -43,6 +43,62 @@ describe('crawler routes', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('POST /api/app_search/engines/{name}/crawler/domains', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockRouter = new MockRouter({
|
||||
method: 'post',
|
||||
path: '/api/app_search/engines/{name}/crawler/domains',
|
||||
});
|
||||
|
||||
registerCrawlerRoutes({
|
||||
...mockDependencies,
|
||||
router: mockRouter.router,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a request to enterprise search', () => {
|
||||
expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({
|
||||
path: '/api/as/v0/engines/:name/crawler/domains',
|
||||
});
|
||||
});
|
||||
|
||||
it('validates correctly with params and body', () => {
|
||||
const request = {
|
||||
params: { name: 'some-engine' },
|
||||
body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('accepts a query param', () => {
|
||||
const request = {
|
||||
params: { name: 'some-engine' },
|
||||
body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
|
||||
query: { respond_with: 'crawler_details' },
|
||||
};
|
||||
mockRouter.shouldValidate(request);
|
||||
});
|
||||
|
||||
it('fails validation without a name param', () => {
|
||||
const request = {
|
||||
params: {},
|
||||
body: { name: 'https://elastic.co/guide', entry_points: [{ value: '/guide' }] },
|
||||
};
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
|
||||
it('fails validation without a body', () => {
|
||||
const request = {
|
||||
params: { name: 'some-engine' },
|
||||
body: {},
|
||||
};
|
||||
mockRouter.shouldThrow(request);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api/app_search/engines/{name}/crawler/domains/{id}', () => {
|
||||
let mockRouter: MockRouter;
|
||||
|
||||
|
|
|
@ -27,6 +27,31 @@ export function registerCrawlerRoutes({
|
|||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
{
|
||||
path: '/api/app_search/engines/{name}/crawler/domains',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
name: schema.string(),
|
||||
}),
|
||||
body: schema.object({
|
||||
name: schema.string(),
|
||||
entry_points: schema.arrayOf(
|
||||
schema.object({
|
||||
value: schema.string(),
|
||||
})
|
||||
),
|
||||
}),
|
||||
query: schema.object({
|
||||
respond_with: schema.maybe(schema.string()),
|
||||
}),
|
||||
},
|
||||
},
|
||||
enterpriseSearchRequestHandler.createRequest({
|
||||
path: '/api/as/v0/engines/:name/crawler/domains',
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
{
|
||||
path: '/api/app_search/engines/{name}/crawler/domains/{id}',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue