Enable endpoint backend pagination (#2989)

* feat(api): remove SnapshotRaw from EndpointList response

* feat(api): add pagination for EndpointList operation

* feat(api): rename last_id query parameter to start

* feat(api): implement filter for EndpointList operation

* feat(home): front - endpoint backend pagination (#2990)

* feat(home): endpoint pagination with backend

* feat(api): remove default limit value

* fix(endpoints): fix a minor issue with column span

* fix(endpointgroup-create): fix an issue with endpoint group creation

* feat(app): minor loading optimizations

* refactor(api): small refactor of EndpointList operation

* fix(home): fix minor loading text display issue

* refactor(api): document bolt services functions

* feat(home): minor optimization

* fix(api): replace seek with index scanning for EndpointPaginated

* fix(api): fix invalid starting index issue

* fix(api): first implementation of working filter

* fix(home): endpoints list keeps backend pagination when it needs to

* fix(api): endpoint pagination doesn't drop the first item on pages >=2 anymore

* fix(home): UI flickering on page/filter load/change

* feat(api): support searching in associated endpoint group data

* feat(api): declare EndpointList params as optional

* feat(endpoints): backend pagination for endpoints view (#3004)

* feat(endpoint-group): enable backend pagination (#3017)

* feat(api): support groupID filter on endpoints route

* feat(api): add new API operations endpointGroupAddEndpoint and endpointGroupDeleteEndpoint

* feat(endpoint-groups): backend pagination support for create and edit

* feat(endpoint-groups): debounce on filter for create/edit views

* feat(endpoint-groups): filter assigned on create view

* (endpoint-groups): unassigned endpoints edit view

* refactor(endpoint-groups): code clean

* feat(endpoint-groups): remove message for Unassigned group

* refactor(api): endpoint group endpoint association refactor

* refactor(api): rename files and remove comments

* refactor(api): remove usage of utils

* refactor(api): optional parameters

* feat(api): update endpointListOperation behavior and parameters

* refactor(api): remove unused methods associated to EndpointService

* refactor(api): remove unused methods associated to EndpointService

* refactor(api): minor refactor
pull/2763/head^2
Anthony Lapenna 2019-07-20 16:28:11 -07:00 committed by GitHub
parent d52a1a870c
commit 90d3f3a358
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 681 additions and 243 deletions

View File

@ -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 (

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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.

View File

@ -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"
)

View File

@ -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 = {};

View File

@ -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>

View File

@ -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: '<'
}
});

View File

@ -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();
};
}
]);

View File

@ -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();
}
}
}

View File

@ -10,6 +10,8 @@ angular.module('portainer.app').component('endpointList', {
snapshotAction: '<',
showSnapshotAction: '<',
editAction: '<',
isAdmin:'<'
isAdmin:'<',
totalCount: '<',
retrievePage: '<'
}
});

View File

@ -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>

View File

@ -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: '<',

View File

@ -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 -->

View File

@ -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: '@'

View File

@ -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>

View File

@ -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' } },

View File

@ -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'} }
});
}]);

View File

@ -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;
}

View File

@ -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) {

View File

@ -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;
};

View File

@ -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');

View File

@ -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>

View File

@ -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();
}]);

View File

@ -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');
});
}

View File

@ -10,6 +10,8 @@
<rd-widget>
<rd-widget-body>
<group-form
loaded="loaded"
page-type="create"
model="model"
available-endpoints="availableEndpoints"
available-tags="availableTags"

View File

@ -10,6 +10,8 @@
<rd-widget>
<rd-widget-body>
<group-form
loaded="loaded"
page-type="edit"
model="group"
available-endpoints="availableEndpoints"
available-tags="availableTags"

View File

@ -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');
});
}

View File

@ -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>

View File

@ -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();

View File

@ -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');

View File

@ -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) {

View File

@ -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) {

View File

@ -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;