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