[ML] MultiMetric/Population Job creation: Allow model plot enablement via checkbox (#24914)

* Add route/api-mapping for validateCardinality

* Create directive for enableModelPlot checkbox

* Ensure model plot enabled prior to cardinality check

* Add callout when cardinality high

* ensure correct cardinality success check

* Population wizard: add enableModelPlot checkbox

* Update with suggested changes from review

* Remove warning when invalid. Add tests.

* Ensure checkbox updated on uncheck
This commit is contained in:
Melissa Alvarez 2018-11-06 11:52:26 +00:00 committed by GitHub
parent 89efb81fca
commit 830e149787
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 358 additions and 1 deletions

View file

@ -107,6 +107,9 @@
"new_job_dedicated_index": {
"text": "Select to store results in a separate index for this job."
},
"new_job_enable_model_plot": {
"text": "Select to enable model plot. Stores model information along with results. Can add considerable overhead to the performance of the system."
},
"new_job_model_memory_limit": {
"text": "An approximate limit for the amount of memory used by the analytical models."
},

View file

@ -0,0 +1,40 @@
/*
* 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 { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js';
const defaultProps = {
checkboxText: 'Enable model plot',
onCheckboxChange: () => {},
warningStatus: false,
warningContent: 'Test warning content',
};
describe('EnableModelPlotCheckbox', () => {
test('checkbox default is rendered correctly', () => {
const wrapper = mount(<EnableModelPlotCheckbox {...defaultProps} />);
const checkbox = wrapper.find({ type: 'checkbox' });
const label = wrapper.find('label');
expect(checkbox.props().checked).toBe(false);
expect(label.text()).toBe('Enable model plot');
});
test('onCheckboxChange function prop is called when checkbox is toggled', () => {
const mockOnChange = jest.fn();
defaultProps.onCheckboxChange = mockOnChange;
const wrapper = mount(<EnableModelPlotCheckbox {...defaultProps} />);
const checkbox = wrapper.find({ type: 'checkbox' });
checkbox.simulate('change', { target: { checked: true } });
expect(mockOnChange).toBeCalled();
});
});

View file

@ -0,0 +1,154 @@
/*
* 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 ReactDOM from 'react-dom';
import { EnableModelPlotCheckbox } from './enable_model_plot_checkbox_view.js';
import { ml } from '../../../../../services/ml_api_service';
import { uiModules } from 'ui/modules';
const module = uiModules.get('apps/ml');
module.directive('mlEnableModelPlotCheckbox', function () {
return {
restrict: 'AE',
replace: false,
scope: {
formConfig: '=',
ui: '=ui',
getJobFromConfig: '='
},
link: function ($scope, $element) {
const STATUS = {
FAILED: -1,
NOT_RUNNING: 0,
RUNNING: 1,
FINISHED: 2,
WARNING: 3,
};
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;
}
function validateCardinality() {
$scope.ui.cardinalityValidator.status = STATUS.RUNNING;
$scope.ui.cardinalityValidator.message = '';
// create temporary job since cardinality validation expects that format
const tempJob = $scope.getJobFromConfig($scope.formConfig);
ml.validateCardinality(tempJob)
.then((response) => {
const validationResult = checkCardinalitySuccess(response);
if (validationResult.success === true) {
$scope.formConfig.enableModelPlot = true;
$scope.ui.cardinalityValidator.status = STATUS.FINISHED;
} 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 use a dedicated results index.`;
$scope.ui.cardinalityValidator.status = STATUS.WARNING;
// Go ahead and check the dedicated index box for them
$scope.formConfig.useDedicatedIndex = true;
// show the advanced section so the warning message is visible since validation failed
$scope.ui.showAdvanced = true;
}
})
.catch(errorHandler);
}
// Re-validate cardinality for updated fields/splitField
// when enable model plot is checked and form valid
function revalidateCardinalityOnFieldChange() {
if ($scope.formConfig.enableModelPlot === true && $scope.ui.formValid === true) {
validateCardinality();
}
}
$scope.handleCheckboxChange = (isChecked) => {
if (isChecked) {
$scope.formConfig.enableModelPlot = true;
validateCardinality();
} else {
$scope.formConfig.enableModelPlot = false;
$scope.ui.cardinalityValidator.status = STATUS.FINISHED;
$scope.ui.cardinalityValidator.message = '';
updateCheckbox();
}
};
// Update checkbox on these changes
$scope.$watch('ui.formValid', updateCheckbox, true);
$scope.$watch('ui.cardinalityValidator.status', updateCheckbox, true);
// MultiMetric: Fire off cardinality validatation when fields and/or split by field is updated
$scope.$watch('formConfig.fields', revalidateCardinalityOnFieldChange, true);
$scope.$watch('formConfig.splitField', revalidateCardinalityOnFieldChange, true);
// Population: Fire off cardinality validatation when overField is updated
$scope.$watch('formConfig.overField', revalidateCardinalityOnFieldChange, true);
function updateCheckbox() {
// disable if (check is running && checkbox checked) or (form is invalid && checkbox unchecked)
const checkboxDisabled = (
($scope.ui.cardinalityValidator.status === STATUS.RUNNING &&
$scope.formConfig.enableModelPlot === true) ||
($scope.ui.formValid !== true &&
$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 checkboxText = (validatorRunning) ? 'Validating cardinality...' : 'Enable model plot';
const props = {
checkboxDisabled,
checkboxText,
onCheckboxChange: $scope.handleCheckboxChange,
warningContent: $scope.ui.cardinalityValidator.message,
warningStatus,
};
ReactDOM.render(
React.createElement(EnableModelPlotCheckbox, props),
$element[0]
);
}
updateCheckbox();
}
};
});

View file

@ -0,0 +1,92 @@
/*
* 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, Component } from 'react';
import {
EuiCallOut,
EuiCheckbox,
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { JsonTooltip } from '../../../../../components/json_tooltip/json_tooltip';
export class EnableModelPlotCheckbox extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
};
}
warningTitle = 'Proceed with caution!';
onChange = (e) => {
this.setState({
checked: e.target.checked,
});
this.props.onCheckboxChange(e.target.checked);
};
renderWarningCallout = () => (
<Fragment>
<EuiFlexGroup direction="column">
<EuiFlexItem grow={false}>
<EuiCallOut
title={this.warningTitle}
color="warning"
iconType="help"
>
<p>
{this.props.warningContent}
</p>
</EuiCallOut>
</EuiFlexItem>
</EuiFlexGroup>
</Fragment>
);
render() {
return (
<Fragment>
<EuiFlexGroup alignItems="center" gutterSize="s" responsive={false}>
<EuiFlexItem grow={false}>
<EuiCheckbox
id="new_job_enable_model_plot"
label={this.props.checkboxText}
onChange={this.onChange}
disabled={this.props.checkboxDisabled}
checked={this.state.checked}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<JsonTooltip id={'new_job_enable_model_plot'} position="top" />
</EuiFlexItem>
</EuiFlexGroup>
{ this.props.warningStatus && this.renderWarningCallout() }
</Fragment>
);
}
}
EnableModelPlotCheckbox.propTypes = {
checkboxDisabled: PropTypes.bool,
checkboxText: PropTypes.string.isRequired,
onCheckboxChange: PropTypes.func.isRequired,
warningStatus: PropTypes.bool.isRequired,
warningContent: PropTypes.string.isRequired,
};
EnableModelPlotCheckbox.defaultProps = {
checkboxDisabled: false,
};

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_checkbox_directive.js';

View file

@ -47,6 +47,13 @@
<i ml-info-icon="new_job_advanced_settings" ></i>
</div>
<div class='advanced-group' ng-show="ui.showAdvanced">
<div class="form-group">
<ml-enable-model-plot-checkbox
form-config='formConfig'
ui='ui'
get-job-from-config='getJobFromConfig'>
</ml-enable-model-plot-checkbox>
</div>
<div class="form-group">
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
<input type="checkbox"

View file

@ -130,6 +130,7 @@ module
formValid: false,
bucketSpanValid: true,
bucketSpanEstimator: { status: 0, message: '' },
cardinalityValidator: { status: 0, message: '' },
aggTypeOptions: filterAggTypes(aggTypes.byType[METRIC_AGG_TYPE]),
fields: [],
splitFields: [],
@ -202,6 +203,7 @@ module
description: '',
jobGroups: [],
useDedicatedIndex: false,
enableModelPlot: false,
isSparseData: false,
modelMemoryLimit: DEFAULT_MODEL_MEMORY_LIMIT
};
@ -513,6 +515,9 @@ module
}
};
// expose this function so it can be used in the enable model plot checkbox directive
$scope.getJobFromConfig = mlMultiMetricJobService.getJobFromConfig;
addJobValidationMethods($scope, mlMultiMetricJobService);
function loadCharts() {

View file

@ -173,6 +173,14 @@ export function MultiMetricJobServiceProvider() {
const job = mlJobService.getBlankJob();
job.data_description.time_field = formConfig.timeField;
if (formConfig.enableModelPlot === true) {
job.model_plot_config = {
enabled: true
};
} else if (formConfig.enableModelPlot === false) {
delete job.model_plot_config;
}
_.each(formConfig.fields, (field, key) => {
let func = field.agg.type.mlName;
if (formConfig.isSparseData) {

View file

@ -19,6 +19,7 @@ import 'plugins/ml/jobs/new_job/simple/components/fields_selection';
import 'plugins/ml/jobs/new_job/simple/components/influencers_selection';
import 'plugins/ml/jobs/new_job/simple/components/bucket_span_selection';
import 'plugins/ml/jobs/new_job/simple/components/general_job_details';
import 'plugins/ml/jobs/new_job/simple/components/enable_model_plot_checkbox';
import 'plugins/ml/jobs/new_job/simple/components/agg_types_filter';
import 'plugins/ml/components/job_group_select';
import 'plugins/ml/components/full_time_range_selector';

View file

@ -130,6 +130,7 @@ module
formValid: false,
bucketSpanValid: true,
bucketSpanEstimator: { status: 0, message: '' },
cardinalityValidator: { status: 0, message: '' },
aggTypeOptions: filterAggTypes(aggTypes.byType[METRIC_AGG_TYPE]),
fields: [],
overFields: [],
@ -207,6 +208,7 @@ module
description: '',
jobGroups: [],
useDedicatedIndex: false,
enableModelPlot: false,
modelMemoryLimit: DEFAULT_MODEL_MEMORY_LIMIT
};
@ -540,6 +542,9 @@ module
}
};
// expose this function so it can be used in the enable model plot checkbox directive
$scope.getJobFromConfig = mlPopulationJobService.getJobFromConfig;
addJobValidationMethods($scope, mlPopulationJobService);
function loadCharts() {

View file

@ -196,6 +196,14 @@ export function PopulationJobServiceProvider(Private) {
const job = mlJobService.getBlankJob();
job.data_description.time_field = formConfig.timeField;
if (formConfig.enableModelPlot === true) {
job.model_plot_config = {
enabled: true
};
} else if (formConfig.enableModelPlot === false) {
delete job.model_plot_config;
}
formConfig.fields.forEach(field => {
let func = field.agg.type.mlName;
if (formConfig.isSparseData) {

View file

@ -19,6 +19,7 @@ import 'plugins/ml/jobs/new_job/simple/components/fields_selection_population';
import 'plugins/ml/jobs/new_job/simple/components/influencers_selection';
import 'plugins/ml/jobs/new_job/simple/components/bucket_span_selection';
import 'plugins/ml/jobs/new_job/simple/components/general_job_details';
import 'plugins/ml/jobs/new_job/simple/components/enable_model_plot_checkbox';
import 'plugins/ml/jobs/new_job/simple/components/agg_types_filter';
import 'plugins/ml/components/job_group_select';
import 'plugins/ml/components/full_time_range_selector';

View file

@ -100,6 +100,14 @@ export const ml = {
});
},
validateCardinality(obj) {
return http({
url: `${basePath}/validate/cardinality`,
method: 'POST',
data: obj
});
},
getDatafeeds(obj) {
const datafeedId = (obj && obj.datafeedId) ? `/${obj.datafeedId}` : '';
return http({

View file

@ -7,3 +7,4 @@
export { validateJob } from './job_validation';
export { validateCardinality } from './validate_cardinality';

View file

@ -12,7 +12,7 @@ import { callWithRequestFactory } from '../client/call_with_request_factory';
import { wrapError } from '../client/errors';
import { estimateBucketSpanFactory } from '../models/bucket_span_estimator';
import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit';
import { validateJob } from '../models/job_validation';
import { validateJob, validateCardinality } from '../models/job_validation';
export function jobValidationRoutes(server, commonRouteConfig) {
@ -78,6 +78,22 @@ export function jobValidationRoutes(server, commonRouteConfig) {
}
});
server.route({
method: 'POST',
path: '/api/ml/validate/cardinality',
handler(request, reply) {
const callWithRequest = callWithRequestFactory(server, request);
return validateCardinality(callWithRequest, request.payload)
.then(reply)
.catch((resp) => {
reply(wrapError(resp));
});
},
config: {
...commonRouteConfig
}
});
server.route({
method: 'POST',
path: '/api/ml/validate/job',