mirror of
https://github.com/portainer/portainer.git
synced 2025-07-24 15:59:41 +02:00
Enable endpoint backend pagination (#2989)
* feat(api): remove SnapshotRaw from EndpointList response * feat(api): add pagination for EndpointList operation * feat(api): rename last_id query parameter to start * feat(api): implement filter for EndpointList operation * feat(home): front - endpoint backend pagination (#2990) * feat(home): endpoint pagination with backend * feat(api): remove default limit value * fix(endpoints): fix a minor issue with column span * fix(endpointgroup-create): fix an issue with endpoint group creation * feat(app): minor loading optimizations * refactor(api): small refactor of EndpointList operation * fix(home): fix minor loading text display issue * refactor(api): document bolt services functions * feat(home): minor optimization * fix(api): replace seek with index scanning for EndpointPaginated * fix(api): fix invalid starting index issue * fix(api): first implementation of working filter * fix(home): endpoints list keeps backend pagination when it needs to * fix(api): endpoint pagination doesn't drop the first item on pages >=2 anymore * fix(home): UI flickering on page/filter load/change * feat(api): support searching in associated endpoint group data * feat(api): declare EndpointList params as optional * feat(endpoints): backend pagination for endpoints view (#3004) * feat(endpoint-group): enable backend pagination (#3017) * feat(api): support groupID filter on endpoints route * feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint * feat(endpoint-groups): backend pagination support for create and edit * feat(endpoint-groups): debounce on filter for create/edit views * feat(endpoint-groups): filter assigned on create view * (endpoint-groups): unassigned endpoints edit view * refactor(endpoint-groups): code clean * feat(endpoint-groups): remove message for Unassigned group * refactor(api): endpoint group endpoint association refactor * refactor(api): rename files and remove comments * refactor(api): remove usage of utils * refactor(api): optional parameters * feat(api): update endpointListOperation behavior and parameters * refactor(api): remove unused methods associated to EndpointService * refactor(api): remove unused methods associated to EndpointService * refactor(api): minor refactor
This commit is contained in:
parent
d52a1a870c
commit
90d3f3a358
38 changed files with 681 additions and 243 deletions
|
@ -53,11 +53,17 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
for _, id := range payload.AssociatedEndpoints {
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.ID == id {
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// PUT request on /api/endpoint_groups/:id/endpoints/:endpointId
|
||||
func (handler *Handler) endpointGroupAddEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
endpoint.GroupID = endpointGroup.ID
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package endpointgroups
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// DELETE request on /api/endpoint_groups/:id/endpoints/:endpointId
|
||||
func (handler *Handler) endpointGroupDeleteEndpoint(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err}
|
||||
}
|
||||
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "endpointId")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
_, err = handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID))
|
||||
if err == portainer.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
|
@ -10,12 +10,11 @@ import (
|
|||
)
|
||||
|
||||
type endpointGroupUpdatePayload struct {
|
||||
Name string
|
||||
Description string
|
||||
AssociatedEndpoints []portainer.EndpointID
|
||||
Tags []string
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
Name string
|
||||
Description string
|
||||
Tags []string
|
||||
UserAccessPolicies portainer.UserAccessPolicies
|
||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||
}
|
||||
|
||||
func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error {
|
||||
|
@ -67,19 +66,5 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
|||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||
}
|
||||
|
||||
if payload.AssociatedEndpoints != nil {
|
||||
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 {
|
||||
err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpointGroup)
|
||||
}
|
||||
|
|
|
@ -31,37 +31,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
|||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
||||
|
||||
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupAddEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoint_groups/{id}/endpoints/{endpointId}",
|
||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) checkForGroupUnassignment(endpoint portainer.Endpoint, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
if id == endpoint.ID {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) checkForGroupAssignment(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
for _, id := range associatedEndpoints {
|
||||
|
||||
if id == endpoint.ID {
|
||||
endpoint.GroupID = groupID
|
||||
return handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateEndpointGroup(endpoint portainer.Endpoint, groupID portainer.EndpointGroupID, associatedEndpoints []portainer.EndpointID) error {
|
||||
if endpoint.GroupID == groupID {
|
||||
return handler.checkForGroupUnassignment(endpoint, associatedEndpoints)
|
||||
} else if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
||||
return handler.checkForGroupAssignment(endpoint, groupID, associatedEndpoints)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -2,24 +2,43 @@ package endpoints
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// GET request on /api/endpoints
|
||||
// GET request on /api/endpoints?(start=<start>)&(limit=<limit>)&(search=<search>)&(groupId=<groupId)
|
||||
func (handler *Handler) endpointList(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}
|
||||
start, _ := request.RetrieveNumericQueryParameter(r, "start", true)
|
||||
if start != 0 {
|
||||
start--
|
||||
}
|
||||
|
||||
search, _ := request.RetrieveQueryParameter(r, "search", true)
|
||||
if search != "" {
|
||||
search = strings.ToLower(search)
|
||||
}
|
||||
|
||||
groupID, _ := request.RetrieveNumericQueryParameter(r, "groupId", true)
|
||||
limit, _ := request.RetrieveNumericQueryParameter(r, "limit", true)
|
||||
|
||||
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
|
@ -27,9 +46,113 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
|||
|
||||
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||
|
||||
for idx := range filteredEndpoints {
|
||||
hideFields(&filteredEndpoints[idx])
|
||||
if groupID != 0 {
|
||||
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
|
||||
}
|
||||
|
||||
return response.JSON(w, filteredEndpoints)
|
||||
if search != "" {
|
||||
filteredEndpoints = filterEndpointsBySearchCriteria(filteredEndpoints, endpointGroups, search)
|
||||
}
|
||||
|
||||
filteredEndpointCount := len(filteredEndpoints)
|
||||
|
||||
paginatedEndpoints := paginateEndpoints(filteredEndpoints, start, limit)
|
||||
|
||||
for idx := range paginatedEndpoints {
|
||||
hideFields(&paginatedEndpoints[idx])
|
||||
}
|
||||
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||
return response.JSON(w, paginatedEndpoints)
|
||||
}
|
||||
|
||||
func paginateEndpoints(endpoints []portainer.Endpoint, start, limit int) []portainer.Endpoint {
|
||||
if limit == 0 {
|
||||
return endpoints
|
||||
}
|
||||
|
||||
endpointCount := len(endpoints)
|
||||
|
||||
if start > endpointCount {
|
||||
start = endpointCount
|
||||
}
|
||||
|
||||
end := start + limit
|
||||
if end > endpointCount {
|
||||
end = endpointCount
|
||||
}
|
||||
|
||||
return endpoints[start:end]
|
||||
}
|
||||
|
||||
func filterEndpointsByGroupID(endpoints []portainer.Endpoint, endpointGroupID portainer.EndpointGroupID) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == endpointGroupID {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func filterEndpointsBySearchCriteria(endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) []portainer.Endpoint {
|
||||
filteredEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
|
||||
if endpointMatchSearchCriteria(&endpoint, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
continue
|
||||
}
|
||||
|
||||
if endpointGroupMatchSearchCriteria(&endpoint, endpointGroups, searchCriteria) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints
|
||||
}
|
||||
|
||||
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, searchCriteria string) bool {
|
||||
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.Contains(strings.ToLower(endpoint.URL), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
if endpoint.Status == portainer.EndpointStatusUp && searchCriteria == "up" {
|
||||
return true
|
||||
} else if endpoint.Status == portainer.EndpointStatusDown && searchCriteria == "down" {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, tag := range endpoint.Tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, searchCriteria string) bool {
|
||||
for _, group := range endpointGroups {
|
||||
if group.ID == endpoint.GroupID {
|
||||
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, tag := range group.Tags {
|
||||
if strings.Contains(strings.ToLower(tag), searchCriteria) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -19,6 +19,9 @@ const (
|
|||
|
||||
func hideFields(endpoint *portainer.Endpoint) {
|
||||
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
||||
if len(endpoint.Snapshots) > 0 {
|
||||
endpoint.Snapshots[0].SnapshotRaw = portainer.SnapshotRaw{}
|
||||
}
|
||||
}
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint operations.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue