diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss b/x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss
index 8a3783b1f7d1..1b0c67a44c3a 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss
@@ -1,6 +1,10 @@
.ml-new-job {
display: block;
}
+// Required to prevent overflow of flex item in IE11
+.ml-new-job-callout {
+ width: 100%;
+}
// SASSTODO: Proper calcs. This looks too brittle to touch quickly
.detector {
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js
index 0d1badddc5e1..b8e35ddce841 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/detectors_list_directive.js
@@ -31,6 +31,7 @@ module.directive('mlJobDetectorsList', function ($modal) {
fields: '=mlFields',
catFieldNameSelected: '=mlCatFieldNameSelected',
editMode: '=mlEditMode',
+ onUpdate: '=mlOnDetectorsUpdate'
},
template,
controller: function ($scope) {
@@ -42,11 +43,14 @@ module.directive('mlJobDetectorsList', function ($modal) {
} else {
$scope.detectors.push(dtr);
}
+
+ $scope.onUpdate();
}
};
$scope.removeDetector = function (index) {
$scope.detectors.splice(index, 1);
+ $scope.onUpdate();
};
$scope.editDetector = function (index) {
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout.test.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout.test.js
new file mode 100644
index 000000000000..6c9b597f1ebf
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout.test.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { EnableModelPlotCallout } from './enable_model_plot_callout_view.js';
+
+const message = 'Test message';
+
+describe('EnableModelPlotCallout', () => {
+
+ test('Callout is rendered correctly with message', () => {
+ const wrapper = mount();
+ const calloutText = wrapper.find('EuiText');
+
+ expect(calloutText.text()).toBe(message);
+ });
+
+});
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_directive.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_directive.js
new file mode 100644
index 000000000000..d1a4b6bb6314
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_directive.js
@@ -0,0 +1,22 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+import 'ngreact';
+
+import { uiModules } from 'ui/modules';
+const module = uiModules.get('apps/ml', ['react']);
+
+import { EnableModelPlotCallout } from './enable_model_plot_callout_view.js';
+
+module.directive('mlEnableModelPlotCallout', function (reactDirective) {
+ return reactDirective(
+ EnableModelPlotCallout,
+ undefined,
+ { restrict: 'E' }
+ );
+});
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_view.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_view.js
new file mode 100644
index 000000000000..4b69c0e61fb2
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/enable_model_plot_callout_view.js
@@ -0,0 +1,39 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+
+import PropTypes from 'prop-types';
+import React, { Fragment } from 'react';
+
+import {
+ EuiCallOut,
+ EuiFlexGroup,
+ EuiFlexItem,
+} from '@elastic/eui';
+
+
+export const EnableModelPlotCallout = ({ message }) => (
+
+
+
+
+
+ {message}
+
+
+
+
+
+);
+
+EnableModelPlotCallout.propTypes = {
+ message: PropTypes.string.isRequired,
+};
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/index.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/index.js
new file mode 100644
index 000000000000..195c9129e0ee
--- /dev/null
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/enable_model_plot_callout/index.js
@@ -0,0 +1,8 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License;
+ * you may not use this file except in compliance with the Elastic License.
+ */
+
+
+import './enable_model_plot_callout_directive.js';
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js
index 376ca85b394a..94cefd62d076 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/index.js
@@ -12,3 +12,4 @@ import './save_status_modal';
import './field_select_directive';
import 'plugins/ml/components/job_group_select';
import 'plugins/ml/jobs/components/job_timepicker_modal';
+import './enable_model_plot_callout';
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html
index 59e4ec2bfa62..89ebace8bc93 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html
@@ -238,6 +238,7 @@
ml-fields="fields"
ml-cat-field-name-selected="(job.analysis_config.categorization_field_name?true:false)"
ml-edit-mode="'NEW'"
+ ml-on-detectors-update="onDetectorsUpdate"
>
{{ ( ui.validation.tabs[1].checks.detectors.message || "At least one detector should be configured" ) }}
@@ -275,6 +276,33 @@
{{ ( ui.validation.tabs[1].checks.influencers.message || "At least one influencer should be selected" ) }}
+
+
+
+
diff --git a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js
index 802b88b321c3..3624c63d3a94 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js
@@ -18,7 +18,12 @@ import { checkFullLicense } from 'plugins/ml/license/check_license';
import { checkCreateJobsPrivilege } from 'plugins/ml/privilege/check_privilege';
import template from './new_job.html';
import saveStatusTemplate from 'plugins/ml/jobs/new_job/advanced/save_status_modal/save_status_modal.html';
-import { createSearchItems, createJobForSaving } from 'plugins/ml/jobs/new_job/utils/new_job_utils';
+import {
+ createSearchItems,
+ createJobForSaving,
+ checkCardinalitySuccess,
+ getMinimalValidJob,
+} from 'plugins/ml/jobs/new_job/utils/new_job_utils';
import { loadIndexPatterns, loadCurrentIndexPattern, loadCurrentSavedSearch, timeBasedIndexCheck } from 'plugins/ml/util/index_utils';
import { ML_JOB_FIELD_TYPES, ES_FIELD_TYPES } from 'plugins/ml/../common/constants/field_types';
import { ALLOWED_DATA_UNITS } from 'plugins/ml/../common/constants/validation';
@@ -114,6 +119,8 @@ module.controller('MlNewJob',
const mlConfirm = mlConfirmModalService;
msgs.clear();
const jobDefaults = newJobDefaults();
+ // For keeping a copy of the detectors for comparison
+ const currentConfigs = { detectors: [], model_plot_config: { enabled: false } };
$scope.job = {};
$scope.mode = MODE.NEW;
@@ -156,6 +163,15 @@ module.controller('MlNewJob',
$scope.ui.validation.tabs[tab].valid = valid;
}
},
+ cardinalityValidator: {
+ status: 0, message: '', STATUS: {
+ FAILED: -1,
+ NOT_RUNNING: 0,
+ RUNNING: 1,
+ FINISHED: 2,
+ WARNING: 3,
+ }
+ },
jsonText: '',
changeTab: changeTab,
influencers: [],
@@ -181,6 +197,7 @@ module.controller('MlNewJob',
types: {},
isDatafeed: true,
useDedicatedIndex: false,
+ enableModelPlot: false,
modelMemoryLimit: '',
modelMemoryLimitDefault: jobDefaults.anomaly_detectors.model_memory_limit,
@@ -282,9 +299,37 @@ module.controller('MlNewJob',
});
}
+ function checkForConfigUpdates() {
+ const { STATUS } = $scope.ui.cardinalityValidator;
+ // Check if enable model plot was set/has changed and update if it has.
+ const jobModelPlotValue = $scope.job.model_plot_config ? $scope.job.model_plot_config : { enabled: false };
+ const modelPlotSettingsEqual = _.isEqual(currentConfigs.model_plot_config, jobModelPlotValue);
+
+ if (!modelPlotSettingsEqual) {
+ // Update currentConfigs.
+ currentConfigs.model_plot_config.enabled = jobModelPlotValue.enabled;
+ // Update ui portion so checkbox is checked
+ $scope.ui.enableModelPlot = jobModelPlotValue.enabled;
+ }
+
+ if ($scope.ui.enableModelPlot === true) {
+ const unchanged = _.isEqual(currentConfigs.detectors, $scope.job.analysis_config.detectors);
+ // if detectors changed OR model plot was just toggled on run cardinality
+ if (!unchanged || !modelPlotSettingsEqual) {
+ runValidateCardinality();
+ }
+ } else {
+ $scope.ui.cardinalityValidator.status = STATUS.FINISHED;
+ $scope.ui.cardinalityValidator.message = '';
+ }
+ }
+
function changeTab(tab) {
$scope.ui.currentTab = tab.index;
- if (tab.index === 4) {
+ // Selecting Analysis Configuration tab
+ if (tab.index === 1) {
+ checkForConfigUpdates();
+ } else if (tab.index === 4) {
createJSONText();
} else if (tab.index === 5) {
if ($scope.ui.dataLocation === 'ES') {
@@ -651,6 +696,83 @@ module.controller('MlNewJob',
}
};
+ function runValidateCardinality() {
+ const { STATUS } = $scope.ui.cardinalityValidator;
+ $scope.ui.cardinalityValidator.status = $scope.ui.cardinalityValidator.STATUS.RUNNING;
+
+ const tempJob = mlJobService.cloneJob($scope.job);
+ _.merge(tempJob, getMinimalValidJob());
+
+ ml.validateCardinality(tempJob)
+ .then((response) => {
+ const validationResult = checkCardinalitySuccess(response);
+
+ if (validationResult.success === true) {
+ $scope.ui.cardinalityValidator.status = STATUS.FINISHED;
+ $scope.ui.cardinalityValidator.message = '';
+ } 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 select a dedicated results index on the Job Details tab.`;
+
+ $scope.ui.cardinalityValidator.status = STATUS.WARNING;
+ }
+ })
+ .catch((error) => {
+ console.log('Cardinality check error:', error);
+ $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.status = STATUS.FAILED;
+ });
+ }
+
+ $scope.onDetectorsUpdate = function () {
+ const { STATUS } = $scope.ui.cardinalityValidator;
+
+ if ($scope.ui.enableModelPlot === true) {
+ // Update currentConfigs since config changed
+ currentConfigs.detectors = _.cloneDeep($scope.job.analysis_config.detectors);
+
+ if ($scope.job.analysis_config.detectors.length === 0) {
+ $scope.ui.cardinalityValidator.status = STATUS.FINISHED;
+ $scope.ui.cardinalityValidator.message = '';
+ } else {
+ runValidateCardinality();
+ }
+ }
+ };
+
+ $scope.setModelPlotEnabled = function () {
+ const { STATUS } = $scope.ui.cardinalityValidator;
+
+ if ($scope.ui.enableModelPlot === true) {
+ // Start keeping track of the config in case of changes from Edit JSON tab requiring another cardinality check
+ currentConfigs.detectors = _.cloneDeep($scope.job.analysis_config.detectors);
+
+ $scope.job.model_plot_config = {
+ enabled: true
+ };
+
+ currentConfigs.model_plot_config.enabled = true;
+ // return early if there's nothing to run a check on yet.
+ if ($scope.job.analysis_config.detectors.length === 0) {
+ return;
+ }
+
+ runValidateCardinality();
+ } else {
+ currentConfigs.model_plot_config.enabled = false;
+ $scope.ui.cardinalityValidator.status = STATUS.FINISHED;
+ $scope.ui.cardinalityValidator.message = '';
+ delete $scope.job.model_plot_config;
+ }
+ };
+
// function called by field-select components to set
// properties in the analysis_config
$scope.setAnalysisConfigProperty = function (value, field) {
diff --git a/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js b/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js
index e153c695994b..4664b451d29e 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/simple/components/enable_model_plot_checkbox/enable_model_plot_checkbox_directive.js
@@ -9,6 +9,7 @@ import ReactDOM from 'react-dom';
import { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js';
import { ml } from '../../../../../services/ml_api_service';
+import { checkCardinalitySuccess } from '../../../utils/new_job_utils';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
@@ -34,33 +35,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 = 'Cardinality could not be validated';
- }
-
- // Only model plot cardinality relevant
- // format:[{id:"cardinality_model_plot_high",modelPlotCardinality:11405}, {id:"cardinality_partition_field",fieldName:"clientip"}]
- function checkCardinalitySuccess(data) {
- const response = {
- success: true,
- };
- // There were no fields to run cardinality on.
- if (Array.isArray(data) && data.length === 0) {
- return response;
- }
-
- for (let i = 0; i < data.length; i++) {
- if (data[i].id === 'success_cardinality') {
- break;
- }
-
- if (data[i].id === 'cardinality_model_plot_high') {
- response.success = false;
- response.highCardinality = data[i].modelPlotCardinality;
- break;
- }
- }
-
- return response;
+ $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.`;
+ // Go ahead and check the dedicated index box for them
+ $scope.formConfig.useDedicatedIndex = true;
}
function validateCardinality() {
@@ -131,7 +111,10 @@ module.directive('mlEnableModelPlotCheckbox', function () {
$scope.formConfig.enableModelPlot === false)
);
const validatorRunning = ($scope.ui.cardinalityValidator.status === STATUS.RUNNING);
- const warningStatus = ($scope.ui.cardinalityValidator.status === STATUS.WARNING && $scope.ui.formValid === true);
+ const warningStatus = (
+ ($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 props = {
diff --git a/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js b/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js
index cf55e0d43e1a..f4b5e2a2c559 100644
--- a/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js
+++ b/x-pack/plugins/ml/public/jobs/new_job/utils/new_job_utils.js
@@ -115,3 +115,44 @@ export function focusOnResultsLink(linkId, $timeout) {
$(`#${linkId}`).focus();
}, 0);
}
+
+// Only model plot cardinality relevant
+// format:[{id:"cardinality_model_plot_high",modelPlotCardinality:11405}, {id:"cardinality_partition_field",fieldName:"clientip"}]
+export function checkCardinalitySuccess(data) {
+ const response = {
+ success: true,
+ };
+ // There were no fields to run cardinality on.
+ if (Array.isArray(data) && data.length === 0) {
+ return response;
+ }
+
+ for (let i = 0; i < data.length; i++) {
+ if (data[i].id === 'success_cardinality') {
+ break;
+ }
+
+ if (data[i].id === 'cardinality_model_plot_high') {
+ response.success = false;
+ response.highCardinality = data[i].modelPlotCardinality;
+ break;
+ }
+ }
+
+ return response;
+}
+
+// Ensure validation endpoints are given job with expected minimum fields
+export function getMinimalValidJob() {
+ return {
+ analysis_config: {
+ bucket_span: '15m',
+ detectors: [],
+ influencers: []
+ },
+ data_description: { time_field: '@timestamp' },
+ datafeed_config: {
+ indices: []
+ }
+ };
+}
diff --git a/x-pack/plugins/ml/server/routes/job_validation.js b/x-pack/plugins/ml/server/routes/job_validation.js
index 2096169e232d..73e3a8685bb8 100644
--- a/x-pack/plugins/ml/server/routes/job_validation.js
+++ b/x-pack/plugins/ml/server/routes/job_validation.js
@@ -85,9 +85,7 @@ export function jobValidationRoutes(server, commonRouteConfig) {
const callWithRequest = callWithRequestFactory(server, request);
return validateCardinality(callWithRequest, request.payload)
.then(reply)
- .catch((resp) => {
- reply(wrapError(resp));
- });
+ .catch(resp => wrapError(resp));
},
config: {
...commonRouteConfig