mirror of https://github.com/portainer/portainer
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 refactorpull/2763/head^2
parent
d52a1a870c
commit
90d3f3a358
|
@ -1,10 +1,9 @@
|
||||||
package endpoint
|
package endpoint
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/boltdb/bolt"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/bolt/internal"
|
"github.com/portainer/portainer/api/bolt/internal"
|
||||||
|
|
||||||
"github.com/boltdb/bolt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -53,12 +53,18 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, id := range payload.AssociatedEndpoints {
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if endpoint.GroupID == portainer.EndpointGroupID(1) {
|
if endpoint.ID == id {
|
||||||
err = handler.checkForGroupAssignment(endpoint, endpointGroup.ID, payload.AssociatedEndpoints)
|
endpoint.GroupID = endpointGroup.ID
|
||||||
|
|
||||||
|
err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err}
|
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)
|
||||||
|
}
|
|
@ -12,7 +12,6 @@ import (
|
||||||
type endpointGroupUpdatePayload struct {
|
type endpointGroupUpdatePayload struct {
|
||||||
Name string
|
Name string
|
||||||
Description string
|
Description string
|
||||||
AssociatedEndpoints []portainer.EndpointID
|
|
||||||
Tags []string
|
Tags []string
|
||||||
UserAccessPolicies portainer.UserAccessPolicies
|
UserAccessPolicies portainer.UserAccessPolicies
|
||||||
TeamAccessPolicies portainer.TeamAccessPolicies
|
TeamAccessPolicies portainer.TeamAccessPolicies
|
||||||
|
@ -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}
|
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)
|
return response.JSON(w, endpointGroup)
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,37 +31,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/endpoint_groups/{id}",
|
h.Handle("/endpoint_groups/{id}",
|
||||||
bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete)
|
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
|
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 (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"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 {
|
func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
endpoints, err := handler.EndpointService.Endpoints()
|
start, _ := request.RetrieveNumericQueryParameter(r, "start", true)
|
||||||
if err != nil {
|
if start != 0 {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
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()
|
endpointGroups, err := handler.EndpointGroupService.EndpointGroups()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoint groups from the database", err}
|
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)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
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)
|
filteredEndpoints := security.FilterEndpoints(endpoints, endpointGroups, securityContext)
|
||||||
|
|
||||||
for idx := range filteredEndpoints {
|
if groupID != 0 {
|
||||||
hideFields(&filteredEndpoints[idx])
|
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) {
|
func hideFields(endpoint *portainer.Endpoint) {
|
||||||
endpoint.AzureCredentials = portainer.AzureCredentials{}
|
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.
|
// Handler is the HTTP handler used to handle endpoint operations.
|
||||||
|
|
|
@ -103,7 +103,7 @@ class AccessViewerController {
|
||||||
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
|
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
|
||||||
if (this.rbacEnabled) {
|
if (this.rbacEnabled) {
|
||||||
this.users = await this.UserService.users();
|
this.users = await this.UserService.users();
|
||||||
this.endpoints = _.keyBy(await this.EndpointService.endpoints(), 'Id');
|
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
|
||||||
const groups = await this.GroupService.groups();
|
const groups = await this.GroupService.groups();
|
||||||
this.groupUserAccessPolicies = {};
|
this.groupUserAccessPolicies = {};
|
||||||
this.groupTeamAccessPolicies = {};
|
this.groupTeamAccessPolicies = {};
|
||||||
|
|
|
@ -17,7 +17,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="searchBar">
|
<div class="searchBar">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search..." auto-focus>
|
<input type="text" class="searchInput" auto-focus
|
||||||
|
placeholder="Search..."
|
||||||
|
ng-model="$ctrl.state.textFilter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover nowrap-cells">
|
<table class="table table-hover nowrap-cells">
|
||||||
|
@ -59,7 +64,9 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))" ng-class="{active: item.Checked}">
|
<tr dir-paginate="item in $ctrl.state.filteredDataSet | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
||||||
|
total-items="$ctrl.state.totalFilteredDataSet"
|
||||||
|
ng-class="{active: item.Checked}">
|
||||||
<td>
|
<td>
|
||||||
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
|
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
|
||||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||||
|
@ -82,16 +89,16 @@
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="$ctrl.state.loading">
|
||||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
<tr ng-if="!$ctrl.state.loading && $ctrl.state.filteredDataSet.length === 0">
|
||||||
<td colspan="3" class="text-center text-muted">No endpoint available.</td>
|
<td colspan="5" class="text-center text-muted">No endpoint available.</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer" ng-if="$ctrl.dataset">
|
<div class="footer" ng-if="!$ctrl.state.loading">
|
||||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||||
</div>
|
</div>
|
||||||
|
@ -109,7 +116,7 @@
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
<dir-pagination-controls max-size="5" on-page-change="$ctrl.onPageChange(newPageNumber, oldPageNumber)"></dir-pagination-controls>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
angular.module('portainer.app').component('endpointsDatatable', {
|
angular.module('portainer.app').component('endpointsDatatable', {
|
||||||
templateUrl: './endpointsDatatable.html',
|
templateUrl: './endpointsDatatable.html',
|
||||||
controller: 'GenericDatatableController',
|
controller: 'EndpointsDatatableController',
|
||||||
bindings: {
|
bindings: {
|
||||||
titleText: '@',
|
titleText: '@',
|
||||||
titleIcon: '@',
|
titleIcon: '@',
|
||||||
dataset: '<',
|
|
||||||
tableKey: '@',
|
tableKey: '@',
|
||||||
orderBy: '@',
|
orderBy: '@',
|
||||||
reverseOrder: '<',
|
reverseOrder: '<',
|
||||||
endpointManagement: '<',
|
endpointManagement: '<',
|
||||||
accessManagement: '<',
|
accessManagement: '<',
|
||||||
removeAction: '<'
|
removeAction: '<',
|
||||||
|
retrievePage: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,80 @@
|
||||||
|
angular.module('portainer.app')
|
||||||
|
.controller('EndpointsDatatableController', ['$scope', '$controller', 'DatatableService', 'PaginationService',
|
||||||
|
function ($scope, $controller, DatatableService, PaginationService) {
|
||||||
|
|
||||||
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
|
this.state = Object.assign(this.state, {
|
||||||
|
orderBy: this.orderBy,
|
||||||
|
loading: true,
|
||||||
|
filteredDataSet: [],
|
||||||
|
totalFilteredDataset: 0,
|
||||||
|
pageNumber: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
this.paginationChanged = function() {
|
||||||
|
this.state.loading = true;
|
||||||
|
this.state.filteredDataSet = [];
|
||||||
|
const start = (this.state.pageNumber - 1) * this.state.paginatedItemLimit + 1;
|
||||||
|
this.retrievePage(start, this.state.paginatedItemLimit, this.state.textFilter)
|
||||||
|
.then((data) => {
|
||||||
|
this.state.filteredDataSet = data.endpoints;
|
||||||
|
this.state.totalFilteredDataSet = data.totalCount;
|
||||||
|
}).finally(() => {
|
||||||
|
this.state.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onPageChange = function(newPageNumber) {
|
||||||
|
this.state.pageNumber = newPageNumber;
|
||||||
|
this.paginationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden
|
||||||
|
*/
|
||||||
|
this.onTextFilterChange = function() {
|
||||||
|
var filterValue = this.state.textFilter;
|
||||||
|
DatatableService.setDataTableTextFilters(this.tableKey, filterValue);
|
||||||
|
this.paginationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden
|
||||||
|
*/
|
||||||
|
this.changePaginationLimit = function() {
|
||||||
|
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||||
|
this.paginationChanged();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden
|
||||||
|
*/
|
||||||
|
this.$onInit = function() {
|
||||||
|
this.setDefaults();
|
||||||
|
this.prepareTableFromDataset();
|
||||||
|
|
||||||
|
var storedOrder = DatatableService.getDataTableOrder(this.tableKey);
|
||||||
|
if (storedOrder !== null) {
|
||||||
|
this.state.reverseOrder = storedOrder.reverse;
|
||||||
|
this.state.orderBy = storedOrder.orderBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
|
if (textFilter !== null) {
|
||||||
|
this.state.textFilter = textFilter;
|
||||||
|
this.onTextFilterChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
var storedFilters = DatatableService.getDataTableFilters(this.tableKey);
|
||||||
|
if (storedFilters !== null) {
|
||||||
|
this.filters = storedFilters;
|
||||||
|
}
|
||||||
|
if (this.filters && this.filters.state) {
|
||||||
|
this.filters.state.open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.paginationChanged();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
]);
|
|
@ -2,42 +2,39 @@ import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.app').controller('EndpointListController', ['DatatableService', 'PaginationService',
|
angular.module('portainer.app').controller('EndpointListController', ['DatatableService', 'PaginationService',
|
||||||
function EndpointListController(DatatableService, PaginationService) {
|
function EndpointListController(DatatableService, PaginationService) {
|
||||||
var ctrl = this;
|
this.state = {
|
||||||
ctrl.state = {
|
totalFilteredEndpoints: this.totalCount,
|
||||||
textFilter: '',
|
textFilter: '',
|
||||||
filteredEndpoints: [],
|
filteredEndpoints: [],
|
||||||
paginatedItemLimit: '10'
|
paginatedItemLimit: '10',
|
||||||
|
pageNumber: 1,
|
||||||
|
loading: true
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.$onChanges = $onChanges;
|
this.$onChanges = function(changesObj) {
|
||||||
ctrl.onTextFilterChange = onTextFilterChange;
|
this.handleEndpointsChange(changesObj.endpoints);
|
||||||
ctrl.$onInit = $onInit
|
|
||||||
|
|
||||||
function $onChanges(changesObj) {
|
|
||||||
handleEndpointsChange(changesObj.endpoints);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEndpointsChange(endpoints) {
|
this.handleEndpointsChange = function(endpoints) {
|
||||||
if (!endpoints) {
|
if (!endpoints || !endpoints.currentValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!endpoints.currentValue) {
|
this.onTextFilterChange();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextFilterChange();
|
this.onTextFilterChange = function() {
|
||||||
|
this.state.loading = true;
|
||||||
|
var filterValue = this.state.textFilter;
|
||||||
|
DatatableService.setDataTableTextFilters(this.tableKey, filterValue);
|
||||||
|
if (this.hasBackendPagination()) {
|
||||||
|
this.paginationChangedAction();
|
||||||
|
} else {
|
||||||
|
this.state.filteredEndpoints = frontEndpointFilter(this.endpoints, filterValue);
|
||||||
|
this.state.loading = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTextFilterChange() {
|
function frontEndpointFilter(endpoints, filterValue) {
|
||||||
var filterValue = ctrl.state.textFilter;
|
|
||||||
ctrl.state.filteredEndpoints = filterEndpoints(
|
|
||||||
ctrl.endpoints,
|
|
||||||
filterValue
|
|
||||||
);
|
|
||||||
DatatableService.setDataTableTextFilters(ctrl.tableKey, filterValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterEndpoints(endpoints, filterValue) {
|
|
||||||
if (!endpoints || !endpoints.length || !filterValue) {
|
if (!endpoints || !endpoints.length || !filterValue) {
|
||||||
return endpoints;
|
return endpoints;
|
||||||
}
|
}
|
||||||
|
@ -59,20 +56,46 @@ angular.module('portainer.app').controller('EndpointListController', ['Datatable
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.hasBackendPagination = function() {
|
||||||
|
return this.totalCount && this.totalCount > 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.paginationChangedAction = function() {
|
||||||
|
if (this.hasBackendPagination()) {
|
||||||
|
this.state.loading = true;
|
||||||
|
this.state.filteredEndpoints = [];
|
||||||
|
const start = (this.state.pageNumber - 1) * this.state.paginatedItemLimit + 1;
|
||||||
|
this.retrievePage(start, this.state.paginatedItemLimit, this.state.textFilter)
|
||||||
|
.then((data) => {
|
||||||
|
this.state.filteredEndpoints = data.endpoints;
|
||||||
|
this.state.totalFilteredEndpoints = data.totalCount;
|
||||||
|
this.state.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.pageChangeHandler = function(newPageNumber) {
|
||||||
|
this.state.pageNumber = newPageNumber;
|
||||||
|
this.paginationChangedAction();
|
||||||
|
}
|
||||||
|
|
||||||
this.changePaginationLimit = function() {
|
this.changePaginationLimit = function() {
|
||||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||||
|
this.paginationChangedAction();
|
||||||
};
|
};
|
||||||
|
|
||||||
function convertStatusToString(status) {
|
function convertStatusToString(status) {
|
||||||
return status === 1 ? 'up' : 'down';
|
return status === 1 ? 'up' : 'down';
|
||||||
}
|
}
|
||||||
|
|
||||||
function $onInit() {
|
this.$onInit = function() {
|
||||||
var textFilter = DatatableService.getDataTableTextFilters(ctrl.tableKey);
|
this.state.loading = true;
|
||||||
|
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||||
this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey);
|
this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey);
|
||||||
if (textFilter !== null) {
|
if (textFilter !== null) {
|
||||||
ctrl.state.textFilter = textFilter;
|
this.state.textFilter = textFilter;
|
||||||
onTextFilterChange();
|
} else {
|
||||||
|
this.paginationChangedAction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,8 @@ angular.module('portainer.app').component('endpointList', {
|
||||||
snapshotAction: '<',
|
snapshotAction: '<',
|
||||||
showSnapshotAction: '<',
|
showSnapshotAction: '<',
|
||||||
editAction: '<',
|
editAction: '<',
|
||||||
isAdmin:'<'
|
isAdmin:'<',
|
||||||
|
totalCount: '<',
|
||||||
|
retrievePage: '<'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -21,21 +21,30 @@
|
||||||
class="searchInput"
|
class="searchInput"
|
||||||
ng-model="$ctrl.state.textFilter"
|
ng-model="$ctrl.state.textFilter"
|
||||||
ng-change="$ctrl.onTextFilterChange()"
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
placeholder="Search by name, group, tag, status, URL..." auto-focus>
|
placeholder="Search by name, group, tag, status, URL..." auto-focus>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="blocklist">
|
<div class="blocklist">
|
||||||
<endpoint-item
|
<endpoint-item ng-if="$ctrl.hasBackendPagination()"
|
||||||
|
dir-paginate="endpoint in $ctrl.state.filteredEndpoints | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
||||||
|
model="endpoint"
|
||||||
|
total-items="$ctrl.state.totalFilteredEndpoints"
|
||||||
|
on-select="$ctrl.dashboardAction"
|
||||||
|
on-edit="$ctrl.editAction"
|
||||||
|
is-admin="$ctrl.isAdmin"
|
||||||
|
></endpoint-item>
|
||||||
|
<endpoint-item ng-if="!$ctrl.hasBackendPagination()"
|
||||||
dir-paginate="endpoint in $ctrl.state.filteredEndpoints | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
dir-paginate="endpoint in $ctrl.state.filteredEndpoints | itemsPerPage: $ctrl.state.paginatedItemLimit"
|
||||||
model="endpoint"
|
model="endpoint"
|
||||||
on-select="$ctrl.dashboardAction"
|
on-select="$ctrl.dashboardAction"
|
||||||
on-edit="$ctrl.editAction"
|
on-edit="$ctrl.editAction"
|
||||||
is-admin="$ctrl.isAdmin"
|
is-admin="$ctrl.isAdmin"
|
||||||
></endpoint-item>
|
></endpoint-item>
|
||||||
<div ng-if="!$ctrl.endpoints" class="text-center text-muted">
|
<div ng-if="$ctrl.state.loading" class="text-center text-muted">
|
||||||
Loading...
|
Loading...
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="!$ctrl.state.filteredEndpoints.length" class="text-center text-muted">
|
<div ng-if="!$ctrl.state.loading && !$ctrl.state.filteredEndpoints.length" class="text-center text-muted">
|
||||||
No endpoint available.
|
No endpoint available.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -48,14 +57,14 @@
|
||||||
Items per page
|
Items per page
|
||||||
</span>
|
</span>
|
||||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||||
<option value="0">All</option>
|
<option value="0" ng-if="!$ctrl.hasBackendPagination()">All</option>
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
<dir-pagination-controls max-size="5" on-page-change="$ctrl.pageChangeHandler(newPageNumber, oldPageNumber)"></dir-pagination-controls>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,26 +1,99 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
import angular from 'angular';
|
||||||
|
|
||||||
|
class GroupFormController {
|
||||||
|
/* @ngInject */
|
||||||
|
constructor($q, EndpointService, GroupService, Notifications) {
|
||||||
|
this.$q = $q;
|
||||||
|
this.EndpointService = EndpointService;
|
||||||
|
this.GroupService = GroupService;
|
||||||
|
this.Notifications = Notifications;
|
||||||
|
|
||||||
|
this.associateEndpoint = this.associateEndpoint.bind(this);
|
||||||
|
this.dissociateEndpoint = this.dissociateEndpoint.bind(this);
|
||||||
|
this.getPaginatedEndpointsByGroup = this.getPaginatedEndpointsByGroup.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
$onInit() {
|
||||||
|
this.state = {
|
||||||
|
available: {
|
||||||
|
limit: '10',
|
||||||
|
filter: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
totalCount: 0
|
||||||
|
},
|
||||||
|
associated: {
|
||||||
|
limit: '10',
|
||||||
|
filter: '',
|
||||||
|
pageNumber: 1,
|
||||||
|
totalCount: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
associateEndpoint(endpoint) {
|
||||||
|
if (this.pageType === 'create' && !_.includes(this.associatedEndpoints, endpoint)) {
|
||||||
|
this.associatedEndpoints.push(endpoint);
|
||||||
|
} else if (this.pageType === 'edit') {
|
||||||
|
this.GroupService.addEndpoint(this.model.Id, endpoint)
|
||||||
|
.then(() => {
|
||||||
|
this.Notifications.success('Success', 'Endpoint successfully added to group');
|
||||||
|
this.reloadTablesContent();
|
||||||
|
})
|
||||||
|
.catch((err) => this.Notifications.error('Error', err, 'Unable to add endpoint to group'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dissociateEndpoint(endpoint) {
|
||||||
|
if (this.pageType === 'create') {
|
||||||
|
_.remove(this.associatedEndpoints, (item) => item.Id === endpoint.Id);
|
||||||
|
} else if (this.pageType === 'edit') {
|
||||||
|
this.GroupService.removeEndpoint(this.model.Id, endpoint.Id)
|
||||||
|
.then(() => {
|
||||||
|
this.Notifications.success('Success', 'Endpoint successfully removed from group');
|
||||||
|
this.reloadTablesContent();
|
||||||
|
})
|
||||||
|
.catch((err) => this.Notifications.error('Error', err, 'Unable to remove endpoint from group'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadTablesContent() {
|
||||||
|
this.getPaginatedEndpointsByGroup(this.pageType, 'available');
|
||||||
|
this.getPaginatedEndpointsByGroup(this.pageType, 'associated');
|
||||||
|
this.GroupService.group(this.model.Id)
|
||||||
|
.then((data) => {
|
||||||
|
this.model = data;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
getPaginatedEndpointsByGroup(pageType, tableType) {
|
||||||
|
if (tableType === 'available') {
|
||||||
|
const context = this.state.available;
|
||||||
|
const start = (context.pageNumber - 1) * context.limit + 1;
|
||||||
|
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, 1)
|
||||||
|
.then((data) => {
|
||||||
|
this.availableEndpoints = data.value;
|
||||||
|
this.state.available.totalCount = data.totalCount;
|
||||||
|
});
|
||||||
|
} else if (tableType === 'associated' && pageType === 'edit') {
|
||||||
|
const groupId = this.model.Id ? this.model.Id : 1;
|
||||||
|
const context = this.state.associated;
|
||||||
|
const start = (context.pageNumber - 1) * context.limit + 1;
|
||||||
|
this.EndpointService.endpointsByGroup(start, context.limit, context.filter, groupId)
|
||||||
|
.then((data) => {
|
||||||
|
this.associatedEndpoints = data.value;
|
||||||
|
this.state.associated.totalCount = data.totalCount;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// ignore (associated + create) group as there is no backend pagination for this table
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
angular.module('portainer.app').component('groupForm', {
|
angular.module('portainer.app').component('groupForm', {
|
||||||
templateUrl: './groupForm.html',
|
templateUrl: './groupForm.html',
|
||||||
controller: function() {
|
controller: GroupFormController,
|
||||||
var ctrl = this;
|
|
||||||
|
|
||||||
this.associateEndpoint = function(endpoint) {
|
|
||||||
ctrl.associatedEndpoints.push(endpoint);
|
|
||||||
_.remove(ctrl.availableEndpoints, function(n) {
|
|
||||||
return n.Id === endpoint.Id;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
this.dissociateEndpoint = function(endpoint) {
|
|
||||||
ctrl.availableEndpoints.push(endpoint);
|
|
||||||
_.remove(ctrl.associatedEndpoints, function(n) {
|
|
||||||
return n.Id === endpoint.Id;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
},
|
|
||||||
bindings: {
|
bindings: {
|
||||||
|
loaded: '<',
|
||||||
|
pageType: '@',
|
||||||
model: '=',
|
model: '=',
|
||||||
availableEndpoints: '=',
|
availableEndpoints: '=',
|
||||||
availableTags: '<',
|
availableTags: '<',
|
||||||
|
|
|
@ -49,8 +49,13 @@
|
||||||
<div class="text-center small text-muted">Available endpoints</div>
|
<div class="text-center small text-muted">Available endpoints</div>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<group-association-table
|
<group-association-table
|
||||||
|
loaded="$ctrl.loaded"
|
||||||
|
page-type="$ctrl.pageType"
|
||||||
|
table-type="available"
|
||||||
|
retrieve-page="$ctrl.getPaginatedEndpointsByGroup"
|
||||||
dataset="$ctrl.availableEndpoints"
|
dataset="$ctrl.availableEndpoints"
|
||||||
entry-click="$ctrl.associateEndpoint"
|
entry-click="$ctrl.associateEndpoint"
|
||||||
|
pagination-state="$ctrl.state.available"
|
||||||
empty-dataset-message="No endpoint available"
|
empty-dataset-message="No endpoint available"
|
||||||
></group-association-table>
|
></group-association-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,8 +66,13 @@
|
||||||
<div class="text-center small text-muted">Associated endpoints</div>
|
<div class="text-center small text-muted">Associated endpoints</div>
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<group-association-table
|
<group-association-table
|
||||||
|
loaded="$ctrl.loaded"
|
||||||
|
page-type="$ctrl.pageType"
|
||||||
|
table-type="associated"
|
||||||
|
retrieve-page="$ctrl.getPaginatedEndpointsByGroup"
|
||||||
dataset="$ctrl.associatedEndpoints"
|
dataset="$ctrl.associatedEndpoints"
|
||||||
entry-click="$ctrl.dissociateEndpoint"
|
entry-click="$ctrl.dissociateEndpoint"
|
||||||
|
pagination-state="$ctrl.state.associated"
|
||||||
empty-dataset-message="No associated endpoint"
|
empty-dataset-message="No associated endpoint"
|
||||||
></group-association-table>
|
></group-association-table>
|
||||||
</div>
|
</div>
|
||||||
|
@ -75,18 +85,18 @@
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
Unassociated endpoints
|
Unassociated endpoints
|
||||||
</div>
|
</div>
|
||||||
<div ng-if="$ctrl.associatedEndpoints.length > 0">
|
|
||||||
<div style="margin-top: 10px;">
|
<div style="margin-top: 10px;">
|
||||||
<group-association-table
|
<group-association-table
|
||||||
|
loaded="$ctrl.loaded"
|
||||||
|
page-type="$ctrl.pageType"
|
||||||
|
table-type="associated"
|
||||||
|
retrieve-page="$ctrl.getPaginatedEndpointsByGroup"
|
||||||
dataset="$ctrl.associatedEndpoints"
|
dataset="$ctrl.associatedEndpoints"
|
||||||
|
pagination-state="$ctrl.state.associated"
|
||||||
empty-dataset-message="No endpoint available"
|
empty-dataset-message="No endpoint available"
|
||||||
></group-association-table>
|
></group-association-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12" ng-if="$ctrl.associatedEndpoints.length === 0">
|
|
||||||
<span class="text-muted small">All the endpoints are assigned to a group.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- !endpoints -->
|
<!-- !endpoints -->
|
||||||
<!-- actions -->
|
<!-- actions -->
|
||||||
<div class="col-sm-12 form-section-title">
|
<div class="col-sm-12 form-section-title">
|
||||||
|
|
|
@ -5,15 +5,48 @@ angular.module('portainer.app').component('groupAssociationTable', {
|
||||||
orderBy: 'Name',
|
orderBy: 'Name',
|
||||||
reverseOrder: false,
|
reverseOrder: false,
|
||||||
paginatedItemLimit: '10',
|
paginatedItemLimit: '10',
|
||||||
textFilter: ''
|
textFilter: '',
|
||||||
|
loading:true,
|
||||||
|
pageNumber: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
this.changeOrderBy = function(orderField) {
|
this.changeOrderBy = function(orderField) {
|
||||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||||
this.state.orderBy = orderField;
|
this.state.orderBy = orderField;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.hasBackendPagination = function() {
|
||||||
|
return !(this.pageType === 'create' && this.tableType === 'associated');
|
||||||
|
}
|
||||||
|
this.onTextFilterChange = function() {
|
||||||
|
this.paginationChangedAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onPageChanged = function(newPageNumber) {
|
||||||
|
this.paginationState.pageNumber = newPageNumber;
|
||||||
|
this.paginationChangedAction();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onPaginationLimitChanged = function() {
|
||||||
|
this.paginationChangedAction();
|
||||||
|
};
|
||||||
|
|
||||||
|
this.paginationChangedAction = function() {
|
||||||
|
this.retrievePage(this.pageType, this.tableType);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$onChanges = function(changes) {
|
||||||
|
if (changes.loaded && changes.loaded.currentValue) {
|
||||||
|
this.paginationChangedAction();
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
bindings: {
|
bindings: {
|
||||||
|
paginationState: '=',
|
||||||
|
loaded: '<',
|
||||||
|
pageType: '<',
|
||||||
|
tableType: '@',
|
||||||
|
retrievePage: '<',
|
||||||
dataset: '<',
|
dataset: '<',
|
||||||
entryClick: '<',
|
entryClick: '<',
|
||||||
emptyDatasetMessage: '@'
|
emptyDatasetMessage: '@'
|
||||||
|
|
|
@ -2,7 +2,11 @@
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" ng-change="$ctrl.onTextFilterChange()" placeholder="Search...">
|
<input type="text" class="searchInput"
|
||||||
|
ng-model="$ctrl.paginationState.filter"
|
||||||
|
ng-change="$ctrl.onTextFilterChange()"
|
||||||
|
ng-model-options="{ debounce: 300 }"
|
||||||
|
placeholder="Search...">
|
||||||
</div>
|
</div>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -16,13 +20,25 @@
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr ng-click="$ctrl.entryClick(item)" class="interactive" dir-paginate="item in $ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit">
|
<tr ng-if="!$ctrl.hasBackendPagination();"
|
||||||
|
ng-click="$ctrl.entryClick(item)"
|
||||||
|
class="interactive"
|
||||||
|
dir-paginate="item in $ctrl.dataset | filter:$ctrl.paginationState.filter | itemsPerPage: $ctrl.paginationState.limit"
|
||||||
|
pagination-id="$ctrl.tableType">
|
||||||
|
<td>{{ item.Name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr ng-if="$ctrl.hasBackendPagination();"
|
||||||
|
ng-click="$ctrl.entryClick(item)"
|
||||||
|
class="interactive"
|
||||||
|
dir-paginate="item in $ctrl.dataset | itemsPerPage: $ctrl.paginationState.limit"
|
||||||
|
pagination-id="$ctrl.tableType"
|
||||||
|
total-items="$ctrl.paginationState.totalCount">
|
||||||
<td>{{ item.Name }}</td>
|
<td>{{ item.Name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="!$ctrl.dataset">
|
<tr ng-if="!$ctrl.dataset">
|
||||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr ng-if="$ctrl.dataset.length === 0 || ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit).length === 0">
|
<tr ng-if="$ctrl.dataset.length === 0">
|
||||||
<td colspan="2" class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
|
<td colspan="2" class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
|
@ -34,15 +50,14 @@
|
||||||
<span style="margin-right: 5px;">
|
<span style="margin-right: 5px;">
|
||||||
Items per page
|
Items per page
|
||||||
</span>
|
</span>
|
||||||
<select ng-model="$ctrl.state.paginatedItemLimit">
|
<select ng-model="$ctrl.paginationState.limit" ng-change="$ctrl.onPaginationLimitChanged()">
|
||||||
<option value="0">All</option>
|
|
||||||
<option value="10">10</option>
|
<option value="10">10</option>
|
||||||
<option value="25">25</option>
|
<option value="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
</select>
|
</select>
|
||||||
</span>
|
</span>
|
||||||
<dir-pagination-controls max-size="5"></dir-pagination-controls>
|
<dir-pagination-controls pagination-id="$ctrl.tableType" max-size="5" on-page-change="$ctrl.onPageChanged(newPageNumber, oldPageNumber)"></dir-pagination-controls>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,14 @@
|
||||||
|
import getEndpointsTotalCount from './transform/getEndpointsTotalCount';
|
||||||
|
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
.factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
||||||
'use strict';
|
'use strict';
|
||||||
return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, {
|
return $resource(API_ENDPOINT_ENDPOINTS + '/:id/:action', {}, {
|
||||||
query: { method: 'GET', isArray: true },
|
query: {
|
||||||
|
method: 'GET',
|
||||||
|
params: {start: '@start', limit: '@limit', search: '@search', groupId: '@groupId'},
|
||||||
|
transformResponse: getEndpointsTotalCount
|
||||||
|
},
|
||||||
get: { method: 'GET', params: { id: '@id' } },
|
get: { method: 'GET', params: { id: '@id' } },
|
||||||
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' } },
|
||||||
|
|
|
@ -7,6 +7,8 @@ angular.module('portainer.app')
|
||||||
get: { method: 'GET', params: { id: '@id' } },
|
get: { method: 'GET', params: { id: '@id' } },
|
||||||
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' } },
|
||||||
|
addEndpoint: {method: 'PUT', params: {id: '@id', action: '@action'}},
|
||||||
|
removeEndpoint: {method: 'DELETE', params:{id:'@id', action: '@action'}},
|
||||||
remove: { method: 'DELETE', params: { id: '@id'} }
|
remove: { method: 'DELETE', params: { id: '@id'} }
|
||||||
});
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
export default function getEndpointsTotalCount(data, headers) {
|
||||||
|
const response = {};
|
||||||
|
response.value = angular.fromJson(data);
|
||||||
|
response.totalCount = headers('X-Total-Count');
|
||||||
|
return response;
|
||||||
|
}
|
|
@ -8,8 +8,8 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return Endpoints.get({id: endpointID}).$promise;
|
return Endpoints.get({id: endpointID}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.endpoints = function() {
|
service.endpoints = function(start, limit, search) {
|
||||||
return Endpoints.query({}).$promise;
|
return Endpoints.query({start, limit, search}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.snapshotEndpoints = function() {
|
service.snapshotEndpoints = function() {
|
||||||
|
@ -20,21 +20,8 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
||||||
return Endpoints.snapshot({ id: endpointID }, {}).$promise;
|
return Endpoints.snapshot({ id: endpointID }, {}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
service.endpointsByGroup = function(groupId) {
|
service.endpointsByGroup = function(start, limit, search, groupId) {
|
||||||
var deferred = $q.defer();
|
return Endpoints.query({ start, limit, search, groupId }).$promise;
|
||||||
|
|
||||||
Endpoints.query({}).$promise
|
|
||||||
.then(function success(data) {
|
|
||||||
var endpoints = data.filter(function (endpoint) {
|
|
||||||
return endpoint.GroupId === groupId;
|
|
||||||
});
|
|
||||||
deferred.resolve(endpoints);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
deferred.reject({msg: 'Unable to retrieve endpoints', err: err});
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
service.updateAccess = function(id, userAccessPolicies, teamAccessPolicies) {
|
service.updateAccess = function(id, userAccessPolicies, teamAccessPolicies) {
|
||||||
|
|
|
@ -43,6 +43,14 @@ function GroupService($q, EndpointGroups) {
|
||||||
return EndpointGroups.updateAccess({ id: groupId }, {UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies}).$promise;
|
return EndpointGroups.updateAccess({ id: groupId }, {UserAccessPolicies: userAccessPolicies, TeamAccessPolicies: teamAccessPolicies}).$promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
service.addEndpoint = function(groupId, endpoint) {
|
||||||
|
return EndpointGroups.addEndpoint({id: groupId, action: 'endpoints/' + endpoint.Id}, endpoint).$promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
service.removeEndpoint = function(groupId, endpointId) {
|
||||||
|
return EndpointGroups.removeEndpoint({id: groupId, action: 'endpoints/' + endpointId}).$promise
|
||||||
|
}
|
||||||
|
|
||||||
service.deleteGroup = function(groupId) {
|
service.deleteGroup = function(groupId) {
|
||||||
return EndpointGroups.remove({ id: groupId }).$promise;
|
return EndpointGroups.remove({ id: groupId }).$promise;
|
||||||
};
|
};
|
||||||
|
|
|
@ -61,9 +61,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
|
||||||
};
|
};
|
||||||
|
|
||||||
function unauthenticatedFlow() {
|
function unauthenticatedFlow() {
|
||||||
EndpointService.endpoints()
|
EndpointService.endpoints(0, 100)
|
||||||
.then(function success(endpoints) {
|
.then(function success(endpoints) {
|
||||||
if (endpoints.length === 0) {
|
if (endpoints.value.length === 0) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else {
|
} else {
|
||||||
$state.go('portainer.home');
|
$state.go('portainer.home');
|
||||||
|
@ -87,9 +87,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkForEndpoints() {
|
function checkForEndpoints() {
|
||||||
EndpointService.endpoints()
|
EndpointService.endpoints(0, 100)
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoints = data;
|
var endpoints = data.value;
|
||||||
|
|
||||||
if (endpoints.length === 0 && Authentication.isAdmin()) {
|
if (endpoints.length === 0 && Authentication.isAdmin()) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
|
|
|
@ -26,11 +26,12 @@
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<endpoints-datatable
|
<endpoints-datatable
|
||||||
title-text="Endpoints" title-icon="fa-plug"
|
title-text="Endpoints" title-icon="fa-plug"
|
||||||
dataset="endpoints" table-key="endpoints"
|
table-key="endpoints"
|
||||||
order-by="Name"
|
order-by="Name"
|
||||||
endpoint-management="applicationState.application.endpointManagement"
|
endpoint-management="applicationState.application.endpointManagement"
|
||||||
access-management="applicationState.application.authentication"
|
access-management="applicationState.application.authentication"
|
||||||
remove-action="removeAction"
|
remove-action="removeAction"
|
||||||
|
retrieve-page="getPaginatedEndpoints"
|
||||||
></endpoints-datatable>
|
></endpoints-datatable>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,8 +8,6 @@ function ($q, $scope, $state, EndpointService, GroupService, EndpointHelper, Not
|
||||||
EndpointService.deleteEndpoint(endpoint.Id)
|
EndpointService.deleteEndpoint(endpoint.Id)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Endpoint successfully removed', endpoint.Name);
|
Notifications.success('Endpoint successfully removed', endpoint.Name);
|
||||||
var index = $scope.endpoints.indexOf(endpoint);
|
|
||||||
$scope.endpoints.splice(index, 1);
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to remove endpoint');
|
Notifications.error('Failure', err, 'Unable to remove endpoint');
|
||||||
|
@ -23,22 +21,22 @@ function ($q, $scope, $state, EndpointService, GroupService, EndpointHelper, Not
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
function initView() {
|
$scope.getPaginatedEndpoints = getPaginatedEndpoints;
|
||||||
|
function getPaginatedEndpoints(lastId, limit, filter) {
|
||||||
|
const deferred = $q.defer();
|
||||||
$q.all({
|
$q.all({
|
||||||
endpoints: EndpointService.endpoints(),
|
endpoints: EndpointService.endpoints(lastId, limit, filter),
|
||||||
groups: GroupService.groups()
|
groups: GroupService.groups()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var endpoints = data.endpoints;
|
var endpoints = data.endpoints.value;
|
||||||
var groups = data.groups;
|
var groups = data.groups;
|
||||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||||
$scope.groups = groups;
|
deferred.resolve({endpoints: endpoints, totalCount: data.endpoints.totalCount});
|
||||||
$scope.endpoints = endpoints;
|
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to load view');
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||||
});
|
});
|
||||||
|
return deferred.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
initView();
|
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -32,19 +32,15 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific
|
||||||
};
|
};
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.model = new EndpointGroupDefaultModel();
|
TagService.tagNames()
|
||||||
|
.then((tags) => {
|
||||||
$q.all({
|
$scope.availableTags = tags;
|
||||||
endpoints: EndpointService.endpointsByGroup(1),
|
|
||||||
tags: TagService.tagNames()
|
|
||||||
})
|
|
||||||
.then(function success(data) {
|
|
||||||
$scope.availableEndpoints = data.endpoints;
|
|
||||||
$scope.associatedEndpoints = [];
|
$scope.associatedEndpoints = [];
|
||||||
$scope.availableTags = data.tags;
|
$scope.model = new EndpointGroupDefaultModel();
|
||||||
|
$scope.loaded = true;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch((err) => {
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
Notifications.error('Failure', err, 'Unable to retrieve tags');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<group-form
|
<group-form
|
||||||
|
loaded="loaded"
|
||||||
|
page-type="create"
|
||||||
model="model"
|
model="model"
|
||||||
available-endpoints="availableEndpoints"
|
available-endpoints="availableEndpoints"
|
||||||
available-tags="availableTags"
|
available-tags="availableTags"
|
||||||
|
|
|
@ -10,6 +10,8 @@
|
||||||
<rd-widget>
|
<rd-widget>
|
||||||
<rd-widget-body>
|
<rd-widget-body>
|
||||||
<group-form
|
<group-form
|
||||||
|
loaded="loaded"
|
||||||
|
page-type="edit"
|
||||||
model="group"
|
model="group"
|
||||||
available-endpoints="availableEndpoints"
|
available-endpoints="availableEndpoints"
|
||||||
available-tags="availableTags"
|
available-tags="availableTags"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
angular.module('portainer.app')
|
angular.module('portainer.app')
|
||||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
|
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'TagService', 'Notifications',
|
||||||
function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagService, Notifications) {
|
function ($q, $scope, $state, $transition$, GroupService, TagService, Notifications) {
|
||||||
|
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
actionInProgress: false
|
actionInProgress: false
|
||||||
|
@ -9,14 +9,8 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagSe
|
||||||
$scope.update = function() {
|
$scope.update = function() {
|
||||||
var model = $scope.group;
|
var model = $scope.group;
|
||||||
|
|
||||||
var associatedEndpoints = [];
|
|
||||||
for (var i = 0; i < $scope.associatedEndpoints.length; i++) {
|
|
||||||
var endpoint = $scope.associatedEndpoints[i];
|
|
||||||
associatedEndpoints.push(endpoint.Id);
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.state.actionInProgress = true;
|
$scope.state.actionInProgress = true;
|
||||||
GroupService.updateGroup(model, associatedEndpoints)
|
GroupService.updateGroup(model)
|
||||||
.then(function success() {
|
.then(function success() {
|
||||||
Notifications.success('Group successfully updated');
|
Notifications.success('Group successfully updated');
|
||||||
$state.go('portainer.groups', {}, {reload: true});
|
$state.go('portainer.groups', {}, {reload: true});
|
||||||
|
@ -34,29 +28,15 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagSe
|
||||||
|
|
||||||
$q.all({
|
$q.all({
|
||||||
group: GroupService.group(groupId),
|
group: GroupService.group(groupId),
|
||||||
endpoints: EndpointService.endpoints(),
|
|
||||||
tags: TagService.tagNames()
|
tags: TagService.tagNames()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.group = data.group;
|
$scope.group = data.group;
|
||||||
|
|
||||||
var availableEndpoints = [];
|
|
||||||
var associatedEndpoints = [];
|
|
||||||
for (var i = 0; i < data.endpoints.length; i++) {
|
|
||||||
var endpoint = data.endpoints[i];
|
|
||||||
if (endpoint.GroupId === +groupId) {
|
|
||||||
associatedEndpoints.push(endpoint);
|
|
||||||
} else if (endpoint.GroupId === 1) {
|
|
||||||
availableEndpoints.push(endpoint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.availableEndpoints = availableEndpoints;
|
|
||||||
$scope.associatedEndpoints = associatedEndpoints;
|
|
||||||
$scope.availableTags = data.tags;
|
$scope.availableTags = data.tags;
|
||||||
|
$scope.loaded = true;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
Notifications.error('Failure', err, 'Unable to load view');
|
Notifications.error('Failure', err, 'Unable to load group details');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
</span>
|
</span>
|
||||||
</information-panel>
|
</information-panel>
|
||||||
|
|
||||||
<div class="row" ng-if="endpoints.length > 0">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
<endpoint-list
|
<endpoint-list
|
||||||
title-text="Endpoints" title-icon="fa-plug"
|
title-text="Endpoints" title-icon="fa-plug"
|
||||||
|
@ -34,6 +34,8 @@
|
||||||
snapshot-action="triggerSnapshot"
|
snapshot-action="triggerSnapshot"
|
||||||
edit-action="goToEdit"
|
edit-action="goToEdit"
|
||||||
is-admin="isAdmin"
|
is-admin="isAdmin"
|
||||||
|
total-count="totalCount"
|
||||||
|
retrieve-page="getPaginatedEndpoints"
|
||||||
></endpoint-list>
|
></endpoint-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -111,6 +111,26 @@ angular.module('portainer.app')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.getPaginatedEndpoints = getPaginatedEndpoints;
|
||||||
|
function getPaginatedEndpoints(lastId, limit, filter) {
|
||||||
|
const deferred = $q.defer();
|
||||||
|
$q.all({
|
||||||
|
endpoints: EndpointService.endpoints(lastId, limit, filter),
|
||||||
|
groups: GroupService.groups()
|
||||||
|
})
|
||||||
|
.then(function success(data) {
|
||||||
|
var endpoints = data.endpoints.value;
|
||||||
|
var groups = data.groups;
|
||||||
|
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||||
|
EndpointProvider.setEndpoints(endpoints);
|
||||||
|
deferred.resolve({endpoints: endpoints, totalCount: data.endpoints.totalCount});
|
||||||
|
})
|
||||||
|
.catch(function error(err) {
|
||||||
|
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
function initView() {
|
function initView() {
|
||||||
$scope.isAdmin = Authentication.isAdmin();
|
$scope.isAdmin = Authentication.isAdmin();
|
||||||
|
|
||||||
|
@ -119,19 +139,15 @@ angular.module('portainer.app')
|
||||||
$scope.motd = data;
|
$scope.motd = data;
|
||||||
});
|
});
|
||||||
|
|
||||||
$q.all({
|
getPaginatedEndpoints(0, 100)
|
||||||
endpoints: EndpointService.endpoints(),
|
.then((data) => {
|
||||||
groups: GroupService.groups()
|
const totalCount = data.totalCount;
|
||||||
})
|
$scope.totalCount = totalCount;
|
||||||
.then(function success(data) {
|
if (totalCount > 100) {
|
||||||
var endpoints = data.endpoints;
|
$scope.endpoints = [];
|
||||||
var groups = data.groups;
|
} else {
|
||||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
$scope.endpoints = data.endpoints;
|
||||||
$scope.endpoints = endpoints;
|
}
|
||||||
EndpointProvider.setEndpoints(endpoints);
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,10 +39,10 @@ function ($async, $scope, $state, Notifications, Authentication, StateManager, U
|
||||||
return retrieveAndSaveEnabledExtensions();
|
return retrieveAndSaveEnabledExtensions();
|
||||||
})
|
})
|
||||||
.then(function () {
|
.then(function () {
|
||||||
return EndpointService.endpoints();
|
return EndpointService.endpoints(0, 100);
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
if (data.length === 0) {
|
if (data.value.length === 0) {
|
||||||
$state.go('portainer.init.endpoint');
|
$state.go('portainer.init.endpoint');
|
||||||
} else {
|
} else {
|
||||||
$state.go('portainer.home');
|
$state.go('portainer.home');
|
||||||
|
|
|
@ -42,7 +42,7 @@ function ($q, $scope, $state, Notifications, EndpointService, GroupService, Sche
|
||||||
groups: GroupService.groups()
|
groups: GroupService.groups()
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
$scope.endpoints = data.endpoints;
|
$scope.endpoints = data.endpoints.value;
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -60,13 +60,13 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
||||||
var schedule = data.schedule;
|
var schedule = data.schedule;
|
||||||
schedule.Job.FileContent = data.file.ScheduleFileContent;
|
schedule.Job.FileContent = data.file.ScheduleFileContent;
|
||||||
|
|
||||||
var endpoints = data.endpoints;
|
var endpoints = data.endpoints.value;
|
||||||
var tasks = data.tasks;
|
var tasks = data.tasks;
|
||||||
associateEndpointsToTasks(tasks, endpoints);
|
associateEndpointsToTasks(tasks, endpoints);
|
||||||
|
|
||||||
$scope.schedule = schedule;
|
$scope.schedule = schedule;
|
||||||
$scope.tasks = data.tasks;
|
$scope.tasks = data.tasks;
|
||||||
$scope.endpoints = data.endpoints;
|
$scope.endpoints = data.endpoints.value;
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
})
|
})
|
||||||
.catch(function error(err) {
|
.catch(function error(err) {
|
||||||
|
|
|
@ -165,7 +165,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
||||||
})
|
})
|
||||||
.then(function success(data) {
|
.then(function success(data) {
|
||||||
var stack = data.stack;
|
var stack = data.stack;
|
||||||
$scope.endpoints = data.endpoints;
|
$scope.endpoints = data.endpoints.value;
|
||||||
$scope.groups = data.groups;
|
$scope.groups = data.groups;
|
||||||
$scope.stack = stack;
|
$scope.stack = stack;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue