Skip to content

Commit

Permalink
[7.x] [ML] Anomaly Detection: Annotations enhancements (#70198) (#71707)
Browse files Browse the repository at this point in the history
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
qn895 and elasticmachine authored Jul 14, 2020
1 parent 661dcce commit 6f6eb3b
Show file tree
Hide file tree
Showing 23 changed files with 697 additions and 120 deletions.
3 changes: 3 additions & 0 deletions x-pack/plugins/ml/common/constants/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ 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;

export const ANNOTATION_EVENT_USER = 'user';
export const ANNOTATION_EVENT_DELAYED_DATA = 'delayed_data';
2 changes: 2 additions & 0 deletions x-pack/plugins/ml/common/constants/anomalies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ export enum ANOMALY_THRESHOLD {
WARNING = 3,
LOW = 0,
}

export const PARTITION_FIELDS = ['partition_field', 'over_field', 'by_field'] as const;
45 changes: 44 additions & 1 deletion x-pack/plugins/ml/common/types/annotations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,20 @@
// ]
// }

import { PartitionFieldsType } from './anomalies';
import { ANNOTATION_TYPE } from '../constants/annotations';

export type AnnotationFieldName = 'partition_field_name' | 'over_field_name' | 'by_field_name';
export type AnnotationFieldValue = 'partition_field_value' | 'over_field_value' | 'by_field_value';

export function getAnnotationFieldName(fieldType: PartitionFieldsType): AnnotationFieldName {
return `${fieldType}_name` as AnnotationFieldName;
}

export function getAnnotationFieldValue(fieldType: PartitionFieldsType): AnnotationFieldValue {
return `${fieldType}_value` as AnnotationFieldValue;
}

export interface Annotation {
_id?: string;
create_time?: number;
Expand All @@ -73,8 +85,15 @@ export interface Annotation {
annotation: string;
job_id: string;
type: ANNOTATION_TYPE.ANNOTATION | ANNOTATION_TYPE.COMMENT;
event?: string;
detector_index?: number;
partition_field_name?: string;
partition_field_value?: string;
over_field_name?: string;
over_field_value?: string;
by_field_name?: string;
by_field_value?: string;
}

export function isAnnotation(arg: any): arg is Annotation {
return (
arg.timestamp !== undefined &&
Expand All @@ -93,3 +112,27 @@ export function isAnnotations(arg: any): arg is Annotations {
}
return arg.every((d: Annotation) => isAnnotation(d));
}

export interface FieldToBucket {
field: string;
missing?: string | number;
}

export interface FieldToBucketResult {
key: string;
doc_count: number;
}

export interface TermAggregationResult {
doc_count_error_upper_bound: number;
sum_other_doc_count: number;
buckets: FieldToBucketResult[];
}

export type EsAggregationResult = Record<string, TermAggregationResult>;

export interface GetAnnotationsResponse {
aggregations?: EsAggregationResult;
annotations: Record<string, Annotations>;
success: boolean;
}
4 changes: 4 additions & 0 deletions x-pack/plugins/ml/common/types/anomalies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/

import { PARTITION_FIELDS } from '../constants/anomalies';

export interface Influencer {
influencer_field_name: string;
influencer_field_values: string[];
Expand Down Expand Up @@ -53,3 +55,5 @@ export interface AnomaliesTableRecord {
typicalSort?: any;
metricDescriptionSort?: number;
}

export type PartitionFieldsType = typeof PARTITION_FIELDS[number];
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ import { formatHumanReadableDateTimeSeconds } from '../../../util/date_utils';

interface Props {
annotation: Annotation;
detectorDescription?: string;
}

export const AnnotationDescriptionList = ({ annotation }: Props) => {
export const AnnotationDescriptionList = ({ annotation, detectorDescription }: Props) => {
const listItems = [
{
title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.jobIdTitle', {
Expand Down Expand Up @@ -81,6 +82,33 @@ export const AnnotationDescriptionList = ({ annotation }: Props) => {
description: annotation.modified_username,
});
}
if (detectorDescription !== undefined) {
listItems.push({
title: i18n.translate('xpack.ml.timeSeriesExplorer.annotationDescriptionList.detectorTitle', {
defaultMessage: 'Detector',
}),
description: detectorDescription,
});
}

if (annotation.partition_field_name !== undefined) {
listItems.push({
title: annotation.partition_field_name,
description: annotation.partition_field_value,
});
}
if (annotation.over_field_name !== undefined) {
listItems.push({
title: annotation.over_field_name,
description: annotation.over_field_value,
});
}
if (annotation.by_field_name !== undefined) {
listItems.push({
title: annotation.by_field_name,
description: annotation.by_field_value,
});
}

return (
<EuiDescriptionList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import React, { Component, Fragment, FC, ReactNode } from 'react';
import useObservable from 'react-use/lib/useObservable';
import * as Rx from 'rxjs';
import { cloneDeep } from 'lodash';

import {
EuiButton,
Expand All @@ -21,35 +22,62 @@ import {
EuiSpacer,
EuiTextArea,
EuiTitle,
EuiCheckbox,
} from '@elastic/eui';

import { CommonProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';

import { ANNOTATION_MAX_LENGTH_CHARS } from '../../../../../common/constants/annotations';
import {
ANNOTATION_MAX_LENGTH_CHARS,
ANNOTATION_EVENT_USER,
} from '../../../../../common/constants/annotations';
import {
annotation$,
annotationsRefreshed,
AnnotationState,
} from '../../../services/annotations_service';
import { AnnotationDescriptionList } from '../annotation_description_list';
import { DeleteAnnotationModal } from '../delete_annotation_modal';

import { ml } from '../../../services/ml_api_service';
import { getToastNotifications } from '../../../util/dependency_cache';
import {
getAnnotationFieldName,
getAnnotationFieldValue,
} from '../../../../../common/types/annotations';
import { PartitionFieldsType } from '../../../../../common/types/anomalies';
import { PARTITION_FIELDS } from '../../../../../common/constants/anomalies';

interface ViewableDetector {
index: number;
detector_description: string;
}

interface Entity {
fieldName: string;
fieldType: string;
fieldValue: string;
}

interface Props {
annotation: AnnotationState;
chartDetails: {
entityData: { entities: Entity[] };
functionLabel: string;
};
detectorIndex: number;
detectors: ViewableDetector[];
}

interface State {
isDeleteModalVisible: boolean;
applyAnnotationToSeries: boolean;
}

class AnnotationFlyoutUI extends Component<CommonProps & Props> {
public state: State = {
isDeleteModalVisible: false,
applyAnnotationToSeries: true,
};

public annotationSub: Rx.Subscription | null = null;
Expand Down Expand Up @@ -150,11 +178,31 @@ class AnnotationFlyoutUI extends Component<CommonProps & Props> {
};

public saveOrUpdateAnnotation = () => {
const { annotation } = this.props;

if (annotation === null) {
const { annotation: originalAnnotation, chartDetails, detectorIndex } = this.props;
if (originalAnnotation === null) {
return;
}
const annotation = cloneDeep(originalAnnotation);

if (this.state.applyAnnotationToSeries && chartDetails?.entityData?.entities) {
chartDetails.entityData.entities.forEach((entity: Entity) => {
const { fieldName, fieldValue } = entity;
const fieldType = entity.fieldType as PartitionFieldsType;
annotation[getAnnotationFieldName(fieldType)] = fieldName;
annotation[getAnnotationFieldValue(fieldType)] = fieldValue;
});
annotation.detector_index = detectorIndex;
}
// if unchecked, remove all the partitions before indexing
if (!this.state.applyAnnotationToSeries) {
delete annotation.detector_index;
PARTITION_FIELDS.forEach((fieldType) => {
delete annotation[getAnnotationFieldName(fieldType)];
delete annotation[getAnnotationFieldValue(fieldType)];
});
}
// 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);

Expand Down Expand Up @@ -214,7 +262,7 @@ class AnnotationFlyoutUI extends Component<CommonProps & Props> {
};

public render(): ReactNode {
const { annotation } = this.props;
const { annotation, detectors, detectorIndex } = this.props;
const { isDeleteModalVisible } = this.state;

if (annotation === null) {
Expand Down Expand Up @@ -242,10 +290,13 @@ class AnnotationFlyoutUI extends Component<CommonProps & Props> {
}
);
}
const detector = detectors ? detectors.find((d) => d.index === detectorIndex) : undefined;
const detectorDescription =
detector && 'detector_description' in detector ? detector.detector_description : '';

return (
<Fragment>
<EuiFlyout onClose={this.cancelEditingHandler} size="s" aria-labelledby="Add annotation">
<EuiFlyout onClose={this.cancelEditingHandler} size="m" aria-labelledby="Add annotation">
<EuiFlyoutHeader hasBorder>
<EuiTitle size="s">
<h2 id="mlAnnotationFlyoutTitle">
Expand All @@ -264,7 +315,10 @@ class AnnotationFlyoutUI extends Component<CommonProps & Props> {
</EuiTitle>
</EuiFlyoutHeader>
<EuiFlyoutBody>
<AnnotationDescriptionList annotation={annotation} />
<AnnotationDescriptionList
annotation={annotation}
detectorDescription={detectorDescription}
/>
<EuiSpacer size="m" />
<EuiFormRow
label={
Expand All @@ -286,6 +340,23 @@ class AnnotationFlyoutUI extends Component<CommonProps & Props> {
value={annotation.annotation}
/>
</EuiFormRow>
<EuiFormRow>
<EuiCheckbox
id={'xpack.ml.annotationFlyout.applyToPartition'}
label={
<FormattedMessage
id="xpack.ml.annotationFlyout.applyToPartitionTextLabel"
defaultMessage="Apply annotation to this series"
/>
}
checked={this.state.applyAnnotationToSeries}
onChange={() =>
this.setState({
applyAnnotationToSeries: !this.state.applyAnnotationToSeries,
})
}
/>
</EuiFormRow>
</EuiFlyoutBody>
<EuiFlyoutFooter>
<EuiFlexGroup justifyContent="spaceBetween">
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 6f6eb3b

Please sign in to comment.