From d73af3282f6cf9c9f8520999982c5085db4a7cb4 Mon Sep 17 00:00:00 2001 From: Constance Date: Fri, 18 Dec 2020 10:16:27 -0800 Subject: [PATCH] [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 * [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 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../document_creation/constants.tsx | 46 ++++++- .../api_code_example.test.tsx | 75 ++++++++++ .../api_code_example.tsx | 128 ++++++++++++++++++ .../creation_mode_components/index.ts | 10 ++ .../paste_json_text.scss | 9 ++ .../paste_json_text.test.tsx | 80 +++++++++++ .../paste_json_text.tsx | 101 ++++++++++++++ .../show_creation_modes.test.tsx | 37 +++++ .../show_creation_modes.tsx | 45 ++++++ .../upload_json_file.test.tsx | 85 ++++++++++++ .../upload_json_file.tsx | 98 ++++++++++++++ .../document_creation_logic.test.ts | 33 +++++ .../document_creation_logic.ts | 20 +++ .../document_creation_modal.test.tsx | 45 +++--- .../document_creation_modal.tsx | 77 ++++++----- 15 files changed, 830 insertions(+), 59 deletions(-) create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/index.ts create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.scss create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx create mode 100644 x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx index 5406a90a75a35..c4237da0d0e80 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/constants.tsx @@ -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" + } + ]`; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx new file mode 100644 index 0000000000000..2dd46419528c1 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.test.tsx @@ -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(); + 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(); + expect(wrapper.find('h2').text()).toEqual('Indexing by API'); + }); + }); + + describe('ModalBody', () => { + let wrapper: ShallowWrapper; + + beforeAll(() => { + wrapper = shallow(); + }); + + 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(); + + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(actions.closeDocumentCreation).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx new file mode 100644 index 0000000000000..1dd57ffe8bc01 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/api_code_example.tsx @@ -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 = () => ( + <> + + + + +); + +export const ModalHeader: React.FC = () => { + return ( + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.api.title', { + defaultMessage: 'Indexing by API', + })} +

+
+
+ ); +}; + +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 ( + + +

+ + documents API + + ), + clientLibrariesLink: ( + + client libraries + + ), + }} + /> +

+

+ {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.', + })} +

+
+ + + POST + {documentsApiUrl} + + + {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": [] + # } + # ] + `)} + +
+ ); +}; + +export const ModalFooter: React.FC = () => { + const { closeDocumentCreation } = useActions(DocumentCreationLogic); + + return ( + + {MODAL_CANCEL_BUTTON} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/index.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/index.ts new file mode 100644 index 0000000000000..b9a6f2b3e750f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/index.ts @@ -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'; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.scss b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.scss new file mode 100644 index 0000000000000..cca179e8c0608 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.scss @@ -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; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx new file mode 100644 index 0000000000000..ede1529c049d7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.test.tsx @@ -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(); + 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(); + 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(); + 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(); + + 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(); + expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(false); + + setMockValues({ ...values, textInput: '' }); + rerender(wrapper); + expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx new file mode 100644 index 0000000000000..614704701222b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/paste_json_text.tsx @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues, useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiTextArea, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { AppLogic } from '../../../app_logic'; + +import { MODAL_CANCEL_BUTTON, MODAL_CONTINUE_BUTTON } from '../constants'; +import { DocumentCreationLogic } from '../'; + +import './paste_json_text.scss'; + +export const PasteJsonText: React.FC = () => ( + <> + + + + +); + +export const ModalHeader: React.FC = () => { + return ( + + +

+ {i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.pasteJsonText.title', { + defaultMessage: 'Create documents', + })} +

+
+
+ ); +}; + +export const ModalBody: React.FC = () => { + const { configuredLimits } = useValues(AppLogic); + const maxDocumentByteSize = configuredLimits?.engine?.maxDocumentByteSize; + + const { textInput } = useValues(DocumentCreationLogic); + const { setTextInput } = useActions(DocumentCreationLogic); + + return ( + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.pasteJsonText.description', + { + defaultMessage: + 'Paste an array of JSON documents. Ensure the JSON is valid and that each document object is less than {maxDocumentByteSize} bytes.', + values: { maxDocumentByteSize }, + } + )} +

+
+ + setTextInput(e.target.value)} + aria-label={i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.pasteJsonText.label', + { defaultMessage: 'Paste JSON here' } + )} + className="pasteJsonTextArea" + fullWidth + rows={12} + /> +
+ ); +}; + +export const ModalFooter: React.FC = () => { + const { textInput } = useValues(DocumentCreationLogic); + const { closeDocumentCreation } = useActions(DocumentCreationLogic); + + return ( + + {MODAL_CANCEL_BUTTON} + + {MODAL_CONTINUE_BUTTON} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx new file mode 100644 index 0000000000000..eadcf6df473e5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { setMockActions } from '../../../../__mocks__/kea.mock'; + +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { EuiButtonEmpty } from '@elastic/eui'; + +import { DocumentCreationButtons } from '../'; +import { ShowCreationModes } from './'; + +describe('ShowCreationModes', () => { + const actions = { + closeDocumentCreation: jest.fn(), + }; + let wrapper: ShallowWrapper; + + beforeAll(() => { + jest.clearAllMocks(); + setMockActions(actions); + wrapper = shallow(); + }); + + it('renders', () => { + expect(wrapper.find('h2').text()).toEqual('Add new documents'); + expect(wrapper.find(DocumentCreationButtons)).toHaveLength(1); + }); + + it('closes the modal', () => { + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(actions.closeDocumentCreation).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx new file mode 100644 index 0000000000000..1f7c4db83ab06 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/show_creation_modes.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButtonEmpty, +} from '@elastic/eui'; + +import { MODAL_CANCEL_BUTTON } from '../constants'; +import { DocumentCreationLogic, DocumentCreationButtons } from '../'; + +export const ShowCreationModes: React.FC = () => { + const { closeDocumentCreation } = useActions(DocumentCreationLogic); + + return ( + <> + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.showCreationModes.title', + { defaultMessage: 'Add new documents' } + )} +

+
+
+ + + + + {MODAL_CANCEL_BUTTON} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx new file mode 100644 index 0000000000000..dae085617cad8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.test.tsx @@ -0,0 +1,85 @@ +/* + * 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. + */ +/* + * 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 { EuiFilePicker, EuiButtonEmpty, EuiButton } from '@elastic/eui'; + +import { UploadJsonFile, ModalHeader, ModalBody, ModalFooter } from './upload_json_file'; + +describe('UploadJsonFile', () => { + const mockFile = new File(['mock'], 'mock.json', { type: 'application/json' }); + const values = { + fileInput: null, + configuredLimits: { + engine: { + maxDocumentByteSize: 102400, + }, + }, + }; + const actions = { + setFileInput: jest.fn(), + closeDocumentCreation: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions(actions); + }); + + it('renders', () => { + const wrapper = shallow(); + 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(); + expect(wrapper.find('h2').text()).toEqual('Drag and drop .json'); + }); + }); + + describe('ModalBody', () => { + it('updates fileInput when files are added & removed', () => { + const wrapper = shallow(); + + wrapper.find(EuiFilePicker).simulate('change', [mockFile]); + expect(actions.setFileInput).toHaveBeenCalledWith(mockFile); + + wrapper.find(EuiFilePicker).simulate('change', []); + expect(actions.setFileInput).toHaveBeenCalledWith(null); + }); + }); + + describe('ModalFooter', () => { + it('closes the modal', () => { + const wrapper = shallow(); + + wrapper.find(EuiButtonEmpty).simulate('click'); + expect(actions.closeDocumentCreation).toHaveBeenCalled(); + }); + + it('disables/enables the Continue button based on whether files have been uploaded', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true); + + setMockValues({ ...values, fineInput: mockFile }); + rerender(wrapper); + expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx new file mode 100644 index 0000000000000..d4c005d5cfa2b --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/creation_mode_components/upload_json_file.tsx @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useValues, useActions } from 'kea'; + +import { i18n } from '@kbn/i18n'; +import { + EuiModalHeader, + EuiModalHeaderTitle, + EuiModalBody, + EuiModalFooter, + EuiButton, + EuiButtonEmpty, + EuiFilePicker, + EuiSpacer, + EuiText, +} from '@elastic/eui'; + +import { AppLogic } from '../../../app_logic'; + +import { MODAL_CANCEL_BUTTON, MODAL_CONTINUE_BUTTON } from '../constants'; +import { DocumentCreationLogic } from '../'; + +export const UploadJsonFile: React.FC = () => ( + <> + + + + +); + +export const ModalHeader: React.FC = () => { + return ( + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.uploadJsonFile.title', + { defaultMessage: 'Drag and drop .json' } + )} +

+
+
+ ); +}; + +export const ModalBody: React.FC = () => { + const { configuredLimits } = useValues(AppLogic); + const maxDocumentByteSize = configuredLimits?.engine?.maxDocumentByteSize; + + const { setFileInput } = useActions(DocumentCreationLogic); + + return ( + + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.documentCreation.uploadJsonFile.label', + { + defaultMessage: + 'If you have a .json file, drag and drop or upload it. Ensure the JSON is valid and that each document object is less than {maxDocumentByteSize} bytes.', + values: { maxDocumentByteSize }, + } + )} +

+
+ + setFileInput(files?.length ? files[0] : null)} + accept="application/json" + fullWidth + /> +
+ ); +}; + +export const ModalFooter: React.FC = () => { + const { fileInput } = useValues(DocumentCreationLogic); + const { closeDocumentCreation } = useActions(DocumentCreationLogic); + + return ( + + {MODAL_CANCEL_BUTTON} + + {MODAL_CONTINUE_BUTTON} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index ff38ab5add367..1145d7853cb1a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -5,7 +5,9 @@ */ import { resetContext } from 'kea'; +import dedent from 'dedent'; +import { DOCUMENTS_API_JSON_EXAMPLE } from './constants'; import { DocumentCreationStep } from './types'; import { DocumentCreationLogic } from './'; @@ -14,7 +16,10 @@ describe('DocumentCreationLogic', () => { isDocumentCreationOpen: false, creationMode: 'text', creationStep: DocumentCreationStep.AddDocuments, + textInput: dedent(DOCUMENTS_API_JSON_EXAMPLE), + fileInput: null, }; + const mockFile = new File(['mockFile'], 'mockFile.json'); const mount = () => { resetContext({}); @@ -130,5 +135,33 @@ describe('DocumentCreationLogic', () => { }); }); }); + + describe('setTextInput', () => { + describe('textInput', () => { + it('should be set to the provided value', () => { + mount(); + DocumentCreationLogic.actions.setTextInput('hello world'); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + textInput: 'hello world', + }); + }); + }); + }); + + describe('setFileInput', () => { + describe('fileInput', () => { + it('should be set to the provided value', () => { + mount(); + DocumentCreationLogic.actions.setFileInput(mockFile); + + expect(DocumentCreationLogic.values).toEqual({ + ...DEFAULT_VALUES, + fileInput: mockFile, + }); + }); + }); + }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts index 26f7a1f3d50ec..a5e015391d8fd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.ts @@ -5,13 +5,17 @@ */ import { kea, MakeLogicType } from 'kea'; +import dedent from 'dedent'; +import { DOCUMENTS_API_JSON_EXAMPLE } from './constants'; import { DocumentCreationMode, DocumentCreationStep } from './types'; interface DocumentCreationValues { isDocumentCreationOpen: boolean; creationMode: DocumentCreationMode; creationStep: DocumentCreationStep; + textInput: string; + fileInput: File | null; } interface DocumentCreationActions { @@ -19,6 +23,8 @@ interface DocumentCreationActions { openDocumentCreation(creationMode: DocumentCreationMode): { creationMode: DocumentCreationMode }; closeDocumentCreation(): void; setCreationStep(creationStep: DocumentCreationStep): { creationStep: DocumentCreationStep }; + setTextInput(textInput: string): { textInput: string }; + setFileInput(fileInput: File | null): { fileInput: File | null }; } export const DocumentCreationLogic = kea< @@ -30,6 +36,8 @@ export const DocumentCreationLogic = kea< openDocumentCreation: (creationMode) => ({ creationMode }), closeDocumentCreation: () => null, setCreationStep: (creationStep) => ({ creationStep }), + setTextInput: (textInput) => ({ textInput }), + setFileInput: (fileInput) => ({ fileInput }), }), reducers: () => ({ isDocumentCreationOpen: [ @@ -54,5 +62,17 @@ export const DocumentCreationLogic = kea< setCreationStep: (_, { creationStep }) => creationStep, }, ], + textInput: [ + dedent(DOCUMENTS_API_JSON_EXAMPLE), + { + setTextInput: (_, { textInput }) => textInput, + }, + ], + fileInput: [ + null, + { + setFileInput: (_, { fileInput }) => fileInput, + }, + ], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.test.tsx index a00aed96a6fbc..a0bca62dc7419 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.test.tsx @@ -8,10 +8,17 @@ import { setMockValues, setMockActions } from '../../../__mocks__/kea.mock'; import React from 'react'; import { shallow } from 'enzyme'; -import { EuiModal, EuiModalBody } from '@elastic/eui'; - +import { EuiModal } from '@elastic/eui'; + +import { + ShowCreationModes, + ApiCodeExample, + PasteJsonText, + UploadJsonFile, +} from './creation_mode_components'; import { DocumentCreationStep } from './types'; -import { DocumentCreationModal, DocumentCreationButtons } from './'; + +import { DocumentCreationModal, ModalContent } from './document_creation_modal'; describe('DocumentCreationModal', () => { const values = { @@ -44,58 +51,58 @@ describe('DocumentCreationModal', () => { expect(wrapper.isEmptyRender()).toBe(true); }); - describe('modal content', () => { - it('renders document creation mode buttons', () => { + describe('ModalContent', () => { + it('renders ShowCreationModes', () => { setMockValues({ ...values, creationStep: DocumentCreationStep.ShowCreationModes }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(DocumentCreationButtons)).toHaveLength(1); + expect(wrapper.find(ShowCreationModes)).toHaveLength(1); }); describe('creation modes', () => { it('renders ApiCodeExample', () => { setMockValues({ ...values, creationMode: 'api' }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(EuiModalBody).dive().text()).toBe('ApiCodeExample'); // TODO: actual component + expect(wrapper.find(ApiCodeExample)).toHaveLength(1); }); it('renders PasteJsonText', () => { setMockValues({ ...values, creationMode: 'text' }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(EuiModalBody).dive().text()).toBe('PasteJsonText'); // TODO: actual component + expect(wrapper.find(PasteJsonText)).toHaveLength(1); }); it('renders UploadJsonFile', () => { setMockValues({ ...values, creationMode: 'file' }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(EuiModalBody).dive().text()).toBe('UploadJsonFile'); // TODO: actual component + expect(wrapper.find(UploadJsonFile)).toHaveLength(1); }); }); describe('creation steps', () => { it('renders an error page', () => { setMockValues({ ...values, creationStep: DocumentCreationStep.ShowError }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(EuiModalBody).dive().text()).toBe('DocumentCreationError'); // TODO: actual component + expect(wrapper.text()).toBe('DocumentCreationError'); // TODO: actual component }); it('renders an error summary', () => { setMockValues({ ...values, creationStep: DocumentCreationStep.ShowErrorSummary }); - const wrapper = shallow(); + const wrapper = shallow(); - expect(wrapper.find(EuiModalBody).dive().text()).toBe('DocumentCreationSummary'); // TODO: actual component + expect(wrapper.text()).toBe('DocumentCreationSummary'); // TODO: actual component }); it('renders a success summary', () => { setMockValues({ ...values, creationStep: DocumentCreationStep.ShowSuccessSummary }); - const wrapper = shallow(); + const wrapper = shallow(); // TODO: Figure out if the error and success summary should remain the same vs different components - expect(wrapper.find(EuiModalBody).dive().text()).toBe('DocumentCreationSummary'); // TODO: actual component + expect(wrapper.text()).toBe('DocumentCreationSummary'); // TODO: actual component }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.tsx index 95ce5456ef9a8..e6662a7c30407 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_modal.tsx @@ -7,52 +7,51 @@ import React from 'react'; import { useValues, useActions } from 'kea'; -import { i18n } from '@kbn/i18n'; -import { - EuiOverlayMask, - EuiModal, - EuiModalHeader, - EuiModalHeaderTitle, - EuiModalBody, - EuiModalFooter, -} from '@elastic/eui'; - -import { DocumentCreationLogic, DocumentCreationButtons } from './'; +import { EuiOverlayMask, EuiModal } from '@elastic/eui'; + +import { DocumentCreationLogic } from './'; import { DocumentCreationStep } from './types'; +import { + ShowCreationModes, + ApiCodeExample, + PasteJsonText, + UploadJsonFile, +} from './creation_mode_components'; + export const DocumentCreationModal: React.FC = () => { const { closeDocumentCreation } = useActions(DocumentCreationLogic); - const { isDocumentCreationOpen, creationMode, creationStep } = useValues(DocumentCreationLogic); + const { isDocumentCreationOpen } = useValues(DocumentCreationLogic); - if (!isDocumentCreationOpen) return null; - - return ( + return isDocumentCreationOpen ? ( - - - {i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.modalTitle', { - defaultMessage: 'Document Import', - })} - - - - {creationStep === DocumentCreationStep.ShowError && <>DocumentCreationError} - {creationStep === DocumentCreationStep.ShowCreationModes && } - {creationStep === DocumentCreationStep.AddDocuments && creationMode === 'api' && ( - <>ApiCodeExample - )} - {creationStep === DocumentCreationStep.AddDocuments && creationMode === 'text' && ( - <>PasteJsonText - )} - {creationStep === DocumentCreationStep.AddDocuments && creationMode === 'file' && ( - <>UploadJsonFile - )} - {creationStep === DocumentCreationStep.ShowErrorSummary && <>DocumentCreationSummary} - {creationStep === DocumentCreationStep.ShowSuccessSummary && <>DocumentCreationSummary} - - + - ); + ) : null; +}; + +export const ModalContent: React.FC = () => { + const { creationStep, creationMode } = useValues(DocumentCreationLogic); + + switch (creationStep) { + case DocumentCreationStep.ShowCreationModes: + return ; + case DocumentCreationStep.AddDocuments: + switch (creationMode) { + case 'api': + return ; + case 'text': + return ; + case 'file': + return ; + } + case DocumentCreationStep.ShowError: + return <>DocumentCreationError; + case DocumentCreationStep.ShowErrorSummary: + return <>DocumentCreationSummary; + case DocumentCreationStep.ShowSuccessSummary: + return <>DocumentCreationSummary; + } };