From 14fd0061e4c009ec537d8a4e748a8bb26b85a99b Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Fri, 17 Jul 2020 17:05:53 +0200 Subject: [PATCH] [ML] improve annotation flyout performance --- .../annotations/annotation_flyout/index.tsx | 270 ++++++++++-------- 1 file changed, 148 insertions(+), 122 deletions(-) diff --git a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx index 2196f3d6cc6d2..84abe3ed8a821 100644 --- a/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx +++ b/x-pack/plugins/ml/public/application/components/annotations/annotation_flyout/index.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Component, Fragment, FC, ReactNode } from 'react'; +import React, { Component, FC, ReactNode, useCallback } from 'react'; import useObservable from 'react-use/lib/useObservable'; import * as Rx from 'rxjs'; import { cloneDeep } from 'lodash'; @@ -28,6 +28,7 @@ import { import { CommonProps } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import { distinctUntilChanged } from 'rxjs/operators'; import { ANNOTATION_MAX_LENGTH_CHARS, ANNOTATION_EVENT_USER, @@ -60,7 +61,6 @@ interface Entity { } interface Props { - annotation: AnnotationState; chartDetails: { entityData: { entities: Entity[] }; functionLabel: string; @@ -70,25 +70,39 @@ interface Props { } interface State { + annotationState: AnnotationState | null; isDeleteModalVisible: boolean; applyAnnotationToSeries: boolean; } -class AnnotationFlyoutUI extends Component { +export class AnnotationFlyoutUI extends Component { public state: State = { isDeleteModalVisible: false, applyAnnotationToSeries: true, + annotationState: null, }; public annotationSub: Rx.Subscription | null = null; + componentDidMount() { + this.annotationSub = annotation$.subscribe((v) => { + this.setState({ + annotationState: v, + }); + }); + } + + componentWillUnmount() { + this.annotationSub!.unsubscribe(); + } + public annotationTextChangeHandler = (e: React.ChangeEvent) => { - if (this.props.annotation === null) { + if (this.state.annotationState === null) { return; } annotation$.next({ - ...this.props.annotation, + ...this.state.annotationState, annotation: e.target.value, }); }; @@ -102,21 +116,21 @@ class AnnotationFlyoutUI extends Component { }; public deleteHandler = async () => { - const { annotation } = this.props; + const { annotationState } = this.state; const toastNotifications = getToastNotifications(); - if (annotation === null || annotation._id === undefined) { + if (annotationState === null || annotationState._id === undefined) { return; } try { - await ml.annotations.deleteAnnotation(annotation._id); + await ml.annotations.deleteAnnotation(annotationState._id); toastNotifications.addSuccess( i18n.translate( 'xpack.ml.timeSeriesExplorer.timeSeriesChart.deletedAnnotationNotificationMessage', { defaultMessage: 'Deleted annotation for job with ID {jobId}.', - values: { jobId: annotation.job_id }, + values: { jobId: annotationState.job_id }, } ) ); @@ -127,7 +141,7 @@ class AnnotationFlyoutUI extends Component { { defaultMessage: 'An error occurred deleting the annotation for job with ID {jobId}: {error}', - values: { jobId: annotation.job_id, error: JSON.stringify(err) }, + values: { jobId: annotationState.job_id, error: JSON.stringify(err) }, } ) ); @@ -145,13 +159,13 @@ class AnnotationFlyoutUI extends Component { 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 } = this.props; + const { annotationState } = this.state; const errors: string[] = []; - if (annotation === null) { + if (annotationState === null) { return errors; } - if (annotation.annotation.trim().length === 0) { + if (annotationState.annotation.trim().length === 0) { errors.push( i18n.translate('xpack.ml.timeSeriesExplorer.annotationFlyout.noAnnotationTextError', { defaultMessage: 'Enter annotation text', @@ -159,7 +173,7 @@ class AnnotationFlyoutUI extends Component { ); } - const textLength = annotation.annotation.length; + const textLength = annotationState.annotation.length; if (textLength > ANNOTATION_MAX_LENGTH_CHARS) { const charsOver = textLength - ANNOTATION_MAX_LENGTH_CHARS; errors.push( @@ -178,7 +192,8 @@ class AnnotationFlyoutUI extends Component { }; public saveOrUpdateAnnotation = () => { - const { annotation: originalAnnotation, chartDetails, detectorIndex } = this.props; + const { annotationState: originalAnnotation } = this.state; + const { chartDetails, detectorIndex } = this.props; if (originalAnnotation === null) { return; } @@ -262,14 +277,12 @@ class AnnotationFlyoutUI extends Component { }; public render(): ReactNode { - const { annotation, detectors, detectorIndex } = this.props; - const { isDeleteModalVisible } = this.state; + const { detectors, detectorIndex } = this.props; + const { annotationState, isDeleteModalVisible } = this.state; - if (annotation === null) { - return null; - } + if (!annotationState) return null; - const isExistingAnnotation = typeof annotation._id !== 'undefined'; + const isExistingAnnotation = typeof annotationState._id !== 'undefined'; // Check the length of the text is within the max length limit, // and warn if the length is approaching the limit. @@ -279,14 +292,16 @@ class AnnotationFlyoutUI extends Component { let helpText = null; if ( isInvalid === false && - annotation.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning + annotationState.annotation.length > ANNOTATION_MAX_LENGTH_CHARS * lengthRatioToShowWarning ) { helpText = i18n.translate( 'xpack.ml.timeSeriesExplorer.annotationFlyout.approachingMaxLengthWarning', { defaultMessage: '{charsRemaining, number} {charsRemaining, plural, one {character} other {characters}} remaining', - values: { charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotation.annotation.length }, + values: { + charsRemaining: ANNOTATION_MAX_LENGTH_CHARS - annotationState.annotation.length, + }, } ); } @@ -295,127 +310,138 @@ class AnnotationFlyoutUI extends Component { detector && 'detector_description' in detector ? detector.detector_description : ''; return ( - - - - -

- {isExistingAnnotation ? ( - - ) : ( - - )} -

-
-
- - + + + + + } + fullWidth + helpText={helpText} + isInvalid={isInvalid} + error={validationErrors} + > + - - + + } - fullWidth - helpText={helpText} - isInvalid={isInvalid} - error={validationErrors} - > - - - - + this.setState({ + applyAnnotationToSeries: !this.state.applyAnnotationToSeries, + }) + } + /> + + + + + + + + + + + {isExistingAnnotation && ( + - } - checked={this.state.applyAnnotationToSeries} - onChange={() => - this.setState({ - applyAnnotationToSeries: !this.state.applyAnnotationToSeries, - }) - } - /> - - - - - - + + )} + + + + {isExistingAnnotation ? ( + ) : ( + - - - - {isExistingAnnotation && ( - - - )} - - - - {isExistingAnnotation ? ( - - ) : ( - - )} - - - - -
+ + + + -
+ ); } } export const AnnotationFlyout: FC = (props) => { - const annotationProp = useObservable(annotation$); + const annotationProp = useObservable( + annotation$.pipe( + distinctUntilChanged((prev, curr) => { + // prevent re-rendering + return prev !== null && curr !== null; + }) + ) + ); - if (annotationProp === undefined) { + const cancelEditingHandler = useCallback(() => { + annotation$.next(null); + }, []); + + if (annotationProp === undefined || annotationProp === null) { return null; } - return ; + const isExistingAnnotation = typeof annotationProp._id !== 'undefined'; + + return ( + + + +

+ {isExistingAnnotation ? ( + + ) : ( + + )} +

+
+
+ +
+ ); };