Skip to content

Commit

Permalink
[ML] MultiMetric/Population Job creation: Allow model plot enablement…
Browse files Browse the repository at this point in the history
… via checkbox (#24914) (#25202)

* 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
  • Loading branch information
alvarezmelissa87 authored Nov 6, 2018
1 parent ccc6b25 commit 4eedf46
Show file tree
Hide file tree
Showing 15 changed files with 358 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -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."
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});

});
Original file line number Diff line number Diff line change
@@ -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();
}
};
});
Original file line number Diff line number Diff line change
@@ -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,
};
Original file line number Diff line number Diff line change
@@ -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';
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ module
formValid: false,
bucketSpanValid: true,
bucketSpanEstimator: { status: 0, message: '' },
cardinalityValidator: { status: 0, message: '' },
aggTypeOptions: filterAggTypes(aggTypes.byType[METRIC_AGG_TYPE]),
fields: [],
splitFields: [],
Expand Down Expand Up @@ -203,6 +204,7 @@ module
description: '',
jobGroups: [],
useDedicatedIndex: false,
enableModelPlot: false,
isSparseData: false,
modelMemoryLimit: DEFAULT_MODEL_MEMORY_LIMIT
};
Expand Down Expand Up @@ -514,6 +516,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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down Expand Up @@ -207,6 +208,7 @@ module
description: '',
jobGroups: [],
useDedicatedIndex: false,
enableModelPlot: false,
modelMemoryLimit: DEFAULT_MODEL_MEMORY_LIMIT
};

Expand Down Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit 4eedf46

Please sign in to comment.