[ML] Add checkbox to enable model plot in Advanced job wizard (#25468)

* Move cardinality success check to utils

* enableModelPlot checkbox base added

* Run cardinality check on add/update fields

* Handle changes made via json

* only run cardinality check if model plot enabled

* Handle model plot enabled via EditJSON tab

* show message on cardinality check error

* multi-metric + pop: show message on cardinality check error

* add test for callout component

* Fix flexitem overflow in IE11
This commit is contained in:
Melissa Alvarez 2018-11-16 13:41:08 -08:00 committed by GitHub
parent c6fece7607
commit 354d7cc431
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 305 additions and 33 deletions

View file

@ -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 {

View file

@ -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) {

View file

@ -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(<EnableModelPlotCallout message={message} />);
const calloutText = wrapper.find('EuiText');
expect(calloutText.text()).toBe(message);
});
});

View file

@ -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' }
);
});

View file

@ -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 }) => (
<Fragment>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiCallOut
title={'Proceed with caution!'}
color="warning"
iconType="help"
>
<p>
{message}
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
EnableModelPlotCallout.propTypes = {
message: PropTypes.string.isRequired,
};

View file

@ -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';

View file

@ -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';

View file

@ -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"
></div>
<div ng-hide="ui.validation.tabs[1].checks.detectors.valid" class="validation-error">
{{ ( ui.validation.tabs[1].checks.detectors.message || "At least one detector should be configured" ) }}
@ -275,6 +276,33 @@
<div ng-hide="ui.validation.tabs[1].checks.influencers.valid" class="validation-error">
{{ ( ui.validation.tabs[1].checks.influencers.message || "At least one influencer should be selected" ) }}
</div>
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
<div class="form-group">
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
<input
type="checkbox"
aria-labelledby="ml_aria_label_new_job_enable_model_plot"
aria-describedby="ml_aria_description_new_job_enable_model_plot"
class='kuiCheckBox'
ng-change="setModelPlotEnabled()"
ng-model="ui.enableModelPlot" />
<span class='kuiCheckBoxLabel__text'>
<span id="ml_aria_label_new_job_enable_model_plot">
{{ ui.cardinalityValidator.status === ui.cardinalityValidator.STATUS.RUNNING ? 'Validating cardinality...' : 'Enable model plot' }}
</span>
<i ml-info-icon="new_job_enable_model_plot" />
</span>
</label>
<div class='ml-new-job-callout kuiVerticalRhythm'>
<ml-enable-model-plot-callout
message='ui.cardinalityValidator.message'
ng-show="ui.cardinalityValidator.status === ui.cardinalityValidator.STATUS.WARNING ||
ui.cardinalityValidator.status === ui.cardinalityValidator.STATUS.FAILED">
</ml-enable-model-plot-callout>
</div>
</div>
</div>
</ml-job-tab-1>

View file

@ -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) {

View file

@ -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 = {

View file

@ -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: []
}
};
}

View file

@ -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