Skip to content

Commit

Permalink
[ML] Converting the anomaly details functions to typescript (#142769)
Browse files Browse the repository at this point in the history
  • Loading branch information
jgowdyelastic authored Oct 6, 2022
1 parent ad95c57 commit da265da
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 257 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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 <EntityCell entityName={entityName} entityValue={entityValue} filter={filter} />;
}

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: (
<EuiToolTip
position="left"
content={i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.recordScoreTooltip', {
defaultMessage:
'A normalized score between 0-100, which indicates the relative significance of the anomaly record result. This value might change as new data is analyzed.',
})}
>
<span>
{i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.recordScoreTitle', {
defaultMessage: 'Record score',
})}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
description: Math.floor(1000 * source.record_score) / 1000,
});

items.push({
title: (
<EuiToolTip
position="left"
content={i18n.translate(
'xpack.ml.anomaliesTable.anomalyDetails.initialRecordScoreTooltip',
{
defaultMessage:
'A normalized score between 0-100, which indicates the relative significance of the anomaly record when the bucket was initially processed.',
}
)}
>
<span>
{i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.initialRecordScoreTitle', {
defaultMessage: 'Initial record score',
})}
<EuiIcon size="s" color="subdued" type="questionInCircle" className="eui-alignTop" />
</span>
</EuiToolTip>
),
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 = {
Expand Down Expand Up @@ -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 (
<React.Fragment>
<EuiText size="xs">
Expand Down
Loading

0 comments on commit da265da

Please sign in to comment.