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
pull/2466/head
Anthony Lapenna 2018-11-13 16:02:49 +13:00 committed by GitHub
parent cf370f6a4c
commit 0825d05546
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 133 additions and 33 deletions

View File

@ -1,48 +1,51 @@
package endpoints package endpoints
import ( import (
"log"
"net/http" "net/http"
httperror "github.com/portainer/libhttp/error" httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response" "github.com/portainer/libhttp/response"
"github.com/portainer/portainer" "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 { 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 { 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 { endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
if endpoint.Type == portainer.AzureEnvironment { if err == portainer.ErrObjectNotFound {
continue 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) snapshot, snapshotError := handler.Snapshotter.CreateSnapshot(endpoint)
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 latestEndpointReference, err := handler.EndpointService.Endpoint(endpoint.ID)
if snapshotError != nil { if latestEndpointReference == nil {
log.Printf("background schedule error (endpoint snapshot). Unable to create snapshot (endpoint=%s, URL=%s) (err=%s)\n", endpoint.Name, endpoint.URL, snapshotError) return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
latestEndpointReference.Status = portainer.EndpointStatusDown }
}
if snapshot != nil { latestEndpointReference.Status = portainer.EndpointStatusUp
latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot} if snapshotError != nil {
} latestEndpointReference.Status = portainer.EndpointStatusDown
}
err = handler.EndpointService.UpdateEndpoint(latestEndpointReference.ID, latestEndpointReference) if snapshot != nil {
if err != nil { latestEndpointReference.Snapshots = []portainer.Snapshot{*snapshot}
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} }
}
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) return response.Empty(w)

View File

@ -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)
}

View File

@ -45,7 +45,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
h.Handle("/endpoints", h.Handle("/endpoints",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
h.Handle("/endpoints/snapshot", 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", h.Handle("/endpoints",
bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet) bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet)
h.Handle("/endpoints/{id}", h.Handle("/endpoints/{id}",
@ -62,5 +62,7 @@ func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bo
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete) bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete)
h.Handle("/endpoints/{id}/job", h.Handle("/endpoints/{id}/job",
bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) 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 return h
} }

View File

@ -4,5 +4,12 @@
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i> <i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px;"></i>
This endpoint is currently offline (read-only). Data shown is based on the latest available snapshot. This endpoint is currently offline (read-only). Data shown is based on the latest available snapshot.
</p> </p>
<p class="text-muted">
<i class="fa fa-clock" aria-hidden="true" style="margin-right: 2px;"></i>
Last snapshot: {{ $ctrl.snapshotTime | getisodatefromtimestamp }}
</p>
<button type="button" class="btn btn-xs btn-primary" ng-click="$ctrl.triggerSnapshot()" ng-if="$ctrl.showRefreshButton">
<i class="fa fa-sync space-right" aria-hidden="true"></i>Refresh
</button>
</span> </span>
</information-panel> </information-panel>

View File

@ -1,4 +1,4 @@
angular.module('portainer.app').component('informationPanelOffline', { angular.module('portainer.app').component('informationPanelOffline', {
templateUrl: 'app/portainer/components/information-panel-offline/informationPanelOffline.html', templateUrl: 'app/portainer/components/information-panel-offline/informationPanelOffline.html',
transclude: true controller: 'InformationPanelOfflineController'
}); });

View File

@ -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');
});
}
}]);

View File

@ -7,7 +7,8 @@ angular.module('portainer.app')
update: { method: 'PUT', params: { id: '@id' } }, update: { method: 'PUT', params: { id: '@id' } },
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } }, updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
remove: { method: 'DELETE', params: { id: '@id'} }, 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' } } executeJob: { method: 'POST', ignoreLoadingBar: true, params: { id: '@id', action: 'job' } }
}); });
}]); }]);

View File

@ -12,8 +12,12 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
return Endpoints.query({}).$promise; return Endpoints.query({}).$promise;
}; };
service.snapshot = function() { service.snapshotEndpoints = function() {
return Endpoints.snapshot({}, {}).$promise; return Endpoints.snapshots({}, {}).$promise;
};
service.snapshotEndpoint = function(endpointID) {
return Endpoints.snapshot({ id: endpointID }, {}).$promise;
}; };
service.endpointsByGroup = function(groupId) { service.endpointsByGroup = function(groupId) {

View File

@ -101,7 +101,7 @@ function ($q, $scope, $state, Authentication, EndpointService, EndpointHelper, G
} }
function triggerSnapshot() { function triggerSnapshot() {
EndpointService.snapshot() EndpointService.snapshotEndpoints()
.then(function success() { .then(function success() {
Notifications.success('Success', 'Endpoints updated'); Notifications.success('Success', 'Endpoints updated');
$state.reload(); $state.reload();