[ML] translate anomalies table (#27802) (#28661)

[ML] translate anomalies table
This commit is contained in:
pavel06081991 2019-01-14 11:52:29 +03:00 committed by GitHub
parent 7d9b110972
commit a944705110
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 246 additions and 67 deletions

View file

@ -23,6 +23,8 @@ import {
EuiText,
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { getColumns } from './anomalies_table_columns';
import { AnomalyDetails } from './anomaly_details';
@ -158,7 +160,12 @@ class AnomaliesTable extends Component {
<EuiFlexGroup justifyContent="spaceAround">
<EuiFlexItem grow={false}>
<EuiText>
<h4>No matching anomalies found</h4>
<h4>
<FormattedMessage
id="xpack.ml.anomaliesTable.noMatchingAnomaliesFoundTitle"
defaultMessage="No matching anomalies found"
/>
</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -9,6 +9,7 @@ import 'ngreact';
import { uiModules } from 'ui/modules';
import { timefilter } from 'ui/timefilter';
import { injectI18nProvider } from '@kbn/i18n/react';
const module = uiModules.get('apps/ml', ['react']);
import { AnomaliesTable } from './anomalies_table';
@ -17,7 +18,7 @@ module.directive('mlAnomaliesTable', function ($injector) {
const reactDirective = $injector.get('reactDirective');
return reactDirective(
AnomaliesTable,
injectI18nProvider(AnomaliesTable),
[
['filter', { watchDepth: 'reference' }],
['tableData', { watchDepth: 'reference' }]

View file

@ -13,6 +13,8 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
EuiDescriptionList,
@ -113,21 +115,33 @@ function getDetailsItems(anomaly, examples, filter) {
let timeDesc = `${formatHumanReadableDateTimeSeconds(anomalyTime)}`;
if (source.bucket_span !== undefined) {
const anomalyEndTime = anomalyTime + (source.bucket_span * 1000);
timeDesc += ` to ${formatHumanReadableDateTimeSeconds(anomalyEndTime)}`;
timeDesc = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.anomalyTimeRangeLabel', {
defaultMessage: '{anomalyTime} to {anomalyEndTime}',
values: {
anomalyTime: formatHumanReadableDateTimeSeconds(anomalyTime),
anomalyEndTime: formatHumanReadableDateTimeSeconds(anomalyEndTime),
}
});
}
items.push({
title: 'time',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.timeTitle', {
defaultMessage: 'time',
}),
description: timeDesc
});
items.push({
title: 'function',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.functionTitle', {
defaultMessage: 'function',
}),
description: (source.function !== 'metric') ? source.function : source.function_description
});
if (source.field_name !== undefined) {
items.push({
title: 'fieldName',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.fieldNameTitle', {
defaultMessage: 'fieldName',
}),
description: source.field_name
});
}
@ -135,33 +149,43 @@ function getDetailsItems(anomaly, examples, filter) {
const functionDescription = source.function_description || '';
if (anomaly.actual !== undefined && showActualForFunction(functionDescription) === true) {
items.push({
title: 'actual',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.actualTitle', {
defaultMessage: 'actual',
}),
description: formatValue(anomaly.actual, source.function)
});
}
if (anomaly.typical !== undefined && showTypicalForFunction(functionDescription) === true) {
items.push({
title: 'typical',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.typicalTitle', {
defaultMessage: 'typical',
}),
description: formatValue(anomaly.typical, source.function)
});
}
items.push({
title: 'job ID',
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: 'multi-bucket impact',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.multiBucketImpactTitle', {
defaultMessage: 'multi-bucket impact',
}),
description: getMultiBucketImpactLabel(source.multi_bucket_impact)
});
}
items.push({
title: 'probability',
title: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.probabilityTitle', {
defaultMessage: 'probability',
}),
description: source.probability
});
@ -169,9 +193,22 @@ function getDetailsItems(anomaly, examples, filter) {
// will already have been added for display.
if (causes.length > 1) {
causes.forEach((cause, index) => {
const title = (index === 0) ? `${cause.entityName} values` : '';
let description = `${cause.entityValue} (actual ${formatValue(cause.actual, source.function)}, `;
description += `typical ${formatValue(cause.typical, source.function)}, probability ${cause.probability})`;
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 });
});
}
@ -190,7 +227,9 @@ export class AnomalyDetails extends Component {
if (this.props.examples !== undefined && this.props.examples.length > 0) {
this.tabs = [{
id: 'Details',
name: 'Details',
name: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.detailsTitle', {
defaultMessage: 'Details',
}),
content: (
<Fragment>
<div className="ml-anomalies-table-details">
@ -204,7 +243,9 @@ export class AnomalyDetails extends Component {
},
{
id: 'Category examples',
name: 'Category examples',
name: i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.categoryExamplesTitle', {
defaultMessage: 'Category examples',
}),
content: (
<Fragment>
{this.renderCategoryExamples()}
@ -289,28 +330,58 @@ export class AnomalyDetails extends Component {
const anomaly = this.props.anomaly;
const source = anomaly.source;
let anomalyDescription = `${getSeverity(anomaly.severity)} anomaly in ${anomaly.detector}`;
let anomalyDescription = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.anomalyInLabel', {
defaultMessage: '{anomalySeverity} anomaly in {anomalyDetector}',
values: {
anomalySeverity: getSeverity(anomaly.severity),
anomalyDetector: anomaly.detector,
}
});
if (anomaly.entityName !== undefined) {
anomalyDescription += ` found for ${anomaly.entityName} ${anomaly.entityValue}`;
anomalyDescription += i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.foundForLabel', {
defaultMessage: ' found for {anomalyEntityName} {anomalyEntityValue}',
values: {
anomalyEntityName: anomaly.entityName,
anomalyEntityValue: anomaly.entityValue,
}
});
}
if ((source.partition_field_name !== undefined) &&
(source.partition_field_name !== anomaly.entityName)) {
anomalyDescription += ` detected in ${source.partition_field_name}`;
anomalyDescription += ` ${source.partition_field_value}`;
anomalyDescription += i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.detectedInLabel', {
defaultMessage: ' detected in {sourcePartitionFieldName} {sourcePartitionFieldValue}',
values: {
sourcePartitionFieldName: source.partition_field_name,
sourcePartitionFieldValue: source.partition_field_value,
}
});
}
// Check for a correlatedByFieldValue in the source which will be present for multivariate analyses
// where the record is anomalous due to relationship with another 'by' field value.
let mvDescription = undefined;
if (source.correlated_by_field_value !== undefined) {
mvDescription = `multivariate correlations found in ${source.by_field_name}; `;
mvDescription += `${source.by_field_value} is considered anomalous given ${source.correlated_by_field_value}`;
mvDescription = i18n.translate('xpack.ml.anomaliesTable.anomalyDetails.multivariateDescription', {
defaultMessage: 'multivariate correlations found in {sourceByFieldName}; ' +
'{sourceByFieldValue} is considered anomalous given {sourceCorrelatedByFieldValue}',
values: {
sourceByFieldName: source.by_field_name,
sourceByFieldValue: source.by_field_value,
sourceCorrelatedByFieldValue: source.correlated_by_field_value,
}
});
}
return (
<React.Fragment>
<EuiText size="xs">
<h4>Description</h4>
<h4>
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.descriptionTitle"
defaultMessage="Description"
/>
</h4>
{anomalyDescription}
</EuiText>
{(mvDescription !== undefined) &&
@ -329,13 +400,29 @@ export class AnomalyDetails extends Component {
<React.Fragment>
<EuiText size="xs">
{this.props.isAggregatedData === true ? (
<h4>Details on highest severity anomaly</h4>
<h4>
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.detailsOnHighestSeverityAnomalyTitle"
defaultMessage="Details on highest severity anomaly"
/>
</h4>
) : (
<h4>Anomaly details</h4>
<h4>
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.anomalyDetailsTitle"
defaultMessage="Anomaly details"
/>
</h4>
)}
{isInterimResult === true &&
<React.Fragment>
<EuiIcon type="alert"/><span className="interim-result">Interim result</span>
<EuiIcon type="alert"/>
<span className="interim-result">
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.interimResultLabel"
defaultMessage="Interim result"
/>
</span>
</React.Fragment>
}
</EuiText>
@ -379,7 +466,12 @@ export class AnomalyDetails extends Component {
<React.Fragment>
<EuiSpacer size="m" />
<EuiText size="xs">
<h4>Influencers</h4>
<h4>
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.influencersTitle"
defaultMessage="Influencers"
/>
</h4>
</EuiText>
<EuiDescriptionList
type="column"
@ -390,14 +482,21 @@ export class AnomalyDetails extends Component {
<EuiLink
onClick={() => this.toggleAllInfluencers()}
>
and {othersCount} more
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.anomalyDescriptionListMoreLinkText"
defaultMessage="and {othersCount} more"
values={{ othersCount }}
/>
</EuiLink>
}
{numToDisplay > (this.props.influencersLimit + 1) &&
<EuiLink
onClick={() => this.toggleAllInfluencers()}
>
show less
<FormattedMessage
id="xpack.ml.anomaliesTable.anomalyDetails.anomalyDescriptionShowLessLinkText"
defaultMessage="show less"
/>
</EuiLink>
}
</React.Fragment>

View file

@ -12,6 +12,7 @@ import {
EuiIcon,
EuiToolTip
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
/*
* Component for rendering a detector cell in the anomalies table, displaying the
@ -21,7 +22,12 @@ export function DetectorCell({ detectorDescription, numberOfRules }) {
let rulesIcon;
if (numberOfRules !== undefined && numberOfRules > 0) {
rulesIcon = (
<EuiToolTip content="rules have been configured for this detector">
<EuiToolTip
content={<FormattedMessage
id="xpack.ml.anomaliesTable.detectorCell.rulesConfiguredTooltip"
defaultMessage="rules have been configured for this detector"
/>}
>
<EuiIcon
type="controlsHorizontal"
className="detector-rules-icon"

View file

@ -12,44 +12,61 @@ import {
EuiButtonIcon,
EuiToolTip
} from '@elastic/eui';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
/*
* Component for rendering an entity cell in the anomalies table, displaying the value
* of the 'partition', 'by' or 'over' field, and optionally links for adding or removing
* a filter on this entity.
*/
export function EntityCell({ entityName, entityValue, filter }) {
export const EntityCell = injectI18n(function EntityCell({ entityName, entityValue, filter, intl }) {
const valueText = (entityName !== 'mlcategory') ? entityValue : `mlcategory ${entityValue}`;
return (
<React.Fragment>
{valueText}
{filter !== undefined && entityName !== undefined && entityValue !== undefined &&
<React.Fragment>
<EuiToolTip content="Add filter">
<EuiToolTip
content={<FormattedMessage
id="xpack.ml.anomaliesTable.entityCell.addFilterTooltip"
defaultMessage="Add filter"
/>}
>
<EuiButtonIcon
size="xs"
className="filter-button"
onClick={() => filter(entityName, entityValue, '+')}
iconType="plusInCircle"
aria-label="Add filter"
aria-label={intl.formatMessage({
id: 'xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel',
defaultMessage: 'Add filter'
})}
/>
</EuiToolTip>
<EuiToolTip content="Remove filter">
<EuiToolTip
content={<FormattedMessage
id="xpack.ml.anomaliesTable.entityCell.removeFilterTooltip"
defaultMessage="Remove filter"
/>}
>
<EuiButtonIcon
size="xs"
className="filter-button"
onClick={() => filter(entityName, entityValue, '-')}
iconType="minusInCircle"
aria-label="Remove filter"
aria-label={intl.formatMessage({
id: 'xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel',
defaultMessage: 'Remove filter'
})}
/>
</EuiToolTip>
</React.Fragment>
}
</React.Fragment>
);
}
});
EntityCell.propTypes = {
EntityCell.WrappedComponent.propTypes = {
entityName: PropTypes.string,
entityValue: PropTypes.any,
filter: PropTypes.func

View file

@ -10,6 +10,8 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { EuiLink } from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
/*
* Component for rendering a list of record influencers inside a cell in the anomalies table.
@ -60,7 +62,13 @@ export class InfluencersCell extends Component {
<EuiLink
onClick={() => this.toggleAllInfluencers()}
>
and {othersCount} more
<FormattedMessage
id="xpack.ml.anomaliesTable.influencersCell.moreInfluencersLinkText"
defaultMessage="and {othersCount} more"
values={{
othersCount,
}}
/>
</EuiLink>
</div>
);
@ -70,7 +78,10 @@ export class InfluencersCell extends Component {
<EuiLink
onClick={() => this.toggleAllInfluencers()}
>
show less
<FormattedMessage
id="xpack.ml.anomaliesTable.influencersCell.showLessInfluencersLinkText"
defaultMessage="show less"
/>
</EuiLink>
</div>
);

View file

@ -17,6 +17,7 @@ import {
EuiContextMenuItem,
EuiPopover
} from '@elastic/eui';
import { injectI18n, FormattedMessage } from '@kbn/i18n/react';
import chrome from 'ui/chrome';
import { toastNotifications } from 'ui/notify';
@ -36,7 +37,16 @@ import { replaceStringTokens } from '../../util/string_utils';
/*
* Component for rendering the links menu inside a cell in the anomalies table.
*/
export class LinksMenu extends Component {
export const LinksMenu = injectI18n(class LinksMenu extends Component {
static propTypes = {
anomaly: PropTypes.object.isRequired,
showViewSeriesLink: PropTypes.bool,
isAggregatedData: PropTypes.bool,
interval: PropTypes.string,
timefilter: PropTypes.object.isRequired,
showRuleEditorFlyout: PropTypes.func
};
constructor(props) {
super(props);
@ -47,7 +57,7 @@ export class LinksMenu extends Component {
}
openCustomUrl = (customUrl) => {
const { anomaly, interval, isAggregatedData } = this.props;
const { anomaly, interval, isAggregatedData, intl } = this.props;
console.log('Anomalies Table - open customUrl for record:', anomaly);
@ -112,8 +122,12 @@ export class LinksMenu extends Component {
}).catch((resp) => {
console.log('openCustomUrl(): error loading categoryDefinition:', resp);
toastNotifications.addDanger(
`Unable to open link as an error occurred loading details on category ID ${categoryId}`);
toastNotifications.addDanger(intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.unableToOpenLinkErrorMessage',
defaultMessage: 'Unable to open link as an error occurred loading details on category ID {categoryId}'
}, {
categoryId,
}));
});
} else {
@ -126,6 +140,7 @@ export class LinksMenu extends Component {
};
viewSeries = () => {
const { intl } = this.props;
const record = this.props.anomaly.source;
const bounds = this.props.timefilter.getActiveBounds();
const from = bounds.min.toISOString(); // e.g. 2016-02-08T16:00:00.000Z
@ -160,7 +175,10 @@ export class LinksMenu extends Component {
jobIds: [record.job_id]
},
refreshInterval: {
display: 'Off',
display: intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.offLabel',
defaultMessage: 'Off'
}),
pause: false,
value: 0
},
@ -196,6 +214,7 @@ export class LinksMenu extends Component {
}
viewExamples = () => {
const { intl } = this.props;
const categoryId = this.props.anomaly.entityValue;
const record = this.props.anomaly.source;
const indexPatterns = getIndexPatterns();
@ -203,8 +222,12 @@ export class LinksMenu extends Component {
const job = mlJobService.getJob(this.props.anomaly.jobId);
if (job === undefined) {
console.log(`viewExamples(): no job found with ID: ${this.props.anomaly.jobId}`);
toastNotifications.addDanger(
`Unable to view examples as no details could be found for job ID ${this.props.anomaly.jobId}`);
toastNotifications.addDanger(intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.unableToViewExamplesErrorMessage',
defaultMessage: 'Unable to view examples as no details could be found for job ID {jobId}'
}, {
jobId: this.props.anomaly.jobId,
}));
return;
}
const categorizationFieldName = job.analysis_config.categorization_field_name;
@ -274,7 +297,10 @@ export class LinksMenu extends Component {
// Use rison to build the URL .
const _g = rison.encode({
refreshInterval: {
display: 'Off',
display: intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.offLabel',
defaultMessage: 'Off'
}),
pause: false,
value: 0
},
@ -308,8 +334,12 @@ export class LinksMenu extends Component {
}).catch((resp) => {
console.log('viewExamples(): error loading categoryDefinition:', resp);
toastNotifications.addDanger(
`Unable to view examples as an error occurred loading details on category ID ${categoryId}`);
toastNotifications.addDanger(intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.loadingDetailsErrorMessage',
defaultMessage: 'Unable to view examples as an error occurred loading details on category ID {categoryId}'
}, {
categoryId,
}));
});
}
@ -317,9 +347,14 @@ export class LinksMenu extends Component {
function error() {
console.log(`viewExamples(): error finding type of field ${categorizationFieldName} in indices:`,
datafeedIndices);
toastNotifications.addDanger(
`Unable to view examples of documents with mlcategory ${categoryId} ` +
`as no mapping could be found for the categorization field ${categorizationFieldName}`);
toastNotifications.addDanger(intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.noMappingCouldBeFoundErrorMessage',
defaultMessage: 'Unable to view examples of documents with mlcategory {categoryId} ' +
'as no mapping could be found for the categorization field {categorizationFieldName}'
}, {
categoryId,
categorizationFieldName,
}));
}
};
@ -336,7 +371,7 @@ export class LinksMenu extends Component {
};
render() {
const { anomaly, showViewSeriesLink } = this.props;
const { anomaly, showViewSeriesLink, intl } = this.props;
const canConfigureRules = (isRuleSupported(anomaly.source) && checkPermission('canUpdateJob'));
const button = (
@ -345,7 +380,10 @@ export class LinksMenu extends Component {
color="text"
onClick={this.onButtonClick}
iconType="gear"
aria-label="Select action"
aria-label={intl.formatMessage({
id: 'xpack.ml.anomaliesTable.linksMenu.selectActionAriaLabel',
defaultMessage: 'Select action',
})}
/>
);
@ -371,7 +409,10 @@ export class LinksMenu extends Component {
icon="popout"
onClick={() => { this.closePopover(); this.viewSeries(); }}
>
View series
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewSeriesLabel"
defaultMessage="View series"
/>
</EuiContextMenuItem>
);
}
@ -383,7 +424,10 @@ export class LinksMenu extends Component {
icon="popout"
onClick={() => { this.closePopover(); this.viewExamples(); }}
>
View examples
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.viewExamplesLabel"
defaultMessage="View examples"
/>
</EuiContextMenuItem>
);
}
@ -395,7 +439,10 @@ export class LinksMenu extends Component {
icon="controlsHorizontal"
onClick={() => { this.closePopover(); this.props.showRuleEditorFlyout(anomaly); }}
>
Configure rules
<FormattedMessage
id="xpack.ml.anomaliesTable.linksMenu.configureRulesLabel"
defaultMessage="Configure rules"
/>
</EuiContextMenuItem>
);
}
@ -415,13 +462,4 @@ export class LinksMenu extends Component {
</EuiPopover>
);
}
}
LinksMenu.propTypes = {
anomaly: PropTypes.object.isRequired,
showViewSeriesLink: PropTypes.bool,
isAggregatedData: PropTypes.bool,
interval: PropTypes.string,
timefilter: PropTypes.object.isRequired,
showRuleEditorFlyout: PropTypes.func
};
});