Skip to content

Commit

Permalink
[App Search] Final Document Creation API, Logic, & Summary/Error views (
Browse files Browse the repository at this point in the history
elastic#86822)

* [Setup] Server API route

* [Cleanup] Remove unnecessary DocumentCreationSteps

- errors can/should be shown in the EuiFlyoutBody banner (better UX since the JSON/file is right there for reference) vs its own page
- No need to distinguish between ShowErrorSummary and ShowSuccessSummary

+ placeholder Summary view for now

* Add DocumentCreationLogic file upload logic

* Update creation form components to show error/warning feedback

* Add final post-upload summary view

- split up into subcomponents for easier reading/testing

* [lint] oops, double licenses

* [PR feedback] map -> forEach

* [PR feedback] Reset form state on flyout close

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
2 people authored and cee-chen committed Jan 4, 2021
1 parent a146dd6 commit dde5e00
Show file tree
Hide file tree
Showing 27 changed files with 1,687 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,34 @@ export const FLYOUT_CONTINUE_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.documentCreation.flyoutContinue',
{ defaultMessage: 'Continue' }
);
export const FLYOUT_CLOSE_BUTTON = i18n.translate(
'xpack.enterpriseSearch.appSearch.documentCreation.modalClose',
{ defaultMessage: 'Close' }
);

export const DOCUMENT_CREATION_ERRORS = {
TITLE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.errorsTitle', {
defaultMessage: 'Something went wrong. Please address the errors and try again.',
}),
NO_FILE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.noFileFound', {
defaultMessage: 'No file found.',
}),
NO_VALID_FILE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.noValidFile', {
defaultMessage: 'Problem parsing file.',
}),
NOT_VALID: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.notValidJson', {
defaultMessage: 'Document contents must be a valid JSON array or object.',
}),
};
export const DOCUMENT_CREATION_WARNINGS = {
TITLE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.warningsTitle', {
defaultMessage: 'Warning!',
}),
LARGE_FILE: i18n.translate('xpack.enterpriseSearch.appSearch.documentCreation.largeFile', {
defaultMessage:
"You're uploading an extremely large file. This could potentially lock your browser, or take a very long time to process. If possible, try splitting your data up into multiple smaller files.",
}),
};

// This is indented the way it is to work with ApiCodeExample.
// Use dedent() when calling this alone
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@ import React from 'react';
import { shallow } from 'enzyme';
import { EuiTextArea, EuiButtonEmpty, EuiButton } from '@elastic/eui';

import { Errors } from '../creation_response_components';
import { PasteJsonText, FlyoutHeader, FlyoutBody, FlyoutFooter } from './paste_json_text';

describe('PasteJsonText', () => {
const values = {
textInput: 'hello world',
isUploading: false,
errors: [],
configuredLimits: {
engine: {
maxDocumentByteSize: 102400,
Expand All @@ -24,6 +27,7 @@ describe('PasteJsonText', () => {
};
const actions = {
setTextInput: jest.fn(),
onSubmitJson: jest.fn(),
closeDocumentCreation: jest.fn(),
};

Expand Down Expand Up @@ -58,6 +62,16 @@ describe('PasteJsonText', () => {
textarea.simulate('change', { target: { value: 'dolor sit amet' } });
expect(actions.setTextInput).toHaveBeenCalledWith('dolor sit amet');
});

it('shows an error banner and sets invalid form props if errors exist', () => {
const wrapper = shallow(<FlyoutBody />);
expect(wrapper.find(EuiTextArea).prop('isInvalid')).toBe(false);

setMockValues({ ...values, errors: ['some error'] });
rerender(wrapper);
expect(wrapper.find(EuiTextArea).prop('isInvalid')).toBe(true);
expect(wrapper.prop('banner').type).toEqual(Errors);
});
});

describe('FlyoutFooter', () => {
Expand All @@ -68,6 +82,13 @@ describe('PasteJsonText', () => {
expect(actions.closeDocumentCreation).toHaveBeenCalled();
});

it('submits json', () => {
const wrapper = shallow(<FlyoutFooter />);

wrapper.find(EuiButton).simulate('click');
expect(actions.onSubmitJson).toHaveBeenCalled();
});

it('disables/enables the Continue button based on whether text has been entered', () => {
const wrapper = shallow(<FlyoutFooter />);
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(false);
Expand All @@ -76,5 +97,14 @@ describe('PasteJsonText', () => {
rerender(wrapper);
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true);
});

it('sets isLoading based on isUploading', () => {
const wrapper = shallow(<FlyoutFooter />);
expect(wrapper.find(EuiButton).prop('isLoading')).toBe(false);

setMockValues({ ...values, isUploading: true });
rerender(wrapper);
expect(wrapper.find(EuiButton).prop('isLoading')).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { AppLogic } from '../../../app_logic';

import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants';
import { Errors } from '../creation_response_components';
import { DocumentCreationLogic } from '../';

import './paste_json_text.scss';
Expand Down Expand Up @@ -55,11 +56,11 @@ export const FlyoutBody: React.FC = () => {
const { configuredLimits } = useValues(AppLogic);
const maxDocumentByteSize = configuredLimits?.engine?.maxDocumentByteSize;

const { textInput } = useValues(DocumentCreationLogic);
const { textInput, errors } = useValues(DocumentCreationLogic);
const { setTextInput } = useActions(DocumentCreationLogic);

return (
<EuiFlyoutBody>
<EuiFlyoutBody banner={<Errors />}>
<EuiText color="subdued">
<p>
{i18n.translate(
Expand All @@ -76,6 +77,7 @@ export const FlyoutBody: React.FC = () => {
<EuiTextArea
value={textInput}
onChange={(e) => setTextInput(e.target.value)}
isInvalid={errors.length > 0}
aria-label={i18n.translate(
'xpack.enterpriseSearch.appSearch.documentCreation.pasteJsonText.label',
{ defaultMessage: 'Paste JSON here' }
Expand All @@ -89,8 +91,8 @@ export const FlyoutBody: React.FC = () => {
};

export const FlyoutFooter: React.FC = () => {
const { textInput } = useValues(DocumentCreationLogic);
const { closeDocumentCreation } = useActions(DocumentCreationLogic);
const { textInput, isUploading } = useValues(DocumentCreationLogic);
const { onSubmitJson, closeDocumentCreation } = useActions(DocumentCreationLogic);

return (
<EuiFlyoutFooter>
Expand All @@ -99,7 +101,7 @@ export const FlyoutFooter: React.FC = () => {
<EuiButtonEmpty onClick={closeDocumentCreation}>{FLYOUT_CANCEL_BUTTON}</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill isDisabled={!textInput.length}>
<EuiButton fill onClick={onSubmitJson} isLoading={isUploading} isDisabled={!textInput}>
{FLYOUT_CONTINUE_BUTTON}
</EuiButton>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
* 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__';
Expand All @@ -16,12 +11,15 @@ import React from 'react';
import { shallow } from 'enzyme';
import { EuiFilePicker, EuiButtonEmpty, EuiButton } from '@elastic/eui';

import { Errors } from '../creation_response_components';
import { UploadJsonFile, FlyoutHeader, FlyoutBody, FlyoutFooter } from './upload_json_file';

describe('UploadJsonFile', () => {
const mockFile = new File(['mock'], 'mock.json', { type: 'application/json' });
const values = {
fileInput: null,
isUploading: false,
errors: [],
configuredLimits: {
engine: {
maxDocumentByteSize: 102400,
Expand All @@ -30,6 +28,7 @@ describe('UploadJsonFile', () => {
};
const actions = {
setFileInput: jest.fn(),
onSubmitFile: jest.fn(),
closeDocumentCreation: jest.fn(),
};

Expand Down Expand Up @@ -63,6 +62,25 @@ describe('UploadJsonFile', () => {
wrapper.find(EuiFilePicker).simulate('change', []);
expect(actions.setFileInput).toHaveBeenCalledWith(null);
});

it('sets isLoading based on isUploading', () => {
const wrapper = shallow(<FlyoutBody />);
expect(wrapper.find(EuiFilePicker).prop('isLoading')).toBe(false);

setMockValues({ ...values, isUploading: true });
rerender(wrapper);
expect(wrapper.find(EuiFilePicker).prop('isLoading')).toBe(true);
});

it('shows an error banner and sets invalid form props if errors exist', () => {
const wrapper = shallow(<FlyoutBody />);
expect(wrapper.find(EuiFilePicker).prop('isInvalid')).toBe(false);

setMockValues({ ...values, errors: ['some error'] });
rerender(wrapper);
expect(wrapper.find(EuiFilePicker).prop('isInvalid')).toBe(true);
expect(wrapper.prop('banner').type).toEqual(Errors);
});
});

describe('FlyoutFooter', () => {
Expand All @@ -73,6 +91,13 @@ describe('UploadJsonFile', () => {
expect(actions.closeDocumentCreation).toHaveBeenCalled();
});

it('submits the json file', () => {
const wrapper = shallow(<FlyoutFooter />);

wrapper.find(EuiButton).simulate('click');
expect(actions.onSubmitFile).toHaveBeenCalled();
});

it('disables/enables the Continue button based on whether files have been uploaded', () => {
const wrapper = shallow(<FlyoutFooter />);
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true);
Expand All @@ -81,5 +106,14 @@ describe('UploadJsonFile', () => {
rerender(wrapper);
expect(wrapper.find(EuiButton).prop('isDisabled')).toBe(true);
});

it('sets isLoading based on isUploading', () => {
const wrapper = shallow(<FlyoutFooter />);
expect(wrapper.find(EuiButton).prop('isLoading')).toBe(false);

setMockValues({ ...values, isUploading: true });
rerender(wrapper);
expect(wrapper.find(EuiButton).prop('isLoading')).toBe(true);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,6 @@
* 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';
Expand All @@ -30,6 +25,7 @@ import {
import { AppLogic } from '../../../app_logic';

import { FLYOUT_ARIA_LABEL_ID, FLYOUT_CANCEL_BUTTON, FLYOUT_CONTINUE_BUTTON } from '../constants';
import { Errors } from '../creation_response_components';
import { DocumentCreationLogic } from '../';

export const UploadJsonFile: React.FC = () => (
Expand Down Expand Up @@ -59,10 +55,11 @@ export const FlyoutBody: React.FC = () => {
const { configuredLimits } = useValues(AppLogic);
const maxDocumentByteSize = configuredLimits?.engine?.maxDocumentByteSize;

const { isUploading, errors } = useValues(DocumentCreationLogic);
const { setFileInput } = useActions(DocumentCreationLogic);

return (
<EuiFlyoutBody>
<EuiFlyoutBody banner={<Errors />}>
<EuiText color="subdued">
<p>
{i18n.translate(
Expand All @@ -80,14 +77,16 @@ export const FlyoutBody: React.FC = () => {
onChange={(files) => setFileInput(files?.length ? files[0] : null)}
accept="application/json"
fullWidth
isLoading={isUploading}
isInvalid={errors.length > 0}
/>
</EuiFlyoutBody>
);
};

export const FlyoutFooter: React.FC = () => {
const { fileInput } = useValues(DocumentCreationLogic);
const { closeDocumentCreation } = useActions(DocumentCreationLogic);
const { fileInput, isUploading } = useValues(DocumentCreationLogic);
const { onSubmitFile, closeDocumentCreation } = useActions(DocumentCreationLogic);

return (
<EuiFlyoutFooter>
Expand All @@ -96,7 +95,7 @@ export const FlyoutFooter: React.FC = () => {
<EuiButtonEmpty onClick={closeDocumentCreation}>{FLYOUT_CANCEL_BUTTON}</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill isDisabled={!fileInput}>
<EuiButton fill onClick={onSubmitFile} isLoading={isUploading} isDisabled={!fileInput}>
{FLYOUT_CONTINUE_BUTTON}
</EuiButton>
</EuiFlexItem>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 } from '../../../../__mocks__/kea.mock';

import React from 'react';
import { shallow } from 'enzyme';
import { EuiCallOut } from '@elastic/eui';

import { Errors } from './';

describe('Errors', () => {
it('does not render if no errors or warnings to render', () => {
setMockValues({ errors: [], warnings: [] });
const wrapper = shallow(<Errors />);

expect(wrapper.find(EuiCallOut)).toHaveLength(0);
});

it('renders errors', () => {
setMockValues({ errors: ['error 1', 'error 2'], warnings: [] });
const wrapper = shallow(<Errors />);

expect(wrapper.find(EuiCallOut)).toHaveLength(1);
expect(wrapper.find(EuiCallOut).prop('title')).toEqual(
'Something went wrong. Please address the errors and try again.'
);
expect(wrapper.find('p').first().text()).toEqual('error 1');
expect(wrapper.find('p').last().text()).toEqual('error 2');
});

it('renders warnings', () => {
setMockValues({ errors: [], warnings: ['document size warning'] });
const wrapper = shallow(<Errors />);

expect(wrapper.find(EuiCallOut)).toHaveLength(1);
expect(wrapper.find(EuiCallOut).prop('title')).toEqual('Warning!');
expect(wrapper.find('p').text()).toEqual('document size warning');
});

it('renders both errors and warnings', () => {
setMockValues({ errors: ['some error'], warnings: ['some warning'] });
const wrapper = shallow(<Errors />);

expect(wrapper.find(EuiCallOut)).toHaveLength(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* 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 } from 'kea';

import { EuiCallOut } from '@elastic/eui';

import { DOCUMENT_CREATION_ERRORS, DOCUMENT_CREATION_WARNINGS } from '../constants';
import { DocumentCreationLogic } from '../';

export const Errors: React.FC = () => {
const { errors, warnings } = useValues(DocumentCreationLogic);

return (
<>
{errors.length > 0 && (
<EuiCallOut color="danger" iconType="alert" title={DOCUMENT_CREATION_ERRORS.TITLE}>
{errors.map((message, index) => (
<p key={index}>{message}</p>
))}
</EuiCallOut>
)}
{warnings.length > 0 && (
<EuiCallOut color="warning" iconType="alert" title={DOCUMENT_CREATION_WARNINGS.TITLE}>
{warnings.map((message, index) => (
<p key={index}>{message}</p>
))}
</EuiCallOut>
)}
</>
);
};
Loading

0 comments on commit dde5e00

Please sign in to comment.