mirror of https://github.com/portainer/portainer
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 bannerpull/2466/head
parent
cf370f6a4c
commit
0825d05546
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -4,5 +4,12 @@
|
|||
<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.
|
||||
</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>
|
||||
</information-panel>
|
||||
</information-panel>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
angular.module('portainer.app').component('informationPanelOffline', {
|
||||
templateUrl: 'app/portainer/components/information-panel-offline/informationPanelOffline.html',
|
||||
transclude: true
|
||||
controller: 'InformationPanelOfflineController'
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
}
|
||||
|
||||
}]);
|
|
@ -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' } }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue