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;
+}