Skip to content

Commit

Permalink
[Enterprise Search] Basic DocumentCreation creation mode modal views (#…
Browse files Browse the repository at this point in the history
…86056)

* Add ApiCodeExample modal component

- Previously lived in EngineOverview / Onboarding

* Add basic PasteJsonText component

* Add basic UploadJsonFile component

* [Refactor] Have all modal components manage their own ModalHeader & ModalFooters

- Per feedback from Casey
+ Update DocumentCreationModal to use switch

* Set basic empty/disabled validation on ModalFooter continue buttons

* Update x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx

Co-authored-by: Jason Stoltzfus <jastoltz24@gmail.com>

* [PR feedback] Typescript improvements

* [PR feedback] Remove need for hasFile reducer

- by storing either 1 file or null
- which gets around the stored FileList reference not triggering a rerender/change

Co-authored-by: Jason Stoltzfus <jastoltz24@gmail.com>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 18, 2020
1 parent fc7ae0e commit d73af32
Show file tree
Hide file tree
Showing 15 changed files with 830 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,48 @@
* you may not use this file except in compliance with the Elastic License.
*/

// TODO: This will be used shortly in an upcoming PR
import { i18n } from '@kbn/i18n';

export const MODAL_CANCEL_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.documentCreation.modalCancel',
{ defaultMessage: 'Cancel' }
);
export const MODAL_CONTINUE_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.documentCreation.modalContinue',
{ defaultMessage: 'Continue' }
);

// This is indented the way it is to work with ApiCodeExample.
// Use dedent() when calling this alone
export const DOCUMENTS_API_JSON_EXAMPLE = `[
{
"id": "park_rocky-mountain",
"title": "Rocky Mountain",
"description": "Bisected north to south by the Continental Divide, this portion of the Rockies has ecosystems varying from over 150 riparian lakes to montane and subalpine forests to treeless alpine tundra. Wildlife including mule deer, bighorn sheep, black bears, and cougars inhabit its igneous mountains and glacial valleys. Longs Peak, a classic Colorado fourteener, and the scenic Bear Lake are popular destinations, as well as the historic Trail Ridge Road, which reaches an elevation of more than 12,000 feet (3,700 m).",
"nps_link": "https://www.nps.gov/romo/index.htm",
"states": [
"Colorado"
],
"visitors": 4517585,
"world_heritage_site": false,
"location": "40.4,-105.58",
"acres": 265795.2,
"square_km": 1075.6,
"date_established": "1915-01-26T06:00:00Z"
},
{
"id": "park_saguaro",
"title": "Saguaro",
"description": "Split into the separate Rincon Mountain and Tucson Mountain districts, this park is evidence that the dry Sonoran Desert is still home to a great variety of life spanning six biotic communities. Beyond the namesake giant saguaro cacti, there are barrel cacti, chollas, and prickly pears, as well as lesser long-nosed bats, spotted owls, and javelinas.",
"nps_link": "https://www.nps.gov/sagu/index.htm",
"states": [
"Arizona"
],
"visitors": 820426,
"world_heritage_site": false,
"location": "32.25,-110.5",
"acres": 91715.72,
"square_km": 371.2,
"date_established": "1994-10-14T05:00:00Z"
}
]`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* 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 '../../../../__mocks__/enterprise_search_url.mock';
import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';

import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme';
import { EuiCode, EuiCodeBlock, EuiButtonEmpty } from '@elastic/eui';

import { ApiCodeExample, ModalHeader, ModalBody, ModalFooter } from './api_code_example';

describe('ApiCodeExample', () => {
const values = {
engineName: 'test-engine',
engine: { apiKey: 'test-key' },
};
const actions = {
closeDocumentCreation: jest.fn(),
};

beforeAll(() => {
setMockValues(values);
setMockActions(actions);
});

it('renders', () => {
const wrapper = shallow(<ApiCodeExample />);
expect(wrapper.find(ModalHeader)).toHaveLength(1);
expect(wrapper.find(ModalBody)).toHaveLength(1);
expect(wrapper.find(ModalFooter)).toHaveLength(1);
});

describe('ModalHeader', () => {
it('renders', () => {
const wrapper = shallow(<ModalHeader />);
expect(wrapper.find('h2').text()).toEqual('Indexing by API');
});
});

describe('ModalBody', () => {
let wrapper: ShallowWrapper;

beforeAll(() => {
wrapper = shallow(<ModalBody />);
});

it('renders with the full remote Enterprise Search API URL', () => {
expect(wrapper.find(EuiCode).dive().dive().text()).toEqual(
'http://localhost:3002/api/as/v1/engines/test-engine/documents'
);
expect(wrapper.find(EuiCodeBlock).dive().dive().text()).toEqual(
expect.stringContaining('http://localhost:3002/api/as/v1/engines/test-engine/documents')
);
});

it('renders with the API key', () => {
expect(wrapper.find(EuiCodeBlock).dive().dive().text()).toEqual(
expect.stringContaining('test-key')
);
});
});

describe('ModalFooter', () => {
it('closes the modal', () => {
const wrapper = shallow(<ModalFooter />);

wrapper.find(EuiButtonEmpty).simulate('click');
expect(actions.closeDocumentCreation).toHaveBeenCalled();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* 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 dedent from 'dedent';
import React from 'react';
import { useValues, useActions } from 'kea';

import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiModalHeader,
EuiModalHeaderTitle,
EuiModalBody,
EuiModalFooter,
EuiButtonEmpty,
EuiText,
EuiLink,
EuiSpacer,
EuiPanel,
EuiBadge,
EuiCode,
EuiCodeBlock,
} from '@elastic/eui';

import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url';
import { EngineLogic } from '../../engine';
import { EngineDetails } from '../../engine/types';

import { DOCS_PREFIX } from '../../../routes';
import { DOCUMENTS_API_JSON_EXAMPLE, MODAL_CANCEL_BUTTON } from '../constants';
import { DocumentCreationLogic } from '../';

export const ApiCodeExample: React.FC = () => (
<>
<ModalHeader />
<ModalBody />
<ModalFooter />
</>
);

export const ModalHeader: React.FC = () => {
return (
<EuiModalHeader>
<EuiModalHeaderTitle>
<h2>
{i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.api.title', {
defaultMessage: 'Indexing by API',
})}
</h2>
</EuiModalHeaderTitle>
</EuiModalHeader>
);
};

export const ModalBody: React.FC = () => {
const { engineName, engine } = useValues(EngineLogic);
const { apiKey } = engine as EngineDetails;

const documentsApiUrl = getEnterpriseSearchUrl(`/api/as/v1/engines/${engineName}/documents`);

return (
<EuiModalBody>
<EuiText color="subdued">
<p>
<FormattedMessage
id="xpack.enterpriseSearch.appSearch.documentCreation.api.description"
defaultMessage="The {documentsApiLink} can be used to add new documents to your engine, update documents, retrieve documents by id, and delete documents. There are a variety of {clientLibrariesLink} to help you get started."
values={{
documentsApiLink: (
<EuiLink target="_blank" href={`${DOCS_PREFIX}/indexing-documents-guide.html`}>
documents API
</EuiLink>
),
clientLibrariesLink: (
<EuiLink target="_blank" href={`${DOCS_PREFIX}/api-clients.html`}>
client libraries
</EuiLink>
),
}}
/>
</p>
<p>
{i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.api.example', {
defaultMessage:
'To see the API in action, you can experiment with the example request below using a command line or a client library.',
})}
</p>
</EuiText>
<EuiSpacer />
<EuiPanel hasShadow={false} paddingSize="s" className="eui-textBreakAll">
<EuiBadge color="primary">POST</EuiBadge>
<EuiCode transparentBackground>{documentsApiUrl}</EuiCode>
</EuiPanel>
<EuiCodeBlock language="bash" fontSize="m" isCopyable>
{dedent(`
curl -X POST '${documentsApiUrl}'
-H 'Content-Type: application/json'
-H 'Authorization: Bearer ${apiKey}'
-d '${DOCUMENTS_API_JSON_EXAMPLE}'
# Returns
# [
# {
# "id": "park_rocky-mountain",
# "errors": []
# },
# {
# "id": "park_saguaro",
# "errors": []
# }
# ]
`)}
</EuiCodeBlock>
</EuiModalBody>
);
};

export const ModalFooter: React.FC = () => {
const { closeDocumentCreation } = useActions(DocumentCreationLogic);

return (
<EuiModalFooter>
<EuiButtonEmpty onClick={closeDocumentCreation}>{MODAL_CANCEL_BUTTON}</EuiButtonEmpty>
</EuiModalFooter>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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 { ShowCreationModes } from './show_creation_modes';
export { ApiCodeExample } from './api_code_example';
export { PasteJsonText } from './paste_json_text';
export { UploadJsonFile } from './upload_json_file';
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* 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.
*/

.pasteJsonTextArea {
font-family: $euiCodeFontFamily;
}
Original file line number Diff line number Diff line change
@@ -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 { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';
import { rerender } from '../../../../__mocks__';

import React from 'react';
import { shallow } from 'enzyme';
import { EuiTextArea, EuiButtonEmpty, EuiButton } from '@elastic/eui';

import { PasteJsonText, ModalHeader, ModalBody, ModalFooter } from './paste_json_text';

describe('PasteJsonText', () => {
const values = {
textInput: 'hello world',
configuredLimits: {
engine: {
maxDocumentByteSize: 102400,
},
},
};
const actions = {
setTextInput: jest.fn(),
closeDocumentCreation: jest.fn(),
};

beforeEach(() => {
jest.clearAllMocks();
setMockValues(values);
setMockActions(actions);
});

it('renders', () => {
const wrapper = shallow(<PasteJsonText />);
expect(wrapper.find(ModalHeader)).toHaveLength(1);
expect(wrapper.find(ModalBody)).toHaveLength(1);
expect(wrapper.find(ModalFooter)).toHaveLength(1);
});

describe('ModalHeader', () => {
it('renders', () => {
const wrapper = shallow(<ModalHeader />);
expect(wrapper.find('h2').text()).toEqual('Create documents');
});
});

describe('ModalBody', () => {
it('renders and updates the textarea value', () => {
setMockValues({ ...values, textInput: 'lorem ipsum' });
const wrapper = shallow(<ModalBody />);
const textarea = wrapper.find(EuiTextArea);

expect(textarea.prop('value')).toEqual('lorem ipsum');

textarea.simulate('change', { target: { value: 'dolor sit amet' } });
expect(actions.setTextInput).toHaveBeenCalledWith('dolor sit amet');
});
});

describe('ModalFooter', () => {
it('closes the modal', () => {
const wrapper = shallow(<ModalFooter />);

wrapper.find(EuiButtonEmpty).simulate('click');
expect(actions.closeDocumentCreation).toHaveBeenCalled();
});

it('disables/enables the Continue button based on whether text has been entered', () => {
const wrapper = shallow(<ModalFooter />);
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(false);

setMockValues({ ...values, textInput: '' });
rerender(wrapper);
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true);
});
});
});
Loading

0 comments on commit d73af32

Please sign in to comment.