From da265da530e65de6a587c0cbb31a217e7484f011 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Thu, 6 Oct 2022 22:42:38 +0100 Subject: [PATCH] [ML] Converting the anomaly details functions to typescript (#142769) --- .../anomalies_table/anomaly_details.js | 260 +--------------- .../anomalies_table/anomaly_details_utils.tsx | 280 ++++++++++++++++++ 2 files changed, 283 insertions(+), 257 deletions(-) create mode 100644 x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js index d406784344f6d..a4f8ab231d086 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details.js @@ -12,7 +12,6 @@ import PropTypes from 'prop-types'; import React, { Component, Fragment } from 'react'; -import { get, pick } from 'lodash'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -26,265 +25,12 @@ import { EuiSpacer, EuiTabbedContent, EuiText, - EuiToolTip, } from '@elastic/eui'; -import { formatHumanReadableDateTimeSeconds } from '../../../../common/util/date_utils'; -import { EntityCell } from '../entity_cell'; -import { - getMultiBucketImpactLabel, - getSeverity, - showActualForFunction, - showTypicalForFunction, -} from '../../../../common/util/anomaly_utils'; -import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; -import { formatValue } from '../../formatters/format_value'; +import { getSeverity } from '../../../../common/util/anomaly_utils'; import { MAX_CHARS } from './anomalies_table_constants'; -import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; - -const TIME_FIELD_NAME = 'timestamp'; - -function getFilterEntity(entityName, entityValue, filter) { - return ; -} - -function getDetailsItems(anomaly, filter) { - const source = anomaly.source; - - // TODO - when multivariate analyses are more common, - // look in each cause for a 'correlatedByFieldValue' field, - let causes = []; - const sourceCauses = source.causes || []; - let singleCauseByFieldName = undefined; - let singleCauseByFieldValue = undefined; - if (sourceCauses.length === 1) { - // Metrics and probability will already have been placed at the top level. - // If cause has byFieldValue, move it to a top level fields for display. - if (sourceCauses[0].by_field_name !== undefined) { - singleCauseByFieldName = sourceCauses[0].by_field_name; - singleCauseByFieldValue = sourceCauses[0].by_field_value; - } - } else { - causes = sourceCauses.map((cause) => { - const simplified = pick(cause, 'typical', 'actual', 'probability'); - // Get the 'entity field name/value' to display in the cause - - // For by and over, use by_field_name/value (over_field_name/value are in the top level fields) - // For just an 'over' field - the over_field_name/value appear in both top level and cause. - simplified.entityName = cause.by_field_name ? cause.by_field_name : cause.over_field_name; - simplified.entityValue = cause.by_field_value ? cause.by_field_value : cause.over_field_value; - return simplified; - }); - } - - const items = []; - if (source.partition_field_value !== undefined) { - items.push({ - title: source.partition_field_name, - description: getFilterEntity( - source.partition_field_name, - source.partition_field_value, - filter - ), - }); - } - - if (source.by_field_value !== undefined) { - items.push({ - title: source.by_field_name, - description: getFilterEntity(source.by_field_name, source.by_field_value, filter), - }); - } - - if (singleCauseByFieldName !== undefined) { - // Display byField of single cause. - items.push({ - title: singleCauseByFieldName, - description: getFilterEntity(singleCauseByFieldName, singleCauseByFieldValue, filter), - }); - } - - if (source.over_field_value !== undefined) { - items.push({ - title: source.over_field_name, - description: getFilterEntity(source.over_field_name, source.over_field_value, filter), - }); - } - - const anomalyTime = source[TIME_FIELD_NAME]; - let timeDesc = `${formatHumanReadableDateTimeSeconds(anomalyTime)}`; - if (source.bucket_span !== undefined) { - const anomalyEndTime = anomalyTime + source.bucket_span * 1000; - timeDesc = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.anomalyTimeRangeLabel', { - defaultMessage: '{anomalyTime} to {anomalyEndTime}', - values: { - anomalyTime: formatHumanReadableDateTimeSeconds(anomalyTime), - anomalyEndTime: formatHumanReadableDateTimeSeconds(anomalyEndTime), - }, - }); - } - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.timeTitle', { - defaultMessage: 'Time', - }), - description: timeDesc, - }); - - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.functionTitle', { - defaultMessage: 'Function', - }), - description: - source.function !== ML_JOB_AGGREGATION.METRIC ? source.function : source.function_description, - }); - - if (source.field_name !== undefined) { - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.fieldNameTitle', { - defaultMessage: 'Field name', - }), - description: source.field_name, - }); - } - - const functionDescription = source.function_description || ''; - if (anomaly.actual !== undefined && showActualForFunction(functionDescription) === true) { - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', { - defaultMessage: 'Actual', - }), - description: formatValue(anomaly.actual, source.function, undefined, source), - }); - } - - if (anomaly.typical !== undefined && showTypicalForFunction(functionDescription) === true) { - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', { - defaultMessage: 'Typical', - }), - description: formatValue(anomaly.typical, source.function, undefined, source), - }); - } - - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.jobIdTitle', { - defaultMessage: 'Job ID', - }), - description: anomaly.jobId, - }); - if ( - source.multi_bucket_impact !== undefined && - source.multi_bucket_impact >= MULTI_BUCKET_IMPACT.LOW - ) { - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.multiBucketImpactTitle', { - defaultMessage: 'Multi-bucket impact', - }), - description: getMultiBucketImpactLabel(source.multi_bucket_impact), - }); - } - - items.push({ - title: ( - - - {i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.recordScoreTitle', { - defaultMessage: 'Record score', - })} - - - - ), - description: Math.floor(1000 * source.record_score) / 1000, - }); - - items.push({ - title: ( - - - {i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.initialRecordScoreTitle', { - defaultMessage: 'Initial record score', - })} - - - - ), - description: Math.floor(1000 * source.initial_record_score) / 1000, - }); - - items.push({ - title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.probabilityTitle', { - defaultMessage: 'Probability', - }), - description: - source.probability !== undefined ? Number.parseFloat(source.probability).toPrecision(3) : '', - }); - - // If there was only one cause, the actual, typical and by_field - // will already have been added for display. - if (causes.length > 1) { - causes.forEach((cause, index) => { - const title = - index === 0 - ? i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.causeValuesTitle', { - defaultMessage: '{causeEntityName} values', - values: { - causeEntityName: cause.entityName, - }, - }) - : ''; - const description = i18n.translate( - 'xpack.ml.anomaliesTable.anomalyDetails.causeValuesDescription', - { - defaultMessage: - '{causeEntityValue} (actual {actualValue}, ' + - 'typical {typicalValue}, probability {probabilityValue})', - values: { - causeEntityValue: cause.entityValue, - actualValue: formatValue(cause.actual, source.function), - typicalValue: formatValue(cause.typical, source.function), - probabilityValue: cause.probability, - }, - } - ); - items.push({ title, description }); - }); - } - - return items; -} -// anomalyInfluencers: [ {fieldName: fieldValue}, {fieldName: fieldValue}, ... ] -function getInfluencersItems(anomalyInfluencers, influencerFilter, numToDisplay) { - const items = []; - - for (let i = 0; i < numToDisplay; i++) { - Object.keys(anomalyInfluencers[i]).forEach((influencerFieldName) => { - const value = anomalyInfluencers[i][influencerFieldName]; - - items.push({ - title: influencerFieldName, - description: getFilterEntity(influencerFieldName, value, influencerFilter), - }); - }); - } - - return items; -} +import { getDetailsItems, getInfluencersItems } from './anomaly_details_utils'; export class AnomalyDetails extends Component { static propTypes = { @@ -518,7 +264,7 @@ export class AnomalyDetails extends Component { renderDetails() { const detailItems = getDetailsItems(this.props.anomaly, this.props.filter); - const isInterimResult = get(this.props.anomaly, 'source.is_interim', false); + const isInterimResult = this.props.anomaly.source?.is_interim ?? false; return ( diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx new file mode 100644 index 0000000000000..89eedee36f344 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/anomaly_details_utils.tsx @@ -0,0 +1,280 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip, EuiIcon } from '@elastic/eui'; +import { EntityCell, EntityCellFilter } from '../entity_cell'; +import { formatHumanReadableDateTimeSeconds } from '../../../../common/util/date_utils'; +import { + getMultiBucketImpactLabel, + showActualForFunction, + showTypicalForFunction, +} from '../../../../common/util/anomaly_utils'; +import { MULTI_BUCKET_IMPACT } from '../../../../common/constants/multi_bucket_impact'; +import { AnomaliesTableRecord } from '../../../../common/types/anomalies'; +import { formatValue } from '../../formatters/format_value'; +import { ML_JOB_AGGREGATION } from '../../../../common/constants/aggregation_types'; + +const TIME_FIELD_NAME = 'timestamp'; + +interface Cause { + typical: number[]; + actual: number[]; + probability: number; + entityName?: string; + entityValue?: string; +} + +function getFilterEntity(entityName: string, entityValue: string, filter: EntityCellFilter) { + return ; +} + +export function getInfluencersItems( + anomalyInfluencers: Array>, + influencerFilter: EntityCellFilter, + numToDisplay: number +) { + const items: Array<{ title: string; description: React.ReactElement }> = []; + for (let i = 0; i < numToDisplay; i++) { + Object.keys(anomalyInfluencers[i]).forEach((influencerFieldName) => { + const value = anomalyInfluencers[i][influencerFieldName]; + + items.push({ + title: influencerFieldName, + description: getFilterEntity(influencerFieldName, value, influencerFilter), + }); + }); + } + + return items; +} + +export function getDetailsItems(anomaly: AnomaliesTableRecord, filter: EntityCellFilter) { + const source = anomaly.source; + + // TODO - when multivariate analyses are more common, + // look in each cause for a 'correlatedByFieldValue' field, + let causes: Cause[] = []; + const sourceCauses = source.causes || []; + let singleCauseByFieldName; + let singleCauseByFieldValue; + if (sourceCauses.length === 1) { + // Metrics and probability will already have been placed at the top level. + // If cause has byFieldValue, move it to a top level fields for display. + if (sourceCauses[0].by_field_name !== undefined) { + singleCauseByFieldName = sourceCauses[0].by_field_name; + singleCauseByFieldValue = sourceCauses[0].by_field_value; + } + } else { + causes = sourceCauses.map((cause) => { + return { + typical: cause.typical, + actual: cause.actual, + probability: cause.probability, + // // Get the 'entity field name/value' to display in the cause - + // // For by and over, use by_field_name/value (over_field_name/value are in the top level fields) + // // For just an 'over' field - the over_field_name/value appear in both top level and cause. + entityName: cause.by_field_name ? cause.by_field_name : cause.over_field_name, + entityValue: cause.by_field_value ? cause.by_field_value : cause.over_field_value, + }; + }); + } + + const items = []; + if (source.partition_field_value !== undefined && source.partition_field_name !== undefined) { + items.push({ + title: source.partition_field_name, + description: getFilterEntity( + source.partition_field_name, + String(source.partition_field_value), + filter + ), + }); + } + + if (source.by_field_value !== undefined && source.by_field_name !== undefined) { + items.push({ + title: source.by_field_name, + description: getFilterEntity(source.by_field_name, source.by_field_value, filter), + }); + } + + if (singleCauseByFieldName !== undefined && singleCauseByFieldValue !== undefined) { + // Display byField of single cause. + items.push({ + title: singleCauseByFieldName, + description: getFilterEntity(singleCauseByFieldName, singleCauseByFieldValue, filter), + }); + } + + if (source.over_field_value !== undefined && source.over_field_name !== undefined) { + items.push({ + title: source.over_field_name, + description: getFilterEntity(source.over_field_name, source.over_field_value, filter), + }); + } + + const anomalyTime = source[TIME_FIELD_NAME]; + let timeDesc = `${formatHumanReadableDateTimeSeconds(anomalyTime)}`; + if (source.bucket_span !== undefined) { + const anomalyEndTime = anomalyTime + source.bucket_span * 1000; + timeDesc = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.anomalyTimeRangeLabel', { + defaultMessage: '{anomalyTime} to {anomalyEndTime}', + values: { + anomalyTime: formatHumanReadableDateTimeSeconds(anomalyTime), + anomalyEndTime: formatHumanReadableDateTimeSeconds(anomalyEndTime), + }, + }); + } + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.timeTitle', { + defaultMessage: 'Time', + }), + description: timeDesc, + }); + + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.functionTitle', { + defaultMessage: 'Function', + }), + description: + source.function !== ML_JOB_AGGREGATION.METRIC ? source.function : source.function_description, + }); + + if (source.field_name !== undefined) { + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.fieldNameTitle', { + defaultMessage: 'Field name', + }), + description: source.field_name, + }); + } + + const functionDescription = source.function_description || ''; + if (anomaly.actual !== undefined && showActualForFunction(functionDescription) === true) { + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', { + defaultMessage: 'Actual', + }), + description: formatValue(anomaly.actual, source.function, undefined, source), + }); + } + + if (anomaly.typical !== undefined && showTypicalForFunction(functionDescription) === true) { + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', { + defaultMessage: 'Typical', + }), + description: formatValue(anomaly.typical, source.function, undefined, source), + }); + } + + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.jobIdTitle', { + defaultMessage: 'Job ID', + }), + description: anomaly.jobId, + }); + + if ( + source.multi_bucket_impact !== undefined && + source.multi_bucket_impact >= MULTI_BUCKET_IMPACT.LOW + ) { + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.multiBucketImpactTitle', { + defaultMessage: 'Multi-bucket impact', + }), + description: getMultiBucketImpactLabel(source.multi_bucket_impact), + }); + } + + items.push({ + title: ( + + + {i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.recordScoreTitle', { + defaultMessage: 'Record score', + })} + + + + ), + description: Math.floor(1000 * source.record_score) / 1000, + }); + + items.push({ + title: ( + + + {i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.initialRecordScoreTitle', { + defaultMessage: 'Initial record score', + })} + + + + ), + description: Math.floor(1000 * source.initial_record_score) / 1000, + }); + + items.push({ + title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.probabilityTitle', { + defaultMessage: 'Probability', + }), + description: + // @ts-expect-error parseFloat accept take a number + source.probability !== undefined ? Number.parseFloat(source.probability).toPrecision(3) : '', + }); + + // If there was only one cause, the actual, typical and by_field + // will already have been added for display. + if (causes.length > 1) { + causes.forEach((cause, index) => { + const title = + index === 0 + ? i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.causeValuesTitle', { + defaultMessage: '{causeEntityName} values', + values: { + causeEntityName: cause.entityName, + }, + }) + : ''; + const description = i18n.translate( + 'xpack.ml.anomaliesTable.anomalyDetails.causeValuesDescription', + { + defaultMessage: + '{causeEntityValue} (actual {actualValue}, ' + + 'typical {typicalValue}, probability {probabilityValue})', + values: { + causeEntityValue: cause.entityValue, + actualValue: formatValue(cause.actual, source.function), + typicalValue: formatValue(cause.typical, source.function), + probabilityValue: cause.probability, + }, + } + ); + items.push({ title, description }); + }); + } + + return items; +}