Skip to content

Commit

Permalink
* feat(UX): schedule creation UX overhaul (portainer#2485)
Browse files Browse the repository at this point in the history
* feat(api): add a new Recurring property on Schedule

* feat(schedules): date to cron convert + recurring flag

* feat(schedules): update angularjs-datetime-picker from v1 to v2

* chore(app): use minified dependency for angularjs-datetime-picker

* chore(vendor): rollback version of angularjs-datetime-picker

* * feat(ux): replace datepicker for schedule creation/details

* feat(container-stats): add refresh rate of 1 and 3 seconds (portainer#2493)

* fix(templates): set var to default value if no value selected (portainer#2323)

* fix(templates): set preset to true iff var type is preset

* fix(templates): add env var value when changing type

* feat(security): shutdown instance after 5minutes if no admin account created (portainer#2500)

* feat(security): skip admin check if --no-auth

* fix(security): change error message

* fix(vendor): use datepicker minified version

* feat(schedule-creation): replace angular-datetime-picker

* feat(schedule): parse cron to datetime

* fix(schedule): fix zero based months
  • Loading branch information
xAt0mZ authored and deviantony committed Dec 6, 2018
1 parent 9e1800e commit 1a94158
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 25 deletions.
2 changes: 2 additions & 0 deletions api/cmd/portainer/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ func loadSnapshotSystemSchedule(jobScheduler portainer.JobScheduler, snapshotter
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_snapshot",
CronExpression: "@every " + *flags.SnapshotInterval,
Recurring: true,
JobType: portainer.SnapshotJobType,
SnapshotJob: snapshotJob,
Created: time.Now().Unix(),
Expand Down Expand Up @@ -174,6 +175,7 @@ func loadEndpointSyncSystemSchedule(jobScheduler portainer.JobScheduler, schedul
ID: portainer.ScheduleID(scheduleService.GetNextIdentifier()),
Name: "system_endpointsync",
CronExpression: "@every " + *flags.SyncInterval,
Recurring: true,
JobType: portainer.EndpointSyncJobType,
EndpointSyncJob: endpointSyncJob,
Created: time.Now().Unix(),
Expand Down
15 changes: 11 additions & 4 deletions api/cron/job_script_execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import (

// ScriptExecutionJobRunner is used to run a ScriptExecutionJob
type ScriptExecutionJobRunner struct {
schedule *portainer.Schedule
context *ScriptExecutionJobContext
schedule *portainer.Schedule
context *ScriptExecutionJobContext
executedOnce bool
}

// ScriptExecutionJobContext represents the context of execution of a ScriptExecutionJob
Expand All @@ -32,15 +33,21 @@ func NewScriptExecutionJobContext(jobService portainer.JobService, endpointServi
// NewScriptExecutionJobRunner returns a new runner that can be scheduled
func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptExecutionJobContext) *ScriptExecutionJobRunner {
return &ScriptExecutionJobRunner{
schedule: schedule,
context: context,
schedule: schedule,
context: context,
executedOnce: false,
}
}

// Run triggers the execution of the job.
// It will iterate through all the endpoints specified in the context to
// execute the script associated to the job.
func (runner *ScriptExecutionJobRunner) Run() {
if !runner.schedule.Recurring && runner.executedOnce {
return
}
runner.executedOnce = true

scriptFile, err := runner.context.fileService.GetFileContent(runner.schedule.ScriptExecutionJob.ScriptPath)
if err != nil {
log.Printf("scheduled job error (script execution). Unable to retrieve script file (err=%s)\n", err)
Expand Down
14 changes: 8 additions & 6 deletions api/http/handler/schedules/schedule_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type scheduleCreateFromFilePayload struct {
Name string
Image string
CronExpression string
Recurring bool
Endpoints []portainer.EndpointID
File []byte
RetryCount int
Expand All @@ -27,6 +28,7 @@ type scheduleCreateFromFilePayload struct {
type scheduleCreateFromFileContentPayload struct {
Name string
CronExpression string
Recurring bool
Image string
Endpoints []portainer.EndpointID
FileContent string
Expand Down Expand Up @@ -174,9 +176,8 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())

job := &portainer.ScriptExecutionJob{
Endpoints: payload.Endpoints,
Image: payload.Image,
// ScheduleID: scheduleIdentifier,
Endpoints: payload.Endpoints,
Image: payload.Image,
RetryCount: payload.RetryCount,
RetryInterval: payload.RetryInterval,
}
Expand All @@ -185,6 +186,7 @@ func (handler *Handler) createScheduleObjectFromFilePayload(payload *scheduleCre
ID: scheduleIdentifier,
Name: payload.Name,
CronExpression: payload.CronExpression,
Recurring: payload.Recurring,
JobType: portainer.ScriptExecutionJobType,
ScriptExecutionJob: job,
Created: time.Now().Unix(),
Expand All @@ -197,9 +199,8 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
scheduleIdentifier := portainer.ScheduleID(handler.ScheduleService.GetNextIdentifier())

job := &portainer.ScriptExecutionJob{
Endpoints: payload.Endpoints,
Image: payload.Image,
// ScheduleID: scheduleIdentifier,
Endpoints: payload.Endpoints,
Image: payload.Image,
RetryCount: payload.RetryCount,
RetryInterval: payload.RetryInterval,
}
Expand All @@ -208,6 +209,7 @@ func (handler *Handler) createScheduleObjectFromFileContentPayload(payload *sche
ID: scheduleIdentifier,
Name: payload.Name,
CronExpression: payload.CronExpression,
Recurring: payload.Recurring,
JobType: portainer.ScriptExecutionJobType,
ScriptExecutionJob: job,
Created: time.Now().Unix(),
Expand Down
6 changes: 6 additions & 0 deletions api/http/handler/schedules/schedule_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type scheduleUpdatePayload struct {
Name *string
Image *string
CronExpression *string
Recurring *bool
Endpoints []portainer.EndpointID
FileContent *string
RetryCount *int
Expand Down Expand Up @@ -101,6 +102,11 @@ func updateSchedule(schedule *portainer.Schedule, payload *scheduleUpdatePayload
updateJobSchedule = true
}

if payload.Recurring != nil {
schedule.Recurring = *payload.Recurring
updateJobSchedule = true
}

if payload.Image != nil {
schedule.ScriptExecutionJob.Image = *payload.Image
updateJobSchedule = true
Expand Down
4 changes: 3 additions & 1 deletion api/portainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,11 +244,13 @@ type (

// Schedule represents a scheduled job.
// It only contains a pointer to one of the JobRunner implementations
// based on the JobType
// based on the JobType.
// NOTE: The Recurring option is only used by ScriptExecutionJob at the moment
Schedule struct {
ID ScheduleID `json:"Id"`
Name string
CronExpression string
Recurring bool
Created int64
JobType JobType
ScriptExecutionJob *ScriptExecutionJob
Expand Down
4 changes: 3 additions & 1 deletion app/__module.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ angular.module('portainer', [
'portainer.azure',
'portainer.docker',
'extension.storidge',
'rzModule']);
'rzModule',
'moment-picker'
]);
41 changes: 41 additions & 0 deletions app/portainer/components/forms/schedule-form/schedule-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,38 @@ angular.module('portainer.app').component('scheduleForm', {
formValidationError: ''
};

ctrl.scheduleValues = [{
displayed: 'Every hour',
cron: '0 0 * * *'
},
{
displayed: 'Every 2 hours',
cron: '0 0 0/2 * *'
}, {
displayed: 'Every day',
cron: '0 0 0 * *'
}
];

ctrl.formValues = {
datetime: ctrl.model.CronExpression ? cronToDatetime(ctrl.model.CronExpression) : moment(),
scheduleValue: ctrl.scheduleValues[0],
cronMethod: 'basic'
};

function cronToDatetime(cron) {
strings = cron.split(' ');
if (strings.length !== 5) {
return moment();
}
return moment(cron, 's m H D M');
}

function datetimeToCron(datetime) {
var date = moment(datetime);
return '0 '.concat(date.minutes(), ' ', date.hours(), ' ', date.date(), ' ', (date.month() + 1));
}

this.action = function() {
ctrl.state.formValidationError = '';

Expand All @@ -15,6 +47,15 @@ angular.module('portainer.app').component('scheduleForm', {
return;
}

if (ctrl.formValues.cronMethod === 'basic') {
if (ctrl.model.Recurring === false) {
ctrl.model.CronExpression = datetimeToCron(ctrl.formValues.datetime);
} else {
ctrl.model.CronExpression = ctrl.formValues.scheduleValue.cron;
}
} else {
ctrl.model.Recurring = true;
}
ctrl.formAction();
};

Expand Down
107 changes: 95 additions & 12 deletions app/portainer/components/forms/schedule-form/scheduleForm.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,107 @@
</div>
<!-- !name-input -->
<!-- cron-input -->
<div class="form-group">
<label for="schedule_cron" class="col-sm-1 control-label text-left">Cron rule</label>
<div class="col-sm-11">
<input type="text" class="form-control" ng-model="$ctrl.model.CronExpression" id="schedule_cron" name="schedule_cron" placeholder="0 2 * * *" required>
<!-- schedule-method-select -->
<div class="col-sm-12 form-section-title">
Schedule configuration
</div>
<div class="form-group"></div>
<div class="form-group" style="margin-bottom: 0">
<div class="boxselector_wrapper">
<div>
<input type="radio" id="config_basic" ng-model="$ctrl.formValues.cronMethod" value="basic">
<label for="config_basic">
<div class="boxselector_header">
<i class="fa fa-calendar-alt" aria-hidden="true" style="margin-right: 2px;"></i>
Basic configuration
</div>
<p>Select date from calendar</p>
</label>
</div>
<div>
<input type="radio" id="config_advanced" ng-model="$ctrl.formValues.cronMethod" value="advanced">
<label for="config_advanced">
<div class="boxselector_header">
<i class="fa fa-edit" aria-hidden="true" style="margin-right: 2px;"></i>
Advanced configuration
</div>
<p>Write your own cron rule</p>
</label>
</div>
</div>
</div>
<div class="form-group" ng-show="scheduleForm.schedule_cron.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="scheduleForm.schedule_cron.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
<!-- !schedule-method-select -->
<!-- basic-schedule -->
<div ng-if="$ctrl.formValues.cronMethod === 'basic'">
<div class="form-group">
<label for="recurring" class="col-sm-2 control-label text-left">Recurring schedule</label>
<div class="col-sm-10">
<label class="switch" style="margin-left: 20px;">
<input type="checkbox" name="recurring" ng-model="$ctrl.model.Recurring"><i></i>
</label>
</div>
</div>
<!-- not-recurring -->
<div ng-if="!$ctrl.model.Recurring">
<div class="form-group">
<label for="schedule_cron" class="col-sm-2 control-label text-left">Schedule date</label>
<div class="col-sm-10">
<input class="form-control" moment-picker ng-model="$ctrl.formValues.datetime" format="YYYY-MM-DD HH:mm">
</div>
<div ng-show="scheduleForm.datepicker.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="scheduleForm.datepicker.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
</div>
<!-- !not-recurring -->
<!-- recurring -->
<div ng-if="$ctrl.model.Recurring">
<div class="form-group">
<label for="schedule_value" class="col-sm-2 control-label text-left">Schedule time</label>
<div class="col-sm-10">
<select id="schedule_value" name="schedule_value" class="form-control"
ng-model="$ctrl.formValues.scheduleValue" ng-options="value.displayed for value in $ctrl.scheduleValues" required
></select>
</div>
<div ng-show="scheduleForm.schedule_value.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="scheduleForm.schedule_value.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
</div>
</div>
<!-- !recurring -->
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can refer to the <a href="https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format" target="_blank">following documentation</a> to get more information about the supported cron expression format.
</span>
<!-- !basic-schedule -->
<!-- advanced-schedule -->
<div ng-if="$ctrl.formValues.cronMethod === 'advanced'">
<div class="form-group">
<label for="schedule_cron" class="col-sm-2 control-label text-left">Cron rule</label>
<div class="col-sm-10">
<input type="text" class="form-control" ng-model="$ctrl.model.CronExpression" id="schedule_cron" name="schedule_cron"
placeholder="0 2 * * *" required>
</div>
</div>
<div class="form-group" ng-show="scheduleForm.schedule_cron.$invalid">
<div class="col-sm-12 small text-warning">
<div ng-messages="scheduleForm.schedule_cron.$error">
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
</div>
</div>
</div>
<div class="form-group">
<span class="col-sm-12 text-muted small">
You can refer to the <a href="https://godoc.org/github.com/robfig/cron#hdr-CRON_Expression_Format" target="_blank">following documentation</a> to get more information about the supported cron expression format.
</span>
</div>
</div>
<!-- !advanced-schedule -->
<!-- !cron-input -->
<div class="col-sm-12 form-section-title">
Job configuration
Expand Down
4 changes: 4 additions & 0 deletions app/portainer/models/schedule.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
function ScheduleDefaultModel() {
this.Name = '';
this.Recurring = false;
this.CronExpression = '';
this.JobType = 1;
this.Job = new ScriptExecutionDefaultJobModel();
Expand All @@ -16,6 +17,7 @@ function ScriptExecutionDefaultJobModel() {
function ScheduleModel(data) {
this.Id = data.Id;
this.Name = data.Name;
this.Recurring = data.Recurring;
this.JobType = data.JobType;
this.CronExpression = data.CronExpression;
this.Created = data.Created;
Expand All @@ -42,6 +44,7 @@ function ScriptExecutionTaskModel(data) {

function ScheduleCreateRequest(model) {
this.Name = model.Name;
this.Recurring = model.Recurring;
this.CronExpression = model.CronExpression;
this.Image = model.Job.Image;
this.Endpoints = model.Job.Endpoints;
Expand All @@ -54,6 +57,7 @@ function ScheduleCreateRequest(model) {
function ScheduleUpdateRequest(model) {
this.id = model.Id;
this.Name = model.Name;
this.Recurring = model.Recurring;
this.CronExpression = model.CronExpression;
this.Image = model.Job.Image;
this.Endpoints = model.Job.Endpoints;
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
<![endif]-->

<!-- build:js js/app.js -->
<script src="js/angular.js"></script>
<script src="js/vendor.js"></script>
<script src="js/angular.js"></script>
<script src="js/portainer.js"></script>
<!-- endbuild -->

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"angular-local-storage": "~0.5.2",
"angular-messages": "~1.5.0",
"angular-mocks": "~1.5.0",
"angular-moment-picker": "^0.10.2",
"angular-resource": "~1.5.0",
"angular-sanitize": "~1.5.0",
"angular-ui-bootstrap": "~2.5.0",
Expand Down
2 changes: 2 additions & 0 deletions vendor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ css:
- 'codemirror/addon/lint/lint.css'
- 'angular-json-tree/dist/angular-json-tree.css'
- 'angular-loading-bar/build/loading-bar.css'
- 'angular-moment-picker/dist/angular-moment-picker.min.css'
angular:
- 'angular/angular.js'
- 'angular-ui-bootstrap/dist/ui-bootstrap-tpls.js'
Expand All @@ -53,3 +54,4 @@ angular:
- 'angularjs-scroll-glue/src/scrollglue.js'
- 'angular-clipboard/angular-clipboard.js'
- 'angular-file-saver/dist/angular-file-saver.bundle.js'
- 'angular-moment-picker/dist/angular-moment-picker.min.js'
Loading

0 comments on commit 1a94158

Please sign in to comment.