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