mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[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:
parent
89efb81fca
commit
830e149787
15 changed files with 358 additions and 1 deletions
|
@ -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."
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
});
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
});
|
|
@ -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,
|
||||
};
|
|
@ -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';
|
|
@ -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"
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
|
||||
|
||||
export { validateJob } from './job_validation';
|
||||
export { validateCardinality } from './validate_cardinality';
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue