Skip to content
Merged
4 changes: 4 additions & 0 deletions x-pack/plugins/ml/public/jobs/new_job/advanced/_advanced.scss
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.directive('mlJobDetectorsList', function ($modal) {
fields: '=mlFields',
catFieldNameSelected: '=mlCatFieldNameSelected',
editMode: '=mlEditMode',
onUpdate: '=mlOnDetectorsUpdate'
},
template,
controller: function ($scope) {
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});

});
Original file line number Diff line number Diff line change
@@ -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' }
);
});
Original file line number Diff line number Diff line change
@@ -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 }) => (
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably have a unit test for this component, even though it is fairly simple.

<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,
};
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_callout_directive.js';
1 change: 1 addition & 0 deletions x-pack/plugins/ml/public/jobs/new_job/advanced/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
28 changes: 28 additions & 0 deletions x-pack/plugins/ml/public/jobs/new_job/advanced/new_job.html
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ <h3 class="euiTitle euiTitle--large">{{ui.pageTitle}}</h3>
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" ) }}
Expand Down Expand Up @@ -275,6 +276,33 @@ <h3 class="euiTitle euiTitle--large">{{ui.pageTitle}}</h3>
<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>

Expand Down
126 changes: 124 additions & 2 deletions x-pack/plugins/ml/public/jobs/new_job/advanced/new_job_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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: [],
Expand All @@ -181,6 +197,7 @@ module.controller('MlNewJob',
types: {},
isDatafeed: true,
useDedicatedIndex: false,
enableModelPlot: false,
modelMemoryLimit: '',
modelMemoryLimitDefault: jobDefaults.anomaly_detectors.model_memory_limit,

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