Skip to content

Commit d73af32

Browse files
ConstanceJasonStoltzkibanamachine
authored
[Enterprise Search] Basic DocumentCreation creation mode modal views (#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 <[email protected]> * [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 <[email protected]> Co-authored-by: Kibana Machine <[email protected]>
1 parent fc7ae0e commit d73af32

15 files changed

+830
-59
lines changed

x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,48 @@
44
* you may not use this file except in compliance with the Elastic License.
55
*/
66

7-
// TODO: This will be used shortly in an upcoming PR
7+
import { i18n } from '@kbn/i18n';
8+
9+
export const MODAL_CANCEL_BUTTON = i18n.translate(
10+
'xpack.enterpriseSearch.appSearch.documentCreation.modalCancel',
11+
{ defaultMessage: 'Cancel' }
12+
);
13+
export const MODAL_CONTINUE_BUTTON = i18n.translate(
14+
'xpack.enterpriseSearch.appSearch.documentCreation.modalContinue',
15+
{ defaultMessage: 'Continue' }
16+
);
17+
18+
// This is indented the way it is to work with ApiCodeExample.
19+
// Use dedent() when calling this alone
20+
export const DOCUMENTS_API_JSON_EXAMPLE = `[
21+
{
22+
"id": "park_rocky-mountain",
23+
"title": "Rocky Mountain",
24+
"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).",
25+
"nps_link": "https://www.nps.gov/romo/index.htm",
26+
"states": [
27+
"Colorado"
28+
],
29+
"visitors": 4517585,
30+
"world_heritage_site": false,
31+
"location": "40.4,-105.58",
32+
"acres": 265795.2,
33+
"square_km": 1075.6,
34+
"date_established": "1915-01-26T06:00:00Z"
35+
},
36+
{
37+
"id": "park_saguaro",
38+
"title": "Saguaro",
39+
"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.",
40+
"nps_link": "https://www.nps.gov/sagu/index.htm",
41+
"states": [
42+
"Arizona"
43+
],
44+
"visitors": 820426,
45+
"world_heritage_site": false,
46+
"location": "32.25,-110.5",
47+
"acres": 91715.72,
48+
"square_km": 371.2,
49+
"date_established": "1994-10-14T05:00:00Z"
50+
}
51+
]`;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import '../../../../__mocks__/enterprise_search_url.mock';
8+
import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';
9+
10+
import React from 'react';
11+
import { shallow, ShallowWrapper } from 'enzyme';
12+
import { EuiCode, EuiCodeBlock, EuiButtonEmpty } from '@elastic/eui';
13+
14+
import { ApiCodeExample, ModalHeader, ModalBody, ModalFooter } from './api_code_example';
15+
16+
describe('ApiCodeExample', () => {
17+
const values = {
18+
engineName: 'test-engine',
19+
engine: { apiKey: 'test-key' },
20+
};
21+
const actions = {
22+
closeDocumentCreation: jest.fn(),
23+
};
24+
25+
beforeAll(() => {
26+
setMockValues(values);
27+
setMockActions(actions);
28+
});
29+
30+
it('renders', () => {
31+
const wrapper = shallow(<ApiCodeExample />);
32+
expect(wrapper.find(ModalHeader)).toHaveLength(1);
33+
expect(wrapper.find(ModalBody)).toHaveLength(1);
34+
expect(wrapper.find(ModalFooter)).toHaveLength(1);
35+
});
36+
37+
describe('ModalHeader', () => {
38+
it('renders', () => {
39+
const wrapper = shallow(<ModalHeader />);
40+
expect(wrapper.find('h2').text()).toEqual('Indexing by API');
41+
});
42+
});
43+
44+
describe('ModalBody', () => {
45+
let wrapper: ShallowWrapper;
46+
47+
beforeAll(() => {
48+
wrapper = shallow(<ModalBody />);
49+
});
50+
51+
it('renders with the full remote Enterprise Search API URL', () => {
52+
expect(wrapper.find(EuiCode).dive().dive().text()).toEqual(
53+
'http://localhost:3002/api/as/v1/engines/test-engine/documents'
54+
);
55+
expect(wrapper.find(EuiCodeBlock).dive().dive().text()).toEqual(
56+
expect.stringContaining('http://localhost:3002/api/as/v1/engines/test-engine/documents')
57+
);
58+
});
59+
60+
it('renders with the API key', () => {
61+
expect(wrapper.find(EuiCodeBlock).dive().dive().text()).toEqual(
62+
expect.stringContaining('test-key')
63+
);
64+
});
65+
});
66+
67+
describe('ModalFooter', () => {
68+
it('closes the modal', () => {
69+
const wrapper = shallow(<ModalFooter />);
70+
71+
wrapper.find(EuiButtonEmpty).simulate('click');
72+
expect(actions.closeDocumentCreation).toHaveBeenCalled();
73+
});
74+
});
75+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import dedent from 'dedent';
8+
import React from 'react';
9+
import { useValues, useActions } from 'kea';
10+
11+
import { i18n } from '@kbn/i18n';
12+
import { FormattedMessage } from '@kbn/i18n/react';
13+
import {
14+
EuiModalHeader,
15+
EuiModalHeaderTitle,
16+
EuiModalBody,
17+
EuiModalFooter,
18+
EuiButtonEmpty,
19+
EuiText,
20+
EuiLink,
21+
EuiSpacer,
22+
EuiPanel,
23+
EuiBadge,
24+
EuiCode,
25+
EuiCodeBlock,
26+
} from '@elastic/eui';
27+
28+
import { getEnterpriseSearchUrl } from '../../../../shared/enterprise_search_url';
29+
import { EngineLogic } from '../../engine';
30+
import { EngineDetails } from '../../engine/types';
31+
32+
import { DOCS_PREFIX } from '../../../routes';
33+
import { DOCUMENTS_API_JSON_EXAMPLE, MODAL_CANCEL_BUTTON } from '../constants';
34+
import { DocumentCreationLogic } from '../';
35+
36+
export const ApiCodeExample: React.FC = () => (
37+
<>
38+
<ModalHeader />
39+
<ModalBody />
40+
<ModalFooter />
41+
</>
42+
);
43+
44+
export const ModalHeader: React.FC = () => {
45+
return (
46+
<EuiModalHeader>
47+
<EuiModalHeaderTitle>
48+
<h2>
49+
{i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.api.title', {
50+
defaultMessage: 'Indexing by API',
51+
})}
52+
</h2>
53+
</EuiModalHeaderTitle>
54+
</EuiModalHeader>
55+
);
56+
};
57+
58+
export const ModalBody: React.FC = () => {
59+
const { engineName, engine } = useValues(EngineLogic);
60+
const { apiKey } = engine as EngineDetails;
61+
62+
const documentsApiUrl = getEnterpriseSearchUrl(`/api/as/v1/engines/${engineName}/documents`);
63+
64+
return (
65+
<EuiModalBody>
66+
<EuiText color="subdued">
67+
<p>
68+
<FormattedMessage
69+
id="xpack.enterpriseSearch.appSearch.documentCreation.api.description"
70+
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."
71+
values={{
72+
documentsApiLink: (
73+
<EuiLink target="_blank" href={`${DOCS_PREFIX}/indexing-documents-guide.html`}>
74+
documents API
75+
</EuiLink>
76+
),
77+
clientLibrariesLink: (
78+
<EuiLink target="_blank" href={`${DOCS_PREFIX}/api-clients.html`}>
79+
client libraries
80+
</EuiLink>
81+
),
82+
}}
83+
/>
84+
</p>
85+
<p>
86+
{i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.api.example', {
87+
defaultMessage:
88+
'To see the API in action, you can experiment with the example request below using a command line or a client library.',
89+
})}
90+
</p>
91+
</EuiText>
92+
<EuiSpacer />
93+
<EuiPanel hasShadow={false} paddingSize="s" className="eui-textBreakAll">
94+
<EuiBadge color="primary">POST</EuiBadge>
95+
<EuiCode transparentBackground>{documentsApiUrl}</EuiCode>
96+
</EuiPanel>
97+
<EuiCodeBlock language="bash" fontSize="m" isCopyable>
98+
{dedent(`
99+
curl -X POST '${documentsApiUrl}'
100+
-H 'Content-Type: application/json'
101+
-H 'Authorization: Bearer ${apiKey}'
102+
-d '${DOCUMENTS_API_JSON_EXAMPLE}'
103+
# Returns
104+
# [
105+
# {
106+
# "id": "park_rocky-mountain",
107+
# "errors": []
108+
# },
109+
# {
110+
# "id": "park_saguaro",
111+
# "errors": []
112+
# }
113+
# ]
114+
`)}
115+
</EuiCodeBlock>
116+
</EuiModalBody>
117+
);
118+
};
119+
120+
export const ModalFooter: React.FC = () => {
121+
const { closeDocumentCreation } = useActions(DocumentCreationLogic);
122+
123+
return (
124+
<EuiModalFooter>
125+
<EuiButtonEmpty onClick={closeDocumentCreation}>{MODAL_CANCEL_BUTTON}</EuiButtonEmpty>
126+
</EuiModalFooter>
127+
);
128+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
export { ShowCreationModes } from './show_creation_modes';
8+
export { ApiCodeExample } from './api_code_example';
9+
export { PasteJsonText } from './paste_json_text';
10+
export { UploadJsonFile } from './upload_json_file';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
.pasteJsonTextArea {
8+
font-family: $euiCodeFontFamily;
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { setMockValues, setMockActions } from '../../../../__mocks__/kea.mock';
8+
import { rerender } from '../../../../__mocks__';
9+
10+
import React from 'react';
11+
import { shallow } from 'enzyme';
12+
import { EuiTextArea, EuiButtonEmpty, EuiButton } from '@elastic/eui';
13+
14+
import { PasteJsonText, ModalHeader, ModalBody, ModalFooter } from './paste_json_text';
15+
16+
describe('PasteJsonText', () => {
17+
const values = {
18+
textInput: 'hello world',
19+
configuredLimits: {
20+
engine: {
21+
maxDocumentByteSize: 102400,
22+
},
23+
},
24+
};
25+
const actions = {
26+
setTextInput: jest.fn(),
27+
closeDocumentCreation: jest.fn(),
28+
};
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
setMockValues(values);
33+
setMockActions(actions);
34+
});
35+
36+
it('renders', () => {
37+
const wrapper = shallow(<PasteJsonText />);
38+
expect(wrapper.find(ModalHeader)).toHaveLength(1);
39+
expect(wrapper.find(ModalBody)).toHaveLength(1);
40+
expect(wrapper.find(ModalFooter)).toHaveLength(1);
41+
});
42+
43+
describe('ModalHeader', () => {
44+
it('renders', () => {
45+
const wrapper = shallow(<ModalHeader />);
46+
expect(wrapper.find('h2').text()).toEqual('Create documents');
47+
});
48+
});
49+
50+
describe('ModalBody', () => {
51+
it('renders and updates the textarea value', () => {
52+
setMockValues({ ...values, textInput: 'lorem ipsum' });
53+
const wrapper = shallow(<ModalBody />);
54+
const textarea = wrapper.find(EuiTextArea);
55+
56+
expect(textarea.prop('value')).toEqual('lorem ipsum');
57+
58+
textarea.simulate('change', { target: { value: 'dolor sit amet' } });
59+
expect(actions.setTextInput).toHaveBeenCalledWith('dolor sit amet');
60+
});
61+
});
62+
63+
describe('ModalFooter', () => {
64+
it('closes the modal', () => {
65+
const wrapper = shallow(<ModalFooter />);
66+
67+
wrapper.find(EuiButtonEmpty).simulate('click');
68+
expect(actions.closeDocumentCreation).toHaveBeenCalled();
69+
});
70+
71+
it('disables/enables the Continue button based on whether text has been entered', () => {
72+
const wrapper = shallow(<ModalFooter />);
73+
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(false);
74+
75+
setMockValues({ ...values, textInput: '' });
76+
rerender(wrapper);
77+
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true);
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)