[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:
Pete Harverson 2021-02-05 16:32:49 +00:00 committed by GitHub
parent ae609c4aea
commit d3fd7bb7ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 126 additions and 102 deletions

View file

@ -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%',
});
}

View file

@ -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 ? (

View file

@ -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,50 +13,67 @@ 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 }) {
return (
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.anomaliesTable.entityCell.addFilterTooltip"
defaultMessage="Add filter"
/>
}
>
<EuiButtonIcon
size="s"
className="filter-button"
onClick={() => filter(entityName, entityValue, '+')}
iconType="plusInCircle"
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel', {
defaultMessage: 'Add filter',
})}
/>
</EuiToolTip>
);
export type EntityCellFilter = (
entityName: string,
entityValue: string,
direction: '+' | '-'
) => void;
interface EntityCellProps {
entityName: string;
entityValue: string;
filter?: EntityCellFilter;
wrapText?: boolean;
}
function getRemoveFilter({ entityName, entityValue, filter }) {
return (
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.anomaliesTable.entityCell.removeFilterTooltip"
defaultMessage="Remove filter"
function getAddFilter({ entityName, entityValue, filter }: EntityCellProps) {
if (filter !== undefined) {
return (
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.anomaliesTable.entityCell.addFilterTooltip"
defaultMessage="Add filter"
/>
}
>
<EuiButtonIcon
size="s"
className="filter-button"
onClick={() => filter(entityName, entityValue, '+')}
iconType="plusInCircle"
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel', {
defaultMessage: 'Add filter',
})}
/>
}
>
<EuiButtonIcon
size="s"
className="filter-button"
onClick={() => filter(entityName, entityValue, '-')}
iconType="minusInCircle"
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel', {
defaultMessage: 'Remove filter',
})}
/>
</EuiToolTip>
);
</EuiToolTip>
);
}
}
function getRemoveFilter({ entityName, entityValue, filter }: EntityCellProps) {
if (filter !== undefined) {
return (
<EuiToolTip
content={
<FormattedMessage
id="xpack.ml.anomaliesTable.entityCell.removeFilterTooltip"
defaultMessage="Remove filter"
/>
}
>
<EuiButtonIcon
size="s"
className="filter-button"
onClick={() => filter(entityName, entityValue, '-')}
iconType="minusInCircle"
aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel', {
defaultMessage: 'Remove filter',
})}
/>
</EuiToolTip>
);
}
}
/*
@ -65,12 +81,12 @@ function getRemoveFilter({ entityName, entityValue, filter }) {
* 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,
};

View file

@ -5,4 +5,4 @@
* 2.0.
*/
export { EntityCell } from './entity_cell';
export { EntityCell, EntityCellFilter } from './entity_cell';

View file

@ -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>
)}
<EntityCell
entityName={influencerFieldName}
entityValue={valueData.influencerFieldValue}
filter={influencerFilter}
/>
</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,
};

View file

@ -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',

View file

@ -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,

View file

@ -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>
);
}

View file

@ -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,