diff --git a/api/bolt/endpoint/endpoint.go b/api/bolt/endpoint/endpoint.go index 723c2046b..53156d2a2 100644 --- a/api/bolt/endpoint/endpoint.go +++ b/api/bolt/endpoint/endpoint.go @@ -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 ( diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index 6cfb72468..32a617c92 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -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 } } } diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go new file mode 100644 index 000000000..c67f730e0 --- /dev/null +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_add.go @@ -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) +} diff --git a/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go new file mode 100644 index 000000000..2054b428f --- /dev/null +++ b/api/http/handler/endpointgroups/endpointgroup_endpoint_delete.go @@ -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) +} diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index 1aa523403..92dbc9037 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -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) } diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index c210373e9..fa10d92c9 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -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 -} diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index d7e1ba173..b89899d16 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -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=)&(limit=)&(search=)&(groupId= 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 } diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index 03192f47f..9896bd92a 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -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. diff --git a/api/portainer.go b/api/portainer.go index 1cb48f826..852552ba6 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -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" ) diff --git a/app/extensions/rbac/components/access-viewer/accessViewerController.js b/app/extensions/rbac/components/access-viewer/accessViewerController.js index 40c06754b..7b4fc5d4d 100644 --- a/app/extensions/rbac/components/access-viewer/accessViewerController.js +++ b/app/extensions/rbac/components/access-viewer/accessViewerController.js @@ -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 = {}; diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html index e61721001..a9f360e8e 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.html @@ -17,7 +17,12 @@
@@ -59,7 +64,9 @@ - + - - + + - - + +
@@ -82,16 +89,16 @@
Loading...
Loading...
No endpoint available.
No endpoint available.
- diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js index f355e559f..a452cf396 100644 --- a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatable.js @@ -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: '<' } }); diff --git a/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js new file mode 100644 index 000000000..0cdc1573d --- /dev/null +++ b/app/portainer/components/datatables/endpoints-datatable/endpointsDatatableController.js @@ -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(); + }; + } +]); diff --git a/app/portainer/components/endpoint-list/endpoint-list-controller.js b/app/portainer/components/endpoint-list/endpoint-list-controller.js index b9851a804..bff29eb47 100644 --- a/app/portainer/components/endpoint-list/endpoint-list-controller.js +++ b/app/portainer/components/endpoint-list/endpoint-list-controller.js @@ -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(); } } } diff --git a/app/portainer/components/endpoint-list/endpoint-list.js b/app/portainer/components/endpoint-list/endpoint-list.js index e4ce9cff5..06835c9e0 100644 --- a/app/portainer/components/endpoint-list/endpoint-list.js +++ b/app/portainer/components/endpoint-list/endpoint-list.js @@ -10,6 +10,8 @@ angular.module('portainer.app').component('endpointList', { snapshotAction: '<', showSnapshotAction: '<', editAction: '<', - isAdmin:'<' + isAdmin:'<', + totalCount: '<', + retrievePage: '<' } }); diff --git a/app/portainer/components/endpoint-list/endpointList.html b/app/portainer/components/endpoint-list/endpointList.html index 9909a6ed7..028ccc122 100644 --- a/app/portainer/components/endpoint-list/endpointList.html +++ b/app/portainer/components/endpoint-list/endpointList.html @@ -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>
- + -
+
Loading...
-
+
No endpoint available.
@@ -48,14 +57,14 @@ Items per page - +
diff --git a/app/portainer/components/forms/group-form/group-form.js b/app/portainer/components/forms/group-form/group-form.js index ff5b47b33..10e12f62c 100644 --- a/app/portainer/components/forms/group-form/group-form.js +++ b/app/portainer/components/forms/group-form/group-form.js @@ -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: '<', diff --git a/app/portainer/components/forms/group-form/groupForm.html b/app/portainer/components/forms/group-form/groupForm.html index 9d3b9a8a3..f16d3bd2c 100644 --- a/app/portainer/components/forms/group-form/groupForm.html +++ b/app/portainer/components/forms/group-form/groupForm.html @@ -49,8 +49,13 @@
Available endpoints
@@ -61,8 +66,13 @@
Associated endpoints
@@ -75,16 +85,16 @@
Unassociated endpoints
-
-
- -
-
-
- All the endpoints are assigned to a group. +
+
diff --git a/app/portainer/components/group-association-table/group-association-table.js b/app/portainer/components/group-association-table/group-association-table.js index d61bbddc5..0fd36822f 100644 --- a/app/portainer/components/group-association-table/group-association-table.js +++ b/app/portainer/components/group-association-table/group-association-table.js @@ -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: '@' diff --git a/app/portainer/components/group-association-table/groupAssociationTable.html b/app/portainer/components/group-association-table/groupAssociationTable.html index c457bd295..348420919 100644 --- a/app/portainer/components/group-association-table/groupAssociationTable.html +++ b/app/portainer/components/group-association-table/groupAssociationTable.html @@ -2,7 +2,11 @@
- +
@@ -16,13 +20,25 @@ - + + + + - + @@ -34,15 +50,14 @@ Items per page - - + diff --git a/app/portainer/rest/endpoint.js b/app/portainer/rest/endpoint.js index 068a8ef66..85f671b60 100644 --- a/app/portainer/rest/endpoint.js +++ b/app/portainer/rest/endpoint.js @@ -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' } }, diff --git a/app/portainer/rest/group.js b/app/portainer/rest/group.js index be4507756..ed8f6180f 100644 --- a/app/portainer/rest/group.js +++ b/app/portainer/rest/group.js @@ -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'} } }); }]); diff --git a/app/portainer/rest/transform/getEndpointsTotalCount.js b/app/portainer/rest/transform/getEndpointsTotalCount.js new file mode 100644 index 000000000..e3205eac1 --- /dev/null +++ b/app/portainer/rest/transform/getEndpointsTotalCount.js @@ -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; +} \ No newline at end of file diff --git a/app/portainer/services/api/endpointService.js b/app/portainer/services/api/endpointService.js index e61ebbfc4..9ee3db98d 100644 --- a/app/portainer/services/api/endpointService.js +++ b/app/portainer/services/api/endpointService.js @@ -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) { diff --git a/app/portainer/services/api/groupService.js b/app/portainer/services/api/groupService.js index b81342b7e..073b4d122 100644 --- a/app/portainer/services/api/groupService.js +++ b/app/portainer/services/api/groupService.js @@ -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; }; diff --git a/app/portainer/views/auth/authController.js b/app/portainer/views/auth/authController.js index 9e239636f..120de69af 100644 --- a/app/portainer/views/auth/authController.js +++ b/app/portainer/views/auth/authController.js @@ -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'); diff --git a/app/portainer/views/endpoints/endpoints.html b/app/portainer/views/endpoints/endpoints.html index 31591fcdc..3f9564b27 100644 --- a/app/portainer/views/endpoints/endpoints.html +++ b/app/portainer/views/endpoints/endpoints.html @@ -26,11 +26,12 @@
diff --git a/app/portainer/views/endpoints/endpointsController.js b/app/portainer/views/endpoints/endpointsController.js index b47d9afba..3b2313700 100644 --- a/app/portainer/views/endpoints/endpointsController.js +++ b/app/portainer/views/endpoints/endpointsController.js @@ -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(); }]); diff --git a/app/portainer/views/groups/create/createGroupController.js b/app/portainer/views/groups/create/createGroupController.js index b825abfe4..0f1e86ad3 100644 --- a/app/portainer/views/groups/create/createGroupController.js +++ b/app/portainer/views/groups/create/createGroupController.js @@ -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'); }); } diff --git a/app/portainer/views/groups/create/creategroup.html b/app/portainer/views/groups/create/creategroup.html index 36334979c..480d04f81 100644 --- a/app/portainer/views/groups/create/creategroup.html +++ b/app/portainer/views/groups/create/creategroup.html @@ -10,6 +10,8 @@ -
+
diff --git a/app/portainer/views/home/homeController.js b/app/portainer/views/home/homeController.js index ee890a937..f43a9ffc5 100644 --- a/app/portainer/views/home/homeController.js +++ b/app/portainer/views/home/homeController.js @@ -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(); diff --git a/app/portainer/views/init/admin/initAdminController.js b/app/portainer/views/init/admin/initAdminController.js index bc7f66c0e..6cbf202f7 100644 --- a/app/portainer/views/init/admin/initAdminController.js +++ b/app/portainer/views/init/admin/initAdminController.js @@ -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'); diff --git a/app/portainer/views/schedules/create/createScheduleController.js b/app/portainer/views/schedules/create/createScheduleController.js index 770981511..6b6b34734 100644 --- a/app/portainer/views/schedules/create/createScheduleController.js +++ b/app/portainer/views/schedules/create/createScheduleController.js @@ -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) { diff --git a/app/portainer/views/schedules/edit/scheduleController.js b/app/portainer/views/schedules/edit/scheduleController.js index 06313f3ab..bfb87deeb 100644 --- a/app/portainer/views/schedules/edit/scheduleController.js +++ b/app/portainer/views/schedules/edit/scheduleController.js @@ -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) { diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index 3185fcdda..fa6f1218f 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -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;
{{ item.Name }}
{{ item.Name }}
Loading...
{{ $ctrl.emptyDatasetMessage }}