[ML] Restore missing job timepicker modal. (#25288)
* [ML] Restore missing job timepicker modal. * [ML] Added a karma/mocha test to verify dependencies are loaded correctly for new_job_controller. * [ML] Use consistent import style.
This commit is contained in:
parent
1acf855f2d
commit
e26a59ef7c
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 jobTimePickerTemplate from './job_timepicker_modal.html';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.service('mlDatafeedService', function ($modal) {
|
||||
|
||||
this.openJobTimepickerWindow = function (job) {
|
||||
$modal.open({
|
||||
template: jobTimePickerTemplate,
|
||||
controller: 'MlJobTimepickerModal',
|
||||
backdrop: 'static',
|
||||
keyboard: false,
|
||||
resolve: {
|
||||
params: function () {
|
||||
return {
|
||||
job
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* 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 './datafeed_service';
|
||||
import './job_timepicker_modal_controller';
|
||||
import './styles/main.less';
|
||||
import 'plugins/ml/jobs/new_job/simple/components/watcher';
|
|
@ -0,0 +1,109 @@
|
|||
<div class="job-timepicker-modal">
|
||||
<ml-message-bar ></ml-message-bar>
|
||||
<h1 tooltip="Start datafeed for {{jobId}}" class="euiTitle">Start datafeed for {{jobId}}</h1>
|
||||
|
||||
<div class="euiSpacer euiSpacer--s"></div>
|
||||
|
||||
<div class="ml-timepicker-contents" >
|
||||
<div class="row">
|
||||
<div
|
||||
class="ml-timepicker-section"
|
||||
ng-class="{
|
||||
'ml-timepicker-right-border':
|
||||
(+ui.startRadio <= 1 && ui.endRadio === '0') ||
|
||||
(ui.startRadio === '2' && +ui.endRadio <= 1)
|
||||
}">
|
||||
<label class="kuiFormLabel">Search start time</label>
|
||||
|
||||
<div class="ml-timepicker-radios" >
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li ng-class="{ active: ui.startRadio === '1' }">
|
||||
<a ng-click="ui.startRadio = '1'" >{{ ( isNew?"Start at beginning of data" : "Continue from " + ui.lastTime ) }}</a>
|
||||
</li>
|
||||
<li ng-class="{ active: ui.startRadio === '0' }">
|
||||
<a ng-click="ui.startRadio = '0'">{{ ( isNew?"Start now" : "Continue from now" ) }}</a>
|
||||
</li>
|
||||
<li ng-class="{ active: ui.startRadio === '2' }">
|
||||
<a ng-click="ui.startRadio = '2'" ng-class="{'ml-timepicker-radio-bottom': ui.startRadio === '2'}">{{ ( isNew?"Specify start time" : "Continue from specified time" ) }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='ml-timepicker' ng-show="ui.startRadio == '2'">
|
||||
<div>
|
||||
<input type="text" class="form-control" input-datetime="YYYY-MM-DD HH:mm:ss" ng-model="ui.timepicker.from" >
|
||||
</div>
|
||||
<div>
|
||||
<datepicker
|
||||
offset-timezone
|
||||
ng-model="ui.timepicker.from"
|
||||
show-weeks="false">
|
||||
</datepicker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ml-timepicker-section"
|
||||
ng-class="{
|
||||
'ml-timepicker-left-border':
|
||||
(+ui.startRadio <= 1 && ui.endRadio === '1')
|
||||
}">
|
||||
<label class="kuiFormLabel">Search end time</label>
|
||||
<div class="ml-timepicker-radios" >
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li ng-class="{ active: ui.endRadio === '0' }">
|
||||
<a ng-click="ui.endRadio = '0'">No end time (Real-time search)</a>
|
||||
</li>
|
||||
<li ng-class="{ active: ui.endRadio === '1' }">
|
||||
<a ng-click="ui.endRadio = '1'" ng-class="{'ml-timepicker-radio-bottom': ui.endRadio === '1'}">Specify end time</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class='ml-timepicker' ng-show="ui.endRadio == '1'">
|
||||
<div>
|
||||
<input type="text" class="form-control" input-datetime="{{format}}" ng-model="ui.timepicker.to">
|
||||
</div>
|
||||
<div>
|
||||
<datepicker
|
||||
offset-timezone
|
||||
ng-model="ui.timepicker.to"
|
||||
show-weeks="false">
|
||||
</datepicker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-if="ui.endRadio === '0' && watcherEnabled">
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
<label class='kuiCheckBoxLabel kuiVerticalRhythm'>
|
||||
<input ng-model='ui.createWatch' type="checkbox" class='kuiCheckBox'/>
|
||||
<span class="kuiCheckBoxLabel__text">
|
||||
Create watch after datafeed has started
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<hr class="euiHorizontalRule euiHorizontalRule--full euiHorizontalRule--marginMedium">
|
||||
|
||||
<button
|
||||
ng-click="save()"
|
||||
ng-disabled="(
|
||||
saveLock === true ||
|
||||
( ui.startRadio==='2' && ui.timepicker.from==='' ) ||
|
||||
( ui.endRadio==='1' && ui.timepicker.to==='' )
|
||||
)"
|
||||
class="kuiButton kuiButton--primary" >
|
||||
Start
|
||||
</button>
|
||||
<button
|
||||
ng-click="cancel()"
|
||||
ng-disabled="(saveLock === true)"
|
||||
class="kuiButton kuiButton--primary"
|
||||
aria-label="Cancel">
|
||||
Cancel
|
||||
</button>
|
||||
|
||||
</div>
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* 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 moment from 'moment';
|
||||
import angular from 'angular';
|
||||
|
||||
import { mlJobService } from 'plugins/ml/services/job_service';
|
||||
import { mlMessageBarService } from 'plugins/ml/components/messagebar/messagebar_service';
|
||||
import { xpackFeatureProvider } from 'plugins/ml/license/check_license';
|
||||
|
||||
import { uiModules } from 'ui/modules';
|
||||
const module = uiModules.get('apps/ml');
|
||||
|
||||
module.controller('MlJobTimepickerModal', function (
|
||||
$scope,
|
||||
$rootScope,
|
||||
$modalInstance,
|
||||
params,
|
||||
Private) {
|
||||
const msgs = mlMessageBarService;
|
||||
$scope.saveLock = false;
|
||||
const xpackFeature = Private(xpackFeatureProvider);
|
||||
$scope.watcherEnabled = xpackFeature.isAvailable('watcher');
|
||||
|
||||
const job = angular.copy(params.job);
|
||||
$scope.jobId = job.job_id;
|
||||
|
||||
$scope.datafeedId = mlJobService.getDatafeedId(job.job_id);
|
||||
|
||||
$scope.start = '';
|
||||
$scope.end = '';
|
||||
|
||||
let lastTime = '';
|
||||
if (job.data_counts && job.data_counts.latest_record_timestamp) {
|
||||
const time = moment(job.data_counts.latest_record_timestamp);
|
||||
lastTime = time.format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
$scope.isNew = (job.data_counts && job.data_counts.input_record_count > 0) ? false : true;
|
||||
|
||||
$scope.ui = {
|
||||
lastTime: lastTime,
|
||||
startDateText: '',
|
||||
startRadio: '1',
|
||||
endDateText: '',
|
||||
endRadio: '1',
|
||||
timepicker: {
|
||||
from: '',
|
||||
to: moment()
|
||||
},
|
||||
setStartRadio: function (i) {
|
||||
$scope.ui.startRadio = i;
|
||||
},
|
||||
createWatch: false
|
||||
};
|
||||
|
||||
function extractForm() {
|
||||
if ($scope.ui.startRadio === '0') {
|
||||
$scope.start = 'now';
|
||||
}
|
||||
else if ($scope.ui.startRadio === '1') {
|
||||
$scope.start = '0';
|
||||
}
|
||||
else if ($scope.ui.startRadio === '2') {
|
||||
$scope.start = moment($scope.ui.timepicker.from).unix() * 1000;
|
||||
}
|
||||
|
||||
if ($scope.ui.endRadio === '0') {
|
||||
$scope.end = undefined;
|
||||
} else if ($scope.ui.endRadio === '1') {
|
||||
$scope.end = moment($scope.ui.timepicker.to).unix() * 1000;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.save = function () {
|
||||
$scope.saveLock = true;
|
||||
|
||||
extractForm();
|
||||
|
||||
let doStartCalled = false;
|
||||
// in 10s call the function to start the datafeed.
|
||||
// if the job has already opened and doStart has already been called, nothing will happen.
|
||||
// However, if the job is still waiting to be opened, the datafeed can be started anyway.
|
||||
window.setTimeout(doStart, 10000);
|
||||
|
||||
// Attempt to open the job first.
|
||||
// If it's already open, ignore the 409 error
|
||||
mlJobService.openJob($scope.jobId)
|
||||
.then(() => {
|
||||
doStart();
|
||||
})
|
||||
.catch((resp) => {
|
||||
if (resp.statusCode === 409) {
|
||||
doStart();
|
||||
} else {
|
||||
if (resp.statusCode === 500) {
|
||||
if (doStartCalled === false) {
|
||||
// doStart hasn't been called yet, this 500 has returned before 10s,
|
||||
// so it's not due to a timeout
|
||||
msgs.error(`Could not open ${$scope.jobId}`, resp);
|
||||
}
|
||||
} else {
|
||||
// console.log(resp);
|
||||
msgs.error(`Could not open ${$scope.jobId}`, resp);
|
||||
}
|
||||
$scope.saveLock = false;
|
||||
}
|
||||
});
|
||||
|
||||
// start the datafeed
|
||||
function doStart() {
|
||||
if (doStartCalled === false) {
|
||||
doStartCalled = true;
|
||||
mlJobService.startDatafeed($scope.datafeedId, $scope.jobId, $scope.start, $scope.end)
|
||||
.then(() => {
|
||||
$rootScope.$broadcast('jobsUpdated');
|
||||
|
||||
if ($scope.ui.createWatch) {
|
||||
$rootScope.$broadcast('openCreateWatchWindow', job);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
$scope.saveLock = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$modalInstance.close();
|
||||
window.setTimeout(() => {
|
||||
$rootScope.$broadcast('jobsUpdated');
|
||||
}, 500);
|
||||
};
|
||||
|
||||
$scope.cancel = function () {
|
||||
$modalInstance.close();
|
||||
};
|
||||
});
|
|
@ -0,0 +1,85 @@
|
|||
.job-timepicker-modal {
|
||||
font-size: 14px;
|
||||
padding:20px;
|
||||
cursor: auto;
|
||||
|
||||
h3 {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.date_container {
|
||||
width: 200px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ml-timepicker-contents {
|
||||
margin-top: 5px;
|
||||
|
||||
.btn-info.active, .kuiButton--primary.active {
|
||||
color: #ffffff;
|
||||
background-color: #154751;
|
||||
border-color: #134049;
|
||||
span {
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-default, .kuiButton--basic {
|
||||
background: transparent;
|
||||
color: #444444;
|
||||
border: 0px;
|
||||
box-shadow: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
[ml-time-input] {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-timepicker-modes {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.ml-timepicker-section {
|
||||
float: left;
|
||||
padding: 0px 15px;
|
||||
min-width: 294px;
|
||||
width: 294px;
|
||||
border-left: 1px solid #FFFFFF;
|
||||
border-right: 1px solid #FFFFFF;
|
||||
|
||||
.ml-timepicker {
|
||||
padding: 13px;
|
||||
padding-top: none;
|
||||
border: 2px solid #ecf0f1;
|
||||
border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
border-top-left-radius: 0px;
|
||||
border-top-right-radius: 0px;
|
||||
border-top: none;
|
||||
|
||||
.btn, .kuiButton {
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-timepicker-radio-bottom {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-timepicker-left-border {
|
||||
border-left: 1px solid #ecf0f1;
|
||||
}
|
||||
|
||||
.ml-timepicker-right-border {
|
||||
border-right: 1px solid #ecf0f1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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 ngMock from 'ng_mock';
|
||||
import expect from 'expect.js';
|
||||
import sinon from 'sinon';
|
||||
|
||||
// Import this way to be able to stub/mock `createSearchItems` later on in the test using sinon.
|
||||
import * as newJobUtils from 'plugins/ml/jobs/new_job/utils/new_job_utils';
|
||||
|
||||
describe('ML - Advanced Job Wizard - New Job Controller', () => {
|
||||
beforeEach(() => {
|
||||
ngMock.module('kibana');
|
||||
});
|
||||
|
||||
it('Initialize New Job Controller', (done) => {
|
||||
sinon.stub(newJobUtils, 'createSearchItems').callsFake(() => ({
|
||||
indexPattern: {},
|
||||
savedSearch: {},
|
||||
combinedQuery: {}
|
||||
}));
|
||||
ngMock.inject(function ($rootScope, $controller) {
|
||||
const scope = $rootScope.$new();
|
||||
$controller('MlNewJob', { $scope: scope });
|
||||
|
||||
// This is just about initializing the controller and making sure
|
||||
// all angularjs based dependencies get loaded without error.
|
||||
// This simple scope test is just a final sanity check.
|
||||
expect(scope.ui.pageTitle).to.be('Create a new job');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -12,3 +12,4 @@ import './detectors_list_directive';
|
|||
import './save_status_modal';
|
||||
import './field_select_directive';
|
||||
import 'plugins/ml/components/job_group_select';
|
||||
import 'plugins/ml/jobs/components/job_timepicker_modal';
|
||||
|
|
Loading…
Reference in a new issue