[ML] Performance improvements to annotations editing in Single Metric Viewer & buttons placement (#83216)

This commit is contained in:
Quynh Nguyen 2020-11-18 10:49:26 -06:00 committed by GitHub
parent 21c0258e6b
commit 7a7057eba7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 239 additions and 114 deletions

View file

@ -1,3 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationFlyout Initialization. 1`] = `""`;

View file

@ -5,58 +5,102 @@
*/
import useObservable from 'react-use/lib/useObservable';
import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json';
import React from 'react';
import { mountWithIntl, shallowWithIntl } from '@kbn/test/jest';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { Annotation } from '../../../../../common/types/annotations';
import { annotation$ } from '../../../services/annotations_service';
import { AnnotationUpdatesService } from '../../../services/annotations_service';
import { AnnotationFlyout } from './index';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
jest.mock('../../../util/dependency_cache', () => ({
getToastNotifications: () => ({ addSuccess: jest.fn(), addDanger: jest.fn() }),
}));
const MlAnnotationUpdatesContextProvider = ({
annotationUpdatesService,
children,
}: {
annotationUpdatesService: AnnotationUpdatesService;
children: React.ReactElement;
}) => {
return (
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
<IntlProvider>{children}</IntlProvider>
</MlAnnotationUpdatesContext.Provider>
);
};
const ObservableComponent = (props: any) => {
const { annotationUpdatesService } = props;
const annotationProp = useObservable(annotationUpdatesService!.isAnnotationInitialized$());
if (annotationProp === undefined) {
return null;
}
return (
<AnnotationFlyout
annotation={annotationProp}
annotationUpdatesService={annotationUpdatesService}
{...props}
/>
);
};
describe('AnnotationFlyout', () => {
test('Initialization.', () => {
const wrapper = shallowWithIntl(<AnnotationFlyout />);
expect(wrapper).toMatchSnapshot();
let annotationUpdatesService: AnnotationUpdatesService | null = null;
beforeEach(() => {
annotationUpdatesService = new AnnotationUpdatesService();
});
test('Update button is disabled with empty annotation', () => {
test('Update button is disabled with empty annotation', async () => {
const annotation = mockAnnotations[1] as Annotation;
annotation$.next(annotation);
// useObservable wraps the observable in a new component
const ObservableComponent = (props: any) => {
const annotationProp = useObservable(annotation$);
if (annotationProp === undefined) {
return null;
}
return <AnnotationFlyout annotation={annotationProp} {...props} />;
};
annotationUpdatesService!.setValue(annotation);
const wrapper = mountWithIntl(<ObservableComponent />);
const updateBtn = wrapper.find('EuiButton').first();
expect(updateBtn.prop('isDisabled')).toEqual(true);
const { getByTestId } = render(
<MlAnnotationUpdatesContextProvider annotationUpdatesService={annotationUpdatesService!}>
<ObservableComponent annotationUpdatesService={annotationUpdatesService!} />
</MlAnnotationUpdatesContextProvider>
);
const updateBtn = getByTestId('annotationFlyoutUpdateButton');
expect(updateBtn).toBeDisabled();
});
test('Error displayed and update button displayed if annotation text is longer than max chars', () => {
test('Error displayed and update button displayed if annotation text is longer than max chars', async () => {
const annotation = mockAnnotations[2] as Annotation;
annotation$.next(annotation);
annotationUpdatesService!.setValue(annotation);
// useObservable wraps the observable in a new component
const ObservableComponent = (props: any) => {
const annotationProp = useObservable(annotation$);
if (annotationProp === undefined) {
return null;
}
return <AnnotationFlyout annotation={annotationProp} {...props} />;
};
const { getByTestId } = render(
<MlAnnotationUpdatesContextProvider annotationUpdatesService={annotationUpdatesService!}>
<ObservableComponent annotationUpdatesService={annotationUpdatesService!} />
</MlAnnotationUpdatesContextProvider>
);
const updateBtn = getByTestId('annotationFlyoutUpdateButton');
expect(updateBtn).toBeDisabled();
await waitFor(() => {
const errorText = screen.queryByText(/characters above maximum length/);
expect(errorText).not.toBe(undefined);
});
});
const wrapper = mountWithIntl(<ObservableComponent />);
const updateBtn = wrapper.find('EuiButton').first();
expect(updateBtn.prop('isDisabled')).toEqual(true);
test('Flyout disappears when annotation is updated', async () => {
const annotation = mockAnnotations[0] as Annotation;
expect(wrapper.find('EuiFormErrorText')).toHaveLength(1);
annotationUpdatesService!.setValue(annotation);
const { getByTestId } = render(
<MlAnnotationUpdatesContextProvider annotationUpdatesService={annotationUpdatesService!}>
<ObservableComponent annotationUpdatesService={annotationUpdatesService!} />
</MlAnnotationUpdatesContextProvider>
);
const updateBtn = getByTestId('annotationFlyoutUpdateButton');
expect(updateBtn).not.toBeDisabled();
expect(screen.queryByTestId('mlAnnotationFlyout')).toBeInTheDocument();
await fireEvent.click(updateBtn);
expect(screen.queryByTestId('mlAnnotationFlyout')).not.toBeInTheDocument();
});
});

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { Component, FC, ReactNode, useCallback } from 'react';
import React, { Component, FC, ReactNode, useCallback, useContext } from 'react';
import useObservable from 'react-use/lib/useObservable';
import * as Rx from 'rxjs';
import { cloneDeep } from 'lodash';
@ -28,15 +28,14 @@ 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,
} from '../../../../../common/constants/annotations';
import {
annotation$,
annotationsRefreshed,
AnnotationState,
AnnotationUpdatesService,
} from '../../../services/annotations_service';
import { AnnotationDescriptionList } from '../annotation_description_list';
import { DeleteAnnotationModal } from '../delete_annotation_modal';
@ -48,6 +47,7 @@ import {
} from '../../../../../common/types/annotations';
import { PartitionFieldsType } from '../../../../../common/types/anomalies';
import { PARTITION_FIELDS } from '../../../../../common/constants/anomalies';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
interface ViewableDetector {
index: number;
@ -67,6 +67,7 @@ interface Props {
};
detectorIndex: number;
detectors: ViewableDetector[];
annotationUpdatesService: AnnotationUpdatesService;
}
interface State {
@ -85,7 +86,8 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
public annotationSub: Rx.Subscription | null = null;
componentDidMount() {
this.annotationSub = annotation$.subscribe((v) => {
const { annotationUpdatesService } = this.props;
this.annotationSub = annotationUpdatesService.update$().subscribe((v) => {
this.setState({
annotationState: v,
});
@ -100,15 +102,17 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
if (this.state.annotationState === null) {
return;
}
const { annotationUpdatesService } = this.props;
annotation$.next({
annotationUpdatesService.setValue({
...this.state.annotationState,
annotation: e.target.value,
});
};
public cancelEditingHandler = () => {
annotation$.next(null);
const { annotationUpdatesService } = this.props;
annotationUpdatesService.setValue(null);
};
public deleteConfirmHandler = () => {
@ -148,7 +152,10 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
}
this.closeDeleteModal();
annotation$.next(null);
const { annotationUpdatesService } = this.props;
annotationUpdatesService.setValue(null);
annotationsRefreshed();
};
@ -193,7 +200,8 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
public saveOrUpdateAnnotation = () => {
const { annotationState: originalAnnotation } = this.state;
const { chartDetails, detectorIndex } = this.props;
const { chartDetails, detectorIndex, annotationUpdatesService } = this.props;
if (originalAnnotation === null) {
return;
}
@ -218,8 +226,7 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
}
// Mark the annotation created by `user` if and only if annotation is being created, not updated
annotation.event = annotation.event ?? ANNOTATION_EVENT_USER;
annotation$.next(null);
annotationUpdatesService.setValue(null);
ml.annotations
.indexAnnotation(annotation)
@ -356,16 +363,16 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
</EuiFormRow>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiButtonEmpty iconType="cross" onClick={this.cancelEditingHandler} flush="left">
<EuiButtonEmpty onClick={this.cancelEditingHandler} flush="left">
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.annotationFlyout.cancelButtonLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFlexItem grow={false} style={{ marginLeft: 'auto' }}>
{isExistingAnnotation && (
<EuiButtonEmpty color="danger" onClick={this.deleteConfirmHandler}>
<FormattedMessage
@ -376,7 +383,12 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton fill isDisabled={isInvalid === true} onClick={this.saveOrUpdateAnnotation}>
<EuiButton
fill
isDisabled={isInvalid === true}
onClick={this.saveOrUpdateAnnotation}
data-test-subj={'annotationFlyoutUpdateButton'}
>
{isExistingAnnotation ? (
<FormattedMessage
id="xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel"
@ -403,17 +415,11 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
}
export const AnnotationFlyout: FC<any> = (props) => {
const annotationProp = useObservable(
annotation$.pipe(
distinctUntilChanged((prev, curr) => {
// prevent re-rendering
return prev !== null && curr !== null;
})
)
);
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
const annotationProp = useObservable(annotationUpdatesService.isAnnotationInitialized$());
const cancelEditingHandler = useCallback(() => {
annotation$.next(null);
annotationUpdatesService.setValue(null);
}, []);
if (annotationProp === undefined || annotationProp === null) {
@ -423,7 +429,12 @@ export const AnnotationFlyout: FC<any> = (props) => {
const isExistingAnnotation = typeof annotationProp._id !== 'undefined';
return (
<EuiFlyout onClose={cancelEditingHandler} size="m" aria-labelledby="Add annotation">
<EuiFlyout
onClose={cancelEditingHandler}
size="m"
aria-labelledby="Add annotation"
data-test-subj={'mlAnnotationFlyout'}
>
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id="mlAnnotationFlyoutTitle">
@ -441,7 +452,7 @@ export const AnnotationFlyout: FC<any> = (props) => {
</h2>
</EuiTitle>
</EuiFlyoutHeader>
<AnnotationFlyoutUI {...props} />
<AnnotationFlyoutUI {...props} annotationUpdatesService={annotationUpdatesService} />
</EuiFlyout>
);
};

View file

@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
<AnnotationsTableUI
<Component
annotations={
Array [
Object {
@ -141,7 +141,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
`;
exports[`AnnotationsTable Initialization with job config prop. 1`] = `
<AnnotationsTableUI
<Component
intl={
Object {
"defaultFormats": Object {},
@ -403,7 +403,7 @@ exports[`AnnotationsTable Initialization with job config prop. 1`] = `
`;
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
<AnnotationsTableUI
<Component
intl={
Object {
"defaultFormats": Object {},

View file

@ -13,7 +13,7 @@
import { uniq } from 'lodash';
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import React, { Component, Fragment, useContext } from 'react';
import memoizeOne from 'memoize-one';
import {
EuiBadge,
@ -41,11 +41,7 @@ import {
isTimeSeriesViewJob,
} from '../../../../../common/util/job_utils';
import {
annotation$,
annotationsRefresh$,
annotationsRefreshed,
} from '../../../services/annotations_service';
import { annotationsRefresh$, annotationsRefreshed } from '../../../services/annotations_service';
import {
ANNOTATION_EVENT_USER,
ANNOTATION_EVENT_DELAYED_DATA,
@ -54,6 +50,7 @@ import { withKibana } from '../../../../../../../../src/plugins/kibana_react/pub
import { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ml_url_generator';
import { PLUGIN_ID } from '../../../../../common/constants/app';
import { timeFormatter } from '../../../../../common/util/date_utils';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
const CURRENT_SERIES = 'current_series';
/**
@ -319,7 +316,11 @@ class AnnotationsTableUI extends Component {
};
render() {
const { isSingleMetricViewerLinkVisible = true, isNumberBadgeVisible = false } = this.props;
const {
isSingleMetricViewerLinkVisible = true,
isNumberBadgeVisible = false,
annotationUpdatesService,
} = this.props;
const { queryText, searchError } = this.state;
@ -474,7 +475,7 @@ class AnnotationsTableUI extends Component {
return (
<EuiToolTip position="bottom" content={editAnnotationsTooltipText}>
<EuiButtonIcon
onClick={() => annotation$.next(originalAnnotation ?? annotation)}
onClick={() => annotationUpdatesService.setValue(originalAnnotation ?? annotation)}
iconType="pencil"
aria-label={editAnnotationsTooltipAriaLabelText}
/>
@ -693,4 +694,7 @@ class AnnotationsTableUI extends Component {
}
}
export const AnnotationsTable = withKibana(AnnotationsTableUI);
export const AnnotationsTable = withKibana((props) => {
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
return <AnnotationsTableUI annotationUpdatesService={annotationUpdatesService} {...props} />;
});

View file

@ -0,0 +1,14 @@
/*
* 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 { createContext } from 'react';
import { AnnotationUpdatesService } from '../../services/annotations_service';
export type MlAnnotationUpdatesContextValue = AnnotationUpdatesService;
export const MlAnnotationUpdatesContext = createContext<MlAnnotationUpdatesContextValue>(
new AnnotationUpdatesService()
);

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useEffect, useState, useCallback } from 'react';
import React, { FC, useEffect, useState, useCallback, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { i18n } from '@kbn/i18n';
@ -34,6 +34,8 @@ import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
import { JOB_ID } from '../../../../common/constants/anomalies';
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';
import { AnnotationUpdatesService } from '../../services/annotations_service';
export const explorerRouteFactory = (
navigateToPath: NavigateToPath,
@ -59,10 +61,13 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
jobs: mlJobService.loadJobsWrapper,
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),
});
const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []);
return (
<PageLoader context={context}>
<ExplorerUrlStateManager jobsWithTimeRange={results.jobsWithTimeRange.jobs} />
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
<ExplorerUrlStateManager jobsWithTimeRange={results.jobsWithTimeRange.jobs} />
</MlAnnotationUpdatesContext.Provider>
</PageLoader>
);
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { useEffect, FC } from 'react';
import React, { useEffect, FC, useMemo } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { i18n } from '@kbn/i18n';
@ -19,6 +19,8 @@ import { basicResolvers } from '../resolvers';
import { JobsPage } from '../../jobs/jobs_list';
import { useTimefilter } from '../../contexts/kibana';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { AnnotationUpdatesService } from '../../services/annotations_service';
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';
export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
path: '/jobs',
@ -57,10 +59,13 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
setGlobalState({ refreshInterval });
timefilter.setRefreshInterval(refreshInterval);
}, []);
const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []);
return (
<PageLoader context={context}>
<JobsPage blockRefresh={blockRefresh} lastRefresh={lastRefresh} />
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
<JobsPage blockRefresh={blockRefresh} lastRefresh={lastRefresh} />
</MlAnnotationUpdatesContext.Provider>
</PageLoader>
);
};

View file

@ -5,7 +5,7 @@
*/
import { isEqual } from 'lodash';
import React, { FC, useCallback, useEffect, useState } from 'react';
import React, { FC, useCallback, useEffect, useMemo, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import moment from 'moment';
@ -39,7 +39,8 @@ import { basicResolvers } from '../resolvers';
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
import { useTimefilter } from '../../contexts/kibana';
import { useToastNotificationService } from '../../services/toast_notification_service';
import { AnnotationUpdatesService } from '../../services/annotations_service';
import { MlAnnotationUpdatesContext } from '../../contexts/ml/ml_annotation_updates_context';
export const timeSeriesExplorerRouteFactory = (
navigateToPath: NavigateToPath,
basePath: string
@ -64,13 +65,16 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
jobs: mlJobService.loadJobsWrapper,
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),
});
const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []);
return (
<PageLoader context={context}>
<TimeSeriesExplorerUrlStateManager
config={deps.config}
jobsWithTimeRange={results.jobsWithTimeRange.jobs}
/>
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
<TimeSeriesExplorerUrlStateManager
config={deps.config}
jobsWithTimeRange={results.jobsWithTimeRange.jobs}
/>
</MlAnnotationUpdatesContext.Provider>
</PageLoader>
);
};

View file

@ -7,20 +7,29 @@
import mockAnnotations from '../components/annotations/annotations_table/__mocks__/mock_annotations.json';
import { Annotation } from '../../../common/types/annotations';
import { annotation$, annotationsRefresh$, annotationsRefreshed } from './annotations_service';
import {
annotationsRefresh$,
annotationsRefreshed,
AnnotationUpdatesService,
} from './annotations_service';
describe('annotations_service', () => {
test('annotation$', () => {
let annotationUpdatesService: AnnotationUpdatesService | null = null;
beforeEach(() => {
annotationUpdatesService = new AnnotationUpdatesService();
});
test('annotationUpdatesService', () => {
const subscriber = jest.fn();
annotation$.subscribe(subscriber);
annotationUpdatesService!.update$().subscribe(subscriber);
// the subscriber should have been triggered with the initial value of null
expect(subscriber.mock.calls).toHaveLength(1);
expect(subscriber.mock.calls[0][0]).toBe(null);
const annotation = mockAnnotations[0] as Annotation;
annotation$.next(annotation);
annotationUpdatesService!.setValue(annotation);
// the subscriber should have been triggered with the updated annotation value
expect(subscriber.mock.calls).toHaveLength(2);

View file

@ -4,8 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { BehaviorSubject } from 'rxjs';
import { BehaviorSubject, Observable } from 'rxjs';
import { distinctUntilChanged } from 'rxjs/operators';
import { Annotation } from '../../../common/types/annotations';
/*
@ -79,3 +79,25 @@ export const annotation$ = new BehaviorSubject<AnnotationState>(null);
*/
export const annotationsRefresh$ = new BehaviorSubject(Date.now());
export const annotationsRefreshed = () => annotationsRefresh$.next(Date.now());
export class AnnotationUpdatesService {
private _annotation$: BehaviorSubject<AnnotationState> = new BehaviorSubject<AnnotationState>(
null
);
public update$() {
return this._annotation$.asObservable();
}
public isAnnotationInitialized$(): Observable<AnnotationState> {
return this._annotation$.asObservable().pipe(
distinctUntilChanged((prev, curr) => {
// prevent re-rendering
return prev !== null && curr !== null;
})
);
}
public setValue(annotation: AnnotationState) {
this._annotation$.next(annotation);
}
}

View file

@ -10,6 +10,7 @@ import React from 'react';
import { Annotation } from '../../../../../common/types/annotations';
import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
import { ChartTooltipService } from '../../../components/chart_tooltip';
import { AnnotationState, AnnotationUpdatesService } from '../../../services/annotations_service';
interface Props {
selectedJob: CombinedJob;
@ -47,6 +48,11 @@ interface TimeseriesChartProps {
tooltipService: object;
}
declare class TimeseriesChart extends React.Component<Props, any> {
interface TimeseriesChartIntProps {
annotationUpdatesService: AnnotationUpdatesService;
annotationProps: AnnotationState;
}
declare class TimeseriesChart extends React.Component<Props & TimeseriesChartIntProps, any> {
focusXScale: d3.scale.Ordinal<{}, number>;
}

View file

@ -10,7 +10,7 @@
*/
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { Component, useContext } from 'react';
import useObservable from 'react-use/lib/useObservable';
import { isEqual, reduce, each, get } from 'lodash';
import d3 from 'd3';
@ -21,7 +21,6 @@ import {
getSeverityWithLow,
getMultiBucketImpactLabel,
} from '../../../../../common/util/anomaly_utils';
import { annotation$ } from '../../../services/annotations_service';
import { formatValue } from '../../../formatters/format_value';
import {
LINE_CHART_ANOMALY_RADIUS,
@ -51,7 +50,7 @@ import {
unhighlightFocusChartAnnotation,
ANNOTATION_MIN_WIDTH,
} from './timeseries_chart_annotations';
import { distinctUntilChanged } from 'rxjs/operators';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
const focusZoomPanelHeight = 25;
const focusChartHeight = 310;
@ -571,7 +570,6 @@ class TimeseriesChartIntl extends Component {
}
renderFocusChart() {
console.log('renderFocusChart');
const {
focusAggregationInterval,
focusAnnotationData: focusAnnotationDataOriginalPropValue,
@ -742,7 +740,8 @@ class TimeseriesChartIntl extends Component {
this.focusXScale,
showAnnotations,
showFocusChartTooltip,
hideFocusChartTooltip
hideFocusChartTooltip,
this.props.annotationUpdatesService
);
// disable brushing (creation of annotations) when annotations aren't shown
@ -1800,17 +1799,17 @@ class TimeseriesChartIntl extends Component {
}
export const TimeseriesChart = (props) => {
const annotationProp = useObservable(
annotation$.pipe(
distinctUntilChanged((prev, curr) => {
// prevent re-rendering
return prev !== null && curr !== null;
})
)
);
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
const annotationProp = useObservable(annotationUpdatesService.isAnnotationInitialized$());
if (annotationProp === undefined) {
return null;
}
return <TimeseriesChartIntl annotation={annotationProp} {...props} />;
return (
<TimeseriesChartIntl
annotation={annotationProp}
{...props}
annotationUpdatesService={annotationUpdatesService}
/>
);
};

View file

@ -13,13 +13,14 @@ import { Dictionary } from '../../../../../common/types/common';
import { TimeseriesChart } from './timeseries_chart';
import { annotation$ } from '../../../services/annotations_service';
import { AnnotationUpdatesService } from '../../../services/annotations_service';
export const ANNOTATION_MASK_ID = 'mlAnnotationMask';
// getAnnotationBrush() is expected to be called like getAnnotationBrush.call(this)
// so it gets passed on the context of the component it gets called from.
export function getAnnotationBrush(this: TimeseriesChart) {
const { annotationUpdatesService } = this.props;
const focusXScale = this.focusXScale;
const annotateBrush = d3.svg.brush().x(focusXScale).on('brushend', brushend.bind(this));
@ -35,7 +36,7 @@ export function getAnnotationBrush(this: TimeseriesChart) {
const endTimestamp = extent[1].getTime();
if (timestamp === endTimestamp) {
annotation$.next(null);
annotationUpdatesService.setValue(null);
return;
}
@ -47,7 +48,7 @@ export function getAnnotationBrush(this: TimeseriesChart) {
type: ANNOTATION_TYPE.ANNOTATION,
};
annotation$.next(annotation);
annotationUpdatesService.setValue(annotation);
}
return annotateBrush;
@ -105,7 +106,8 @@ export function renderAnnotations(
focusXScale: TimeseriesChart['focusXScale'],
showAnnotations: boolean,
showFocusChartTooltip: (d: Annotation, t: object) => {},
hideFocusChartTooltip: () => void
hideFocusChartTooltip: () => void,
annotationUpdatesService: AnnotationUpdatesService
) {
const upperRectMargin = ANNOTATION_UPPER_RECT_MARGIN;
const upperTextMargin = ANNOTATION_UPPER_TEXT_MARGIN;
@ -153,9 +155,9 @@ export function renderAnnotations(
// clear a possible existing annotation set up for editing before setting the new one.
// this needs to be done explicitly here because a new annotation created using the brush tool
// could still be present in the chart.
annotation$.next(null);
annotationUpdatesService.setValue(null);
// set the actual annotation and trigger the flyout
annotation$.next(d);
annotationUpdatesService.setValue(d);
});
rects

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React, { FC, useEffect, useState, useCallback } from 'react';
import React, { FC, useEffect, useState, useCallback, useContext } from 'react';
import { i18n } from '@kbn/i18n';
import { MlTooltipComponent } from '../../../components/chart_tooltip';
import { TimeseriesChart } from './timeseries_chart';
@ -16,6 +16,7 @@ import { useMlKibana, useNotifications } from '../../../contexts/kibana';
import { getBoundsRoundedToInterval } from '../../../util/time_buckets';
import { ANNOTATION_EVENT_USER } from '../../../../../common/constants/annotations';
import { getControlsForDetector } from '../../get_controls_for_detector';
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
interface TimeSeriesChartWithTooltipsProps {
bounds: any;
@ -50,6 +51,8 @@ export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> =
},
} = useMlKibana();
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
const [annotationData, setAnnotationData] = useState<Annotation[]>([]);
const showAnnotationErrorToastNotification = useCallback((error?: string) => {
@ -123,6 +126,7 @@ export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> =
{(tooltipService) => (
<TimeseriesChart
{...chartProps}
annotationUpdatesService={annotationUpdatesService}
annotationData={annotationData}
bounds={bounds}
detectorIndex={detectorIndex}

View file

@ -1014,7 +1014,6 @@ export class TimeSeriesExplorer extends React.Component {
this.previousShowForecast = showForecast;
this.previousShowModelBounds = showModelBounds;
console.log('Timeseriesexplorer rerendered');
return (
<TimeSeriesExplorerPage dateFormatTz={dateFormatTz} resizeRef={this.resizeRef}>
{fieldNamesWithEmptyValues.length > 0 && (