[ML] Limits maximum annotation text length to 1000 characters (#34540)

* [ML] Limits maximum annotation text length to 1000 characters

* [ML] Fix initialization of annotation errors array

* [ML] Fix typo in annotation flyout comment
This commit is contained in:
Pete Harverson 2019-04-08 11:54:42 +01:00 committed by GitHub
parent 7514fb804b
commit 659e4712eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 135 additions and 6 deletions

View file

@ -10,3 +10,6 @@ export enum ANNOTATION_TYPE {
}
export const ANNOTATION_USER_UNKNOWN = '<user unknown>';
// UI enforced limit to the maximum number of characters that can be entered for an annotation.
export const ANNOTATION_MAX_LENGTH_CHARS = 1000;

View file

@ -4,8 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import { injectObservablesAsProps } from '../../../util/observable_utils';
import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json';
import React, { ComponentType } from 'react';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import { Annotation } from '../../../../common/types/annotations';
import { annotation$ } from '../../../services/annotations_service';
import { AnnotationFlyout } from './index';
@ -14,4 +20,36 @@ describe('AnnotationFlyout', () => {
const wrapper = shallowWithIntl(<AnnotationFlyout />);
expect(wrapper).toMatchSnapshot();
});
test('Update button is disabled with empty annotation', () => {
const annotation = mockAnnotations[1] as Annotation;
annotation$.next(annotation);
// injectObservablesAsProps wraps the observable in a new component
const ObservableComponent = injectObservablesAsProps(
{ annotation: annotation$ },
(AnnotationFlyout as any) as ComponentType
);
const wrapper = mountWithIntl(<ObservableComponent />);
const updateBtn = wrapper.find('EuiButton').first();
expect(updateBtn.prop('isDisabled')).toEqual(true);
});
test('Error displayed and update button displayed if annotation text is longer than max chars', () => {
const annotation = mockAnnotations[2] as Annotation;
annotation$.next(annotation);
// injectObservablesAsProps wraps the observable in a new component
const ObservableComponent = injectObservablesAsProps(
{ annotation: annotation$ },
(AnnotationFlyout as any) as ComponentType
);
const wrapper = mountWithIntl(<ObservableComponent />);
const updateBtn = wrapper.find('EuiButton').first();
expect(updateBtn.prop('isDisabled')).toEqual(true);
expect(wrapper.find('EuiFormErrorText')).toHaveLength(1);
});
});

View file

@ -26,6 +26,7 @@ import { CommonProps } from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { InjectedIntlProps } from 'react-intl';
import { toastNotifications } from 'ui/notify';
import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../common/constants/annotations';
import {
annotation$,
annotationsRefresh$,
@ -112,6 +113,45 @@ class AnnotationFlyoutIntl extends Component<CommonProps & Props & InjectedIntlP
this.setState({ isDeleteModalVisible: false });
};
public validateAnnotationText = () => {
// Validates the entered text, returning an array of error messages
// for display in the form. An empty array is returned if the text is valid.
const { annotation, intl } = this.props;
const errors: string[] = [];
if (annotation === null) {
return errors;
}
if (annotation.annotation.trim().length === 0) {
errors.push(
intl.formatMessage({
id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError',
defaultMessage: 'Enter annotation text',
})
);
}
const textLength = annotation.annotation.length;
if (textLength > ANNOTATION_MAX_LENGTH_CHARS) {
const charsOver = textLength - ANNOTATION_MAX_LENGTH_CHARS;
errors.push(
intl.formatMessage(
{
id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.maxLengthError',
defaultMessage:
'{charsOver, number} {charsOver, plural, one {character} other {characters}} above maximum length of {maxChars}',
},
{
maxChars: ANNOTATION_MAX_LENGTH_CHARS,
charsOver,
}
)
);
}
return errors;
};
public saveOrUpdateAnnotation = () => {
const { annotation, intl } = this.props;
@ -179,7 +219,7 @@ class AnnotationFlyoutIntl extends Component<CommonProps & Props & InjectedIntlP
};
public render(): ReactNode {
const { annotation } = this.props;
const { annotation, intl } = this.props;
const { isDeleteModalVisible } = this.state;
if (annotation === null) {
@ -188,6 +228,26 @@ class AnnotationFlyoutIntl extends Component<CommonProps & Props & InjectedIntlP
const isExistingAnnotation = typeof annotation._id !== 'undefined';
// Check the length of the text is within the max length limit,
// and warn if the length is approaching the limit.
const validationErrors = this.validateAnnotationText();
const isInvalid = validationErrors.length > 0;
const lengthRatioToShowWarning = 0.95;
let helpText = null;
if (
isInvalid === false &&
annotation.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning
) {
helpText = intl.formatMessage(
{
id: 'xpack.ml.timeSeriesExplorer.annotationFlyout.approachingMaxLengthWarning',
defaultMessage:
'{charsRemaining, number} {charsRemaining, plural, one {character} other {characters}} remaining',
},
{ charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotation.annotation.length }
);
}
return (
<Fragment>
<EuiFlyout onClose={this.cancelEditingHandler} size="s" aria-labelledby="Add annotation">
@ -219,10 +279,13 @@ class AnnotationFlyoutIntl extends Component<CommonProps & Props & InjectedIntlP
/>
}
fullWidth
helpText={helpText}
isInvalid={isInvalid}
error={validationErrors}
>
<EuiTextArea
fullWidth
isInvalid={annotation.annotation === ''}
isInvalid={isInvalid}
onChange={this.annotationTextChangeHandler}
placeholder="..."
value={annotation.annotation}
@ -252,7 +315,7 @@ class AnnotationFlyoutIntl extends Component<CommonProps & Props & InjectedIntlP
<EuiFlexItem grow={false}>
<EuiButton
fill
isDisabled={annotation.annotation === ''}
isDisabled={isInvalid === true}
onClick={this.saveOrUpdateAnnotation}
>
{isExistingAnnotation ? (

View file

@ -10,5 +10,30 @@
"modified_time": 1546417097181,
"modified_username": "<user unknown>",
"_id": "KCCkDWgB_ZdQ1MFDSYPi"
},
{
"timestamp": 1455026177994,
"end_timestamp": 1455041968976,
"annotation": "",
"job_id": "farequote",
"type": "annotation",
"create_time": 1554377048000,
"create_username": "sysadmin",
"modified_time": 1554377048000,
"modified_username": "sysadmin",
"_id": "KCCkDWgB_ZdQ1MFDSYPj"
},
{
"timestamp": 1455026177994,
"end_timestamp": 1455041968976,
"annotation":
"A very long annotation with more than the maximum allowed characters. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"job_id": "farequote",
"type": "annotation",
"create_time": 1554377253000,
"create_username": "sysadmin",
"modified_time": 1554377253000,
"modified_username": "sysadmin",
"_id": "KCCkDWgB_ZdQ1MFDSYPk"
}
]

View file

@ -43,7 +43,7 @@ describe('AnnotationsTable', () => {
});
test('Initialization with annotations prop.', () => {
const wrapper = shallowWithIntl(<AnnotationsTable.WrappedComponent annotations={mockAnnotations} />);
const wrapper = shallowWithIntl(<AnnotationsTable.WrappedComponent annotations={mockAnnotations.slice(0, 1)} />);
expect(wrapper).toMatchSnapshot();
});