From 1a94158f77123139ddff3442aacacbfd262a45d8 Mon Sep 17 00:00:00 2001 From: baron_l Date: Thu, 6 Dec 2018 20:53:23 +0100 Subject: [PATCH] * feat(UX): schedule creation UX overhaul (#2485) * 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 (#2493) * fix(templates): set var to default value if no value selected (#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 (#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 --- api/cmd/portainer/main.go | 2 + api/cron/job_script_execution.go | 15 ++- api/http/handler/schedules/schedule_create.go | 14 ++- api/http/handler/schedules/schedule_update.go | 6 + api/portainer.go | 4 +- app/__module.js | 4 +- .../forms/schedule-form/schedule-form.js | 41 +++++++ .../forms/schedule-form/scheduleForm.html | 109 +++++++++++++++--- app/portainer/models/schedule.js | 4 + index.html | 2 +- package.json | 1 + vendor.yml | 2 + yarn.lock | 18 +++ 13 files changed, 196 insertions(+), 26 deletions(-) diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index 7e5b4f581..cd1106159 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -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(), @@ -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(), diff --git a/api/cron/job_script_execution.go b/api/cron/job_script_execution.go index 6f984e8fd..2143a83f5 100644 --- a/api/cron/job_script_execution.go +++ b/api/cron/job_script_execution.go @@ -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 @@ -32,8 +33,9 @@ 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, } } @@ -41,6 +43,11 @@ func NewScriptExecutionJobRunner(schedule *portainer.Schedule, context *ScriptEx // 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) diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 893ec49f3..5c0ecbdbc 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -18,6 +18,7 @@ type scheduleCreateFromFilePayload struct { Name string Image string CronExpression string + Recurring bool Endpoints []portainer.EndpointID File []byte RetryCount int @@ -27,6 +28,7 @@ type scheduleCreateFromFilePayload struct { type scheduleCreateFromFileContentPayload struct { Name string CronExpression string + Recurring bool Image string Endpoints []portainer.EndpointID FileContent string @@ -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, } @@ -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(), @@ -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, } @@ -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(), diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index 7e741631d..0edfd0dde 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -17,6 +17,7 @@ type scheduleUpdatePayload struct { Name *string Image *string CronExpression *string + Recurring *bool Endpoints []portainer.EndpointID FileContent *string RetryCount *int @@ -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 diff --git a/api/portainer.go b/api/portainer.go index 1c6d103c6..ad7891c9d 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -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 diff --git a/app/__module.js b/app/__module.js index 00be9300d..429e2acc0 100644 --- a/app/__module.js +++ b/app/__module.js @@ -23,4 +23,6 @@ angular.module('portainer', [ 'portainer.azure', 'portainer.docker', 'extension.storidge', - 'rzModule']); + 'rzModule', + 'moment-picker' + ]); diff --git a/app/portainer/components/forms/schedule-form/schedule-form.js b/app/portainer/components/forms/schedule-form/schedule-form.js index 27b14790f..a483aef37 100644 --- a/app/portainer/components/forms/schedule-form/schedule-form.js +++ b/app/portainer/components/forms/schedule-form/schedule-form.js @@ -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 = ''; @@ -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(); }; diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html index 4cdae3d96..0307b5dd5 100644 --- a/app/portainer/components/forms/schedule-form/scheduleForm.html +++ b/app/portainer/components/forms/schedule-form/scheduleForm.html @@ -18,24 +18,107 @@ -
- -
- -
+ +
+ Schedule configuration
-
-
-
-

This field is required.

+
+
+
+
+ + +
+
+ +
-
- - You can refer to the following documentation to get more information about the supported cron expression format. - + + +
+
+ +
+ +
+
+ +
+
+ +
+ +
+
+
+
+

This field is required.

+
+
+
+
+
+ + +
+
+ +
+ +
+
+
+
+

This field is required.

+
+
+
+
+
+
+ + +
+
+ +
+ +
+
+
+
+
+

This field is required.

+
+
+
+
+ + You can refer to the following documentation to get more information about the supported cron expression format. + +
+
+
Job configuration diff --git a/app/portainer/models/schedule.js b/app/portainer/models/schedule.js index 874ebc8d5..8b9a88e88 100644 --- a/app/portainer/models/schedule.js +++ b/app/portainer/models/schedule.js @@ -1,5 +1,6 @@ function ScheduleDefaultModel() { this.Name = ''; + this.Recurring = false; this.CronExpression = ''; this.JobType = 1; this.Job = new ScriptExecutionDefaultJobModel(); @@ -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; @@ -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; @@ -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; diff --git a/index.html b/index.html index 0ec14b658..704ec3d3e 100644 --- a/index.html +++ b/index.html @@ -18,8 +18,8 @@ - + diff --git a/package.json b/package.json index e5ca71a22..0f6b5ab6f 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/vendor.yml b/vendor.yml index 097faec26..4ead34d5b 100644 --- a/vendor.yml +++ b/vendor.yml @@ -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' @@ -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' diff --git a/yarn.lock b/yarn.lock index 7592562cb..5e094094b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -136,6 +136,14 @@ angular-mocks@~1.5.0: version "1.5.11" resolved "https://registry.yarnpkg.com/angular-mocks/-/angular-mocks-1.5.11.tgz#a0e1dd0ea55fd77ee7a757d75536c5e964c86f81" +angular-moment-picker@^0.10.2: + version "0.10.2" + resolved "https://registry.yarnpkg.com/angular-moment-picker/-/angular-moment-picker-0.10.2.tgz#54c8b3c228b33dffa3b7b3d0773a585323815c33" + integrity sha512-WvmrQM0zEcFqi50yDELaF34Ilrx4PtL7mWLcpTZCJGQDvMlIsxJrB30LxOkoJv8yrrLxD2s6nnR3t1/SqioWWw== + dependencies: + angular "^1.3" + moment "^2.16.0" + angular-resource@~1.5.0: version "1.5.11" resolved "https://registry.yarnpkg.com/angular-resource/-/angular-resource-1.5.11.tgz#d93ea619184a2e0ee3ae338265758363172929f0" @@ -156,6 +164,11 @@ angular@1.x, angular@~1.5.0: version "1.5.11" resolved "https://registry.yarnpkg.com/angular/-/angular-1.5.11.tgz#8c5ba7386f15965c9acf3429f6881553aada30d6" +angular@^1.3: + version "1.7.5" + resolved "https://registry.yarnpkg.com/angular/-/angular-1.7.5.tgz#d1c1c01c6f5dc835638f3f9aa51012857bdac49e" + integrity sha512-760183yxtGzni740IBTieNuWLtPNAoMqvmC0Z62UoU0I3nqk+VJuO3JbQAXOyvo3Oy/ZsdNQwrSTh/B0OQZjNw== + angularjs-scroll-glue@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/angularjs-scroll-glue/-/angularjs-scroll-glue-2.2.0.tgz#07d3399ac16ca874c63b6b5ee2ee30558b37e5d1" @@ -2905,6 +2918,11 @@ moment@^2.10.6: version "2.14.1" resolved "https://registry.yarnpkg.com/moment/-/moment-2.14.1.tgz#b35b27c47e57ed2ddc70053d6b07becdb291741c" +moment@^2.16.0: + version "2.22.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" + integrity sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y= + moment@^2.21.0: version "2.21.0" resolved "https://registry.yarnpkg.com/moment/-/moment-2.21.0.tgz#2a114b51d2a6ec9e6d83cf803f838a878d8a023a"