mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[ML] Performance improvements to annotations editing in Single Metric Viewer & buttons placement (#83216)
This commit is contained in:
parent
21c0258e6b
commit
7a7057eba7
16 changed files with 239 additions and 114 deletions
|
@ -1,3 +0,0 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
|
||||||
|
|
||||||
exports[`AnnotationFlyout Initialization. 1`] = `""`;
|
|
|
@ -5,58 +5,102 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
|
|
||||||
import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json';
|
import mockAnnotations from '../annotations_table/__mocks__/mock_annotations.json';
|
||||||
|
|
||||||
import React from 'react';
|
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 '../../../../../common/types/annotations';
|
||||||
import { annotation$ } from '../../../services/annotations_service';
|
import { AnnotationUpdatesService } from '../../../services/annotations_service';
|
||||||
|
|
||||||
import { AnnotationFlyout } from './index';
|
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', () => {
|
describe('AnnotationFlyout', () => {
|
||||||
test('Initialization.', () => {
|
let annotationUpdatesService: AnnotationUpdatesService | null = null;
|
||||||
const wrapper = shallowWithIntl(<AnnotationFlyout />);
|
beforeEach(() => {
|
||||||
expect(wrapper).toMatchSnapshot();
|
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;
|
const annotation = mockAnnotations[1] as Annotation;
|
||||||
annotation$.next(annotation);
|
|
||||||
|
|
||||||
// useObservable wraps the observable in a new component
|
annotationUpdatesService!.setValue(annotation);
|
||||||
const ObservableComponent = (props: any) => {
|
|
||||||
const annotationProp = useObservable(annotation$);
|
|
||||||
if (annotationProp === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return <AnnotationFlyout annotation={annotationProp} {...props} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = mountWithIntl(<ObservableComponent />);
|
const { getByTestId } = render(
|
||||||
const updateBtn = wrapper.find('EuiButton').first();
|
<MlAnnotationUpdatesContextProvider annotationUpdatesService={annotationUpdatesService!}>
|
||||||
expect(updateBtn.prop('isDisabled')).toEqual(true);
|
<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;
|
const annotation = mockAnnotations[2] as Annotation;
|
||||||
annotation$.next(annotation);
|
annotationUpdatesService!.setValue(annotation);
|
||||||
|
|
||||||
// useObservable wraps the observable in a new component
|
const { getByTestId } = render(
|
||||||
const ObservableComponent = (props: any) => {
|
<MlAnnotationUpdatesContextProvider annotationUpdatesService={annotationUpdatesService!}>
|
||||||
const annotationProp = useObservable(annotation$);
|
<ObservableComponent annotationUpdatesService={annotationUpdatesService!} />
|
||||||
if (annotationProp === undefined) {
|
</MlAnnotationUpdatesContextProvider>
|
||||||
return null;
|
);
|
||||||
}
|
const updateBtn = getByTestId('annotationFlyoutUpdateButton');
|
||||||
return <AnnotationFlyout annotation={annotationProp} {...props} />;
|
expect(updateBtn).toBeDisabled();
|
||||||
};
|
await waitFor(() => {
|
||||||
|
const errorText = screen.queryByText(/characters above maximum length/);
|
||||||
|
expect(errorText).not.toBe(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const wrapper = mountWithIntl(<ObservableComponent />);
|
test('Flyout disappears when annotation is updated', async () => {
|
||||||
const updateBtn = wrapper.find('EuiButton').first();
|
const annotation = mockAnnotations[0] as Annotation;
|
||||||
expect(updateBtn.prop('isDisabled')).toEqual(true);
|
|
||||||
|
|
||||||
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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 useObservable from 'react-use/lib/useObservable';
|
||||||
import * as Rx from 'rxjs';
|
import * as Rx from 'rxjs';
|
||||||
import { cloneDeep } from 'lodash';
|
import { cloneDeep } from 'lodash';
|
||||||
|
@ -28,15 +28,14 @@ import {
|
||||||
import { CommonProps } from '@elastic/eui';
|
import { CommonProps } from '@elastic/eui';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
import { FormattedMessage } from '@kbn/i18n/react';
|
import { FormattedMessage } from '@kbn/i18n/react';
|
||||||
import { distinctUntilChanged } from 'rxjs/operators';
|
|
||||||
import {
|
import {
|
||||||
ANNOTATION_MAX_LENGTH_CHARS,
|
ANNOTATION_MAX_LENGTH_CHARS,
|
||||||
ANNOTATION_EVENT_USER,
|
ANNOTATION_EVENT_USER,
|
||||||
} from '../../../../../common/constants/annotations';
|
} from '../../../../../common/constants/annotations';
|
||||||
import {
|
import {
|
||||||
annotation$,
|
|
||||||
annotationsRefreshed,
|
annotationsRefreshed,
|
||||||
AnnotationState,
|
AnnotationState,
|
||||||
|
AnnotationUpdatesService,
|
||||||
} from '../../../services/annotations_service';
|
} from '../../../services/annotations_service';
|
||||||
import { AnnotationDescriptionList } from '../annotation_description_list';
|
import { AnnotationDescriptionList } from '../annotation_description_list';
|
||||||
import { DeleteAnnotationModal } from '../delete_annotation_modal';
|
import { DeleteAnnotationModal } from '../delete_annotation_modal';
|
||||||
|
@ -48,6 +47,7 @@ import {
|
||||||
} from '../../../../../common/types/annotations';
|
} from '../../../../../common/types/annotations';
|
||||||
import { PartitionFieldsType } from '../../../../../common/types/anomalies';
|
import { PartitionFieldsType } from '../../../../../common/types/anomalies';
|
||||||
import { PARTITION_FIELDS } from '../../../../../common/constants/anomalies';
|
import { PARTITION_FIELDS } from '../../../../../common/constants/anomalies';
|
||||||
|
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
|
||||||
|
|
||||||
interface ViewableDetector {
|
interface ViewableDetector {
|
||||||
index: number;
|
index: number;
|
||||||
|
@ -67,6 +67,7 @@ interface Props {
|
||||||
};
|
};
|
||||||
detectorIndex: number;
|
detectorIndex: number;
|
||||||
detectors: ViewableDetector[];
|
detectors: ViewableDetector[];
|
||||||
|
annotationUpdatesService: AnnotationUpdatesService;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -85,7 +86,8 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
public annotationSub: Rx.Subscription | null = null;
|
public annotationSub: Rx.Subscription | null = null;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.annotationSub = annotation$.subscribe((v) => {
|
const { annotationUpdatesService } = this.props;
|
||||||
|
this.annotationSub = annotationUpdatesService.update$().subscribe((v) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
annotationState: v,
|
annotationState: v,
|
||||||
});
|
});
|
||||||
|
@ -100,15 +102,17 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
if (this.state.annotationState === null) {
|
if (this.state.annotationState === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const { annotationUpdatesService } = this.props;
|
||||||
|
|
||||||
annotation$.next({
|
annotationUpdatesService.setValue({
|
||||||
...this.state.annotationState,
|
...this.state.annotationState,
|
||||||
annotation: e.target.value,
|
annotation: e.target.value,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public cancelEditingHandler = () => {
|
public cancelEditingHandler = () => {
|
||||||
annotation$.next(null);
|
const { annotationUpdatesService } = this.props;
|
||||||
|
annotationUpdatesService.setValue(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
public deleteConfirmHandler = () => {
|
public deleteConfirmHandler = () => {
|
||||||
|
@ -148,7 +152,10 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.closeDeleteModal();
|
this.closeDeleteModal();
|
||||||
annotation$.next(null);
|
|
||||||
|
const { annotationUpdatesService } = this.props;
|
||||||
|
|
||||||
|
annotationUpdatesService.setValue(null);
|
||||||
annotationsRefreshed();
|
annotationsRefreshed();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -193,7 +200,8 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
|
|
||||||
public saveOrUpdateAnnotation = () => {
|
public saveOrUpdateAnnotation = () => {
|
||||||
const { annotationState: originalAnnotation } = this.state;
|
const { annotationState: originalAnnotation } = this.state;
|
||||||
const { chartDetails, detectorIndex } = this.props;
|
const { chartDetails, detectorIndex, annotationUpdatesService } = this.props;
|
||||||
|
|
||||||
if (originalAnnotation === null) {
|
if (originalAnnotation === null) {
|
||||||
return;
|
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
|
// Mark the annotation created by `user` if and only if annotation is being created, not updated
|
||||||
annotation.event = annotation.event ?? ANNOTATION_EVENT_USER;
|
annotation.event = annotation.event ?? ANNOTATION_EVENT_USER;
|
||||||
|
annotationUpdatesService.setValue(null);
|
||||||
annotation$.next(null);
|
|
||||||
|
|
||||||
ml.annotations
|
ml.annotations
|
||||||
.indexAnnotation(annotation)
|
.indexAnnotation(annotation)
|
||||||
|
@ -356,16 +363,16 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
</EuiFormRow>
|
</EuiFormRow>
|
||||||
</EuiFlyoutBody>
|
</EuiFlyoutBody>
|
||||||
<EuiFlyoutFooter>
|
<EuiFlyoutFooter>
|
||||||
<EuiFlexGroup justifyContent="spaceBetween">
|
<EuiFlexGroup>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButtonEmpty iconType="cross" onClick={this.cancelEditingHandler} flush="left">
|
<EuiButtonEmpty onClick={this.cancelEditingHandler} flush="left">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.cancelButtonLabel"
|
id="xpack.ml.timeSeriesExplorer.annotationFlyout.cancelButtonLabel"
|
||||||
defaultMessage="Cancel"
|
defaultMessage="Cancel"
|
||||||
/>
|
/>
|
||||||
</EuiButtonEmpty>
|
</EuiButtonEmpty>
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false} style={{ marginLeft: 'auto' }}>
|
||||||
{isExistingAnnotation && (
|
{isExistingAnnotation && (
|
||||||
<EuiButtonEmpty color="danger" onClick={this.deleteConfirmHandler}>
|
<EuiButtonEmpty color="danger" onClick={this.deleteConfirmHandler}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -376,7 +383,12 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
)}
|
)}
|
||||||
</EuiFlexItem>
|
</EuiFlexItem>
|
||||||
<EuiFlexItem grow={false}>
|
<EuiFlexItem grow={false}>
|
||||||
<EuiButton fill isDisabled={isInvalid === true} onClick={this.saveOrUpdateAnnotation}>
|
<EuiButton
|
||||||
|
fill
|
||||||
|
isDisabled={isInvalid === true}
|
||||||
|
onClick={this.saveOrUpdateAnnotation}
|
||||||
|
data-test-subj={'annotationFlyoutUpdateButton'}
|
||||||
|
>
|
||||||
{isExistingAnnotation ? (
|
{isExistingAnnotation ? (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel"
|
id="xpack.ml.timeSeriesExplorer.annotationFlyout.updateButtonLabel"
|
||||||
|
@ -403,17 +415,11 @@ export class AnnotationFlyoutUI extends Component<CommonProps & Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AnnotationFlyout: FC<any> = (props) => {
|
export const AnnotationFlyout: FC<any> = (props) => {
|
||||||
const annotationProp = useObservable(
|
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
|
||||||
annotation$.pipe(
|
const annotationProp = useObservable(annotationUpdatesService.isAnnotationInitialized$());
|
||||||
distinctUntilChanged((prev, curr) => {
|
|
||||||
// prevent re-rendering
|
|
||||||
return prev !== null && curr !== null;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
const cancelEditingHandler = useCallback(() => {
|
const cancelEditingHandler = useCallback(() => {
|
||||||
annotation$.next(null);
|
annotationUpdatesService.setValue(null);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (annotationProp === undefined || annotationProp === null) {
|
if (annotationProp === undefined || annotationProp === null) {
|
||||||
|
@ -423,7 +429,12 @@ export const AnnotationFlyout: FC<any> = (props) => {
|
||||||
const isExistingAnnotation = typeof annotationProp._id !== 'undefined';
|
const isExistingAnnotation = typeof annotationProp._id !== 'undefined';
|
||||||
|
|
||||||
return (
|
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>
|
<EuiFlyoutHeader hasBorder>
|
||||||
<EuiTitle size="s">
|
<EuiTitle size="s">
|
||||||
<h2 id="mlAnnotationFlyoutTitle">
|
<h2 id="mlAnnotationFlyoutTitle">
|
||||||
|
@ -441,7 +452,7 @@ export const AnnotationFlyout: FC<any> = (props) => {
|
||||||
</h2>
|
</h2>
|
||||||
</EuiTitle>
|
</EuiTitle>
|
||||||
</EuiFlyoutHeader>
|
</EuiFlyoutHeader>
|
||||||
<AnnotationFlyoutUI {...props} />
|
<AnnotationFlyoutUI {...props} annotationUpdatesService={annotationUpdatesService} />
|
||||||
</EuiFlyout>
|
</EuiFlyout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
|
exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
|
||||||
<AnnotationsTableUI
|
<Component
|
||||||
annotations={
|
annotations={
|
||||||
Array [
|
Array [
|
||||||
Object {
|
Object {
|
||||||
|
@ -141,7 +141,7 @@ exports[`AnnotationsTable Initialization with annotations prop. 1`] = `
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AnnotationsTable Initialization with job config prop. 1`] = `
|
exports[`AnnotationsTable Initialization with job config prop. 1`] = `
|
||||||
<AnnotationsTableUI
|
<Component
|
||||||
intl={
|
intl={
|
||||||
Object {
|
Object {
|
||||||
"defaultFormats": Object {},
|
"defaultFormats": Object {},
|
||||||
|
@ -403,7 +403,7 @@ exports[`AnnotationsTable Initialization with job config prop. 1`] = `
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
|
exports[`AnnotationsTable Minimal initialization without props. 1`] = `
|
||||||
<AnnotationsTableUI
|
<Component
|
||||||
intl={
|
intl={
|
||||||
Object {
|
Object {
|
||||||
"defaultFormats": Object {},
|
"defaultFormats": Object {},
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
import { uniq } from 'lodash';
|
import { uniq } from 'lodash';
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component, Fragment } from 'react';
|
import React, { Component, Fragment, useContext } from 'react';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import {
|
import {
|
||||||
EuiBadge,
|
EuiBadge,
|
||||||
|
@ -41,11 +41,7 @@ import {
|
||||||
isTimeSeriesViewJob,
|
isTimeSeriesViewJob,
|
||||||
} from '../../../../../common/util/job_utils';
|
} from '../../../../../common/util/job_utils';
|
||||||
|
|
||||||
import {
|
import { annotationsRefresh$, annotationsRefreshed } from '../../../services/annotations_service';
|
||||||
annotation$,
|
|
||||||
annotationsRefresh$,
|
|
||||||
annotationsRefreshed,
|
|
||||||
} from '../../../services/annotations_service';
|
|
||||||
import {
|
import {
|
||||||
ANNOTATION_EVENT_USER,
|
ANNOTATION_EVENT_USER,
|
||||||
ANNOTATION_EVENT_DELAYED_DATA,
|
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 { ML_APP_URL_GENERATOR, ML_PAGES } from '../../../../../common/constants/ml_url_generator';
|
||||||
import { PLUGIN_ID } from '../../../../../common/constants/app';
|
import { PLUGIN_ID } from '../../../../../common/constants/app';
|
||||||
import { timeFormatter } from '../../../../../common/util/date_utils';
|
import { timeFormatter } from '../../../../../common/util/date_utils';
|
||||||
|
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
|
||||||
|
|
||||||
const CURRENT_SERIES = 'current_series';
|
const CURRENT_SERIES = 'current_series';
|
||||||
/**
|
/**
|
||||||
|
@ -319,7 +316,11 @@ class AnnotationsTableUI extends Component {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { isSingleMetricViewerLinkVisible = true, isNumberBadgeVisible = false } = this.props;
|
const {
|
||||||
|
isSingleMetricViewerLinkVisible = true,
|
||||||
|
isNumberBadgeVisible = false,
|
||||||
|
annotationUpdatesService,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
const { queryText, searchError } = this.state;
|
const { queryText, searchError } = this.state;
|
||||||
|
|
||||||
|
@ -474,7 +475,7 @@ class AnnotationsTableUI extends Component {
|
||||||
return (
|
return (
|
||||||
<EuiToolTip position="bottom" content={editAnnotationsTooltipText}>
|
<EuiToolTip position="bottom" content={editAnnotationsTooltipText}>
|
||||||
<EuiButtonIcon
|
<EuiButtonIcon
|
||||||
onClick={() => annotation$.next(originalAnnotation ?? annotation)}
|
onClick={() => annotationUpdatesService.setValue(originalAnnotation ?? annotation)}
|
||||||
iconType="pencil"
|
iconType="pencil"
|
||||||
aria-label={editAnnotationsTooltipAriaLabelText}
|
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} />;
|
||||||
|
});
|
||||||
|
|
|
@ -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()
|
||||||
|
);
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 useObservable from 'react-use/lib/useObservable';
|
||||||
|
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
@ -34,6 +34,8 @@ import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
||||||
import { useTimefilter } from '../../contexts/kibana';
|
import { useTimefilter } from '../../contexts/kibana';
|
||||||
import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
|
import { isViewBySwimLaneData } from '../../explorer/swimlane_container';
|
||||||
import { JOB_ID } from '../../../../common/constants/anomalies';
|
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 = (
|
export const explorerRouteFactory = (
|
||||||
navigateToPath: NavigateToPath,
|
navigateToPath: NavigateToPath,
|
||||||
|
@ -59,10 +61,13 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
|
||||||
jobs: mlJobService.loadJobsWrapper,
|
jobs: mlJobService.loadJobsWrapper,
|
||||||
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),
|
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),
|
||||||
});
|
});
|
||||||
|
const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLoader context={context}>
|
<PageLoader context={context}>
|
||||||
<ExplorerUrlStateManager jobsWithTimeRange={results.jobsWithTimeRange.jobs} />
|
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
|
||||||
|
<ExplorerUrlStateManager jobsWithTimeRange={results.jobsWithTimeRange.jobs} />
|
||||||
|
</MlAnnotationUpdatesContext.Provider>
|
||||||
</PageLoader>
|
</PageLoader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 useObservable from 'react-use/lib/useObservable';
|
||||||
import { i18n } from '@kbn/i18n';
|
import { i18n } from '@kbn/i18n';
|
||||||
|
|
||||||
|
@ -19,6 +19,8 @@ import { basicResolvers } from '../resolvers';
|
||||||
import { JobsPage } from '../../jobs/jobs_list';
|
import { JobsPage } from '../../jobs/jobs_list';
|
||||||
import { useTimefilter } from '../../contexts/kibana';
|
import { useTimefilter } from '../../contexts/kibana';
|
||||||
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
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 => ({
|
export const jobListRouteFactory = (navigateToPath: NavigateToPath, basePath: string): MlRoute => ({
|
||||||
path: '/jobs',
|
path: '/jobs',
|
||||||
|
@ -57,10 +59,13 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
|
||||||
setGlobalState({ refreshInterval });
|
setGlobalState({ refreshInterval });
|
||||||
timefilter.setRefreshInterval(refreshInterval);
|
timefilter.setRefreshInterval(refreshInterval);
|
||||||
}, []);
|
}, []);
|
||||||
|
const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLoader context={context}>
|
<PageLoader context={context}>
|
||||||
<JobsPage blockRefresh={blockRefresh} lastRefresh={lastRefresh} />
|
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
|
||||||
|
<JobsPage blockRefresh={blockRefresh} lastRefresh={lastRefresh} />
|
||||||
|
</MlAnnotationUpdatesContext.Provider>
|
||||||
</PageLoader>
|
</PageLoader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { isEqual } from 'lodash';
|
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 usePrevious from 'react-use/lib/usePrevious';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
|
@ -39,7 +39,8 @@ import { basicResolvers } from '../resolvers';
|
||||||
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
import { getBreadcrumbWithUrlForApp } from '../breadcrumbs';
|
||||||
import { useTimefilter } from '../../contexts/kibana';
|
import { useTimefilter } from '../../contexts/kibana';
|
||||||
import { useToastNotificationService } from '../../services/toast_notification_service';
|
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 = (
|
export const timeSeriesExplorerRouteFactory = (
|
||||||
navigateToPath: NavigateToPath,
|
navigateToPath: NavigateToPath,
|
||||||
basePath: string
|
basePath: string
|
||||||
|
@ -64,13 +65,16 @@ const PageWrapper: FC<PageProps> = ({ deps }) => {
|
||||||
jobs: mlJobService.loadJobsWrapper,
|
jobs: mlJobService.loadJobsWrapper,
|
||||||
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),
|
jobsWithTimeRange: () => ml.jobs.jobsWithTimerange(getDateFormatTz()),
|
||||||
});
|
});
|
||||||
|
const annotationUpdatesService = useMemo(() => new AnnotationUpdatesService(), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLoader context={context}>
|
<PageLoader context={context}>
|
||||||
<TimeSeriesExplorerUrlStateManager
|
<MlAnnotationUpdatesContext.Provider value={annotationUpdatesService}>
|
||||||
config={deps.config}
|
<TimeSeriesExplorerUrlStateManager
|
||||||
jobsWithTimeRange={results.jobsWithTimeRange.jobs}
|
config={deps.config}
|
||||||
/>
|
jobsWithTimeRange={results.jobsWithTimeRange.jobs}
|
||||||
|
/>
|
||||||
|
</MlAnnotationUpdatesContext.Provider>
|
||||||
</PageLoader>
|
</PageLoader>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,20 +7,29 @@
|
||||||
import mockAnnotations from '../components/annotations/annotations_table/__mocks__/mock_annotations.json';
|
import mockAnnotations from '../components/annotations/annotations_table/__mocks__/mock_annotations.json';
|
||||||
|
|
||||||
import { Annotation } from '../../../common/types/annotations';
|
import { Annotation } from '../../../common/types/annotations';
|
||||||
import { annotation$, annotationsRefresh$, annotationsRefreshed } from './annotations_service';
|
import {
|
||||||
|
annotationsRefresh$,
|
||||||
|
annotationsRefreshed,
|
||||||
|
AnnotationUpdatesService,
|
||||||
|
} from './annotations_service';
|
||||||
describe('annotations_service', () => {
|
describe('annotations_service', () => {
|
||||||
test('annotation$', () => {
|
let annotationUpdatesService: AnnotationUpdatesService | null = null;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
annotationUpdatesService = new AnnotationUpdatesService();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('annotationUpdatesService', () => {
|
||||||
const subscriber = jest.fn();
|
const subscriber = jest.fn();
|
||||||
|
|
||||||
annotation$.subscribe(subscriber);
|
annotationUpdatesService!.update$().subscribe(subscriber);
|
||||||
|
|
||||||
// the subscriber should have been triggered with the initial value of null
|
// the subscriber should have been triggered with the initial value of null
|
||||||
expect(subscriber.mock.calls).toHaveLength(1);
|
expect(subscriber.mock.calls).toHaveLength(1);
|
||||||
expect(subscriber.mock.calls[0][0]).toBe(null);
|
expect(subscriber.mock.calls[0][0]).toBe(null);
|
||||||
|
|
||||||
const annotation = mockAnnotations[0] as Annotation;
|
const annotation = mockAnnotations[0] as Annotation;
|
||||||
annotation$.next(annotation);
|
annotationUpdatesService!.setValue(annotation);
|
||||||
|
|
||||||
// the subscriber should have been triggered with the updated annotation value
|
// the subscriber should have been triggered with the updated annotation value
|
||||||
expect(subscriber.mock.calls).toHaveLength(2);
|
expect(subscriber.mock.calls).toHaveLength(2);
|
||||||
|
|
|
@ -4,8 +4,8 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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';
|
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 annotationsRefresh$ = new BehaviorSubject(Date.now());
|
||||||
export const annotationsRefreshed = () => annotationsRefresh$.next(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import React from 'react';
|
||||||
import { Annotation } from '../../../../../common/types/annotations';
|
import { Annotation } from '../../../../../common/types/annotations';
|
||||||
import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
|
import { CombinedJob } from '../../../../../common/types/anomaly_detection_jobs';
|
||||||
import { ChartTooltipService } from '../../../components/chart_tooltip';
|
import { ChartTooltipService } from '../../../components/chart_tooltip';
|
||||||
|
import { AnnotationState, AnnotationUpdatesService } from '../../../services/annotations_service';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
selectedJob: CombinedJob;
|
selectedJob: CombinedJob;
|
||||||
|
@ -47,6 +48,11 @@ interface TimeseriesChartProps {
|
||||||
tooltipService: object;
|
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>;
|
focusXScale: d3.scale.Ordinal<{}, number>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component, useContext } from 'react';
|
||||||
import useObservable from 'react-use/lib/useObservable';
|
import useObservable from 'react-use/lib/useObservable';
|
||||||
import { isEqual, reduce, each, get } from 'lodash';
|
import { isEqual, reduce, each, get } from 'lodash';
|
||||||
import d3 from 'd3';
|
import d3 from 'd3';
|
||||||
|
@ -21,7 +21,6 @@ import {
|
||||||
getSeverityWithLow,
|
getSeverityWithLow,
|
||||||
getMultiBucketImpactLabel,
|
getMultiBucketImpactLabel,
|
||||||
} from '../../../../../common/util/anomaly_utils';
|
} from '../../../../../common/util/anomaly_utils';
|
||||||
import { annotation$ } from '../../../services/annotations_service';
|
|
||||||
import { formatValue } from '../../../formatters/format_value';
|
import { formatValue } from '../../../formatters/format_value';
|
||||||
import {
|
import {
|
||||||
LINE_CHART_ANOMALY_RADIUS,
|
LINE_CHART_ANOMALY_RADIUS,
|
||||||
|
@ -51,7 +50,7 @@ import {
|
||||||
unhighlightFocusChartAnnotation,
|
unhighlightFocusChartAnnotation,
|
||||||
ANNOTATION_MIN_WIDTH,
|
ANNOTATION_MIN_WIDTH,
|
||||||
} from './timeseries_chart_annotations';
|
} from './timeseries_chart_annotations';
|
||||||
import { distinctUntilChanged } from 'rxjs/operators';
|
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
|
||||||
|
|
||||||
const focusZoomPanelHeight = 25;
|
const focusZoomPanelHeight = 25;
|
||||||
const focusChartHeight = 310;
|
const focusChartHeight = 310;
|
||||||
|
@ -571,7 +570,6 @@ class TimeseriesChartIntl extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFocusChart() {
|
renderFocusChart() {
|
||||||
console.log('renderFocusChart');
|
|
||||||
const {
|
const {
|
||||||
focusAggregationInterval,
|
focusAggregationInterval,
|
||||||
focusAnnotationData: focusAnnotationDataOriginalPropValue,
|
focusAnnotationData: focusAnnotationDataOriginalPropValue,
|
||||||
|
@ -742,7 +740,8 @@ class TimeseriesChartIntl extends Component {
|
||||||
this.focusXScale,
|
this.focusXScale,
|
||||||
showAnnotations,
|
showAnnotations,
|
||||||
showFocusChartTooltip,
|
showFocusChartTooltip,
|
||||||
hideFocusChartTooltip
|
hideFocusChartTooltip,
|
||||||
|
this.props.annotationUpdatesService
|
||||||
);
|
);
|
||||||
|
|
||||||
// disable brushing (creation of annotations) when annotations aren't shown
|
// disable brushing (creation of annotations) when annotations aren't shown
|
||||||
|
@ -1800,17 +1799,17 @@ class TimeseriesChartIntl extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TimeseriesChart = (props) => {
|
export const TimeseriesChart = (props) => {
|
||||||
const annotationProp = useObservable(
|
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
|
||||||
annotation$.pipe(
|
const annotationProp = useObservable(annotationUpdatesService.isAnnotationInitialized$());
|
||||||
distinctUntilChanged((prev, curr) => {
|
|
||||||
// prevent re-rendering
|
|
||||||
return prev !== null && curr !== null;
|
|
||||||
})
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (annotationProp === undefined) {
|
if (annotationProp === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return <TimeseriesChartIntl annotation={annotationProp} {...props} />;
|
return (
|
||||||
|
<TimeseriesChartIntl
|
||||||
|
annotation={annotationProp}
|
||||||
|
{...props}
|
||||||
|
annotationUpdatesService={annotationUpdatesService}
|
||||||
|
/>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -13,13 +13,14 @@ import { Dictionary } from '../../../../../common/types/common';
|
||||||
|
|
||||||
import { TimeseriesChart } from './timeseries_chart';
|
import { TimeseriesChart } from './timeseries_chart';
|
||||||
|
|
||||||
import { annotation$ } from '../../../services/annotations_service';
|
import { AnnotationUpdatesService } from '../../../services/annotations_service';
|
||||||
|
|
||||||
export const ANNOTATION_MASK_ID = 'mlAnnotationMask';
|
export const ANNOTATION_MASK_ID = 'mlAnnotationMask';
|
||||||
|
|
||||||
// getAnnotationBrush() is expected to be called like getAnnotationBrush.call(this)
|
// getAnnotationBrush() is expected to be called like getAnnotationBrush.call(this)
|
||||||
// so it gets passed on the context of the component it gets called from.
|
// so it gets passed on the context of the component it gets called from.
|
||||||
export function getAnnotationBrush(this: TimeseriesChart) {
|
export function getAnnotationBrush(this: TimeseriesChart) {
|
||||||
|
const { annotationUpdatesService } = this.props;
|
||||||
const focusXScale = this.focusXScale;
|
const focusXScale = this.focusXScale;
|
||||||
|
|
||||||
const annotateBrush = d3.svg.brush().x(focusXScale).on('brushend', brushend.bind(this));
|
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();
|
const endTimestamp = extent[1].getTime();
|
||||||
|
|
||||||
if (timestamp === endTimestamp) {
|
if (timestamp === endTimestamp) {
|
||||||
annotation$.next(null);
|
annotationUpdatesService.setValue(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,7 +48,7 @@ export function getAnnotationBrush(this: TimeseriesChart) {
|
||||||
type: ANNOTATION_TYPE.ANNOTATION,
|
type: ANNOTATION_TYPE.ANNOTATION,
|
||||||
};
|
};
|
||||||
|
|
||||||
annotation$.next(annotation);
|
annotationUpdatesService.setValue(annotation);
|
||||||
}
|
}
|
||||||
|
|
||||||
return annotateBrush;
|
return annotateBrush;
|
||||||
|
@ -105,7 +106,8 @@ export function renderAnnotations(
|
||||||
focusXScale: TimeseriesChart['focusXScale'],
|
focusXScale: TimeseriesChart['focusXScale'],
|
||||||
showAnnotations: boolean,
|
showAnnotations: boolean,
|
||||||
showFocusChartTooltip: (d: Annotation, t: object) => {},
|
showFocusChartTooltip: (d: Annotation, t: object) => {},
|
||||||
hideFocusChartTooltip: () => void
|
hideFocusChartTooltip: () => void,
|
||||||
|
annotationUpdatesService: AnnotationUpdatesService
|
||||||
) {
|
) {
|
||||||
const upperRectMargin = ANNOTATION_UPPER_RECT_MARGIN;
|
const upperRectMargin = ANNOTATION_UPPER_RECT_MARGIN;
|
||||||
const upperTextMargin = ANNOTATION_UPPER_TEXT_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.
|
// 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
|
// this needs to be done explicitly here because a new annotation created using the brush tool
|
||||||
// could still be present in the chart.
|
// could still be present in the chart.
|
||||||
annotation$.next(null);
|
annotationUpdatesService.setValue(null);
|
||||||
// set the actual annotation and trigger the flyout
|
// set the actual annotation and trigger the flyout
|
||||||
annotation$.next(d);
|
annotationUpdatesService.setValue(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
rects
|
rects
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
* you may not use this file except in compliance with the Elastic License.
|
* 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 { i18n } from '@kbn/i18n';
|
||||||
import { MlTooltipComponent } from '../../../components/chart_tooltip';
|
import { MlTooltipComponent } from '../../../components/chart_tooltip';
|
||||||
import { TimeseriesChart } from './timeseries_chart';
|
import { TimeseriesChart } from './timeseries_chart';
|
||||||
|
@ -16,6 +16,7 @@ import { useMlKibana, useNotifications } from '../../../contexts/kibana';
|
||||||
import { getBoundsRoundedToInterval } from '../../../util/time_buckets';
|
import { getBoundsRoundedToInterval } from '../../../util/time_buckets';
|
||||||
import { ANNOTATION_EVENT_USER } from '../../../../../common/constants/annotations';
|
import { ANNOTATION_EVENT_USER } from '../../../../../common/constants/annotations';
|
||||||
import { getControlsForDetector } from '../../get_controls_for_detector';
|
import { getControlsForDetector } from '../../get_controls_for_detector';
|
||||||
|
import { MlAnnotationUpdatesContext } from '../../../contexts/ml/ml_annotation_updates_context';
|
||||||
|
|
||||||
interface TimeSeriesChartWithTooltipsProps {
|
interface TimeSeriesChartWithTooltipsProps {
|
||||||
bounds: any;
|
bounds: any;
|
||||||
|
@ -50,6 +51,8 @@ export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> =
|
||||||
},
|
},
|
||||||
} = useMlKibana();
|
} = useMlKibana();
|
||||||
|
|
||||||
|
const annotationUpdatesService = useContext(MlAnnotationUpdatesContext);
|
||||||
|
|
||||||
const [annotationData, setAnnotationData] = useState<Annotation[]>([]);
|
const [annotationData, setAnnotationData] = useState<Annotation[]>([]);
|
||||||
|
|
||||||
const showAnnotationErrorToastNotification = useCallback((error?: string) => {
|
const showAnnotationErrorToastNotification = useCallback((error?: string) => {
|
||||||
|
@ -123,6 +126,7 @@ export const TimeSeriesChartWithTooltips: FC<TimeSeriesChartWithTooltipsProps> =
|
||||||
{(tooltipService) => (
|
{(tooltipService) => (
|
||||||
<TimeseriesChart
|
<TimeseriesChart
|
||||||
{...chartProps}
|
{...chartProps}
|
||||||
|
annotationUpdatesService={annotationUpdatesService}
|
||||||
annotationData={annotationData}
|
annotationData={annotationData}
|
||||||
bounds={bounds}
|
bounds={bounds}
|
||||||
detectorIndex={detectorIndex}
|
detectorIndex={detectorIndex}
|
||||||
|
|
|
@ -1014,7 +1014,6 @@ export class TimeSeriesExplorer extends React.Component {
|
||||||
this.previousShowForecast = showForecast;
|
this.previousShowForecast = showForecast;
|
||||||
this.previousShowModelBounds = showModelBounds;
|
this.previousShowModelBounds = showModelBounds;
|
||||||
|
|
||||||
console.log('Timeseriesexplorer rerendered');
|
|
||||||
return (
|
return (
|
||||||
<TimeSeriesExplorerPage dateFormatTz={dateFormatTz} resizeRef={this.resizeRef}>
|
<TimeSeriesExplorerPage dateFormatTz={dateFormatTz} resizeRef={this.resizeRef}>
|
||||||
{fieldNamesWithEmptyValues.length > 0 && (
|
{fieldNamesWithEmptyValues.length > 0 && (
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue