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
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -876,7 +876,7 @@ const (
|
|||
PortainerAgentSignatureMessage = "Portainer-App"
|
||||
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer
|
||||
SupportedDockerAPIVersion = "1.24"
|
||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||
ExtensionServer = "localhost"
|
||||
)
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ class AccessViewerController {
|
|||
this.rbacEnabled = await this.ExtensionService.extensionEnabled(this.ExtensionService.EXTENSIONS.RBAC);
|
||||
if (this.rbacEnabled) {
|
||||
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();
|
||||
this.groupUserAccessPolicies = {};
|
||||
this.groupTeamAccessPolicies = {};
|
||||
|
|
|
@ -17,7 +17,12 @@
|
|||
</div>
|
||||
<div class="searchBar">
|
||||
<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 class="table-responsive">
|
||||
<table class="table table-hover nowrap-cells">
|
||||
|
@ -59,7 +64,9 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
<span class="md-checkbox" ng-if="$ctrl.endpointManagement">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)"/>
|
||||
|
@ -82,16 +89,16 @@
|
|||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" class="text-center text-muted">Loading...</td>
|
||||
<tr ng-if="$ctrl.state.loading">
|
||||
<td colspan="5" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="3" class="text-center text-muted">No endpoint available.</td>
|
||||
<tr ng-if="!$ctrl.state.loading && $ctrl.state.filteredDataSet.length === 0">
|
||||
<td colspan="5" class="text-center text-muted">No endpoint available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
|
@ -109,7 +116,7 @@
|
|||
<option value="100">100</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
angular.module('portainer.app').component('endpointsDatatable', {
|
||||
templateUrl: './endpointsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
controller: 'EndpointsDatatableController',
|
||||
bindings: {
|
||||
titleText: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
endpointManagement: '<',
|
||||
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',
|
||||
function EndpointListController(DatatableService, PaginationService) {
|
||||
var ctrl = this;
|
||||
ctrl.state = {
|
||||
this.state = {
|
||||
totalFilteredEndpoints: this.totalCount,
|
||||
textFilter: '',
|
||||
filteredEndpoints: [],
|
||||
paginatedItemLimit: '10'
|
||||
paginatedItemLimit: '10',
|
||||
pageNumber: 1,
|
||||
loading: true
|
||||
};
|
||||
|
||||
ctrl.$onChanges = $onChanges;
|
||||
ctrl.onTextFilterChange = onTextFilterChange;
|
||||
ctrl.$onInit = $onInit
|
||||
|
||||
function $onChanges(changesObj) {
|
||||
handleEndpointsChange(changesObj.endpoints);
|
||||
this.$onChanges = function(changesObj) {
|
||||
this.handleEndpointsChange(changesObj.endpoints);
|
||||
}
|
||||
|
||||
function handleEndpointsChange(endpoints) {
|
||||
if (!endpoints) {
|
||||
this.handleEndpointsChange = function(endpoints) {
|
||||
if (!endpoints || !endpoints.currentValue) {
|
||||
return;
|
||||
}
|
||||
if (!endpoints.currentValue) {
|
||||
return;
|
||||
this.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;
|
||||
}
|
||||
|
||||
onTextFilterChange();
|
||||
}
|
||||
|
||||
function onTextFilterChange() {
|
||||
var filterValue = ctrl.state.textFilter;
|
||||
ctrl.state.filteredEndpoints = filterEndpoints(
|
||||
ctrl.endpoints,
|
||||
filterValue
|
||||
);
|
||||
DatatableService.setDataTableTextFilters(ctrl.tableKey, filterValue);
|
||||
}
|
||||
|
||||
function filterEndpoints(endpoints, filterValue) {
|
||||
function frontEndpointFilter(endpoints, filterValue) {
|
||||
if (!endpoints || !endpoints.length || !filterValue) {
|
||||
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() {
|
||||
PaginationService.setPaginationLimit(this.tableKey, this.state.paginatedItemLimit);
|
||||
this.paginationChangedAction();
|
||||
};
|
||||
|
||||
function convertStatusToString(status) {
|
||||
return status === 1 ? 'up' : 'down';
|
||||
}
|
||||
|
||||
function $onInit() {
|
||||
var textFilter = DatatableService.getDataTableTextFilters(ctrl.tableKey);
|
||||
this.$onInit = function() {
|
||||
this.state.loading = true;
|
||||
var textFilter = DatatableService.getDataTableTextFilters(this.tableKey);
|
||||
this.state.paginatedItemLimit = PaginationService.getPaginationLimit(this.tableKey);
|
||||
if (textFilter !== null) {
|
||||
ctrl.state.textFilter = textFilter;
|
||||
onTextFilterChange();
|
||||
this.state.textFilter = textFilter;
|
||||
} else {
|
||||
this.paginationChangedAction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,8 @@ angular.module('portainer.app').component('endpointList', {
|
|||
snapshotAction: '<',
|
||||
showSnapshotAction: '<',
|
||||
editAction: '<',
|
||||
isAdmin:'<'
|
||||
isAdmin:'<',
|
||||
totalCount: '<',
|
||||
retrievePage: '<'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -21,21 +21,30 @@
|
|||
class="searchInput"
|
||||
ng-model="$ctrl.state.textFilter"
|
||||
ng-change="$ctrl.onTextFilterChange()"
|
||||
ng-model-options="{ debounce: 300 }"
|
||||
placeholder="Search by name, group, tag, status, URL..." auto-focus>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
model="endpoint"
|
||||
on-select="$ctrl.dashboardAction"
|
||||
on-edit="$ctrl.editAction"
|
||||
is-admin="$ctrl.isAdmin"
|
||||
></endpoint-item>
|
||||
<div ng-if="!$ctrl.endpoints" class="text-center text-muted">
|
||||
<div ng-if="$ctrl.state.loading" class="text-center text-muted">
|
||||
Loading...
|
||||
</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.
|
||||
</div>
|
||||
</div>
|
||||
|
@ -48,14 +57,14 @@
|
|||
Items per page
|
||||
</span>
|
||||
<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="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,26 +1,99 @@
|
|||
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', {
|
||||
templateUrl: './groupForm.html',
|
||||
controller: function() {
|
||||
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;
|
||||
});
|
||||
};
|
||||
|
||||
},
|
||||
controller: GroupFormController,
|
||||
bindings: {
|
||||
loaded: '<',
|
||||
pageType: '@',
|
||||
model: '=',
|
||||
availableEndpoints: '=',
|
||||
availableTags: '<',
|
||||
|
|
|
@ -49,8 +49,13 @@
|
|||
<div class="text-center small text-muted">Available endpoints</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<group-association-table
|
||||
loaded="$ctrl.loaded"
|
||||
page-type="$ctrl.pageType"
|
||||
table-type="available"
|
||||
retrieve-page="$ctrl.getPaginatedEndpointsByGroup"
|
||||
dataset="$ctrl.availableEndpoints"
|
||||
entry-click="$ctrl.associateEndpoint"
|
||||
pagination-state="$ctrl.state.available"
|
||||
empty-dataset-message="No endpoint available"
|
||||
></group-association-table>
|
||||
</div>
|
||||
|
@ -61,8 +66,13 @@
|
|||
<div class="text-center small text-muted">Associated endpoints</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<group-association-table
|
||||
loaded="$ctrl.loaded"
|
||||
page-type="$ctrl.pageType"
|
||||
table-type="associated"
|
||||
retrieve-page="$ctrl.getPaginatedEndpointsByGroup"
|
||||
dataset="$ctrl.associatedEndpoints"
|
||||
entry-click="$ctrl.dissociateEndpoint"
|
||||
pagination-state="$ctrl.state.associated"
|
||||
empty-dataset-message="No associated endpoint"
|
||||
></group-association-table>
|
||||
</div>
|
||||
|
@ -75,16 +85,16 @@
|
|||
<div class="col-sm-12 form-section-title">
|
||||
Unassociated endpoints
|
||||
</div>
|
||||
<div ng-if="$ctrl.associatedEndpoints.length > 0">
|
||||
<div style="margin-top: 10px;">
|
||||
<group-association-table
|
||||
dataset="$ctrl.associatedEndpoints"
|
||||
empty-dataset-message="No endpoint available"
|
||||
></group-association-table>
|
||||
</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 style="margin-top: 10px;">
|
||||
<group-association-table
|
||||
loaded="$ctrl.loaded"
|
||||
page-type="$ctrl.pageType"
|
||||
table-type="associated"
|
||||
retrieve-page="$ctrl.getPaginatedEndpointsByGroup"
|
||||
dataset="$ctrl.associatedEndpoints"
|
||||
pagination-state="$ctrl.state.associated"
|
||||
empty-dataset-message="No endpoint available"
|
||||
></group-association-table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoints -->
|
||||
|
|
|
@ -5,15 +5,48 @@ angular.module('portainer.app').component('groupAssociationTable', {
|
|||
orderBy: 'Name',
|
||||
reverseOrder: false,
|
||||
paginatedItemLimit: '10',
|
||||
textFilter: ''
|
||||
textFilter: '',
|
||||
loading:true,
|
||||
pageNumber: 1
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
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: {
|
||||
paginationState: '=',
|
||||
loaded: '<',
|
||||
pageType: '<',
|
||||
tableType: '@',
|
||||
retrievePage: '<',
|
||||
dataset: '<',
|
||||
entryClick: '<',
|
||||
emptyDatasetMessage: '@'
|
||||
|
|
|
@ -2,7 +2,11 @@
|
|||
<table class="table table-hover">
|
||||
<div class="col-sm-12">
|
||||
<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>
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -16,13 +20,25 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<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>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</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>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -34,15 +50,14 @@
|
|||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select ng-model="$ctrl.state.paginatedItemLimit">
|
||||
<option value="0">All</option>
|
||||
<select ng-model="$ctrl.paginationState.limit" ng-change="$ctrl.onPaginationLimitChanged()">
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,14 @@
|
|||
import getEndpointsTotalCount from './transform/getEndpointsTotalCount';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.factory('Endpoints', ['$resource', 'API_ENDPOINT_ENDPOINTS', function EndpointsFactory($resource, API_ENDPOINT_ENDPOINTS) {
|
||||
'use strict';
|
||||
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' } },
|
||||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||
|
|
|
@ -7,6 +7,8 @@ angular.module('portainer.app')
|
|||
get: { method: 'GET', params: { id: '@id' } },
|
||||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
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'} }
|
||||
});
|
||||
}]);
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
service.endpoints = function() {
|
||||
return Endpoints.query({}).$promise;
|
||||
service.endpoints = function(start, limit, search) {
|
||||
return Endpoints.query({start, limit, search}).$promise;
|
||||
};
|
||||
|
||||
service.snapshotEndpoints = function() {
|
||||
|
@ -20,21 +20,8 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return Endpoints.snapshot({ id: endpointID }, {}).$promise;
|
||||
};
|
||||
|
||||
service.endpointsByGroup = function(groupId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
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.endpointsByGroup = function(start, limit, search, groupId) {
|
||||
return Endpoints.query({ start, limit, search, groupId }).$promise;
|
||||
};
|
||||
|
||||
service.updateAccess = function(id, userAccessPolicies, teamAccessPolicies) {
|
||||
|
|
|
@ -43,6 +43,14 @@ function GroupService($q, EndpointGroups) {
|
|||
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) {
|
||||
return EndpointGroups.remove({ id: groupId }).$promise;
|
||||
};
|
||||
|
|
|
@ -61,9 +61,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
|
|||
};
|
||||
|
||||
function unauthenticatedFlow() {
|
||||
EndpointService.endpoints()
|
||||
EndpointService.endpoints(0, 100)
|
||||
.then(function success(endpoints) {
|
||||
if (endpoints.length === 0) {
|
||||
if (endpoints.value.length === 0) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
} else {
|
||||
$state.go('portainer.home');
|
||||
|
@ -87,9 +87,9 @@ function($async, $q, $scope, $state, $stateParams, $sanitize, Authentication, Us
|
|||
}
|
||||
|
||||
function checkForEndpoints() {
|
||||
EndpointService.endpoints()
|
||||
EndpointService.endpoints(0, 100)
|
||||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
var endpoints = data.value;
|
||||
|
||||
if (endpoints.length === 0 && Authentication.isAdmin()) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
|
|
|
@ -26,11 +26,12 @@
|
|||
<div class="col-sm-12">
|
||||
<endpoints-datatable
|
||||
title-text="Endpoints" title-icon="fa-plug"
|
||||
dataset="endpoints" table-key="endpoints"
|
||||
table-key="endpoints"
|
||||
order-by="Name"
|
||||
endpoint-management="applicationState.application.endpointManagement"
|
||||
access-management="applicationState.application.authentication"
|
||||
remove-action="removeAction"
|
||||
retrieve-page="getPaginatedEndpoints"
|
||||
></endpoints-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,8 +8,6 @@ function ($q, $scope, $state, EndpointService, GroupService, EndpointHelper, Not
|
|||
EndpointService.deleteEndpoint(endpoint.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Endpoint successfully removed', endpoint.Name);
|
||||
var index = $scope.endpoints.indexOf(endpoint);
|
||||
$scope.endpoints.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
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({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
endpoints: EndpointService.endpoints(lastId, limit, filter),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data.endpoints;
|
||||
var endpoints = data.endpoints.value;
|
||||
var groups = data.groups;
|
||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||
$scope.groups = groups;
|
||||
$scope.endpoints = endpoints;
|
||||
deferred.resolve({endpoints: endpoints, totalCount: data.endpoints.totalCount});
|
||||
})
|
||||
.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();
|
||||
}]);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { EndpointGroupDefaultModel } from '../../../models/group';
|
||||
import {EndpointGroupDefaultModel} from '../../../models/group';
|
||||
|
||||
angular.module('portainer.app')
|
||||
.controller('CreateGroupController', ['$q', '$scope', '$state', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
|
||||
|
@ -32,19 +32,15 @@ function ($q, $scope, $state, GroupService, EndpointService, TagService, Notific
|
|||
};
|
||||
|
||||
function initView() {
|
||||
$scope.model = new EndpointGroupDefaultModel();
|
||||
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpointsByGroup(1),
|
||||
tags: TagService.tagNames()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.availableEndpoints = data.endpoints;
|
||||
TagService.tagNames()
|
||||
.then((tags) => {
|
||||
$scope.availableTags = tags;
|
||||
$scope.associatedEndpoints = [];
|
||||
$scope.availableTags = data.tags;
|
||||
$scope.model = new EndpointGroupDefaultModel();
|
||||
$scope.loaded = true;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
.catch((err) => {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve tags');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<group-form
|
||||
loaded="loaded"
|
||||
page-type="create"
|
||||
model="model"
|
||||
available-endpoints="availableEndpoints"
|
||||
available-tags="availableTags"
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<group-form
|
||||
loaded="loaded"
|
||||
page-type="edit"
|
||||
model="group"
|
||||
available-endpoints="availableEndpoints"
|
||||
available-tags="availableTags"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'TagService', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagService, Notifications) {
|
||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'TagService', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, GroupService, TagService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
|
@ -9,14 +9,8 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagSe
|
|||
$scope.update = function() {
|
||||
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;
|
||||
GroupService.updateGroup(model, associatedEndpoints)
|
||||
GroupService.updateGroup(model)
|
||||
.then(function success() {
|
||||
Notifications.success('Group successfully updated');
|
||||
$state.go('portainer.groups', {}, {reload: true});
|
||||
|
@ -34,29 +28,15 @@ function ($q, $scope, $state, $transition$, GroupService, EndpointService, TagSe
|
|||
|
||||
$q.all({
|
||||
group: GroupService.group(groupId),
|
||||
endpoints: EndpointService.endpoints(),
|
||||
tags: TagService.tagNames()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$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.loaded = true;
|
||||
})
|
||||
.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>
|
||||
</information-panel>
|
||||
|
||||
<div class="row" ng-if="endpoints.length > 0">
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<endpoint-list
|
||||
title-text="Endpoints" title-icon="fa-plug"
|
||||
|
@ -34,6 +34,8 @@
|
|||
snapshot-action="triggerSnapshot"
|
||||
edit-action="goToEdit"
|
||||
is-admin="isAdmin"
|
||||
total-count="totalCount"
|
||||
retrieve-page="getPaginatedEndpoints"
|
||||
></endpoint-list>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -111,28 +111,44 @@ 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() {
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
|
||||
MotdService.motd()
|
||||
.then(function success(data) {
|
||||
$scope.motd = data;
|
||||
});
|
||||
.then(function success(data) {
|
||||
$scope.motd = data;
|
||||
});
|
||||
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data.endpoints;
|
||||
var groups = data.groups;
|
||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||
$scope.endpoints = endpoints;
|
||||
EndpointProvider.setEndpoints(endpoints);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint information');
|
||||
});
|
||||
getPaginatedEndpoints(0, 100)
|
||||
.then((data) => {
|
||||
const totalCount = data.totalCount;
|
||||
$scope.totalCount = totalCount;
|
||||
if (totalCount > 100) {
|
||||
$scope.endpoints = [];
|
||||
} else {
|
||||
$scope.endpoints = data.endpoints;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -39,10 +39,10 @@ function ($async, $scope, $state, Notifications, Authentication, StateManager, U
|
|||
return retrieveAndSaveEnabledExtensions();
|
||||
})
|
||||
.then(function () {
|
||||
return EndpointService.endpoints();
|
||||
return EndpointService.endpoints(0, 100);
|
||||
})
|
||||
.then(function success(data) {
|
||||
if (data.length === 0) {
|
||||
if (data.value.length === 0) {
|
||||
$state.go('portainer.init.endpoint');
|
||||
} else {
|
||||
$state.go('portainer.home');
|
||||
|
|
|
@ -42,7 +42,7 @@ function ($q, $scope, $state, Notifications, EndpointService, GroupService, Sche
|
|||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.endpoints = data.endpoints;
|
||||
$scope.endpoints = data.endpoints.value;
|
||||
$scope.groups = data.groups;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -60,13 +60,13 @@ function ($q, $scope, $transition$, $state, Notifications, EndpointService, Grou
|
|||
var schedule = data.schedule;
|
||||
schedule.Job.FileContent = data.file.ScheduleFileContent;
|
||||
|
||||
var endpoints = data.endpoints;
|
||||
var endpoints = data.endpoints.value;
|
||||
var tasks = data.tasks;
|
||||
associateEndpointsToTasks(tasks, endpoints);
|
||||
|
||||
$scope.schedule = schedule;
|
||||
$scope.tasks = data.tasks;
|
||||
$scope.endpoints = data.endpoints;
|
||||
$scope.endpoints = data.endpoints.value;
|
||||
$scope.groups = data.groups;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -165,7 +165,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe
|
|||
})
|
||||
.then(function success(data) {
|
||||
var stack = data.stack;
|
||||
$scope.endpoints = data.endpoints;
|
||||
$scope.endpoints = data.endpoints.value;
|
||||
$scope.groups = data.groups;
|
||||
$scope.stack = stack;
|
||||
|
||||
|
|
Loading…
Reference in New Issue