-
Notifications
You must be signed in to change notification settings - Fork 8.5k
[ML] Anomaly Detection: Annotations enhancements #70198
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
ff3f180
9ffddf3
e517350
20ce417
2501b5f
e0ce77f
f96e042
4877939
ffd474a
2ee31d4
36a5a44
def94c3
bbdbec6
55aa5c8
a56fa66
07e396d
002eaf3
5b6249d
010c096
0d945fa
b69be14
1f2684d
e57889f
dfe3950
aa98817
02024e5
0e24cb3
687fed9
de5e991
fe58c7d
c06b83b
4871783
ad7a947
00b6c88
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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; | ||
|
|
@@ -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 && | ||
|
|
@@ -93,3 +112,27 @@ export function isAnnotations(arg: any): arg is Annotations { | |
| } | ||
| return arg.every((d: Annotation) => isAnnotation(d)); | ||
| } | ||
|
|
||
| export interface FieldToBucket { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The interfaces defined here will be applicable for other types than annotations I'm sure, but till we hit that use case, probably makes sense to leave them defined in here. I had a quick look to see if I could find something similar in our existing types, and the Kibana data plugin, but couldn't find anything. |
||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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, | ||
|
|
@@ -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; | ||
|
|
@@ -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); | ||
|
|
||
|
|
@@ -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) { | ||
|
|
@@ -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"> | ||
|
|
@@ -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={ | ||
|
|
@@ -286,6 +340,23 @@ class AnnotationFlyoutUI extends Component<CommonProps & Props> { | |
| value={annotation.annotation} | ||
| /> | ||
| </EuiFormRow> | ||
| <EuiFormRow> | ||
peteharverson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When editing a series level annotation, this checkbox should be initialized with the checked state. For me, it is normally opening unchecked.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is now checked on by default here 1f2684d
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @qn895 , I'm not sure if this screenshot is still current. But I'm noticing the footer actions. The 'Delete' action should be next to the 'Update' button instead of even spacing between all buttons. You also do not need the cross next to 'Cancel'
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tested latest edits, and the partitioning and detector fields are now removed as expected if I untick the 'apply to series' checkbox when editing an annotation. |
||
| }) | ||
| } | ||
| /> | ||
| </EuiFormRow> | ||
| </EuiFlyoutBody> | ||
| <EuiFlyoutFooter> | ||
| <EuiFlexGroup justifyContent="spaceBetween"> | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.


Uh oh!
There was an error while loading. Please reload this page.