[ML] Allow filtering by mlcategory in Anomaly Explorer Influencers list (#90282)
* [ML] Allow filtering by mlcategory in Anomaly Explorer Influencers list * [ML] Use getFormattedSeverityScore for formatting anomaly scores
This commit is contained in:
parent
ae609c4aea
commit
d3fd7bb7ca
11 changed files with 126 additions and 102 deletions
|
@ -249,7 +249,6 @@ export function getColumns(
|
|||
name: i18n.translate('xpack.ml.anomaliesTable.categoryExamplesColumnName', {
|
||||
defaultMessage: 'category examples',
|
||||
}),
|
||||
sortable: false,
|
||||
truncateText: true,
|
||||
render: (item) => {
|
||||
const examples = get(examplesByJobId, [item.jobId, item.entityValue], []);
|
||||
|
@ -268,7 +267,6 @@ export function getColumns(
|
|||
</EuiLink>
|
||||
);
|
||||
},
|
||||
textOnly: true,
|
||||
width: '13%',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,10 @@
|
|||
import React, { FC, memo } from 'react';
|
||||
import { EuiHealth, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { MULTI_BUCKET_IMPACT } from '../../../../../common/constants/multi_bucket_impact';
|
||||
import { getSeverityColor } from '../../../../../common/util/anomaly_utils';
|
||||
import {
|
||||
getSeverityColor,
|
||||
getFormattedSeverityScore,
|
||||
} from '../../../../../common/util/anomaly_utils';
|
||||
|
||||
interface SeverityCellProps {
|
||||
/**
|
||||
|
@ -27,7 +30,7 @@ interface SeverityCellProps {
|
|||
* Renders anomaly severity score with single or multi-bucket impact marker.
|
||||
*/
|
||||
export const SeverityCell: FC<SeverityCellProps> = memo(({ score, multiBucketImpact }) => {
|
||||
const severity = score >= 1 ? Math.floor(score) : '< 1';
|
||||
const severity = getFormattedSeverityScore(score);
|
||||
const color = getSeverityColor(score);
|
||||
const isMultiBucket = multiBucketImpact >= MULTI_BUCKET_IMPACT.MEDIUM;
|
||||
return isMultiBucket ? (
|
||||
|
|
|
@ -5,8 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiText, EuiToolTip } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
@ -14,7 +13,21 @@ import { i18n } from '@kbn/i18n';
|
|||
import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control';
|
||||
import { MLCATEGORY } from '../../../../common/constants/field_types';
|
||||
|
||||
function getAddFilter({ entityName, entityValue, filter }) {
|
||||
export type EntityCellFilter = (
|
||||
entityName: string,
|
||||
entityValue: string,
|
||||
direction: '+' | '-'
|
||||
) => void;
|
||||
|
||||
interface EntityCellProps {
|
||||
entityName: string;
|
||||
entityValue: string;
|
||||
filter?: EntityCellFilter;
|
||||
wrapText?: boolean;
|
||||
}
|
||||
|
||||
function getAddFilter({ entityName, entityValue, filter }: EntityCellProps) {
|
||||
if (filter !== undefined) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
|
@ -36,8 +49,10 @@ function getAddFilter({ entityName, entityValue, filter }) {
|
|||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getRemoveFilter({ entityName, entityValue, filter }) {
|
||||
function getRemoveFilter({ entityName, entityValue, filter }: EntityCellProps) {
|
||||
if (filter !== undefined) {
|
||||
return (
|
||||
<EuiToolTip
|
||||
content={
|
||||
|
@ -59,18 +74,19 @@ function getRemoveFilter({ entityName, entityValue, filter }) {
|
|||
</EuiToolTip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Component for rendering an entity, displaying the value
|
||||
* of the entity, such as a partitioning or influencer field value, and optionally links for
|
||||
* adding or removing a filter on this entity.
|
||||
*/
|
||||
export const EntityCell = function EntityCell({
|
||||
export const EntityCell: FC<EntityCellProps> = ({
|
||||
entityName,
|
||||
entityValue,
|
||||
filter,
|
||||
wrapText = false,
|
||||
}) {
|
||||
}) => {
|
||||
let valueText = entityValue === '' ? <i>{EMPTY_FIELD_VALUE_LABEL}</i> : entityValue;
|
||||
if (entityName === MLCATEGORY) {
|
||||
valueText = `${MLCATEGORY} ${valueText}`;
|
||||
|
@ -117,10 +133,3 @@ export const EntityCell = function EntityCell({
|
|||
);
|
||||
}
|
||||
};
|
||||
|
||||
EntityCell.propTypes = {
|
||||
entityName: PropTypes.string,
|
||||
entityValue: PropTypes.any,
|
||||
filter: PropTypes.func,
|
||||
wrapText: PropTypes.bool,
|
||||
};
|
|
@ -5,4 +5,4 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
export { EntityCell } from './entity_cell';
|
||||
export { EntityCell, EntityCellFilter } from './entity_cell';
|
|
@ -9,17 +9,39 @@
|
|||
* React component for rendering a list of Machine Learning influencers.
|
||||
*/
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiSpacer, EuiTitle, EuiToolTip } from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
|
||||
import { abbreviateWholeNumber } from '../../formatters/abbreviate_whole_number';
|
||||
import { getSeverity } from '../../../../common/util/anomaly_utils';
|
||||
import { EntityCell } from '../entity_cell';
|
||||
import { getSeverity, getFormattedSeverityScore } from '../../../../common/util/anomaly_utils';
|
||||
import { EntityCell, EntityCellFilter } from '../entity_cell';
|
||||
|
||||
function getTooltipContent(maxScoreLabel, totalScoreLabel) {
|
||||
interface InfluencerValueData {
|
||||
influencerFieldValue: string;
|
||||
maxAnomalyScore: number;
|
||||
sumAnomalyScore: number;
|
||||
}
|
||||
|
||||
interface InfluencerProps {
|
||||
influencerFieldName: string;
|
||||
influencerFilter: EntityCellFilter;
|
||||
valueData: InfluencerValueData;
|
||||
}
|
||||
|
||||
interface InfluencersByNameProps {
|
||||
influencerFieldName: string;
|
||||
influencerFilter: EntityCellFilter;
|
||||
fieldValues: InfluencerValueData[];
|
||||
}
|
||||
|
||||
interface InfluencersListProps {
|
||||
influencers: { [id: string]: InfluencerValueData[] };
|
||||
influencerFilter: EntityCellFilter;
|
||||
}
|
||||
|
||||
function getTooltipContent(maxScoreLabel: string, totalScoreLabel: string) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<p>
|
||||
|
@ -40,13 +62,12 @@ function getTooltipContent(maxScoreLabel, totalScoreLabel) {
|
|||
);
|
||||
}
|
||||
|
||||
function Influencer({ influencerFieldName, influencerFilter, valueData }) {
|
||||
const maxScorePrecise = valueData.maxAnomalyScore;
|
||||
const maxScore = parseInt(maxScorePrecise);
|
||||
const maxScoreLabel = maxScore !== 0 ? maxScore : '< 1';
|
||||
const Influencer: FC<InfluencerProps> = ({ influencerFieldName, influencerFilter, valueData }) => {
|
||||
const maxScore = Math.floor(valueData.maxAnomalyScore);
|
||||
const maxScoreLabel = getFormattedSeverityScore(valueData.maxAnomalyScore);
|
||||
const severity = getSeverity(maxScore);
|
||||
const totalScore = parseInt(valueData.sumAnomalyScore);
|
||||
const totalScoreLabel = totalScore !== 0 ? totalScore : '< 1';
|
||||
const totalScore = Math.floor(valueData.sumAnomalyScore);
|
||||
const totalScoreLabel = getFormattedSeverityScore(valueData.sumAnomalyScore);
|
||||
|
||||
// Ensure the bar has some width for 0 scores.
|
||||
const barScore = maxScore !== 0 ? maxScore : 1;
|
||||
|
@ -59,17 +80,13 @@ function Influencer({ influencerFieldName, influencerFilter, valueData }) {
|
|||
return (
|
||||
<div data-test-subj={`mlInfluencerEntry field-${influencerFieldName}`}>
|
||||
<div className="field-label" data-test-subj="mlInfluencerEntryFieldLabel">
|
||||
{influencerFieldName !== 'mlcategory' ? (
|
||||
<EntityCell
|
||||
entityName={influencerFieldName}
|
||||
entityValue={valueData.influencerFieldValue}
|
||||
filter={influencerFilter}
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">mlcategory {valueData.influencerFieldValue}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`progress ${severity.id}`} value="{valueData.maxAnomalyScore}" max="100">
|
||||
<div className={`progress ${severity.id}`}>
|
||||
<div className="progress-bar-holder">
|
||||
<div className="progress-bar" style={barStyle} />
|
||||
</div>
|
||||
|
@ -96,14 +113,13 @@ function Influencer({ influencerFieldName, influencerFilter, valueData }) {
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Influencer.propTypes = {
|
||||
influencerFieldName: PropTypes.string.isRequired,
|
||||
influencerFilter: PropTypes.func,
|
||||
valueData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
function InfluencersByName({ influencerFieldName, influencerFilter, fieldValues }) {
|
||||
const InfluencersByName: FC<InfluencersByNameProps> = ({
|
||||
influencerFieldName,
|
||||
influencerFilter,
|
||||
fieldValues,
|
||||
}) => {
|
||||
const influencerValues = fieldValues.map((valueData) => (
|
||||
<Influencer
|
||||
key={valueData.influencerFieldValue}
|
||||
|
@ -122,14 +138,9 @@ function InfluencersByName({ influencerFieldName, influencerFilter, fieldValues
|
|||
{influencerValues}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
InfluencersByName.propTypes = {
|
||||
influencerFieldName: PropTypes.string.isRequired,
|
||||
influencerFilter: PropTypes.func,
|
||||
fieldValues: PropTypes.array.isRequired,
|
||||
};
|
||||
|
||||
export function InfluencersList({ influencers, influencerFilter }) {
|
||||
export const InfluencersList: FC<InfluencersListProps> = ({ influencers, influencerFilter }) => {
|
||||
if (influencers === undefined || Object.keys(influencers).length === 0) {
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceAround" className="ml-influencers-list">
|
||||
|
@ -158,8 +169,4 @@ export function InfluencersList({ influencers, influencerFilter }) {
|
|||
));
|
||||
|
||||
return <div className="ml-influencers-list">{influencersByName}</div>;
|
||||
}
|
||||
InfluencersList.propTypes = {
|
||||
influencers: PropTypes.object,
|
||||
influencerFilter: PropTypes.func,
|
||||
};
|
|
@ -20,7 +20,11 @@ import moment from 'moment';
|
|||
|
||||
import { formatHumanReadableDateTime } from '../../../../common/util/date_utils';
|
||||
import { formatValue } from '../../formatters/format_value';
|
||||
import { getSeverityColor, getSeverityWithLow } from '../../../../common/util/anomaly_utils';
|
||||
import {
|
||||
getFormattedSeverityScore,
|
||||
getSeverityColor,
|
||||
getSeverityWithLow,
|
||||
} from '../../../../common/util/anomaly_utils';
|
||||
import {
|
||||
getChartType,
|
||||
getTickValues,
|
||||
|
@ -458,7 +462,7 @@ export class ExplorerChartDistribution extends React.Component {
|
|||
|
||||
if (marker.anomalyScore !== undefined) {
|
||||
const score = parseInt(marker.anomalyScore);
|
||||
const displayScore = score > 0 ? score : '< 1';
|
||||
const displayScore = getFormattedSeverityScore(score);
|
||||
tooltipData.push({
|
||||
label: i18n.translate('xpack.ml.explorer.distributionChart.anomalyScoreLabel', {
|
||||
defaultMessage: 'anomaly score',
|
||||
|
|
|
@ -21,6 +21,7 @@ import { i18n } from '@kbn/i18n';
|
|||
import { formatHumanReadableDateTime } from '../../../../common/util/date_utils';
|
||||
import { formatValue } from '../../formatters/format_value';
|
||||
import {
|
||||
getFormattedSeverityScore,
|
||||
getSeverityColor,
|
||||
getSeverityWithLow,
|
||||
getMultiBucketImpactLabel,
|
||||
|
@ -380,12 +381,11 @@ export class ExplorerChartSingleMetric extends React.Component {
|
|||
|
||||
if (marker.anomalyScore !== undefined) {
|
||||
const score = parseInt(marker.anomalyScore);
|
||||
const displayScore = score > 0 ? score : '< 1';
|
||||
tooltipData.push({
|
||||
label: i18n.translate('xpack.ml.explorer.singleMetricChart.anomalyScoreLabel', {
|
||||
defaultMessage: 'anomaly score',
|
||||
}),
|
||||
value: displayScore,
|
||||
value: getFormattedSeverityScore(score),
|
||||
color: getSeverityColor(score),
|
||||
seriesIdentifier: {
|
||||
key: seriesKey,
|
||||
|
|
|
@ -30,7 +30,10 @@ import { StatsBar, JobStatsBarStats } from '../../../components/stats_bar';
|
|||
// @ts-ignore
|
||||
import { JobSelectorBadge } from '../../../components/job_selector/job_selector_badge/index';
|
||||
import { toLocaleString } from '../../../util/string_utils';
|
||||
import { getSeverityColor } from '../../../../../common/util/anomaly_utils';
|
||||
import {
|
||||
getFormattedSeverityScore,
|
||||
getSeverityColor,
|
||||
} from '../../../../../common/util/anomaly_utils';
|
||||
|
||||
// Used to pass on attribute names to table columns
|
||||
export enum AnomalyDetectionListColumns {
|
||||
|
@ -125,7 +128,7 @@ export const AnomalyDetectionTable: FC<Props> = ({ items, jobsList, statsBarData
|
|||
return (
|
||||
// @ts-ignore
|
||||
<EuiHealth color={color} compressed="true">
|
||||
{score >= 1 ? Math.floor(score) : '< 1'}
|
||||
{getFormattedSeverityScore(score)}
|
||||
</EuiHealth>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ import moment from 'moment';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
getFormattedSeverityScore,
|
||||
getSeverityWithLow,
|
||||
getMultiBucketImpactLabel,
|
||||
} from '../../../../../common/util/anomaly_utils';
|
||||
|
@ -1442,12 +1443,11 @@ class TimeseriesChartIntl extends Component {
|
|||
|
||||
if (marker.anomalyScore !== undefined) {
|
||||
const score = parseInt(marker.anomalyScore);
|
||||
const displayScore = score > 0 ? score : '< 1';
|
||||
tooltipData.push({
|
||||
label: i18n.translate('xpack.ml.timeSeriesExplorer.timeSeriesChart.anomalyScoreLabel', {
|
||||
defaultMessage: 'anomaly score',
|
||||
}),
|
||||
value: displayScore,
|
||||
value: getFormattedSeverityScore(score),
|
||||
color: anomalyColorScale(score),
|
||||
seriesIdentifier: {
|
||||
key: seriesKey,
|
||||
|
|
Loading…
Reference in a new issue