From 0825d0554675480c651670f402c8489809e23aaf Mon Sep 17 00:00:00 2001 From: Anthony Lapenna Date: Tue, 13 Nov 2018 16:02:49 +1300 Subject: [PATCH] feat(endpoints): improve offline banner UX (#2462) * feat(endpoints): add the last snapshot timestamp in offline banner * feat(endpoints): add the ability to refresh a snapshot in the offline banner --- .../handler/endpoints/endpoint_snapshot.go | 55 ++++++++++--------- .../handler/endpoints/endpoint_snapshots.go | 49 +++++++++++++++++ api/http/handler/endpoints/handler.go | 4 +- .../informationPanelOffline.html | 9 ++- .../informationPanelOffline.js | 2 +- .../informationPanelOfflineController.js | 34 ++++++++++++ app/portainer/rest/endpoint.js | 3 +- app/portainer/services/api/endpointService.js | 8 ++- app/portainer/views/home/homeController.js | 2 +- 9 files changed, 133 insertions(+), 33 deletions(-) create mode 100644 api/http/handler/endpoints/endpoint_snapshots.go create mode 100644 app/portainer/components/information-panel-offline/informationPanelOfflineController.js diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 0a457a383..559cf127c 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -1,48 +1,51 @@ package endpoints import ( - "log" "net/http" httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" "github.com/portainer/portainer" ) -// POST request on /api/endpoints/snapshot +// POST request on /api/endpoints/:id/snapshot func (handler *Handler) endpointSnapshot(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpoints, err := handler.EndpointService.Endpoints() + endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} } - for _, endpoint := range endpoints { - if endpoint.Type == portainer.AzureEnvironment { - continue - } + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } - snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) + if endpoint.Type == portainer.AzureEnvironment { + return &httperror.HandlerError{http.StatusBadRequest, "Snapshots not supported for Azure endpoints", err} + } - latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) - if latestEndpointReference == nil { - log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) - continue - } + snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint) - latestEndpointReference.Status = portainer.EndpointStatusUp - if snapshotError != nil { - log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) - latestEndpointReference.Status = portainer.EndpointStatusDown - } + latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) + if latestEndpointReference == nil { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } - if snapshot != nil { - latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} - } + latestEndpointReference.Status = portainer.EndpointStatusUp + if snapshotError != nil { + latestEndpointReference.Status = portainer.EndpointStatusDown + } - err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} - } + if snapshot != nil { + latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} + } + + err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} } return response.Empty(w) diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go new file mode 100644 index 000000000..f59b7a40d --- /dev/null +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -0,0 +1,49 @@ +package endpoints + +import ( + "log" + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" + "github.com/portainer/portainer" +) + +// POST request on /api/endpoints/snapshot +func (handler *Handler) endpointSnapshots(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + endpoints, err := handler.EndpointService.Endpoints() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + } + + for _, endpoint := range endpoints { + if endpoint.Type == portainer.AzureEnvironment { + continue + } + + snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(&endpoint) + + latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID) + if latestEndpointReference == nil { + log.Printf("background schedule error (endpoint snapshot). Endpoint not found inside the database anymore (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, err) + continue + } + + latestEndpointReference.Status = portainer.EndpointStatusUp + if snapshotError != nil { + log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) + latestEndpointReference.Status = portainer.EndpointStatusDown + } + + if snapshot != nil { + latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} + } + + err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} + } + } + + return response.Empty(w) +} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 1ef8d1727..d8a94d360 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -45,7 +45,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo h.Handle("/endpoints", bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) h.Handle("/endpoints/snapshot", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) + bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) h.Handle("/endpoints", bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet) h.Handle("/endpoints/{id}", @@ -62,5 +62,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete) h.Handle("/endpoints/{id}/job", bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) + h.Handle("/endpoints/{id}/snapshot", + bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) return h } diff --git a/app/portainer/components/information-panel-offline/informationPanelOffline.html b/app/portainer/components/information-panel-offline/informationPanelOffline.html index a7c51a3e5..17942ee5a 100644 --- a/app/portainer/components/information-panel-offline/informationPanelOffline.html +++ b/app/portainer/components/information-panel-offline/informationPanelOffline.html @@ -4,5 +4,12 @@ This endpoint is currently offline (read-only). Data shown is based on the latest available snapshot.

+

+ + Last snapshot: {{ $ctrl.snapshotTime | getisodatefromtimestamp }} +

+ - \ No newline at end of file + diff --git a/app/portainer/components/information-panel-offline/informationPanelOffline.js b/app/portainer/components/information-panel-offline/informationPanelOffline.js index c3f6e9517..e788dfe29 100644 --- a/app/portainer/components/information-panel-offline/informationPanelOffline.js +++ b/app/portainer/components/information-panel-offline/informationPanelOffline.js @@ -1,4 +1,4 @@ angular.module('portainer.app').component('informationPanelOffline', { templateUrl: 'app/portainer/components/information-panel-offline/informationPanelOffline.html', - transclude: true + controller: 'InformationPanelOfflineController' }); diff --git a/app/portainer/components/information-panel-offline/informationPanelOfflineController.js b/app/portainer/components/information-panel-offline/informationPanelOfflineController.js new file mode 100644 index 000000000..efe75545e --- /dev/null +++ b/app/portainer/components/information-panel-offline/informationPanelOfflineController.js @@ -0,0 +1,34 @@ +angular.module('portainer.app').controller('InformationPanelOfflineController', ['$state', 'EndpointProvider', 'EndpointService', 'Authentication', 'Notifications', +function StackDuplicationFormController($state, EndpointProvider, EndpointService, Authentication, Notifications) { + var ctrl = this; + + this.$onInit = onInit; + this.triggerSnapshot = triggerSnapshot; + + function triggerSnapshot() { + var endpointId = EndpointProvider.endpointID(); + + EndpointService.snapshotEndpoint(endpointId) + .then(function onSuccess() { + $state.reload(); + }) + .catch(function onError(err) { + Notifications.error('Failure', err, 'An error occured during endpoint snapshot'); + }); + } + + function onInit() { + var endpointId = EndpointProvider.endpointID(); + ctrl.showRefreshButton = Authentication.getUserDetails().role === 1; + + + EndpointService.endpoint(endpointId) + .then(function onSuccess(data) { + ctrl.snapshotTime = data.Snapshots[0].Time; + }) + .catch(function onError(err) { + Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); + }); + } + +}]); diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 7b50372e9..068a8ef66 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -7,7 +7,8 @@ angular.module('portainer.app') update: { method: 'PUT', params: { id: '@id' } }, updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, remove: { method: 'DELETE', params: { id: '@id'} }, - snapshot: { method: 'POST', params: { id: 'snapshot' }}, + snapshots: { method: 'POST', params: { action: 'snapshot' }}, + snapshot: { method: 'POST', params: { id: '@id', action: 'snapshot' }}, executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } } }); }]); diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index fa17052e5..f7098ae51 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -12,8 +12,12 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) { return Endpoints.query({}).$promise; }; - service.snapshot = function() { - return Endpoints.snapshot({}, {}).$promise; + service.snapshotEndpoints = function() { + return Endpoints.snapshots({}, {}).$promise; + }; + + service.snapshotEndpoint = function(endpointID) { + return Endpoints.snapshot({ id: endpointID }, {}).$promise; }; service.endpointsByGroup = function(groupId) { diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index 083c75625..b422a7e86 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -101,7 +101,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G } function triggerSnapshot() { - EndpointService.snapshot() + EndpointService.snapshotEndpoints() .then(function success() { Notifications.success('Success', 'Endpoints updated'); $state.reload();