diff --git a/api/http/handler/endpoints/endpoint_status_inspect.go b/api/http/handler/endpoints/endpoint_status_inspect.go index dcaf93169..3577e3aac 100644 --- a/api/http/handler/endpoints/endpoint_status_inspect.go +++ b/api/http/handler/endpoints/endpoint_status_inspect.go @@ -5,6 +5,7 @@ import ( "errors" "net/http" "strconv" + "time" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" @@ -100,11 +101,13 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req } else if agentPlatform == portainer.AgentPlatformKubernetes { endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment } + } - err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} - } + endpoint.LastCheckInDate = time.Now().Unix() + + err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err} } settings, err := handler.DataStore.Settings().Settings() diff --git a/api/portainer.go b/api/portainer.go index d78a341f6..e81da59c4 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -249,6 +249,8 @@ type ( ComposeSyntaxMaxVersion string `json:"ComposeSyntaxMaxVersion" example:"3.8"` // Endpoint specific security settings SecuritySettings EndpointSecuritySettings + // LastCheckInDate mark last check-in date on checkin + LastCheckInDate int64 // Deprecated fields // Deprecated in DBVersion == 4 @@ -339,7 +341,7 @@ type ( // EndpointType represents the type of an endpoint EndpointType int - // EndpointRelation represnts a endpoint relation object + // EndpointRelation represents a endpoint relation object EndpointRelation struct { EndpointID EndpointID EdgeStacks map[EdgeStackID]bool @@ -1182,7 +1184,7 @@ type ( DeleteResourceControl(ID ResourceControlID) error } - // ReverseTunnelService represensts a service used to manage reverse tunnel connections. + // ReverseTunnelService represents a service used to manage reverse tunnel connections. ReverseTunnelService interface { StartTunnelServer(addr, port string, snapshotService SnapshotService) error GenerateEdgeKey(url, host string, endpointIdentifier int) string @@ -1224,7 +1226,7 @@ type ( GetNextIdentifier() int } - // StackService represents a service for managing endpoint snapshots + // SnapshotService represents a service for managing endpoint snapshots SnapshotService interface { Start() SetSnapshotInterval(snapshotInterval string) error @@ -1547,6 +1549,7 @@ const ( EdgeAgentActive string = "ACTIVE" ) +// represents an authorization type const ( OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo" OperationDockerContainerList Authorization = "DockerContainerList" diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js b/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js index 3febb031d..1328f655a 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-item/endpoint-item-controller.js @@ -26,8 +26,24 @@ class EndpointItemController { return _.join(tagNames, ','); } + isEdgeEndpoint() { + return this.model.Type === 4 || this.model.Type === 7; + } + + calcIsCheckInValid() { + if (!this.isEdgeEndpoint()) { + return false; + } + const checkInInterval = this.model.EdgeCheckinInterval; + const now = Math.floor(Date.now() / 1000); + + // give checkIn some wiggle room + return now - this.model.LastCheckInDate <= checkInInterval * 2; + } + $onInit() { this.endpointTags = this.joinTags(); + this.isCheckInValid = this.calcIsCheckInValid(); } $onChanges({ tags, model }) { @@ -35,6 +51,10 @@ class EndpointItemController { return; } this.endpointTags = this.joinTags(); + + if (model) { + this.isCheckInValid = this.calcIsCheckInValid(); + } } } diff --git a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html index bf807980e..2b952f126 100644 --- a/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html +++ b/app/portainer/components/endpoint-list/endpoint-item/endpointItem.html @@ -19,18 +19,26 @@ {{ $ctrl.model.Name }} - - associated - associated + + associated + + heartbeat + + {{ $ctrl.model.LastCheckInDate | getisodatefromtimestamp }} + + - - {{ $ctrl.model.Status === 1 ? 'up' : 'down' }} - - - {{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }} - - - {{ $ctrl.model.Kubernetes.Snapshots[0].Time | getisodatefromtimestamp }} + + + + {{ $ctrl.model.Status === 1 ? 'up' : 'down' }} + + + {{ $ctrl.model.Snapshots[0].Time | getisodatefromtimestamp }} + + + {{ $ctrl.model.Kubernetes.Snapshots[0].Time | getisodatefromtimestamp }} + diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index a3fe0ddcc..ba4c83610 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -13,7 +13,8 @@ angular EndpointProvider, StateManager, ModalService, - MotdService + MotdService, + SettingsService ) { $scope.state = { connectingToEdgeEndpoint: false, @@ -82,7 +83,7 @@ angular var groups = data.groups; EndpointHelper.mapGroupNameToEndpoint(endpoints, groups); EndpointProvider.setEndpoints(endpoints); - deferred.resolve({ endpoints: endpoints, totalCount: data.endpoints.totalCount }); + deferred.resolve({ endpoints: decorateEndpoints(endpoints), totalCount: data.endpoints.totalCount }); }) .catch(function error(err) { Notifications.error('Failure', err, 'Unable to retrieve endpoint information'); @@ -98,14 +99,15 @@ angular }); try { - const [{ totalCount, endpoints }, tags] = await Promise.all([getPaginatedEndpoints(0, 100), TagService.tags()]); + const [{ totalCount, endpoints }, tags, settings] = await Promise.all([getPaginatedEndpoints(0, 100), TagService.tags(), SettingsService.settings()]); $scope.tags = tags; + $scope.defaultEdgeCheckInInterval = settings.EdgeAgentCheckinInterval; $scope.totalCount = totalCount; if (totalCount > 100) { $scope.endpoints = []; } else { - $scope.endpoints = endpoints; + $scope.endpoints = decorateEndpoints(endpoints); } } catch (err) { Notifications.error('Failed loading page data', err); @@ -113,4 +115,10 @@ angular } initView(); + + function decorateEndpoints(endpoints) { + return endpoints.map((endpoint) => { + return { ...endpoint, EdgeCheckinInterval: endpoint.EdgeAgentCheckinInterval || $scope.defaultEdgeCheckInInterval }; + }); + } });