mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
c6fece7607
commit
354d7cc431
12 changed files with 305 additions and 33 deletions
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
|
@ -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' }
|
||||
);
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
|
@ -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';
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue