[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:
Walter Rafelsberger 2018-11-07 16:42:25 +01:00 committed by GitHub
parent 1acf855f2d
commit e26a59ef7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 418 additions and 0 deletions

View file

@ -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
};
}
}
});
};
});

View file

@ -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';

View file

@ -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>

View file

@ -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();
};
});

View file

@ -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;
}
}

View file

@ -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();
});
});
});

View file

@ -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';