[i18n] Translate ML - New Job - components (#27587)

* Translate new_job -> components

* Resolve review comments

* Update snapshot
This commit is contained in:
Nox911 2019-01-09 16:25:55 +03:00 committed by GitHub
parent 5f7c9c077f
commit 3faff023c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 333 additions and 107 deletions

View file

@ -5,7 +5,13 @@ exports[`BucketSpanEstimator renders the button 1`] = `
className="bucket-span-estimator"
>
<EuiToolTip
content="Experimental feature for estimating bucket span."
content={
<FormattedMessage
defaultMessage="Experimental feature for estimating bucket span."
id="xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonTooltip"
values={Object {}}
/>
}
delay="regular"
position="bottom"
>
@ -30,7 +36,13 @@ exports[`BucketSpanEstimator renders the loading button 1`] = `
className="bucket-span-estimator"
>
<EuiToolTip
content="Experimental feature for estimating bucket span."
content={
<FormattedMessage
defaultMessage="Experimental feature for estimating bucket span."
id="xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonTooltip"
values={Object {}}
/>
}
delay="regular"
position="bottom"
>

View file

@ -11,10 +11,12 @@ import { BucketSpanEstimator } from './bucket_span_estimator_view';
import { EVENT_RATE_COUNT_FIELD } from 'plugins/ml/jobs/new_job/simple/components/constants/general';
import { ml } from 'plugins/ml/services/ml_api_service';
import { I18nProvider } from '@kbn/i18n/react';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlBucketSpanEstimator', function () {
module.directive('mlBucketSpanEstimator', function (i18n) {
return {
restrict: 'AE',
replace: false,
@ -115,7 +117,13 @@ module.directive('mlBucketSpanEstimator', function () {
$scope.ui.bucketSpanEstimator.status === STATUS.RUNNING
);
const estimatorRunning = ($scope.ui.bucketSpanEstimator.status === STATUS.RUNNING);
const buttonText = (estimatorRunning) ? 'Estimating bucket span' : 'Estimate bucket span';
const buttonText = (estimatorRunning)
? i18n('xpack.ml.newJob.simple.bucketSpanEstimator.estimatingBucketSpanButtonLabel', {
defaultMessage: 'Estimating bucket span'
})
: i18n('xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonLabel', {
defaultMessage: 'Estimate bucket span'
});
const props = {
buttonDisabled,
@ -125,7 +133,9 @@ module.directive('mlBucketSpanEstimator', function () {
};
ReactDOM.render(
React.createElement(BucketSpanEstimator, props),
<I18nProvider>
{React.createElement(BucketSpanEstimator, props)}
</I18nProvider>,
$element[0]
);
}

View file

@ -11,12 +11,16 @@ import {
EuiButton,
EuiToolTip
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
export function BucketSpanEstimator({ buttonDisabled, buttonText, estimatorRunning, guessBucketSpan }) {
return (
<div className="bucket-span-estimator">
<EuiToolTip
content="Experimental feature for estimating bucket span."
content={<FormattedMessage
id="xpack.ml.newJob.simple.bucketSpanEstimator.estimateBucketSpanButtonTooltip"
defaultMessage="Experimental feature for estimating bucket span."
/>}
position="bottom"
>
<EuiButton

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { shallow } from 'enzyme';
import { shallowWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { BucketSpanEstimator } from './bucket_span_estimator_view';
@ -18,7 +18,7 @@ describe('BucketSpanEstimator', () => {
guessBucketSpan: () => { },
buttonText: 'Estimate bucket span'
};
const wrapper = shallow(<BucketSpanEstimator {...props} />);
const wrapper = shallowWithIntl(<BucketSpanEstimator {...props} />);
expect(wrapper).toMatchSnapshot();
});
@ -29,7 +29,7 @@ describe('BucketSpanEstimator', () => {
guessBucketSpan: () => { },
buttonText: 'Estimating bucket span'
};
const wrapper = shallow(<BucketSpanEstimator {...props} />);
const wrapper = shallowWithIntl(<BucketSpanEstimator {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View file

@ -1,5 +1,10 @@
<div class='bucket-span-selection'>
<h4 class="euiTitle euiTitle--small" id="ml_aria_label_new_job_bucketspan">Bucket span</h4><i ml-info-icon="new_job_bucketspan" />
<h4
class="euiTitle euiTitle--small"
id="ml_aria_label_new_job_bucketspan"
i18n-id="xpack.ml.newJob.simple.bucketSpanSelection.bucketSpanTitle"
i18n-default-message="Bucket span"
></h4><i ml-info-icon="new_job_bucketspan" />
<div class="euiSpacer euiSpacer--s"></div>
<div class="row">
<div class="col-md-12">
@ -26,7 +31,12 @@
</ml-bucket-span-estimator>
</div>
<div ng-hide="ui.bucketSpanValid" class="validation-error">Invalid interval format</div>
<div
ng-hide="ui.bucketSpanValid"
class="validation-error"
i18n-id="xpack.ml.newJob.simple.bucketSpanSelection.invalidIntervalFormatLabel"
i18n-default-message="Invalid interval format"
></div>
<div ng-show="ui.bucketSpanEstimator.status===-1" class="validation-error">{{ui.bucketSpanEstimator.message}}</div>
</div>
</div>

View file

@ -5,7 +5,7 @@
*/
import React from 'react';
import { mount } from 'enzyme';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js';
const defaultProps = {
@ -18,7 +18,7 @@ const defaultProps = {
describe('EnableModelPlotCheckbox', () => {
test('checkbox default is rendered correctly', () => {
const wrapper = mount(<EnableModelPlotCheckbox {...defaultProps} />);
const wrapper = mountWithIntl(<EnableModelPlotCheckbox {...defaultProps} />);
const checkbox = wrapper.find({ type: 'checkbox' });
const label = wrapper.find('label');
@ -30,7 +30,7 @@ describe('EnableModelPlotCheckbox', () => {
const mockOnChange = jest.fn();
defaultProps.onCheckboxChange = mockOnChange;
const wrapper = mount(<EnableModelPlotCheckbox {...defaultProps} />);
const wrapper = mountWithIntl(<EnableModelPlotCheckbox {...defaultProps} />);
const checkbox = wrapper.find({ type: 'checkbox' });
checkbox.simulate('change', { target: { checked: true } });

View file

@ -11,10 +11,12 @@ import { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js';
import { ml } from '../../../../../services/ml_api_service';
import { checkCardinalitySuccess } from '../../../utils/new_job_utils';
import { I18nProvider } from '@kbn/i18n/react';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlEnableModelPlotCheckbox', function () {
module.directive('mlEnableModelPlotCheckbox', function (i18n) {
return {
restrict: 'AE',
replace: false,
@ -35,10 +37,12 @@ module.directive('mlEnableModelPlotCheckbox', function () {
function errorHandler(error) {
console.log('Cardinality could not be validated', error);
$scope.ui.cardinalityValidator.status = STATUS.FAILED;
$scope.ui.cardinalityValidator.message = `An error occurred validating the configuration
for running the job with model plot enabled.
Creating model plots can be resource intensive and not recommended where the cardinality of the selected fields is high.
You may want to select a dedicated results index on the Job Details tab.`;
$scope.ui.cardinalityValidator.message = i18n('xpack.ml.newJob.simple.enableModelPlot.validatingConfigurationErrorMessage', {
defaultMessage: 'An error occurred validating the configuration' +
'for running the job with model plot enabled.' +
'Creating model plots can be resource intensive and not recommended where the cardinality of the selected fields is high.' +
'You may want to select a dedicated results index on the Job Details tab.'
});
// Go ahead and check the dedicated index box for them
$scope.formConfig.useDedicatedIndex = true;
}
@ -57,11 +61,14 @@ module.directive('mlEnableModelPlotCheckbox', function () {
if (validationResult.success === true) {
$scope.formConfig.enableModelPlot = true;
$scope.ui.cardinalityValidator.status = STATUS.FINISHED;
} else {
$scope.ui.cardinalityValidator.message = `Creating model plots is resource intensive and not recommended
where the cardinality of the selected fields is greater than 100. Estimated cardinality
for this job is ${validationResult.highCardinality}.
If you enable model plot with this configuration we recommend you use a dedicated results index.`;
$scope.ui.cardinalityValidator.message = i18n('xpack.ml.newJob.simple.enableModelPlot.enableModelPlotDescription', {
defaultMessage: 'Creating model plots is resource intensive and not recommended' +
'where the cardinality of the selected fields is greater than 100. Estimated cardinality' +
'for this job is {highCardinality}.' +
'If you enable model plot with this configuration we recommend you use a dedicated results index.',
values: { highCardinality: validationResult.highCardinality }
});
$scope.ui.cardinalityValidator.status = STATUS.WARNING;
// Go ahead and check the dedicated index box for them
@ -115,7 +122,13 @@ module.directive('mlEnableModelPlotCheckbox', function () {
($scope.ui.cardinalityValidator.status === STATUS.WARNING ||
$scope.ui.cardinalityValidator.status === STATUS.FAILED) &&
$scope.ui.formValid === true);
const checkboxText = (validatorRunning) ? 'Validating cardinality...' : 'Enable model plot';
const checkboxText = (validatorRunning)
? i18n('xpack.ml.newJob.simple.enableModelPlot.validatingCardinalityLabel', {
defaultMessage: 'Validating cardinality…'
})
: i18n('xpack.ml.newJob.simple.enableModelPlot.enableModelPlotLabel', {
defaultMessage: 'Enable model plot'
});
const props = {
checkboxDisabled,
@ -126,7 +139,9 @@ module.directive('mlEnableModelPlotCheckbox', function () {
};
ReactDOM.render(
React.createElement(EnableModelPlotCheckbox, props),
<I18nProvider>
{React.createElement(EnableModelPlotCheckbox, props)}
</I18nProvider>,
$element[0]
);
}

View file

@ -18,6 +18,7 @@ import {
import { JsonTooltip } from '../../../../../components/json_tooltip/json_tooltip';
import { FormattedMessage } from '@kbn/i18n/react';
export class EnableModelPlotCheckbox extends Component {
constructor(props) {
@ -28,7 +29,10 @@ export class EnableModelPlotCheckbox extends Component {
};
}
warningTitle = 'Proceed with caution!';
warningTitle = (<FormattedMessage
id="xpack.ml.newJob.simple.enableModelPlot.proceedWithCautionWarningTitle"
defaultMessage="Proceed with caution!"
/>);
onChange = (e) => {
this.setState({

View file

@ -1,5 +1,9 @@
<div class='fields-selection'>
<h4 class="euiTitle euiTitle--small">Fields</h4>
<h4
class="euiTitle euiTitle--small"
i18n-id="xpack.ml.newJob.simple.fieldsSelection.fieldsTitle"
i18n-default-message="Fields"
></h4>
<div class="euiSpacer euiSpacer--s"></div>
<div class="row">
<div class="col-md-12">
@ -43,7 +47,11 @@
ng-disabled="ui.formValid === false || jobState === JOB_STATE.RUNNING || jobState === JOB_STATE.STOPPING || jobState === JOB_STATE.FINISHED"
ng-model ="formConfig.isSparseData" />
<span class='kuiCheckBoxLabel__text'>
Sparse data <i ml-info-icon="new_job_sparsedata" tooltip-append-to-body="true"></i>
<span
i18n-id="xpack.ml.newJob.simple.fieldsSelection.sparseDataLabel"
i18n-default-message="Sparse data"
></span>
<i ml-info-icon="new_job_sparsedata" tooltip-append-to-body="true"></i>
</span>
</label>
</span>

View file

@ -1,5 +1,9 @@
<div class='fields-selection-population'>
<h4 class="euiTitle euiTitle--small">Fields</h4>
<h4
class="euiTitle euiTitle--small"
i18n-id="xpack.ml.newJob.simple.fieldsSelectionPopulation.fieldsTitle"
i18n-default-message="Fields"
></h4>
<div class="euiSpacer euiSpacer--s"></div>
<div class="row">
<div class="col-md-12">
@ -50,7 +54,7 @@
</select>
</div>
<button
aria-label="Remove Detector"
aria-label="{{ ::'xpack.ml.newJob.simple.fieldsSelectionPopulation.removeDetectorButtonAriaLabel' | i18n: {defaultMessage: 'Remove Detector'} }}"
ng-click="removeField($index, field)"
tooltip-append-to-body="true"
ng-disabled="formConfig.fields[$index] === undefined || jobState === JOB_STATE.RUNNING || jobState === JOB_STATE.STOPPING || jobState === JOB_STATE.FINISHED"

View file

@ -1,13 +1,15 @@
<div class="general-job-details">
<div class="form-group">
<ml-form-label label-id="new_job_id" tooltip-append-to-body="true">Name</ml-form-label>
<ml-form-label label-id="new_job_id" tooltip-append-to-body="true">
{{ ::'xpack.ml.newJob.simple.generalJobDetails.nameLabel' | i18n: {defaultMessage: 'Name'} }}
</ml-form-label>
<input
aria-labelledby="ml_aria_label_new_job_id"
aria-describedby="ml_aria_description_new_job_id"
id="job-id-input"
ng-model="formConfig.jobId"
required
placeholder="Job ID"
placeholder="{{ ::'xpack.ml.newJob.simple.generalJobDetails.jobIdPlaceholder' | i18n: {defaultMessage: 'Job ID'} }}"
ng-change="changeJobIDCase(formConfig)"
ng-disabled="jobState === JOB_STATE.RUNNING || jobState === JOB_STATE.STOPPING || jobState === JOB_STATE.FINISHED"
class="form-control lowercase" />
@ -15,17 +17,21 @@
</div>
<div class="form-group">
<ml-form-label label-id="new_job_description">Description</ml-form-label>
<ml-form-label label-id="new_job_description">
{{ ::'xpack.ml.newJob.simple.generalJobDetails.descriptionLabel' | i18n: {defaultMessage: 'Description'} }}
</ml-form-label>
<input
aria-labelledby="ml_aria_label_new_job_description"
aria-describedby="ml_aria_description_new_job_description"
ng-model="formConfig.description"
placeholder="Job description"
placeholder="{{ ::'xpack.ml.newJob.simple.generalJobDetails.jobDescriptionPlaceholder' | i18n: {defaultMessage: 'Job description'} }}"
ng-disabled="jobState === JOB_STATE.RUNNING || jobState === JOB_STATE.STOPPING || jobState === JOB_STATE.FINISHED"
class="form-control" />
</div>
<div class="form-group">
<ml-form-label label-id="new_job_group">Job Groups</ml-form-label>
<ml-form-label label-id="new_job_group">
{{ ::'xpack.ml.newJob.simple.generalJobDetails.jobGroupsLabel' | i18n: {defaultMessage: 'Job Groups'} }}
</ml-form-label>
<ml-job-group-select
aria-labelledby="ml_aria_label_new_job_group"
aria-describedby="ml_aria_description_new_job_group"
@ -37,13 +43,18 @@
<div class="form-group">
<div ng-click="ui.showAdvanced = (!ui.showAdvanced || formConfig.useDedicatedIndex)" class="advanced-button-container">
<button
aria-label="{{ ui.showAdvanced ? 'Hide Advanced' : 'Show Advanced' }}"
aria-label="{{ ui.showAdvanced ? hideAdvancedButtonAriaLabel : showAdvancedButtonAriaLabel }}"
ng-disabled="formConfig.useDedicatedIndex"
type="button"
class="kuiButton kuiButton--small kuiButton--hollow advanced-button">
<i aria-hidden="true" ng-class="{ 'fa-caret-down': ui.showAdvanced, 'fa-caret-right': !ui.showAdvanced }" class="fa"></i>
</button>
<label class="kuiFormLabel" aria-describedby="ml_aria_description_new_job_advanced_settings">Advanced</label>
<label
class="kuiFormLabel"
aria-describedby="ml_aria_description_new_job_advanced_settings"
i18n-id="xpack.ml.newJob.simple.generalJobDetails.advancedLabel"
i18n-default-message="Advanced"
></label>
<i ml-info-icon="new_job_advanced_settings" ></i>
</div>
<div class='advanced-group' ng-show="ui.showAdvanced">
@ -62,16 +73,22 @@
class='kuiCheckBox'
ng-model ="formConfig.useDedicatedIndex" />
<span class='kuiCheckBoxLabel__text dedicated-index-label'>
<span id="ml_aria_label_new_job_dedicated_index">Use dedicated index</span>
<span
id="ml_aria_label_new_job_dedicated_index"
i18n-id="xpack.ml.newJob.simple.generalJobDetails.useDedicatedIndexLabel"
i18n-default-message="Use dedicated index"
></span>
<i ml-info-icon="new_job_dedicated_index" ></i>
</span>
</label>
</div>
<div class="form-group">
<label class='kuiFormLabel kuiVerticalRhythm'>
<span id="ml_aria_label_new_job_model_memory_limit">
Model memory limit
</span>
<span
id="ml_aria_label_new_job_model_memory_limit"
i18n-id="xpack.ml.newJob.simple.generalJobDetails.modelMemoryLimitLabel"
i18n-default-message="Model memory limit"
></span>
<i ml-info-icon="new_job_model_memory_limit"></i>
</label>
<div></div>

View file

@ -17,9 +17,15 @@ module.directive('mlGeneralJobDetails', function () {
restrict: 'E',
replace: true,
template,
controller: function ($scope) {
controller: function ($scope, i18n) {
// force job ids to be lowercase
$scope.changeJobIDCase = changeJobIDCase;
$scope.hideAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.generalJobDetails.hideAdvancedButtonAriaLabel', {
defaultMessage: 'Hide Advanced'
});
$scope.showAdvancedButtonAriaLabel = i18n('xpack.ml.newJob.simple.generalJobDetails.showAdvancedButtonAriaLabel', {
defaultMessage: 'Show Advanced'
});
}
};
});

View file

@ -1,6 +1,10 @@
<div class="influencers-selection">
<div>
<h4 class="euiTitle euiTitle--small">Key Fields (Influencers)</h4>
<h4
class="euiTitle euiTitle--small"
i18n-id="xpack.ml.newJob.simple.influencersSelection.keyFieldsTitle"
i18n-default-message="Key Fields (Influencers)"
></h4>
<div class="euiSpacer euiSpacer--s"></div>
</div>
<div class="row">
@ -15,7 +19,7 @@
multiple
append-to-body=true
>
<ui-select-match placeholder="Key fields">
<ui-select-match placeholder="{{ ::'xpack.ml.newJob.simple.influencersSelection.keyFieldsAriaLabel' | i18n: {defaultMessage: 'Key fields'} }}">
<span ng-class="{'default-influencer': isDefaultInfluencer($item)}">
<ml-field-type-icon type="$item.mlType"></ml-field-type-icon>{{$item.name}}
</span>

View file

@ -4,7 +4,10 @@
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
<input ng-model='runInRealtime' ng-disabled="status.realtimeJob!==null" type="checkbox" class='kuiCheckBox' ng-click="clickRunInRealtime()"/>
<span class="kuiCheckBoxLabel__text">
Continue job in real-time
<span
i18n-id="xpack.ml.newJob.simple.postSaveOptions.continueJobInRealTimeLabel"
i18n-default-message="Continue job in real-time"
></span>
<span ng-hide="status.realtimeJob===null">
<i ng-show="status.realtimeJob === STATUS.SAVE_FAILED" aria-hidden="true" style="color:red;" class="fa fa-remove"></i>
<i ng-show="status.realtimeJob === STATUS.SAVING" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
@ -19,7 +22,10 @@
ng-class="{'disabled': !runInRealtime}">
<input ng-model='createWatch' ng-disabled="!runInRealtime || status.realtimeJob!==null" type="checkbox" class='kuiCheckBox' />
<span class="kuiCheckBoxLabel__text">
Create watch for real-time job
<span
i18n-id="xpack.ml.newJob.simple.postSaveOptions.createWatchForRealTimeJobLabel"
i18n-default-message="Create watch for real-time job"
></span>
<span ng-hide="status.watch===null">
<i ng-show="status.watch === STATUS.SAVE_FAILED" aria-hidden="true" style="color:red;" class="fa fa-remove"></i>
<i ng-show="status.watch === STATUS.SAVING" aria-hidden="true" class="fa fa-spinner fa-spin"></i>
@ -39,13 +45,14 @@
</div>
<button
aria-label="Create watch"
aria-label="{{ ::'xpack.ml.newJob.simple.postSaveOptions.createWatchButtonAriaLabel' | i18n: {defaultMessage: 'Create watch'} }}"
ng-click="apply()"
ng-hide='(status.realtimeJob===STATUS.SAVED && createWatch===false) || (status.realtimeJob===STATUS.SAVED && status.watch===STATUS.SAVED && createWatch===true)'
ng-disabled='runInRealtime===false'
type="button"
class="kuiButton kuiButton--primary">
Apply
class="kuiButton kuiButton--primary"
i18n-id="xpack.ml.newJob.simple.postSaveOptions.applyButtonLabel"
i18n-default-message="Apply">
</button>
</div>

View file

@ -14,7 +14,7 @@ import template from './post_save_options.html';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlPostSaveOptions', function (Private) {
module.directive('mlPostSaveOptions', function (Private, i18n) {
return {
restrict: 'AE',
replace: false,
@ -43,7 +43,7 @@ module.directive('mlPostSaveOptions', function (Private) {
};
$scope.apply = function () {
postSaveService.apply($scope.jobId, $scope.runInRealtime, $scope.createWatch);
postSaveService.apply($scope.jobId, $scope.runInRealtime, $scope.createWatch, i18n);
};
}
};

View file

@ -30,7 +30,7 @@ class PostSaveService {
this.externalCreateWatch;
}
startRealtimeJob(jobId) {
startRealtimeJob(jobId, i18n) {
return new Promise((resolve, reject) => {
this.status.realtimeJob = this.STATUS.SAVING;
@ -43,7 +43,10 @@ class PostSaveService {
this.status.realtimeJob = this.STATUS.SAVED;
resolve();
}).catch((resp) => {
msgs.error('Could not start datafeed: ', resp);
msgs.error(
i18n('xpack.ml.newJob.simple.postSaveOptions.couldNotStartDatafeedErrorMessage', {
defaultMessage: 'Could not start datafeed:'
}), resp);
this.status.realtimeJob = this.STATUS.SAVE_FAILED;
reject();
});
@ -52,9 +55,9 @@ class PostSaveService {
});
}
apply(jobId, runInRealtime, createWatch) {
apply(jobId, runInRealtime, createWatch, i18n) {
if (runInRealtime) {
this.startRealtimeJob(jobId)
this.startRealtimeJob(jobId, i18n)
.then(() => {
if (createWatch) {
mlCreateWatchService.createNewWatch(jobId);

View file

@ -10,6 +10,7 @@ import { EVENT_RATE_COUNT_FIELD } from 'plugins/ml/jobs/new_job/simple/component
import { ML_JOB_FIELD_TYPES, KBN_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
import { getSafeAggregationName } from 'plugins/ml/../common/util/job_utils';
import { kbnTypeToMLJobType } from 'plugins/ml/util/field_types_utils';
import { i18n } from '@kbn/i18n';
export function createFields(scope, indexPattern) {
const isPopulation = scope.formConfig.hasOwnProperty('overField');
@ -31,7 +32,9 @@ export function createFields(scope, indexPattern) {
const eventRateField = {
id: EVENT_RATE_COUNT_FIELD,
name: 'event rate',
tooltip: 'System defined field',
tooltip: i18n.translate('xpack.ml.newJob.simple.createFields.systemDefinedFieldTooltip', {
defaultMessage: 'System defined field'
}),
isCountField: true,
agg: countAgg,
mlType: ML_JOB_FIELD_TYPES.NUMBER,

View file

@ -8,6 +8,7 @@
import _ from 'lodash';
import angular from 'angular';
import { i18n } from '@kbn/i18n';
export function filterAggTypes(aggTypes) {
const filteredAggTypes = [];
@ -30,12 +31,16 @@ export function filterAggTypes(aggTypes) {
filteredAggTypes.push(type);
typeCopy = angular.copy(type);
typeCopy.title = 'High count';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.highCountLabel', {
defaultMessage: 'High count'
});
typeCopy.mlName = 'high_count';
filteredAggTypes.push(typeCopy);
typeCopy = angular.copy(type);
typeCopy.title = 'Low count';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.lowCountLabel', {
defaultMessage: 'Low count'
});
typeCopy.mlName = 'low_count';
filteredAggTypes.push(typeCopy);
@ -43,27 +48,37 @@ export function filterAggTypes(aggTypes) {
filteredAggTypes.push(type);
typeCopy = angular.copy(type);
typeCopy.title = 'High sum';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.highSumLabel', {
defaultMessage: 'High sum'
});
typeCopy.mlName = 'high_sum';
filteredAggTypes.push(typeCopy);
typeCopy = angular.copy(type);
typeCopy.title = 'Low sum';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.lowSumLabel', {
defaultMessage: 'Low sum'
});
typeCopy.mlName = 'low_sum';
filteredAggTypes.push(typeCopy);
} else if (type.name === 'avg') {
type.title = 'Mean';
type.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.meanLabel', {
defaultMessage: 'Mean'
});
type.mlName = 'mean';
filteredAggTypes.push(type);
typeCopy = angular.copy(type);
typeCopy.title = 'High mean';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.highMeanLabel', {
defaultMessage: 'High mean'
});
typeCopy.mlName = 'high_mean';
filteredAggTypes.push(typeCopy);
typeCopy = angular.copy(type);
typeCopy.title = 'Low mean';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.lowMeanLabel', {
defaultMessage: 'Low mean'
});
typeCopy.mlName = 'low_mean';
filteredAggTypes.push(typeCopy);
@ -72,12 +87,16 @@ export function filterAggTypes(aggTypes) {
filteredAggTypes.push(type);
typeCopy = angular.copy(type);
typeCopy.title = 'High median';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.highMedianLabel', {
defaultMessage: 'High median'
});
typeCopy.mlName = 'high_median';
filteredAggTypes.push(typeCopy);
typeCopy = angular.copy(type);
typeCopy.title = 'Low median';
typeCopy.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.lowMedianLabel', {
defaultMessage: 'Low median'
});
typeCopy.mlName = 'low_median';
filteredAggTypes.push(typeCopy);
@ -87,7 +106,9 @@ export function filterAggTypes(aggTypes) {
} else if (type.name === 'max') {
filteredAggTypes.push(type);
} else if (type.name === 'cardinality') {
type.title = 'Distinct count';
type.title = i18n.translate('xpack.ml.newJob.simple.filterAggTypes.distinctCountLabel', {
defaultMessage: 'Distinct count'
});
type.mlName = 'distinct_count';
type.mlModelPlotAgg = { max: 'max', min: 'min' };
type.isAggregatableStringType = true;

View file

@ -10,6 +10,7 @@ import { basicJobValidation } from 'plugins/ml/../common/util/job_utils';
import { newJobLimits } from 'plugins/ml/jobs/new_job/utils/new_job_defaults';
import { ALLOWED_DATA_UNITS } from 'plugins/ml/../common/constants/validation';
import _ from 'lodash';
import { i18n } from '@kbn/i18n';
export function validateJob(job, checks) {
const limits = newJobLimits();
@ -39,34 +40,46 @@ export function populateValidationMessages(validationResults, checks) {
checks.jobId.valid = false;
} else if (validationResults.contains('job_id_invalid')) {
checks.jobId.valid = false;
let msg = 'Job name can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ';
msg += 'must start and end with an alphanumeric character';
const msg = i18n.translate('xpack.ml.newJob.simple.validateJob.jobNameAllowedCharactersDescription', {
defaultMessage: 'Job name can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' +
'must start and end with an alphanumeric character'
});
checks.jobId.message = msg;
}
if (validationResults.contains('job_group_id_invalid')) {
checks.groupIds.valid = false;
let msg = 'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ';
msg += 'must start and end with an alphanumeric character';
const msg = i18n.translate('xpack.ml.newJob.simple.validateJob.jobGroupAllowedCharactersDescription', {
defaultMessage: 'Job group names can contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; ' +
'must start and end with an alphanumeric character'
});
checks.groupIds.message = msg;
}
if (validationResults.contains('model_memory_limit_units_invalid')) {
checks.modelMemoryLimit.valid = false;
const str = `${(ALLOWED_DATA_UNITS.slice(0, ALLOWED_DATA_UNITS.length - 1).join(', '))} or ${([...ALLOWED_DATA_UNITS].pop())}`;
const msg = `Model memory limit data unit unrecognized. It must be ${str}`;
const msg = i18n.translate('xpack.ml.newJob.simple.validateJob.modelMemoryLimitUnitsInvalidErrorMessage', {
defaultMessage: 'Model memory limit data unit unrecognized. It must be {str}',
values: { str }
});
checks.modelMemoryLimit.message = msg;
}
if (validationResults.contains('model_memory_limit_invalid')) {
checks.modelMemoryLimit.valid = false;
const msg = `Model memory limit cannot be higher than the maximum value of ${limits.max_model_memory_limit.toUpperCase()}`;
const msg = i18n.translate('xpack.ml.newJob.simple.validateJob.modelMemoryLimitRangeInvalidErrorMessage', {
defaultMessage: 'Model memory limit cannot be higher than the maximum value of {maxModelMemoryLimit}',
values: { maxModelMemoryLimit: limits.max_model_memory_limit.toUpperCase() }
});
checks.modelMemoryLimit.message = msg;
}
if (validationResults.contains('detectors_duplicates')) {
checks.duplicateDetectors.valid = false;
const msg = 'Duplicate detectors were found.';
const msg = i18n.translate('xpack.ml.newJob.simple.validateJob.duplicatedDetectorsErrorMessage', {
defaultMessage: 'Duplicate detectors were found.',
});
checks.duplicateDetectors.message = msg;
}
}

View file

@ -2,12 +2,26 @@
<div ng-show="status.watch===null || status.watch===STATUS.SAVING || status.watch===STATUS.SAVE_FAILED">
<div class="form-group">
<div class="sub-form-group">
<label for="selectInterval" class="kuiFormLabel kuiVerticalRhythm">Time range</label><br />
Now - <input id="selectInterval" ng-model="config.interval" type="text" class="kuiTextInput kuiTextInput--small interval" placeholder="m">
<label
for="selectInterval"
class="kuiFormLabel kuiVerticalRhythm"
i18n-id="xpack.ml.newJob.simple.createWatch.timeRangeLabel"
i18n-default-message="Time range"
></label><br />
<span
i18n-id="xpack.ml.newJob.simple.createWatch.nowLabel"
i18n-default-message="Now -"
></span>
<input id="selectInterval" ng-model="config.interval" type="text" class="kuiTextInput kuiTextInput--small interval" placeholder="m">
</div>
<div class="sub-form-group">
<label for="selectSeverity" class="kuiFormLabel kuiVerticalRhythm">Severity threshold</label><br />
<label
for="selectSeverity"
class="kuiFormLabel kuiVerticalRhythm"
i18n-id="xpack.ml.newJob.simple.createWatch.severityThresholdLabel"
i18n-default-message="Severity threshold"
></label><br />
<div class="dropdown-group">
<ml-severity-control
config='config'
@ -18,24 +32,47 @@
<div ng-show="ui.emailEnabled" class="form-group">
<label for="sendEmail" class="kuiCheckBoxLabel kuiVerticalRhythm">
<input id="sendEmail" ng-model="config.includeEmail" type="checkbox" class="kuiCheckBox" />
<span class="kuiCheckBoxLabel__text">Send email</span>
<span
class="kuiCheckBoxLabel__text"
i18n-id="xpack.ml.newJob.simple.createWatch.sendEmailLabel"
i18n-default-message="Send email"
></span>
</label>
<div ng-show="config.includeEmail" class="email-section">
<input type="text" ng-model="config.email" class="kuiTextInput kuiTextInput--large" placeholder="email address">
<input
type="text"
ng-model="config.email"
class="kuiTextInput kuiTextInput--large"
placeholder="{{ ::'xpack.ml.newJob.simple.createWatch.emailAddressPlaceholder' | i18n: {defaultMessage: 'email address'} }}">
</div>
</div>
<div ng-if="ui.watchAlreadyExists" class="watch-exists-warning">
Warning, watch ml-{{jobId}} already exists, clicking apply will overwrite the original.
<div
ng-if="ui.watchAlreadyExists"
class="watch-exists-warning"
i18n-id="xpack.ml.newJob.simple.createWatch.watchAlreadyExistsWarningMessage"
i18n-default-message="Warning, watch ml-{jobId} already exists, clicking apply will overwrite the original."
i18n-values="{ jobId }"
>
</div>
</div>
<div ng-show="status.watch===STATUS.SAVED">
<div>Watch: <strong>{{config.id}}</strong> created</div>
<div
i18n-id="xpack.ml.newJob.simple.createWatch.watchCreatedLabel"
i18n-default-message="Watch: {id} created"
i18n-values="{ html_id: '<strong>' + config.id + '</strong>' }"
></div>
<br />
<div>
<a href="{{config.watcherEditURL}}" target="_blank" rel="noopener noreferrer">Edit {{config.id}} in Watcher <i class="fa fa-external-link"></i></a>
<a href="{{config.watcherEditURL}}" target="_blank" rel="noopener noreferrer">
<span
i18n-id="xpack.ml.newJob.simple.createWatch.editWatchLinkText"
i18n-default-message="Edit {id} in Watcher"
i18n-values="{ id: config.id }"
></span>
<i class="fa fa-external-link"></i></a>
</div>
</div>

View file

@ -26,13 +26,19 @@ import {
import { has } from 'lodash';
import { parseInterval } from 'ui/utils/parse_interval';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
import { ml } from '../../../../../services/ml_api_service';
import { SelectSeverity } from '../../../../../components/controls/select_severity/select_severity';
import { mlCreateWatchService } from './create_watch_service';
const STATUS = mlCreateWatchService.STATUS;
export class CreateWatch extends Component {
export const CreateWatch = injectI18n(class CreateWatch extends Component {
static propTypes = {
jobId: PropTypes.string.isRequired,
bucketSpan: PropTypes.string.isRequired
};
constructor(props) {
super(props);
mlCreateWatchService.reset();
@ -111,6 +117,7 @@ export class CreateWatch extends Component {
}
render() {
const { intl } = this.props;
const mlSelectSeverityService = {
state: {
set: (name, threshold) => {
@ -136,13 +143,22 @@ export class CreateWatch extends Component {
htmlFor="selectInterval"
className="euiFormLabel"
>
Time range
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.timeRangeLabel"
defaultMessage="Time range"
/>
</label>
</div>
Now - <EuiFieldText
id="selectInterval"
value={this.state.interval}
onChange={this.onIntervalChange}
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.nowLabel"
defaultMessage="Now - {selectInterval}"
values={{ selectInterval: (
<EuiFieldText
id="selectInterval"
value={this.state.interval}
onChange={this.onIntervalChange}
/>
) }}
/>
</div>
@ -152,7 +168,10 @@ export class CreateWatch extends Component {
htmlFor="selectSeverity"
className="euiFormLabel"
>
Severity threshold
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.severityThresholdLabel"
defaultMessage="Severity threshold"
/>
</label>
</div>
<div className="dropdown-group">
@ -169,7 +188,10 @@ export class CreateWatch extends Component {
<div className="form-group">
<EuiCheckbox
id="includeEmail"
label="Send email"
label={<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.sendEmailLabel"
defaultMessage="Send email"
/>}
checked={this.state.includeEmail}
onChange={this.onIncludeEmailChanged}
/>
@ -179,8 +201,14 @@ export class CreateWatch extends Component {
<EuiFieldText
value={this.state.email}
onChange={this.onEmailChange}
placeholder="email address"
aria-label="Watch email address"
placeholder={intl.formatMessage({
id: 'xpack.ml.newJob.simple.createWatchView.emailAddressPlaceholder',
defaultMessage: 'email address'
})}
aria-label={intl.formatMessage({
id: 'xpack.ml.newJob.simple.createWatchView.watchEmailAddressAriaLabel',
defaultMessage: 'Watch email address'
})}
/>
</div>
}
@ -189,21 +217,28 @@ export class CreateWatch extends Component {
{
this.state.watchAlreadyExists &&
<EuiCallOut
title={`Warning, watch ml-${this.state.jobId} already exists, clicking apply will overwrite the original.`}
title={<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.watchAlreadyExistsWarningMessage"
defaultMessage="Warning, watch ml-{jobId} already exists, clicking apply will overwrite the original."
values={{
jobId: this.state.jobId
}}
/>}
/>
}
</div>
);
} else if (status === STATUS.SAVED) {
return (
<div>Success</div>
<div>
<FormattedMessage
id="xpack.ml.newJob.simple.createWatchView.successLabel"
defaultMessage="Success"
/>
</div>
);
} else {
return (<div />);
}
}
}
CreateWatch.propTypes = {
jobId: PropTypes.string.isRequired,
bucketSpan: PropTypes.string.isRequired,
};
});

View file

@ -1,4 +1,6 @@
<strong>Top influencers:</strong>
<strong>
{{i18n 'xpack.ml.newJob.simple.emailInfluencers.topInfluencersLabel' '{"defaultMessage": "Top influencers:"'}}
</strong>
<br />
{{#ctx.payload.aggregations.influencer_results.top_influencer_hits.hits.hits}}
{{_source.influencer_field_name}} = {{_source.influencer_field_value}} [{{fields.score.0}}]

View file

@ -1,27 +1,38 @@
<html>
<body>
<strong>Elastic Stack Machine Learning Alert</strong>
<strong>
{{i18n 'xpack.ml.newJob.simple.email.elasticStackMachineLearningAlertLabel' '{"defaultMessage": "Elastic Stack Machine Learning Alert"'}}
</strong>
<br />
<br />
<strong>Job</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}}
<strong>
{{i18n 'xpack.ml.newJob.simple.email.jobLabel' '{"defaultMessage": "Job"'}}
</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}}
<br />
<strong>Time</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_iso8601.0}}
<strong>
{{i18n 'xpack.ml.newJob.simple.email.timeLabel' '{"defaultMessage": "Time"'}}
</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_iso8601.0}}
<br />
<strong>Anomaly score</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.score.0}}
<strong>
{{i18n 'xpack.ml.newJob.simple.email.anomalyScoreLabel' '{"defaultMessage": "Anomaly score"'}}
</strong>: {{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.score.0}}
<br />
<br />
<a href="<%= serverAddress %>#/explorer/?_g=(ml:(jobIds:!('{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0._source.job_id}}')),refreshInterval:(display:Off,pause:!f,value:0),time:(from:'{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.start.0}}',mode:absolute,to:'{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.end.0}}'))&_a=(filters:!(),mlAnomaliesTable:(intervalValue:auto,thresholdValue:0),mlExplorerSwimlane:(selectedLane:Overall,selectedTime:{{ctx.payload.aggregations.bucket_results.top_bucket_hits.hits.hits.0.fields.timestamp_epoch.0}},selectedType:overall),query:(query_string:(analyze_wildcard:!t,query:'*')))">
Click here to open in Anomaly Explorer</a>.
{{i18n 'xpack.ml.newJob.simple.email.openInAnomalyExplorerLinkText' '{"defaultMessage": "Click here to open in Anomaly Explorer."'}}
</a>
<br />
<br />
<%= influencersSection %>
<strong>Top records:</strong>
<strong>
{{i18n 'xpack.ml.newJob.simple.email.topRecordsLabel' '{"defaultMessage": "Top records:"'}}
</strong>
<br />
{{#ctx.payload.aggregations.record_results.top_record_hits.hits.hits}}
{{_source.function}}({{_source.field_name}}) {{_source.by_field_value}} {{_source.over_field_value}} {{_source.partition_field_value}} [{{fields.score.0}}]