From 4740375ba582b59aee593867185f6fd4f627d05e Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Wed, 7 Nov 2018 11:59:21 +1300 Subject: [PATCH] feat(schedules): add schedules UI (#2414) * feat(schedules): add schedules UI mockups * feat(schedules): update controller pattern * feat(schedules): leverages API * feat(schedules): add the ability create/edit a script execution job schedule * feat(schedules): add form validation and details about cron expression --- api/http/handler/schedules/schedule_create.go | 14 +- app/constants.js | 1 + app/portainer/__module.js | 36 ++++ .../schedulesDatatable.html | 101 +++++++++++ .../schedules-datatable/schedulesDatatable.js | 13 ++ .../schedulesDatatableController.js | 58 ++++++ .../forms/schedule-form/schedule-form.js | 35 ++++ .../forms/schedule-form/scheduleForm.html | 165 ++++++++++++++++++ .../multi-endpoint-selector.js | 9 + .../multiEndpointSelector.html | 14 ++ .../multiEndpointSelectorController.js | 35 ++++ app/portainer/models/schedule.js | 47 +++++ app/portainer/rest/schedule.js | 12 ++ app/portainer/services/api/scheduleService.js | 59 +++++++ app/portainer/services/fileUpload.js | 13 ++ .../create/createScheduleController.js | 52 ++++++ .../schedules/create/createschedule.html | 23 +++ .../views/schedules/edit/schedule.html | 27 +++ .../schedules/edit/scheduleController.js | 47 +++++ app/portainer/views/schedules/schedules.html | 19 ++ .../views/schedules/schedulesController.js | 50 ++++++ app/portainer/views/sidebar/sidebar.html | 3 + 22 files changed, 826 insertions(+), 7 deletions(-) create mode 100644 app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html create mode 100644 app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js create mode 100644 app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js create mode 100644 app/portainer/components/forms/schedule-form/schedule-form.js create mode 100644 app/portainer/components/forms/schedule-form/scheduleForm.html create mode 100644 app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js create mode 100644 app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html create mode 100644 app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js create mode 100644 app/portainer/models/schedule.js create mode 100644 app/portainer/rest/schedule.js create mode 100644 app/portainer/services/api/scheduleService.js create mode 100644 app/portainer/views/schedules/create/createScheduleController.js create mode 100644 app/portainer/views/schedules/create/createschedule.html create mode 100644 app/portainer/views/schedules/edit/schedule.html create mode 100644 app/portainer/views/schedules/edit/scheduleController.js create mode 100644 app/portainer/views/schedules/schedules.html create mode 100644 app/portainer/views/schedules/schedulesController.js diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 6a7b0deb0..4d8fbd740 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -32,32 +32,32 @@ type scheduleFromFileContentPayload struct { func (payload *scheduleFromFilePayload) Validate(r *http.Request) error { name, err := request.RetrieveMultiPartFormValue(r, "Name", false) if err != nil { - return err + return errors.New("Invalid name") } payload.Name = name image, err := request.RetrieveMultiPartFormValue(r, "Image", false) if err != nil { - return err + return errors.New("Invalid image") } payload.Image = image - cronExpression, err := request.RetrieveMultiPartFormValue(r, "Schedule", false) + cronExpression, err := request.RetrieveMultiPartFormValue(r, "CronExpression", false) if err != nil { - return err + return errors.New("Invalid cron expression") } payload.CronExpression = cronExpression var endpoints []portainer.EndpointID err = request.RetrieveMultiPartFormJSONValue(r, "Endpoints", &endpoints, false) if err != nil { - return err + return errors.New("Invalid endpoints") } payload.Endpoints = endpoints - file, _, err := request.RetrieveMultiPartFormFile(r, "File") + file, _, err := request.RetrieveMultiPartFormFile(r, "file") if err != nil { - return portainer.Error("Invalid Script file. Ensure that the file is uploaded correctly") + return portainer.Error("Invalid script file. Ensure that the file is uploaded correctly") } payload.File = file diff --git a/app/constants.js b/app/constants.js index b20d44649..0142d4c18 100644 --- a/app/constants.js +++ b/app/constants.js @@ -6,6 +6,7 @@ angular.module('portainer') .constant('API_ENDPOINT_MOTD', 'api/motd') .constant('API_ENDPOINT_REGISTRIES', 'api/registries') .constant('API_ENDPOINT_RESOURCE_CONTROLS', 'api/resource_controls') +.constant('API_ENDPOINT_SCHEDULES', 'api/schedules') .constant('API_ENDPOINT_SETTINGS', 'api/settings') .constant('API_ENDPOINT_STACKS', 'api/stacks') .constant('API_ENDPOINT_STATUS', 'api/status') diff --git a/app/portainer/__module.js b/app/portainer/__module.js index d04e12b6e..e6ab9f5d2 100644 --- a/app/portainer/__module.js +++ b/app/portainer/__module.js @@ -242,6 +242,39 @@ angular.module('portainer.app', []) } }; + var schedules = { + name: 'portainer.schedules', + url: '/schedules', + views: { + 'content@': { + templateUrl: 'app/portainer/views/schedules/schedules.html', + controller: 'SchedulesController' + } + } + }; + + var schedule = { + name: 'portainer.schedules.schedule', + url: '/:id', + views: { + 'content@': { + templateUrl: 'app/portainer/views/schedules/edit/schedule.html', + controller: 'ScheduleController' + } + } + }; + + var scheduleCreation = { + name: 'portainer.schedules.new', + url: '/new', + views: { + 'content@': { + templateUrl: 'app/portainer/views/schedules/create/createschedule.html', + controller: 'CreateScheduleController' + } + } + }; + var settings = { name: 'portainer.settings', url: '/settings', @@ -428,6 +461,9 @@ angular.module('portainer.app', []) $stateRegistryProvider.register(registry); $stateRegistryProvider.register(registryAccess); $stateRegistryProvider.register(registryCreation); + $stateRegistryProvider.register(schedules); + $stateRegistryProvider.register(schedule); + $stateRegistryProvider.register(scheduleCreation); $stateRegistryProvider.register(settings); $stateRegistryProvider.register(settingsAuthentication); $stateRegistryProvider.register(stacks); diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html new file mode 100644 index 000000000..adccfff95 --- /dev/null +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html @@ -0,0 +1,101 @@ +
+ + +
+
+ {{ $ctrl.titleText }} +
+
+
+ + +
+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + + + + + Name + + + + + + Cron expression + + + + + + Created + + + +
+ + + + + {{ item.Name }} + {{ item.Name }} + + {{ item.CronExpression }} + {{ item.Created | getisodatefromtimestamp }}
Loading...
No schedule available.
+
+ +
+
+
diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js new file mode 100644 index 000000000..13a9bd34f --- /dev/null +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatable.js @@ -0,0 +1,13 @@ +angular.module('portainer.app').component('schedulesDatatable', { + templateUrl: 'app/portainer/components/datatables/schedules-datatable/schedulesDatatable.html', + controller: 'SchedulesDatatableController', + bindings: { + titleText: '@', + titleIcon: '@', + dataset: '<', + tableKey: '@', + orderBy: '@', + reverseOrder: '<', + removeAction: '<' + } +}); diff --git a/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js new file mode 100644 index 000000000..bac777070 --- /dev/null +++ b/app/portainer/components/datatables/schedules-datatable/schedulesDatatableController.js @@ -0,0 +1,58 @@ +angular.module('portainer.app') +.controller('SchedulesDatatableController', ['PaginationService', 'DatatableService', +function (PaginationService, DatatableService) { + + this.state = { + selectAll: false, + orderBy: this.orderBy, + paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), + displayTextFilter: false, + selectedItemCount: 0, + selectedItems: [] + }; + + this.changeOrderBy = function(orderField) { + this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false; + this.state.orderBy = orderField; + DatatableService.setDataTableOrder(this.tableKey, orderField, this.state.reverseOrder); + }; + + this.selectItem = function(item) { + if (item.Checked) { + this.state.selectedItemCount++; + this.state.selectedItems.push(item); + } else { + this.state.selectedItems.splice(this.state.selectedItems.indexOf(item), 1); + this.state.selectedItemCount--; + } + }; + + this.selectAll = function() { + for (var i = 0; i < this.state.filteredDataSet.length; i++) { + var item = this.state.filteredDataSet[i]; + if (item.JobType ===1 && item.Checked !== this.state.selectAll) { + item.Checked = this.state.selectAll; + this.selectItem(item); + } + } + }; + + this.changePaginationLimit = function() { + PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit); + }; + + this.$onInit = function() { + setDefaults(this); + + var storedOrder = DatatableService.getDataTableOrder(this.tableKey); + if (storedOrder !== null) { + this.state.reverseOrder = storedOrder.reverse; + this.state.orderBy = storedOrder.orderBy; + } + }; + + function setDefaults(ctrl) { + ctrl.showTextFilter = ctrl.showTextFilter ? ctrl.showTextFilter : false; + ctrl.state.reverseOrder = ctrl.reverseOrder ? ctrl.reverseOrder : false; + } +}]); diff --git a/app/portainer/components/forms/schedule-form/schedule-form.js b/app/portainer/components/forms/schedule-form/schedule-form.js new file mode 100644 index 000000000..27b14790f --- /dev/null +++ b/app/portainer/components/forms/schedule-form/schedule-form.js @@ -0,0 +1,35 @@ +angular.module('portainer.app').component('scheduleForm', { + templateUrl: 'app/portainer/components/forms/schedule-form/scheduleForm.html', + controller: function() { + var ctrl = this; + + ctrl.state = { + formValidationError: '' + }; + + this.action = function() { + ctrl.state.formValidationError = ''; + + if (ctrl.model.Job.Method === 'editor' && ctrl.model.Job.FileContent === '') { + ctrl.state.formValidationError = 'Script file content must not be empty'; + return; + } + + ctrl.formAction(); + }; + + this.editorUpdate = function(cm) { + ctrl.model.Job.FileContent = cm.getValue(); + }; + }, + bindings: { + model: '=', + endpoints: '<', + groups: '<', + addLabelAction: '<', + removeLabelAction: '<', + formAction: '<', + formActionLabel: '@', + actionInProgress: '<' + } +}); diff --git a/app/portainer/components/forms/schedule-form/scheduleForm.html b/app/portainer/components/forms/schedule-form/scheduleForm.html new file mode 100644 index 000000000..1f287ece1 --- /dev/null +++ b/app/portainer/components/forms/schedule-form/scheduleForm.html @@ -0,0 +1,165 @@ +
+
+ Schedule configuration +
+ +
+ +
+ +
+
+
+
+
+

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

This field is required.

+
+
+
+ +
+
+ + This schedule will be executed via a privileged container on the target hosts. You can access the host filesystem under the + /host folder. + +
+ +
+ Job content +
+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ Web editor +
+
+
+ +
+
+
+ + +
+
+ Upload +
+
+ + You can upload a script file from your computer. + +
+
+
+ + + {{ $ctrl.model.Job.File.name }} + + +
+
+
+ +
+
+ Target endpoints +
+ + + + +
+ Actions +
+
+
+ + + {{ $ctrl.state.formValidationError }} + +
+
+ +
diff --git a/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js b/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js new file mode 100644 index 000000000..ecec019c4 --- /dev/null +++ b/app/portainer/components/multi-endpoint-selector/multi-endpoint-selector.js @@ -0,0 +1,9 @@ +angular.module('portainer.app').component('multiEndpointSelector', { + templateUrl: 'app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html', + controller: 'MultiEndpointSelectorController', + bindings: { + 'model': '=', + 'endpoints': '<', + 'groups': '<' + } +}); diff --git a/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html b/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html new file mode 100644 index 000000000..5acf2149d --- /dev/null +++ b/app/portainer/components/multi-endpoint-selector/multiEndpointSelector.html @@ -0,0 +1,14 @@ + + + + {{ $item.Name }} + ({{ $item.Tags | arraytostr }}) + + + + + {{ endpoint.Name }} + ({{ endpoint.Tags | arraytostr }}) + + + diff --git a/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js b/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js new file mode 100644 index 000000000..418682771 --- /dev/null +++ b/app/portainer/components/multi-endpoint-selector/multiEndpointSelectorController.js @@ -0,0 +1,35 @@ +angular.module('portainer.app') +.controller('MultiEndpointSelectorController', function () { + var ctrl = this; + + this.sortGroups = function(groups) { + return _.sortBy(groups, ['name']); + }; + + this.groupEndpoints = function(endpoint) { + for (var i = 0; i < ctrl.availableGroups.length; i++) { + var group = ctrl.availableGroups[i]; + + if (endpoint.GroupId === group.Id) { + return group.Name; + } + } + }; + + this.$onInit = function() { + this.availableGroups = filterEmptyGroups(this.groups, this.endpoints); + }; + + function filterEmptyGroups(groups, endpoints) { + return groups.filter(function f(group) { + for (var i = 0; i < endpoints.length; i++) { + + var endpoint = endpoints[i]; + if (endpoint.GroupId === group.Id) { + return true; + } + } + return false; + }); + } +}); diff --git a/app/portainer/models/schedule.js b/app/portainer/models/schedule.js new file mode 100644 index 000000000..a3bb7cd45 --- /dev/null +++ b/app/portainer/models/schedule.js @@ -0,0 +1,47 @@ +function ScheduleDefaultModel() { + this.Name = ''; + this.CronExpression = ''; + this.JobType = 1; + this.Job = new ScriptExecutionDefaultJobModel(); +} + +function ScriptExecutionDefaultJobModel() { + this.Image = ''; + this.Endpoints = []; + this.FileContent = ''; + this.File = null; + this.Method = 'editor'; +} + +function ScheduleModel(data) { + this.Id = data.Id; + this.Name = data.Name; + this.JobType = data.JobType; + this.CronExpression = data.CronExpression; + this.Created = data.Created; + if (this.JobType === 1) { + this.Job = new ScriptExecutionJobModel(data.ScriptExecutionJob); + } +} + +function ScriptExecutionJobModel(data) { + this.Image = data.Image; + this.Endpoints = data.Endpoints; +} + +function ScheduleCreateRequest(model) { + this.Name = model.Name; + this.CronExpression = model.CronExpression; + this.Image = model.Job.Image; + this.Endpoints = model.Job.Endpoints; + this.FileContent = model.Job.FileContent; + this.File = model.Job.File; +} + +function ScheduleUpdateRequest(model) { + this.id = model.Id; + this.Name = model.Name; + this.CronExpression = model.CronExpression; + this.Image = model.Job.Image; + this.Endpoints = model.Job.Endpoints; +} diff --git a/app/portainer/rest/schedule.js b/app/portainer/rest/schedule.js new file mode 100644 index 000000000..8bd2fa624 --- /dev/null +++ b/app/portainer/rest/schedule.js @@ -0,0 +1,12 @@ +angular.module('portainer.app') +.factory('Schedules', ['$resource', 'API_ENDPOINT_SCHEDULES', +function SchedulesFactory($resource, API_ENDPOINT_SCHEDULES) { + 'use strict'; + return $resource(API_ENDPOINT_SCHEDULES + '/:id', {}, { + create: { method: 'POST' }, + query: { method: 'GET', isArray: true }, + get: { method: 'GET', params: { id: '@id' } }, + update: { method: 'PUT', params: { id: '@id' } }, + remove: { method: 'DELETE', params: { id: '@id'} } + }); +}]); diff --git a/app/portainer/services/api/scheduleService.js b/app/portainer/services/api/scheduleService.js new file mode 100644 index 000000000..f7723ef09 --- /dev/null +++ b/app/portainer/services/api/scheduleService.js @@ -0,0 +1,59 @@ +angular.module('portainer.app') +.factory('ScheduleService', ['$q', 'Schedules', 'FileUploadService', +function ScheduleService($q, Schedules, FileUploadService) { + 'use strict'; + var service = {}; + + service.schedule = function(scheduleId) { + var deferred = $q.defer(); + + Schedules.get({ id: scheduleId }).$promise + .then(function success(data) { + var schedule = new ScheduleModel(data); + deferred.resolve(schedule); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve schedule', err: err }); + }); + + return deferred.promise; + }; + + service.schedules = function() { + var deferred = $q.defer(); + + Schedules.query().$promise + .then(function success(data) { + var schedules = data.map(function (item) { + return new ScheduleModel(item); + }); + deferred.resolve(schedules); + }) + .catch(function error(err) { + deferred.reject({ msg: 'Unable to retrieve schedules', err: err }); + }); + + return deferred.promise; + }; + + service.createScheduleFromFileContent = function(model) { + var payload = new ScheduleCreateRequest(model); + return Schedules.create({ method: 'string' }, payload).$promise; + }; + + service.createScheduleFromFileUpload = function(model) { + var payload = new ScheduleCreateRequest(model); + return FileUploadService.createSchedule(payload); + }; + + service.updateSchedule = function(model) { + var payload = new ScheduleUpdateRequest(model); + return Schedules.update(payload).$promise; + }; + + service.deleteSchedule = function(scheduleId) { + return Schedules.remove({ id: scheduleId }).$promise; + }; + + return service; +}]); diff --git a/app/portainer/services/fileUpload.js b/app/portainer/services/fileUpload.js index 2d43cc44e..15b9ffbd3 100644 --- a/app/portainer/services/fileUpload.js +++ b/app/portainer/services/fileUpload.js @@ -39,6 +39,19 @@ angular.module('portainer.app') }); }; + service.createSchedule = function(payload) { + return Upload.upload({ + url: 'api/schedules?method=file', + data: { + file: payload.File, + Name: payload.Name, + CronExpression: payload.CronExpression, + Image: payload.Image, + Endpoints: Upload.json(payload.Endpoints) + } + }); + }; + service.createSwarmStack = function(stackName, swarmId, file, env, endpointId) { return Upload.upload({ url: 'api/stacks?method=file&type=1&endpointId=' + endpointId, diff --git a/app/portainer/views/schedules/create/createScheduleController.js b/app/portainer/views/schedules/create/createScheduleController.js new file mode 100644 index 000000000..03211d804 --- /dev/null +++ b/app/portainer/views/schedules/create/createScheduleController.js @@ -0,0 +1,52 @@ +angular.module('portainer.app') +.controller('CreateScheduleController', ['$q', '$scope', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService', +function ($q, $scope, $state, Notifications, EndpointService, GroupService, ScheduleService) { + + $scope.state = { + actionInProgress: false + }; + + $scope.create = create; + + function create() { + var model = $scope.model; + + $scope.state.actionInProgress = true; + createSchedule(model) + .then(function success() { + Notifications.success('Schedule successfully created'); + $state.go('portainer.schedules', {}, {reload: true}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to create schedule'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + } + + function createSchedule(model) { + if (model.Job.Method === 'editor') { + return ScheduleService.createScheduleFromFileContent(model); + } + return ScheduleService.createScheduleFromFileUpload(model); + } + + function initView() { + $scope.model = new ScheduleDefaultModel(); + + $q.all({ + endpoints: EndpointService.endpoints(), + groups: GroupService.groups() + }) + .then(function success(data) { + $scope.endpoints = data.endpoints; + $scope.groups = data.groups; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); + }); + } + + initView(); +}]); diff --git a/app/portainer/views/schedules/create/createschedule.html b/app/portainer/views/schedules/create/createschedule.html new file mode 100644 index 000000000..801cc41ca --- /dev/null +++ b/app/portainer/views/schedules/create/createschedule.html @@ -0,0 +1,23 @@ + + + + Schedules > Add schedule + + + +
+
+ + + + + +
+
diff --git a/app/portainer/views/schedules/edit/schedule.html b/app/portainer/views/schedules/edit/schedule.html new file mode 100644 index 000000000..f049459d6 --- /dev/null +++ b/app/portainer/views/schedules/edit/schedule.html @@ -0,0 +1,27 @@ + + + + + + + + Schedules > {{ ::schedule.Name }} + + + +
+
+ + + + + +
+
diff --git a/app/portainer/views/schedules/edit/scheduleController.js b/app/portainer/views/schedules/edit/scheduleController.js new file mode 100644 index 000000000..628f2d5c3 --- /dev/null +++ b/app/portainer/views/schedules/edit/scheduleController.js @@ -0,0 +1,47 @@ +angular.module('portainer.app') +.controller('ScheduleController', ['$q', '$scope', '$transition$', '$state', 'Notifications', 'EndpointService', 'GroupService', 'ScheduleService', +function ($q, $scope, $transition$, $state, Notifications, EndpointService, GroupService, ScheduleService) { + + $scope.state = { + actionInProgress: false + }; + + $scope.update = update; + + function update() { + var model = $scope.schedule; + + $scope.state.actionInProgress = true; + ScheduleService.updateSchedule(model) + .then(function success() { + Notifications.success('Schedule successfully updated'); + $state.go('portainer.schedules', {}, {reload: true}); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to update schedule'); + }) + .finally(function final() { + $scope.state.actionInProgress = false; + }); + } + + function initView() { + var id = $transition$.params().id; + + $q.all({ + schedule: ScheduleService.schedule(id), + endpoints: EndpointService.endpoints(), + groups: GroupService.groups() + }) + .then(function success(data) { + $scope.schedule = data.schedule; + $scope.endpoints = data.endpoints; + $scope.groups = data.groups; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoint list'); + }); + } + + initView(); +}]); diff --git a/app/portainer/views/schedules/schedules.html b/app/portainer/views/schedules/schedules.html new file mode 100644 index 000000000..507640db8 --- /dev/null +++ b/app/portainer/views/schedules/schedules.html @@ -0,0 +1,19 @@ + + + + + + + Schedules + + +
+
+ +
+
diff --git a/app/portainer/views/schedules/schedulesController.js b/app/portainer/views/schedules/schedulesController.js new file mode 100644 index 000000000..2a8632613 --- /dev/null +++ b/app/portainer/views/schedules/schedulesController.js @@ -0,0 +1,50 @@ +angular.module('portainer.app') +.controller('SchedulesController', ['$scope', '$state', 'Notifications', 'ModalService', 'ScheduleService', +function ($scope, $state, Notifications, ModalService, ScheduleService) { + + $scope.removeAction = removeAction; + + function removeAction(selectedItems) { + ModalService.confirmDeletion( + 'Do you want to remove the selected schedule(s) ?', + function onConfirm(confirmed) { + if(!confirmed) { return; } + deleteSelectedSchedules(selectedItems); + } + ); + } + + function deleteSelectedSchedules(schedules) { + var actionCount = schedules.length; + angular.forEach(schedules, function (schedule) { + ScheduleService.deleteSchedule(schedule.Id) + .then(function success() { + Notifications.success('Schedule successfully removed', schedule.Name); + var index = $scope.schedules.indexOf(schedule); + $scope.schedules.splice(index, 1); + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to remove schedule ' + schedule.Name); + }) + .finally(function final() { + --actionCount; + if (actionCount === 0) { + $state.reload(); + } + }); + }); + } + + function initView() { + ScheduleService.schedules() + .then(function success(data) { + $scope.schedules = data; + }) + .catch(function error(err) { + Notifications.error('Failure', err, 'Unable to retrieve schedules'); + $scope.schedules = []; + }); + } + + initView(); +}]); diff --git a/app/portainer/views/sidebar/sidebar.html b/app/portainer/views/sidebar/sidebar.html index af042c575..031cb586f 100644 --- a/app/portainer/views/sidebar/sidebar.html +++ b/app/portainer/views/sidebar/sidebar.html @@ -12,6 +12,9 @@ +