[ML] Localize ml components ( part 1 ) (#27957)

* feature(ml): localize components

* fix xpack.i18n localization strings

* Resolve review comments

* Add I18nProvider

* Fix adding label to angular scope

* Fix case of word

* Update test snapshots.

* Resolve review comments
This commit is contained in:
Ahmad Bamieh 2019-01-16 00:09:51 -08:00 committed by Nox911
parent dca47b89a1
commit bc4b197fbd
20 changed files with 362 additions and 111 deletions

View file

@ -6,7 +6,7 @@
ng-click="ok()"
ng-disabled="(saveLock === true)"
class="kuiButton kuiButton--primary"
aria-label="OK">
aria-label="{{ ::'xpack.ml.confirmModal.okButtonAriaLabel' | i18n: {defaultMessage: 'Ok'} }}">
{{okLabel}}
</button>
<button
@ -14,7 +14,7 @@
ng-click="cancel()"
ng-disabled="(saveLock === true)"
class="kuiButton kuiButton--primary"
aria-label="Cancel">
aria-label="{{ ::'xpack.ml.confirmModal.cancelButtonAriaLabel' | i18n: {defaultMessage: 'Cancel'} }}">
{{cancelLabel}}
</button>
</div>

View file

@ -9,7 +9,7 @@
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.controller('MlConfirmModal', function ($scope, $modalInstance, params) {
module.controller('MlConfirmModal', function ($scope, $modalInstance, params, i18n) {
$scope.okFunc = params.ok;
$scope.cancelFunc = params.cancel;
@ -17,8 +17,13 @@ module.controller('MlConfirmModal', function ($scope, $modalInstance, params) {
$scope.message = params.message || '';
$scope.title = params.title || '';
$scope.okLabel = params.okLabel || 'OK';
$scope.cancelLabel = params.cancelLabel || 'Cancel';
$scope.okLabel = params.okLabel || i18n('xpack.ml.confirmModal.okButtonLabel', {
defaultMessage: 'OK',
});
$scope.cancelLabel = params.cancelLabel || i18n('xpack.ml.confirmModal.cancelButtonLabel', {
defaultMessage: 'Cancel',
});
$scope.hideCancel = params.hideCancel || false;

View file

@ -10,6 +10,7 @@ import React from 'react';
import { EuiText, EuiToolTip } from '@elastic/eui';
import { FieldTypeIcon } from '../field_type_icon';
import { i18n } from '@kbn/i18n';
export function FieldTitleBar({ card }) {
// don't render and fail gracefully if card prop isn't set
@ -26,7 +27,9 @@ export function FieldTitleBar({ card }) {
classNames.push(card.type);
}
const fieldName = card.fieldName || 'document count';
const fieldName = card.fieldName || i18n.translate('xpack.ml.fieldTitleBar.documentCountLabel', {
defaultMessage: 'document count'
});
return (
<EuiText className={classNames.join(' ')}>

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mount } from 'enzyme';
import { mountWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { FieldTitleBar } from './field_title_bar';
@ -27,7 +27,7 @@ describe('FieldTitleBar', () => {
test(`card prop is an empty object`, () => {
const props = { card: {} };
const wrapper = mount(<FieldTitleBar {...props} />);
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual('document count');
@ -40,7 +40,7 @@ describe('FieldTitleBar', () => {
const testFieldName = 'foo';
const props = { card: { fieldName: testFieldName, isUnsupportedType: true } };
const wrapper = mount(<FieldTitleBar {...props} />);
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual(testFieldName);
@ -54,7 +54,7 @@ describe('FieldTitleBar', () => {
const testType = 'bar';
const props = { card: { fieldName: testFieldName, type: testType } };
const wrapper = mount(<FieldTitleBar {...props} />);
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const fieldName = wrapper.find({ className: 'field-name' }).text();
expect(fieldName).toEqual(testFieldName);
@ -65,7 +65,7 @@ describe('FieldTitleBar', () => {
test(`tooltip hovering`, () => {
const props = { card: { fieldName: 'foo', type: 'bar' } };
const wrapper = mount(<FieldTitleBar {...props} />);
const wrapper = mountWithIntl(<FieldTitleBar {...props} />);
const container = wrapper.find({ className: 'field-name' });
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);

View file

@ -10,6 +10,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { FieldTitleBar } from './field_title_bar';
import { I18nProvider } from '@kbn/i18n/react';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
@ -32,7 +33,9 @@ module.directive('mlFieldTitleBar', function () {
};
ReactDOM.render(
React.createElement(FieldTitleBar, props),
<I18nProvider>
{React.createElement(FieldTitleBar, props)}
</I18nProvider>,
element[0]
);
}

View file

@ -12,43 +12,68 @@ import { EuiToolTip } from '@elastic/eui';
// don't use something like plugins/ml/../common
// because it won't work with the jest tests
import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types';
import { FormattedMessage, injectI18n } from '@kbn/i18n/react';
export function FieldTypeIcon({ tooltipEnabled = false, type }) {
export const FieldTypeIcon = injectI18n(function FieldTypeIcon({ tooltipEnabled = false, type, intl }) {
let ariaLabel = '';
let iconClass = '';
let iconChar = '';
switch (type) {
case ML_JOB_FIELD_TYPES.BOOLEAN:
ariaLabel = 'boolean type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.booleanTypeAriaLabel',
defaultMessage: 'boolean type'
});
iconClass = 'fa-adjust';
break;
case ML_JOB_FIELD_TYPES.DATE:
ariaLabel = 'date type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.dateTypeAriaLabel',
defaultMessage: 'date type'
});
iconClass = 'fa-clock-o';
break;
case ML_JOB_FIELD_TYPES.NUMBER:
ariaLabel = 'number type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.numberTypeAriaLabel',
defaultMessage: 'number type'
});
iconChar = '#';
break;
case ML_JOB_FIELD_TYPES.GEO_POINT:
ariaLabel = 'geo_point type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.geoPointTypeAriaLabel',
defaultMessage: '{geoPointParam} type'
}, { geoPointParam: 'geo_point' });
iconClass = 'fa-globe';
break;
case ML_JOB_FIELD_TYPES.KEYWORD:
ariaLabel = 'keyword type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.keywordTypeAriaLabel',
defaultMessage: 'keyword type'
});
iconChar = 't';
break;
case ML_JOB_FIELD_TYPES.TEXT:
ariaLabel = 'text type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.textTypeAriaLabel',
defaultMessage: 'text type'
});
iconClass = 'fa-file-text-o';
break;
case ML_JOB_FIELD_TYPES.IP:
ariaLabel = 'IP type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.ipTypeAriaLabel',
defaultMessage: 'IP type'
});
iconClass = 'fa-laptop';
break;
case ML_JOB_FIELD_TYPES.UNKNOWN:
ariaLabel = 'Unknown type';
ariaLabel = intl.formatMessage({
id: 'xpack.ml.fieldTypeIcon.unknownTypeAriaLabel',
defaultMessage: 'Unknown type'
});
iconChar = '?';
break;
default:
@ -73,15 +98,22 @@ export function FieldTypeIcon({ tooltipEnabled = false, type }) {
// to support having another component directly inside the tooltip anchor
// see https://github.com/elastic/eui/issues/839
return (
<EuiToolTip position="left" content={`${type} type`}>
<EuiToolTip
position="left"
content={<FormattedMessage
id="xpack.ml.fieldTypeIcon.fieldTypeTooltip"
defaultMessage="{type} type"
values={{ type }}
/>}
>
<FieldTypeIconContainer {...containerProps} />
</EuiToolTip>
);
}
return <FieldTypeIconContainer {...containerProps} />;
}
FieldTypeIcon.propTypes = {
});
FieldTypeIcon.WrappedComponent.propTypes = {
tooltipEnabled: PropTypes.bool,
type: PropTypes.string
};

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { mount, shallow } from 'enzyme';
import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers';
import React from 'react';
import { FieldTypeIcon } from './field_type_icon';
@ -13,22 +13,22 @@ import { ML_JOB_FIELD_TYPES } from '../../../common/constants/field_types';
describe('FieldTypeIcon', () => {
test(`don't render component when type is undefined`, () => {
const wrapper = shallow(<FieldTypeIcon />);
const wrapper = shallowWithIntl(<FieldTypeIcon.WrappedComponent />);
expect(wrapper.isEmptyRender()).toBeTruthy();
});
test(`don't render component when type doesn't match a field type`, () => {
const wrapper = shallow(<FieldTypeIcon type="foo" />);
const wrapper = shallowWithIntl(<FieldTypeIcon.WrappedComponent type="foo" />);
expect(wrapper.isEmptyRender()).toBeTruthy();
});
test(`render component when type matches a field type`, () => {
const wrapper = shallow(<FieldTypeIcon type={ML_JOB_FIELD_TYPES.KEYWORD} />);
const wrapper = shallowWithIntl(<FieldTypeIcon.WrappedComponent type={ML_JOB_FIELD_TYPES.KEYWORD} />);
expect(wrapper).toMatchSnapshot();
});
test(`render with tooltip and test hovering`, () => {
const wrapper = mount(<FieldTypeIcon type={ML_JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />);
const wrapper = mountWithIntl(<FieldTypeIcon.WrappedComponent type={ML_JOB_FIELD_TYPES.KEYWORD} tooltipEnabled={true} />);
const container = wrapper.find({ className: 'field-type-icon-container' });
expect(wrapper.find('EuiToolTip').children()).toHaveLength(1);
@ -41,7 +41,7 @@ describe('FieldTypeIcon', () => {
});
test(`update component`, () => {
const wrapper = shallow(<FieldTypeIcon />);
const wrapper = shallowWithIntl(<FieldTypeIcon.WrappedComponent />);
expect(wrapper.isEmptyRender()).toBeTruthy();
wrapper.setProps({ type: ML_JOB_FIELD_TYPES.IP });
expect(wrapper).toMatchSnapshot();

View file

@ -10,6 +10,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import { FieldTypeIcon } from './field_type_icon.js';
import { I18nProvider } from '@kbn/i18n/react';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
@ -34,7 +35,9 @@ module.directive('mlFieldTypeIcon', function () {
};
ReactDOM.render(
React.createElement(FieldTypeIcon, props),
<I18nProvider>
{React.createElement(FieldTypeIcon, props)}
</I18nProvider>,
element[0]
);
}

View file

@ -2,7 +2,7 @@
<input
class="form-control ml-filter-input-left-icon"
type="text"
aria-label="{{placeholder}}"
aria-label="{{ariaLabel}}"
placeholder="{{placeholder}}"
ng-model="filter"
ng-change="filterChanged()" />
@ -11,7 +11,10 @@
<i class='fa fa-spinner fa-spin'></i>
</div>
<div ng-show="filterIcon===0">
<a aria-label="Clear filter" ng-click="clearFilter()">
<a
aria-label="{{ ::'xpack.ml.formFilterInput.clearFilterAriaLabel' | i18n: {defaultMessage: 'Clear filter'} }}"
ng-click="clearFilter()"
>
<i class="fa fa-times-circle"></i>
</a>
</div>

View file

@ -12,7 +12,7 @@ import angular from 'angular';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlFormFilterInput', function () {
module.directive('mlFormFilterInput', function (i18n) {
return {
scope: {
placeholder: '@?',
@ -25,7 +25,15 @@ module.directive('mlFormFilterInput', function () {
replace: false,
template,
link(scope) {
scope.placeholder = angular.isDefined(scope.placeholder) ? scope.placeholder : 'Filter';
const placeholderIsDefined = angular.isDefined(scope.placeholder);
scope.placeholder = placeholderIsDefined
? scope.placeholder
: i18n('xpack.ml.formFilterInput.filterPlaceholder', { defaultMessage: 'Filter' });
scope.ariaLabel = placeholderIsDefined
? scope.placeholder
: i18n('xpack.ml.formFilterInput.filterAriaLabel', { defaultMessage: 'Filter' });
}
};
});

View file

@ -3,7 +3,10 @@
type="button"
ng-disabled="disabled"
class="euiButton euiButton--primary euiButton--small euiButton--fill">
<span class="euiButton__content">
Use full {{indexPattern.title}} data
</span>
<span
class="euiButton__content"
i18n-id="xpack.ml.fullTimeRangeSelector.useFullDataButtonLabel"
i18n-default-message="Use full {indexPatternTitle} data"
i18n-values="{ indexPatternTitle: indexPattern.title }"
></span>
</button>

View file

@ -20,6 +20,7 @@ import {
EuiTitle,
EuiToolTip
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { abbreviateWholeNumber } from 'plugins/ml/formatters/abbreviate_whole_number';
import { getSeverity } from 'plugins/ml/../common/util/anomaly_utils';
@ -28,8 +29,20 @@ import { getSeverity } from 'plugins/ml/../common/util/anomaly_utils';
function getTooltipContent(maxScoreLabel, totalScoreLabel) {
return (
<React.Fragment>
<p>Maximum anomaly score: {maxScoreLabel}</p>
<p>Total anomaly score: {totalScoreLabel}</p>
<p>
<FormattedMessage
id="xpack.ml.influencersList.maxAnomalyScoreTooltipDescription"
defaultMessage="Maximum anomaly score: {maxScoreLabel}"
values={{ maxScoreLabel }}
/>
</p>
<p>
<FormattedMessage
id="xpack.ml.influencersList.totalAnomalyScoreTooltipDescription"
defaultMessage="Total anomaly score: {totalScoreLabel}"
values={{ totalScoreLabel }}
/>
</p>
</React.Fragment>
);
}
@ -124,7 +137,12 @@ export function InfluencersList({ influencers }) {
<EuiFlexItem grow={false}>
<EuiSpacer size="xxl" />
<EuiText>
<h4>No influencers found</h4>
<h4>
<FormattedMessage
id="xpack.ml.influencersList.noInfluencersFoundTitle"
defaultMessage="No influencers found"
/>
</h4>
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>

View file

@ -8,19 +8,25 @@
tagging='mlGroupSelect.createNewItem'
append-to-body=true
>
<ui-select-match placeholder="Job Group">
<ui-select-match
placeholder="{{:: 'xpack.ml.jobGroupSelect.jobGroupPlaceholder' | i18n: { defaultMessage: 'Job Group' } }}">
>
{{$item.id}}
</ui-select-match>
<ui-select-choices
repeat="group in mlGroupSelect.groups | filter: { id: $select.search }"
group-by="mlGroupSelect.groupTypes"
>
<div ng-if="group.isTag" class="select-item" ng-bind-html="(group.id | highlight: $select.search) +' <small>(new group)</small>'"></div>
<div ng-if="group.isTag" class="select-item" ng-bind-html="(group.id | highlight: $select.search) +' <small>' + newGroupLabel + '</small>'"></div>
<div ng-if="!group.isTag" class="select-item" >
<div ng-bind-html="group.id | highlight: $select.search"></div>
<small>
Other jobs in this group: {{group.count}}
</small>
<small
i18n-id="xpack.ml.jobGroupSelect.otherJobsInGroupLabel"
i18n-default-message="Other jobs in this group: {groupCount}"
i18n-values="{
groupCount: group.count,
}"
></small>
</div>
</ui-select-choices>
</ui-select>

View file

@ -16,7 +16,7 @@ import { InitAfterBindingsWorkaround } from 'ui/compat';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlJobGroupSelect', function () {
module.directive('mlJobGroupSelect', function (i18n) {
return {
restrict: 'E',
template,
@ -33,6 +33,7 @@ module.directive('mlJobGroupSelect', function () {
this.$scope = $scope;
this.selectedGroups = [];
this.groups = [];
this.$scope.newGroupLabel = i18n('xpack.ml.jobGroupSelect.newGroupLabel', { defaultMessage: '(new group)' });
// load the jobs, in case they've not been loaded before
// in order to get the job groups
@ -111,7 +112,7 @@ module.directive('mlJobGroupSelect', function () {
groupTypes(group) {
if(group.isTag === false) {
return 'Existing groups';
return i18n('xpack.ml.jobGroupSelect.existingGroupsLabel', { defaultMessage: 'Existing groups' });
}
}
}

View file

@ -16,23 +16,38 @@
<div ng-if="showTabs" class="kuiLocalTabs" role="tablist">
<a kbn-href="#/jobs" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('jobs'), 'disabled-nav-link': disableLinks}">
Job Management
<span
i18n-id="xpack.ml.navMenu.jobManagementTabLinkText"
i18n-default-message="Job Management"
></span>
</a>
<a kbn-href="#/explorer" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('explorer'), 'disabled-nav-link': disableLinks}">
Anomaly Explorer
<span
i18n-id="xpack.ml.navMenu.anomalyExplorerTabLinkText"
i18n-default-message="Anomaly Explorer"
></span>
</a>
<a kbn-href="#/timeseriesexplorer" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('timeseriesexplorer'), 'disabled-nav-link': disableLinks}">
Single Metric Viewer
<span
i18n-id="xpack.ml.navMenu.singleMetricViewerTabLinkText"
i18n-default-message="Single Metric Viewer"
></span>
</a>
<a kbn-href="#/datavisualizer" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('datavisualizer')}">
Data Visualizer
<span
i18n-id="xpack.ml.navMenu.dataVisualizerTabLinkText"
i18n-default-message="Data Visualizer"
></span>
</a>
<a kbn-href="#/settings" class="kuiLocalTab" role="tab"
ng-class="{'kuiLocalTab-isSelected': isActiveTab('settings'), 'disabled-nav-link': disableLinks}">
Settings
<span
i18n-id="xpack.ml.navMenu.settingsTabLinkText"
i18n-default-message="Settings"
></span>
</a>
</div>
</div>

View file

@ -14,7 +14,7 @@ import { isFullLicense } from '../../license/check_license';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlNavMenu', function (config) {
module.directive('mlNavMenu', function (config, i18n) {
return {
restrict: 'E',
transclude: true,
@ -43,27 +43,81 @@ module.directive('mlNavMenu', function (config) {
const isK7Design = chrome.getUiSettingsClient().get('k7design', false);
if (isK7Design === false) {
// Breadcrumbs
const crumbNames = {
jobs: { label: 'Job Management', url: '#/jobs' },
new_job: { label: 'Create New Job', url: '#/jobs/new_job' },
single_metric: { label: 'Single Metric Job', url: '' },
multi_metric: { label: 'Multi Metric job', url: '' },
population: { label: 'Population job', url: '' },
advanced: { label: 'Advanced Job Configuration', url: '' },
datavisualizer: { label: 'Data Visualizer', url: '' },
filedatavisualizer: { label: 'File Data Visualizer (Experimental)', url: '' },
explorer: { label: 'Anomaly Explorer', url: '#/explorer' },
timeseriesexplorer: { label: 'Single Metric Viewer', url: '#/timeseriesexplorer' },
settings: { label: 'Settings', url: '#/settings' },
calendars_list: { label: 'Calendar Management', url: '#/settings/calendars_list' },
new_calendar: { label: 'New Calendar', url: '#/settings/calendars_list/new_calendar' },
edit_calendar: { label: 'Edit Calendar', url: '#/settings/calendars_list/edit_calendar' },
filter_lists: { label: 'Filter Lists', url: '#/settings/filter_lists' },
new_filter_list: { label: 'New Filter List', url: '#/settings/filter_lists/new' },
edit_filter_list: { label: 'Edit Filter List', url: '#/settings/filter_lists/edit' },
};
const breadcrumbs = [{ label: 'Machine Learning', url: '#/' }];
const crumbNames = {
jobs: {
label: i18n('xpack.ml.navMenu.breadcrumbs.jobManagementLabel', { defaultMessage: 'Job Management' }),
url: '#/jobs'
},
new_job: {
label: i18n('xpack.ml.navMenu.breadcrumbs.createNewJobLabel', { defaultMessage: 'Create New Job' }),
url: '#/jobs/new_job'
},
single_metric: {
label: i18n('xpack.ml.navMenu.breadcrumbs.singleMetricJobLabel', { defaultMessage: 'Single Metric Job' }),
url: ''
},
multi_metric: {
label: i18n('xpack.ml.navMenu.breadcrumbs.multiMetricJobLabel', { defaultMessage: 'Multi Metric job' }),
url: ''
},
population: {
label: i18n('xpack.ml.navMenu.breadcrumbs.populationJobLabel', { defaultMessage: 'Population job' }),
url: ''
},
advanced: {
label: i18n('xpack.ml.navMenu.breadcrumbs.advancedJobConfigurationLabel', { defaultMessage: 'Advanced Job Configuration' }),
url: ''
},
datavisualizer: {
label: i18n('xpack.ml.navMenu.breadcrumbs.dataVisualizerLabel', { defaultMessage: 'Data Visualizer' }),
url: ''
},
filedatavisualizer: {
label: i18n('xpack.ml.navMenu.breadcrumbs.fileDataVisualizerLabel', { defaultMessage: 'File Data Visualizer (Experimental)' }),
url: ''
},
explorer: {
label: i18n('xpack.ml.navMenu.breadcrumbs.anomalyExplorerLabel', { defaultMessage: 'Anomaly Explorer' }),
url: '#/explorer'
},
timeseriesexplorer: {
label: i18n('xpack.ml.navMenu.breadcrumbs.singleMetricViewerLabel', { defaultMessage: 'Single Metric Viewer' }),
url: '#/timeseriesexplorer'
},
settings: {
label: i18n('xpack.ml.navMenu.breadcrumbs.settingsLabel', { defaultMessage: 'Settings' }),
url: '#/settings'
},
calendars_list: {
label: i18n('xpack.ml.navMenu.breadcrumbs.calendarManagementLabel', { defaultMessage: 'Calendar Management' }),
url: '#/settings/calendars_list'
},
new_calendar: {
label: i18n('xpack.ml.navMenu.breadcrumbs.newCalendarLabel', { defaultMessage: 'New Calendar' }),
url: '#/settings/calendars_list/new_calendar'
},
edit_calendar: {
label: i18n('xpack.ml.navMenu.breadcrumbs.editCalendarLabel', { defaultMessage: 'Edit Calendar' }),
url: '#/settings/calendars_list/edit_calendar'
},
filter_lists: {
label: i18n('xpack.ml.navMenu.breadcrumbs.filterListsLabel', { defaultMessage: 'Filter Lists' }),
url: '#/settings/filter_lists'
},
new_filter_list: {
label: i18n('xpack.ml.navMenu.breadcrumbs.newFilterListLabel', { defaultMessage: 'New Filter List' }),
url: '#/settings/filter_lists/new'
},
edit_filter_list: {
label: i18n('xpack.ml.navMenu.breadcrumbs.editFilterListLabel', { defaultMessage: 'Edit Filter List' }),
url: '#/settings/filter_lists/edit'
},
};
const breadcrumbs = [{
label: i18n('xpack.ml.navMenu.breadcrumbs.machineLearningLabel', { defaultMessage: 'Machine Learning' }),
url: '#/'
}];
// get crumbs from url
const crumbs = uiRouter.getBreadcrumbs();

View file

@ -13,11 +13,25 @@ exports[`ValidateJob renders button and modal with a message 1`] = `
size="s"
type="button"
>
Validate Job
<FormattedMessage
defaultMessage="Validate Job"
id="xpack.ml.validateJob.validateJobButtonLabel"
values={Object {}}
/>
</EuiButton>
<Modal
close={[Function]}
title="Validate job test-id"
title={
<FormattedMessage
defaultMessage="Validate job {title}"
id="xpack.ml.validateJob.modal.validateJobTitle"
values={
Object {
"title": "test-id",
}
}
/>
}
>
<Callout
key="over_field_low_cardinality_0"
@ -35,22 +49,36 @@ exports[`ValidateJob renders button and modal with a message 1`] = `
grow={true}
size="m"
>
Job validation performs certain checks against job configurations and underlying source data and provides specific advice on how to adjust settings that are more likely to produce insightful results.
<FormattedMessage
defaultMessage="Job validation performs certain checks against job configurations and underlying source data and provides specific advice on how to adjust settings that are more likely to produce insightful results."
id="xpack.ml.validateJob.modal.jobValidationDescriptionText"
values={Object {}}
/>
</EuiText>
<EuiText
grow={true}
size="m"
>
For more information, see
<EuiLink
color="primary"
href="https://www.elastic.co/guide/en/kibana/my-metadata-branch/job-tips.html"
target="_blank"
type="button"
>
Machine Learning Job Tips
</EuiLink>
.
<FormattedMessage
defaultMessage="For more information, see {mlJobTipsLink}."
id="xpack.ml.validateJob.modal.linkToJobTipsText"
values={
Object {
"mlJobTipsLink": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/kibana/my-metadata-branch/job-tips.html"
target="_blank"
type="button"
>
<FormattedMessage
defaultMessage="Machine Learning Job Tips"
id="xpack.ml.validateJob.modal.linkToJobTipsText.mlJobTipsLinkText"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</EuiText>
</Modal>
</div>
@ -69,7 +97,11 @@ exports[`ValidateJob renders the button 1`] = `
size="s"
type="button"
>
Validate Job
<FormattedMessage
defaultMessage="Validate Job"
id="xpack.ml.validateJob.validateJobButtonLabel"
values={Object {}}
/>
</EuiButton>
</div>
`;
@ -87,32 +119,60 @@ exports[`ValidateJob renders the button and modal with a success message 1`] = `
size="s"
type="button"
>
Validate Job
<FormattedMessage
defaultMessage="Validate Job"
id="xpack.ml.validateJob.validateJobButtonLabel"
values={Object {}}
/>
</EuiButton>
<Modal
close={[Function]}
title="Validate job test-id"
title={
<FormattedMessage
defaultMessage="Validate job {title}"
id="xpack.ml.validateJob.modal.validateJobTitle"
values={
Object {
"title": "test-id",
}
}
/>
}
>
<EuiText
grow={true}
size="m"
>
Job validation performs certain checks against job configurations and underlying source data and provides specific advice on how to adjust settings that are more likely to produce insightful results.
<FormattedMessage
defaultMessage="Job validation performs certain checks against job configurations and underlying source data and provides specific advice on how to adjust settings that are more likely to produce insightful results."
id="xpack.ml.validateJob.modal.jobValidationDescriptionText"
values={Object {}}
/>
</EuiText>
<EuiText
grow={true}
size="m"
>
For more information, see
<EuiLink
color="primary"
href="https://www.elastic.co/guide/en/kibana/my-metadata-branch/job-tips.html"
target="_blank"
type="button"
>
Machine Learning Job Tips
</EuiLink>
.
<FormattedMessage
defaultMessage="For more information, see {mlJobTipsLink}."
id="xpack.ml.validateJob.modal.linkToJobTipsText"
values={
Object {
"mlJobTipsLink": <EuiLink
color="primary"
href="https://www.elastic.co/guide/en/kibana/my-metadata-branch/job-tips.html"
target="_blank"
type="button"
>
<FormattedMessage
defaultMessage="Machine Learning Job Tips"
id="xpack.ml.validateJob.modal.linkToJobTipsText.mlJobTipsLinkText"
values={Object {}}
/>
</EuiLink>,
}
}
/>
</EuiText>
</Modal>
</div>

View file

@ -14,10 +14,11 @@ const module = uiModules.get('apps/ml', ['react']);
import { ValidateJob } from './validate_job_view';
import { mlJobService } from 'plugins/ml/services/job_service';
import { injectI18nProvider } from '@kbn/i18n/react';
module.directive('mlValidateJob', function (reactDirective) {
return reactDirective(
ValidateJob,
injectI18nProvider(ValidateJob),
undefined,
{ restrict: 'E' },
{ mlJobService }

View file

@ -27,6 +27,8 @@ import {
EuiText
} from '@elastic/eui';
import { FormattedMessage } from '@kbn/i18n/react';
import { metadata } from 'ui/metadata';
// metadata.branch corresponds to the version used in documentation links.
const jobTipsUrl = `https://www.elastic.co/guide/en/kibana/${metadata.branch}/job-tips.html`;
@ -82,7 +84,14 @@ const statusToEuiIconType = (status) => {
}
};
const Link = ({ url }) => (<EuiLink href={url} target="_BLANK">Learn more</EuiLink>);
const Link = ({ url }) => (
<EuiLink href={url} target="_BLANK">
<FormattedMessage
id="xpack.ml.validateJob.learnMoreLinkText"
defaultMessage="Learn more"
/>
</EuiLink>
);
Link.propTypes = {
url: PropTypes.string.isRequired
};
@ -101,6 +110,7 @@ Message.propTypes = {
})
};
const Callout = ({ message }) => (
<React.Fragment>
<EuiCallOut
@ -142,7 +152,10 @@ const Modal = ({ close, title, children }) => (
size="s"
fill
>
Close
<FormattedMessage
id="xpack.ml.validateJob.modal.closeButtonLabel"
defaultMessage="Close"
/>
</EuiButton>
</EuiModalFooter>
</EuiModal>
@ -230,23 +243,46 @@ class ValidateJob extends Component {
isDisabled={isDisabled}
isLoading={this.state.ui.isLoading}
>
Validate Job
<FormattedMessage
id="xpack.ml.validateJob.validateJobButtonLabel"
defaultMessage="Validate Job"
/>
</EuiButton>
{!isDisabled && this.state.ui.isModalVisible &&
<Modal
close={this.closeModal}
title={`Validate job ${this.state.title}`}
title={<FormattedMessage
id="xpack.ml.validateJob.modal.validateJobTitle"
defaultMessage="Validate job {title}"
values={{ title: this.state.title }}
/>}
>
{this.state.data.messages.map(
(m, i) => <Callout key={`${m.id}_${i}`} message={m} />
)}
<EuiText>
Job validation performs certain checks against job configurations and underlying source data
and provides specific advice on how to adjust settings that are more likely to produce insightful results.
<FormattedMessage
id="xpack.ml.validateJob.modal.jobValidationDescriptionText"
defaultMessage="Job validation performs certain checks against job configurations and underlying source data
and provides specific advice on how to adjust settings that are more likely to produce insightful results."
/>
</EuiText>
<EuiText>
For more information, see <EuiLink href={jobTipsUrl} target="_blank">Machine Learning Job Tips</EuiLink>.
<FormattedMessage
id="xpack.ml.validateJob.modal.linkToJobTipsText"
defaultMessage="For more information, see {mlJobTipsLink}."
values={{
mlJobTipsLink: (
<EuiLink href={jobTipsUrl} target="_blank">
<FormattedMessage
id="xpack.ml.validateJob.modal.linkToJobTipsText.mlJobTipsLinkText"
defaultMessage="Machine Learning Job Tips"
/>
</EuiLink>
)
}}
/>
</EuiText>
</Modal>
}

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 { ValidateJob } from './validate_job_view';
@ -26,7 +26,7 @@ function prepareTest(messages) {
<ValidateJob getJobConfig={getJobConfig} mlJobService={mlJobService} />
);
const wrapper = shallow(component);
const wrapper = shallowWithIntl(component);
return { wrapper, p };
}