mirror of
https://github.com/portainer/portainer.git
synced 2025-08-08 23:35:31 +02:00
feat(endpoint-groups): add endpoint-groups (#1837)
This commit is contained in:
parent
2ffcb946b1
commit
1162549209
58 changed files with 1838 additions and 265 deletions
|
@ -132,6 +132,50 @@ angular.module('portainer.app', [])
|
|||
}
|
||||
};
|
||||
|
||||
var groups = {
|
||||
name: 'portainer.groups',
|
||||
url: '/groups',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/groups/groups.html',
|
||||
controller: 'GroupsController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var group = {
|
||||
name: 'portainer.groups.group',
|
||||
url: '/:id',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/groups/edit/group.html',
|
||||
controller: 'GroupController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var groupCreation = {
|
||||
name: 'portainer.groups.new',
|
||||
url: '/new',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/groups/create/creategroup.html',
|
||||
controller: 'CreateGroupController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var groupAccess = {
|
||||
name: 'portainer.groups.group.access',
|
||||
url: '/access',
|
||||
views: {
|
||||
'content@': {
|
||||
templateUrl: 'app/portainer/views/groups/access/groupAccess.html',
|
||||
controller: 'GroupAccessController'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var registries = {
|
||||
name: 'portainer.registries',
|
||||
url: '/registries',
|
||||
|
@ -253,6 +297,10 @@ angular.module('portainer.app', [])
|
|||
$stateRegistryProvider.register(endpoints);
|
||||
$stateRegistryProvider.register(endpoint);
|
||||
$stateRegistryProvider.register(endpointAccess);
|
||||
$stateRegistryProvider.register(groups);
|
||||
$stateRegistryProvider.register(group);
|
||||
$stateRegistryProvider.register(groupAccess);
|
||||
$stateRegistryProvider.register(groupCreation);
|
||||
$stateRegistryProvider.register(registries);
|
||||
$stateRegistryProvider.register(registry);
|
||||
$stateRegistryProvider.register(registryAccess);
|
||||
|
|
21
app/portainer/components/access-table/access-table.js
Normal file
21
app/portainer/components/access-table/access-table.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
angular.module('portainer.app').component('accessTable', {
|
||||
templateUrl: 'app/portainer/components/access-table/accessTable.html',
|
||||
controller: function() {
|
||||
this.state = {
|
||||
orderBy: 'Name',
|
||||
reverseOrder: false,
|
||||
paginatedItemLimit: '10',
|
||||
textFilter: ''
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
};
|
||||
},
|
||||
bindings: {
|
||||
dataset: '<',
|
||||
entryClick: '<',
|
||||
emptyDatasetMessage: '@'
|
||||
}
|
||||
});
|
64
app/portainer/components/access-table/accessTable.html
Normal file
64
app/portainer/components/access-table/accessTable.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
<div class="datatable">
|
||||
<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" placeholder="Search...">
|
||||
</div>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<span ng-show="$ctrl.state.orderBy == 'Name' && !$ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.orderBy == 'Name' && $ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Type')">
|
||||
Type
|
||||
<span ng-show="$ctrl.state.orderBy == 'Type' && !$ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.orderBy == 'Type' && $ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-click="!item.Inherited && $ctrl.entryClick(item)" ng-class="{ 'interactive': !item.Inherited }" dir-paginate="item in $ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit">
|
||||
<td>
|
||||
{{ item.Name }}
|
||||
<!-- <span class="image-tag label label-">inherited</span> -->
|
||||
<span ng-if="item.Inherited" class="text-muted small" style="margin-left: 2px;"><code style="font-size: 85% !important;">inherited</code></span>
|
||||
</td>
|
||||
<td>
|
||||
<i class="fa" ng-class="item.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ item.Type }}
|
||||
</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">
|
||||
<td colspan="2" class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select ng-model="$ctrl.state.paginatedItemLimit">
|
||||
<option value="0">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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -3,6 +3,8 @@ angular.module('portainer.app').component('porAccessManagement', {
|
|||
controller: 'porAccessManagementController',
|
||||
bindings: {
|
||||
accessControlledEntity: '<',
|
||||
inheritFrom: '<',
|
||||
entityType: '@',
|
||||
updateAccess: '&'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,134 +1,46 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Users and teams">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="$ctrl.state.pagination_count_accesses" ng-change="$ctrl.changePaginationCountAccesses()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.authorizeAllAccesses()" ng-disabled="$ctrl.accesses.length === 0 || $ctrl.filteredUsers.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter" ng-model="$ctrl.state.filterUsers" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.orderAccesses('Name')">
|
||||
Name
|
||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Name' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.orderAccesses('Type')">
|
||||
Type
|
||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && !$ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.sortAccessesBy == 'Type' && $ctrl.state.sortAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-click="$ctrl.authorizeAccess(user)" class="interactive" dir-paginate="user in $ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses">
|
||||
<td>{{ user.Name }}</td>
|
||||
<td>
|
||||
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ user.Type }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.accesses">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.accesses.length === 0 || ($ctrl.accesses | filter:$ctrl.state.filterUsers | orderBy:$ctrl.state.sortAccessesBy:$ctrl.state.sortAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_accesses).length === 0">
|
||||
<td colspan="2" class="text-center text-muted">No user or team available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="$ctrl.accesses" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-users" title="Access management"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="small text-muted">
|
||||
<p>You can select which user or team can access this {{ $ctrl.entityType }} by moving them to the authorized accesses table. Simply click
|
||||
on a user or team entry to move it from one table to the other.</p>
|
||||
<p ng-if="$ctrl.inheritFrom">
|
||||
<b>Note</b>: accesses tagged as <code>inherited</code> are inherited from the group accesses and cannot be remove at the endpoint level.
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<!-- available-endpoints -->
|
||||
<div class="col-sm-6">
|
||||
<div class="text-center small text-muted">Users and teams</div>
|
||||
<div class="text-center small text-muted" style="margin-top: 5px;">
|
||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.authorizeAllAccesses()" ng-disabled="$ctrl.accesses.length === 0"><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Authorize all</button>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<access-table
|
||||
dataset="$ctrl.accesses"
|
||||
entry-click="$ctrl.authorizeAccess"
|
||||
empty-dataset-message="No user or team available"
|
||||
></access-table>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
<rd-widget-header classes="col-sm-12 col-md-6 nopadding" icon="fa-users" title="Authorized users and teams">
|
||||
<div class="pull-md-right pull-lg-right">
|
||||
Items per page:
|
||||
<select ng-model="$ctrl.state.pagination_count_authorizedAccesses" ng-change="$ctrl.changePaginationCountAuthorizedAccesses()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-taskbar classes="col-sm-12 nopadding">
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.unauthorizeAllAccesses()" ng-disabled="$ctrl.authorizedAccesses.length === 0 || $ctrl.filteredAuthorizedUsers.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-6 nopadding">
|
||||
<input type="text" id="filter" ng-model="$ctrl.state.filterAuthorizedUsers" placeholder="Filter..." class="form-control input-sm" />
|
||||
</div>
|
||||
</rd-widget-taskbar>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.orderAuthorizedAccesses('Name')">
|
||||
Name
|
||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Name' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.orderAuthorizedAccesses('Type')">
|
||||
Type
|
||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && !$ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.sortAuthorizedAccessesBy == 'Type' && $ctrl.state.sortAuthorizedAccessesReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-click="$ctrl.unauthorizeAccess(user)" class="interactive" pagination-id="table_authaccess" dir-paginate="user in $ctrl.authorizedAccesses | filter:$ctrl.state.filterAuthorizedUsers | orderBy:$ctrl.state.sortAuthorizedAccessesBy:$ctrl.state.sortAuthorizedAccessesReverse | itemsPerPage: $ctrl.state.pagination_count_authorizedAccesses">
|
||||
<td>{{ user.Name }}</td>
|
||||
<td>
|
||||
<i class="fa" ng-class="user.Type === 'user' ? 'fa-user' : 'fa-users'" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
{{ user.Type }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.authorizedAccesses">
|
||||
<td colspan="2" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.authorizedAccesses.length === 0 || (authorizedAccesses | filter:state.filterAuthorizedUsers | orderBy:sortTypeAuthorizedAccesses:sortReverseAuthorizedAccesses | itemsPerPage: state.pagination_count_authorizedAccesses).length === 0">
|
||||
<td colspan="2" class="text-center text-muted">No authorized user or team.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="$ctrl.authorizedAccesses" class="pull-left pagination-controls">
|
||||
<dir-pagination-controls pagination-id="table_authaccess"></dir-pagination-controls>
|
||||
<!-- !available-endpoints -->
|
||||
<!-- associated-endpoints -->
|
||||
<div class="col-sm-6">
|
||||
<div class="text-center small text-muted">Authorized users and teams</div>
|
||||
<div class="text-center small text-muted" style="margin-top: 5px;">
|
||||
<button class="btn btn-primary btn-sm" ng-click="$ctrl.unauthorizeAllAccesses()" ng-disabled="$ctrl.authorizedAccesses.length === 0"><i class="fa fa-user-times space-right" aria-hidden="true"></i>Deny all</button>
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<access-table
|
||||
dataset="$ctrl.authorizedAccesses"
|
||||
entry-click="$ctrl.unauthorizeAccess"
|
||||
empty-dataset-message="No authorized user or team"
|
||||
></access-table>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !associated-endpoints -->
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -1,40 +1,13 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('porAccessManagementController', ['AccessService', 'PaginationService', 'Notifications',
|
||||
function (AccessService, PaginationService, Notifications) {
|
||||
.controller('porAccessManagementController', ['AccessService', 'Notifications',
|
||||
function (AccessService, Notifications) {
|
||||
var ctrl = this;
|
||||
|
||||
ctrl.state = {
|
||||
pagination_count_accesses: PaginationService.getPaginationLimit('access_management_accesses'),
|
||||
pagination_count_authorizedAccesses: PaginationService.getPaginationLimit('access_management_AuthorizedAccesses'),
|
||||
sortAccessesBy: 'Type',
|
||||
sortAccessesReverse: false,
|
||||
sortAuthorizedAccessesBy: 'Type',
|
||||
sortAuthorizedAccessesReverse: false
|
||||
};
|
||||
|
||||
ctrl.orderAccesses = function(sortBy) {
|
||||
ctrl.state.sortAccessesReverse = (ctrl.state.sortAccessesBy === sortBy) ? !ctrl.state.sortAccessesReverse : false;
|
||||
ctrl.state.sortAccessesBy = sortBy;
|
||||
};
|
||||
|
||||
ctrl.orderAuthorizedAccesses = function(sortBy) {
|
||||
ctrl.state.sortAuthorizedAccessesReverse = (ctrl.state.sortAuthorizedAccessesBy === sortBy) ? !ctrl.state.sortAuthorizedAccessesReverse : false;
|
||||
ctrl.state.sortAuthorizedAccessesBy = sortBy;
|
||||
};
|
||||
|
||||
ctrl.changePaginationCountAuthorizedAccesses = function() {
|
||||
PaginationService.setPaginationLimit('access_management_AuthorizedAccesses', ctrl.state.pagination_count_authorizedAccesses);
|
||||
};
|
||||
|
||||
ctrl.changePaginationCountAccesses = function() {
|
||||
PaginationService.setPaginationLimit('access_management_accesses', ctrl.state.pagination_count_accesses);
|
||||
};
|
||||
|
||||
function dispatchUserAndTeamIDs(accesses, users, teams) {
|
||||
angular.forEach(accesses, function (access) {
|
||||
if (access.Type === 'user') {
|
||||
if (access.Type === 'user' && !access.Inherited) {
|
||||
users.push(access.Id);
|
||||
} else if (access.Type === 'team') {
|
||||
} else if (access.Type === 'team' && !access.Inherited) {
|
||||
teams.push(access.Id);
|
||||
}
|
||||
});
|
||||
|
@ -111,11 +84,20 @@ function (AccessService, PaginationService, Notifications) {
|
|||
});
|
||||
};
|
||||
|
||||
function moveAccesses(source, target) {
|
||||
for (var i = 0; i < source.length; i++) {
|
||||
var access = source[i];
|
||||
if (!access.Inherited) {
|
||||
target.push(access);
|
||||
source.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctrl.unauthorizeAllAccesses = function() {
|
||||
ctrl.updateAccess({ userAccesses: [], teamAccesses: [] })
|
||||
.then(function success(data) {
|
||||
ctrl.accesses = ctrl.accesses.concat(ctrl.authorizedAccesses);
|
||||
ctrl.authorizedAccesses = [];
|
||||
moveAccesses(ctrl.authorizedAccesses, ctrl.accesses);
|
||||
Notifications.success('Accesses successfully updated');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -130,8 +112,7 @@ function (AccessService, PaginationService, Notifications) {
|
|||
|
||||
ctrl.updateAccess({ userAccesses: authorizedUserIDs, teamAccesses: authorizedTeamIDs })
|
||||
.then(function success(data) {
|
||||
ctrl.authorizedAccesses = ctrl.authorizedAccesses.concat(ctrl.accesses);
|
||||
ctrl.accesses = [];
|
||||
moveAccesses(ctrl.accesses, ctrl.authorizedAccesses);
|
||||
Notifications.success('Accesses successfully updated');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
@ -141,7 +122,8 @@ function (AccessService, PaginationService, Notifications) {
|
|||
|
||||
function initComponent() {
|
||||
var entity = ctrl.accessControlledEntity;
|
||||
AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams)
|
||||
var parent = ctrl.inheritFrom;
|
||||
AccessService.accesses(entity.AuthorizedUsers, entity.AuthorizedTeams, parent ? parent.AuthorizedUsers: [], parent ? parent.AuthorizedTeams : [])
|
||||
.then(function success(data) {
|
||||
ctrl.accesses = data.accesses;
|
||||
ctrl.authorizedAccesses = data.authorizedAccesses;
|
||||
|
|
|
@ -43,6 +43,13 @@
|
|||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'URL' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('GroupName')">
|
||||
Group
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'GroupName' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -57,6 +64,7 @@
|
|||
<span ng-if="!$ctrl.endpointManagement">{{ item.Name }}</span>
|
||||
</td>
|
||||
<td>{{ item.URL | stripprotocol }}</td>
|
||||
<td>{{ item.GroupName }}</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.endpoints.endpoint.access({id: item.Id})" ng-if="$ctrl.accessManagement">
|
||||
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
<div class="datatable">
|
||||
<rd-widget>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<div class="toolBar">
|
||||
<div class="toolBarTitle">
|
||||
<i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px;"></i> {{ $ctrl.title }}
|
||||
</div>
|
||||
<div class="settings">
|
||||
<span class="setting" ng-class="{ 'setting-active': $ctrl.state.displayTextFilter }" ng-click="$ctrl.updateDisplayTextFilter()" ng-if="$ctrl.showTextFilter">
|
||||
<i class="fa fa-search" aria-hidden="true"></i> Search
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<button type="button" class="btn btn-sm btn-danger"
|
||||
ng-disabled="$ctrl.state.selectedItemCount === 0" ng-click="$ctrl.removeAction($ctrl.state.selectedItems)">
|
||||
<i class="fa fa-trash-alt space-right" aria-hidden="true"></i>Remove
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" ui-sref="portainer.groups.new">
|
||||
<i class="fa fa-plus space-right" aria-hidden="true"></i>Add group
|
||||
</button>
|
||||
</div>
|
||||
<div class="searchBar" ng-if="$ctrl.state.displayTextFilter">
|
||||
<i class="fa fa-search searchIcon" aria-hidden="true"></i>
|
||||
<input type="text" class="searchInput" ng-model="$ctrl.state.textFilter" placeholder="Search..." auto-focus>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<i class="fa fa-sort-alpha-down" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && !$ctrl.state.reverseOrder"></i>
|
||||
<i class="fa fa-sort-alpha-up" aria-hidden="true" ng-if="$ctrl.state.orderBy === 'Name' && $ctrl.state.reverseOrder"></i>
|
||||
</a>
|
||||
</th>
|
||||
<th>Actions</th>
|
||||
</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}">
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-change="$ctrl.selectItem(item)"/>
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="portainer.groups.group({id: item.Id})">{{ item.Name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.groups.group.access({id: item.Id})" ng-if="$ctrl.accessManagement">
|
||||
<i class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.dataset">
|
||||
<td colspan="3" 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 group available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0">
|
||||
{{ $ctrl.state.selectedItemCount }} item(s) selected
|
||||
</div>
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()">
|
||||
<option value="0">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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
|
@ -0,0 +1,15 @@
|
|||
angular.module('portainer.app').component('groupsDatatable', {
|
||||
templateUrl: 'app/portainer/components/datatables/groups-datatable/groupsDatatable.html',
|
||||
controller: 'GenericDatatableController',
|
||||
bindings: {
|
||||
title: '@',
|
||||
titleIcon: '@',
|
||||
dataset: '<',
|
||||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
showTextFilter: '<',
|
||||
accessManagement: '<',
|
||||
removeAction: '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,9 @@
|
|||
angular.module('portainer.app').component('endpointSelector', {
|
||||
templateUrl: 'app/portainer/components/endpoint-selector/endpointSelector.html',
|
||||
controller: 'EndpointSelectorController',
|
||||
bindings: {
|
||||
'endpoints': '<',
|
||||
'groups': '<',
|
||||
'selectEndpoint': '<'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
<div ng-if="$ctrl.endpoints.length > 1">
|
||||
<div ng-if="!$ctrl.state.show">
|
||||
<li class="sidebar-title">
|
||||
<span class="interactive" style="color: #fff;" ng-click="$ctrl.state.show = true;">
|
||||
<span class="fa fa-plug space-right"></span>Change environment
|
||||
</span>
|
||||
</li>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state.show">
|
||||
<div ng-if="$ctrl.availableGroups.length > 1">
|
||||
<li class="sidebar-title"><span>Group</span></li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="group.Name for group in $ctrl.availableGroups" ng-model="$ctrl.state.selectedGroup" ng-change="$ctrl.selectGroup()">
|
||||
<option value="">Select a group</option>
|
||||
</select>
|
||||
</li>
|
||||
</div>
|
||||
<div ng-if="$ctrl.state.selectedGroup || $ctrl.availableGroups.length <= 1">
|
||||
<li class="sidebar-title"><span>Endpoint</span></li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in $ctrl.availableEndpoints" ng-model="$ctrl.state.selectedEndpoint" ng-change="$ctrl.selectEndpoint($ctrl.state.selectedEndpoint)">
|
||||
<option value="">Select an endpoint</option>
|
||||
</select>
|
||||
</li>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,34 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('EndpointSelectorController', function () {
|
||||
var ctrl = this;
|
||||
|
||||
this.state = {
|
||||
show: false,
|
||||
selectedGroup: null,
|
||||
selectedEndpoint: null
|
||||
};
|
||||
|
||||
this.selectGroup = function() {
|
||||
this.availableEndpoints = this.endpoints.filter(function f(endpoint) {
|
||||
return endpoint.GroupId === ctrl.state.selectedGroup.Id;
|
||||
});
|
||||
};
|
||||
|
||||
this.$onInit = function() {
|
||||
this.availableGroups = filterEmptyGroups(this.groups, this.endpoints);
|
||||
this.availableEndpoints = this.endpoints;
|
||||
};
|
||||
|
||||
function filterEmptyGroups(groups, endpoints) {
|
||||
return groups.filter(function f(group) {
|
||||
for (var i = 0; i < endpoints.length; i++) {
|
||||
|
||||
var endpoint = endpoints[i];
|
||||
if (endpoint.GroupId === group.Id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
});
|
31
app/portainer/components/forms/group-form/group-form.js
Normal file
31
app/portainer/components/forms/group-form/group-form.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
angular.module('portainer.app').component('groupForm', {
|
||||
templateUrl: 'app/portainer/components/forms/group-form/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;
|
||||
});
|
||||
};
|
||||
|
||||
},
|
||||
bindings: {
|
||||
model: '=',
|
||||
availableEndpoints: '=',
|
||||
associatedEndpoints: '=',
|
||||
addLabelAction: '<',
|
||||
removeLabelAction: '<',
|
||||
formAction: '<',
|
||||
formActionLabel: '@',
|
||||
actionInProgress: '<'
|
||||
}
|
||||
});
|
120
app/portainer/components/forms/group-form/groupForm.html
Normal file
120
app/portainer/components/forms/group-form/groupForm.html
Normal file
|
@ -0,0 +1,120 @@
|
|||
<form class="form-horizontal" name="endpointGroupForm">
|
||||
<!-- name-input -->
|
||||
<div class="form-group" ng-class="{ 'has-error': endpointGroupForm.group_name.$invalid }">
|
||||
<label for="group_name" class="col-sm-3 col-lg-2 control-label text-left">Name</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" name="group_name" ng-model="$ctrl.model.Name" placeholder="e.g. my-group" required auto-focus>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-show="endpointGroupForm.group_name.$invalid">
|
||||
<div class="col-sm-12 small text-danger">
|
||||
<div ng-messages="endpointGroupForm.group_name.$error">
|
||||
<p ng-message="required"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> This field is required.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !name-input -->
|
||||
<!-- description-input -->
|
||||
<div class="form-group">
|
||||
<label for="group_description" class="col-sm-3 col-lg-2 control-label text-left">Description</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<input type="text" class="form-control" id="group_description" ng-model="$ctrl.model.Description" placeholder="e.g. production environments...">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !description-input -->
|
||||
<!-- labels -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12" style="margin-top: 5px;">
|
||||
<label class="control-label text-left">Labels</label>
|
||||
<span class="label label-default interactive" style="margin-left: 10px;" ng-click="$ctrl.addLabelAction()">
|
||||
<i class="fa fa-plus-circle" aria-hidden="true"></i> add label
|
||||
</span>
|
||||
</div>
|
||||
<!-- labels-input-list -->
|
||||
<div class="col-sm-12 form-inline" style="margin-top: 10px;">
|
||||
<div ng-repeat="label in $ctrl.model.Labels" style="margin-top: 2px;">
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">name</span>
|
||||
<input type="text" class="form-control" ng-model="label.name" placeholder="e.g. organization">
|
||||
</div>
|
||||
<div class="input-group col-sm-5 input-group-sm">
|
||||
<span class="input-group-addon">value</span>
|
||||
<input type="text" class="form-control" ng-model="label.value" placeholder="e.g. acme">
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="button" ng-click="$ctrl.removeLabelAction($index)">
|
||||
<i class="fa fa-trash" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !labels-input-list -->
|
||||
</div>
|
||||
<!-- !labels -->
|
||||
<!-- endpoints -->
|
||||
<div ng-if="$ctrl.model.Id !== 1">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Associated endpoints
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 small text-muted">
|
||||
You can select which endpoint should be part of this group by moving them to the associated endpoints table. Simply click
|
||||
on any endpoint entry to move it from one table to the other.
|
||||
</div>
|
||||
<div class="col-sm-12" style="margin-top: 20px;">
|
||||
<!-- available-endpoints -->
|
||||
<div class="col-sm-6">
|
||||
<div class="text-center small text-muted">Available endpoints</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<group-association-table
|
||||
dataset="$ctrl.availableEndpoints"
|
||||
entry-click="$ctrl.associateEndpoint"
|
||||
empty-dataset-message="No endpoint available"
|
||||
></group-association-table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !available-endpoints -->
|
||||
<!-- associated-endpoints -->
|
||||
<div class="col-sm-6">
|
||||
<div class="text-center small text-muted">Associated endpoints</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<group-association-table
|
||||
dataset="$ctrl.associatedEndpoints"
|
||||
entry-click="$ctrl.dissociateEndpoint"
|
||||
empty-dataset-message="No associated endpoint"
|
||||
></group-association-table>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !associated-endpoints -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="$ctrl.model.Id === 1">
|
||||
<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">
|
||||
<span class="text-muted small">All the endpoints are assigned to a group.</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !endpoints -->
|
||||
<!-- actions -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Actions
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="$ctrl.formAction()" ng-disabled="$ctrl.actionInProgress || !endpointGroupForm.$valid" button-spinner="$ctrl.actionInProgress">
|
||||
<span ng-hide="$ctrl.actionInProgress">{{ $ctrl.formActionLabel }}</span>
|
||||
<span ng-show="$ctrl.actionInProgress">In progress...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !actions -->
|
||||
</form>
|
|
@ -0,0 +1,21 @@
|
|||
angular.module('portainer.app').component('groupAssociationTable', {
|
||||
templateUrl: 'app/portainer/components/group-association-table/groupAssociationTable.html',
|
||||
controller: function() {
|
||||
this.state = {
|
||||
orderBy: 'Name',
|
||||
reverseOrder: false,
|
||||
paginatedItemLimit: '10',
|
||||
textFilter: ''
|
||||
};
|
||||
|
||||
this.changeOrderBy = function(orderField) {
|
||||
this.state.reverseOrder = this.state.orderBy === orderField ? !this.state.reverseOrder : false;
|
||||
this.state.orderBy = orderField;
|
||||
};
|
||||
},
|
||||
bindings: {
|
||||
dataset: '<',
|
||||
entryClick: '<',
|
||||
emptyDatasetMessage: '@'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
<div class="datatable">
|
||||
<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" placeholder="Search...">
|
||||
</div>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<a ng-click="$ctrl.changeOrderBy('Name')">
|
||||
Name
|
||||
<span ng-show="$ctrl.state.orderBy == 'Name' && !$ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="$ctrl.state.orderBy == 'Name' && $ctrl.state.reverseOrder" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</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">
|
||||
<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">
|
||||
<td colspan="2" class="text-center text-muted">{{ $ctrl.emptyDatasetMessage }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="footer" ng-if="$ctrl.dataset">
|
||||
<div class="paginationControls">
|
||||
<form class="form-inline">
|
||||
<span class="limitSelector">
|
||||
<span style="margin-right: 5px;">
|
||||
Items per page
|
||||
</span>
|
||||
<select ng-model="$ctrl.state.paginatedItemLimit">
|
||||
<option value="0">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>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
23
app/portainer/helpers/endpointHelper.js
Normal file
23
app/portainer/helpers/endpointHelper.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('EndpointHelper', [function EndpointHelperFactory() {
|
||||
'use strict';
|
||||
var helper = {};
|
||||
|
||||
function findAssociatedGroup(endpoint, groups) {
|
||||
return _.find(groups, function(group) {
|
||||
return group.Id === endpoint.GroupId;
|
||||
});
|
||||
}
|
||||
|
||||
helper.mapGroupNameToEndpoint = function(endpoints, groups) {
|
||||
for (var i = 0; i < endpoints.length; i++) {
|
||||
var endpoint = endpoints[i];
|
||||
var group = findAssociatedGroup(endpoint, groups);
|
||||
if (group) {
|
||||
endpoint.GroupName = group.Name;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return helper;
|
||||
}]);
|
|
@ -2,10 +2,12 @@ function UserAccessViewModel(data) {
|
|||
this.Id = data.Id;
|
||||
this.Name = data.Username;
|
||||
this.Type = 'user';
|
||||
this.Inherited = false;
|
||||
}
|
||||
|
||||
function TeamAccessViewModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
this.Type = 'team';
|
||||
this.Inherited = false;
|
||||
}
|
||||
|
|
29
app/portainer/models/group.js
Normal file
29
app/portainer/models/group.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
function EndpointGroupDefaultModel() {
|
||||
this.Name = '';
|
||||
this.Description = '';
|
||||
this.Labels = [];
|
||||
}
|
||||
|
||||
function EndpointGroupModel(data) {
|
||||
this.Id = data.Id;
|
||||
this.Name = data.Name;
|
||||
this.Description = data.Description;
|
||||
this.Labels = data.Labels;
|
||||
this.AuthorizedUsers = data.AuthorizedUsers;
|
||||
this.AuthorizedTeams = data.AuthorizedTeams;
|
||||
}
|
||||
|
||||
function EndpointGroupCreateRequest(model, endpoints) {
|
||||
this.Name = model.Name;
|
||||
this.Description = model.Description;
|
||||
this.Labels = model.Labels;
|
||||
this.AssociatedEndpoints = endpoints;
|
||||
}
|
||||
|
||||
function EndpointGroupUpdateRequest(model, endpoints) {
|
||||
this.id = model.Id;
|
||||
this.Name = model.Name;
|
||||
this.Description = model.Description;
|
||||
this.Labels = model.Labels;
|
||||
this.AssociatedEndpoints = endpoints;
|
||||
}
|
12
app/portainer/rest/group.js
Normal file
12
app/portainer/rest/group.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('EndpointGroups', ['$resource', 'API_ENDPOINT_ENDPOINT_GROUPS', function EndpointGroupsFactory($resource, API_ENDPOINT_ENDPOINT_GROUPS) {
|
||||
'use strict';
|
||||
return $resource(API_ENDPOINT_ENDPOINT_GROUPS + '/:id/:action', {}, {
|
||||
create: { method: 'POST', ignoreLoadingBar: true },
|
||||
query: { method: 'GET', isArray: true },
|
||||
get: { method: 'GET', params: { id: '@id' } },
|
||||
update: { method: 'PUT', params: { id: '@id' } },
|
||||
updateAccess: { method: 'PUT', params: { id: '@id', action: 'access' } },
|
||||
remove: { method: 'DELETE', params: { id: '@id'} }
|
||||
});
|
||||
}]);
|
|
@ -3,33 +3,30 @@ angular.module('portainer.app')
|
|||
'use strict';
|
||||
var service = {};
|
||||
|
||||
function mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs) {
|
||||
var accesses = [];
|
||||
function mapAccessData(accesses, authorizedIDs, inheritedIDs) {
|
||||
var availableAccesses = [];
|
||||
var authorizedAccesses = [];
|
||||
|
||||
angular.forEach(userAccesses, function(access) {
|
||||
if (_.includes(authorizedUserIDs, access.Id)) {
|
||||
authorizedAccesses.push(access);
|
||||
} else {
|
||||
accesses.push(access);
|
||||
}
|
||||
});
|
||||
for (var i = 0; i < accesses.length; i++) {
|
||||
|
||||
angular.forEach(teamAccesses, function(access) {
|
||||
if (_.includes(authorizedTeamIDs, access.Id)) {
|
||||
var access = accesses[i];
|
||||
if (_.includes(inheritedIDs, access.Id)) {
|
||||
access.Inherited = true;
|
||||
authorizedAccesses.push(access);
|
||||
} else if (_.includes(authorizedIDs, access.Id)) {
|
||||
authorizedAccesses.push(access);
|
||||
} else {
|
||||
accesses.push(access);
|
||||
availableAccesses.push(access);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
accesses: accesses,
|
||||
accesses: availableAccesses,
|
||||
authorizedAccesses: authorizedAccesses
|
||||
};
|
||||
}
|
||||
|
||||
service.accesses = function(authorizedUserIDs, authorizedTeamIDs) {
|
||||
service.accesses = function(authorizedUserIDs, authorizedTeamIDs, inheritedUserIDs, inheritedTeamIDs) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
$q.all({
|
||||
|
@ -44,7 +41,14 @@ angular.module('portainer.app')
|
|||
return new TeamAccessViewModel(team);
|
||||
});
|
||||
|
||||
var accessData = mapAccessDataFromAuthorizedIDs(userAccesses, teamAccesses, authorizedUserIDs, authorizedTeamIDs);
|
||||
var userAccessData = mapAccessData(userAccesses, authorizedUserIDs, inheritedUserIDs);
|
||||
var teamAccessData = mapAccessData(teamAccesses, authorizedTeamIDs, inheritedTeamIDs);
|
||||
|
||||
var accessData = {
|
||||
accesses: userAccessData.accesses.concat(teamAccessData.accesses),
|
||||
authorizedAccesses: userAccessData.authorizedAccesses.concat(teamAccessData.authorizedAccesses)
|
||||
};
|
||||
|
||||
deferred.resolve(accessData);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
|
|
|
@ -12,6 +12,23 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return Endpoints.query({}).$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.updateAccess = function(id, authorizedUserIDs, authorizedTeamIDs) {
|
||||
return Endpoints.updateAccess({id: id}, {authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs}).$promise;
|
||||
};
|
||||
|
@ -20,6 +37,7 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
var query = {
|
||||
name: endpointParams.name,
|
||||
PublicURL: endpointParams.PublicURL,
|
||||
GroupId: endpointParams.GroupId,
|
||||
TLS: endpointParams.TLS,
|
||||
TLSSkipVerify: endpointParams.TLSSkipVerify,
|
||||
TLSSkipClientVerify: endpointParams.TLSSkipClientVerify,
|
||||
|
@ -49,10 +67,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return Endpoints.remove({id: endpointID}).$promise;
|
||||
};
|
||||
|
||||
service.createLocalEndpoint = function(name, URL, TLS, active) {
|
||||
service.createLocalEndpoint = function() {
|
||||
var deferred = $q.defer();
|
||||
|
||||
FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', false)
|
||||
FileUploadService.createEndpoint('local', 'unix:///var/run/docker.sock', '', 1, false)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
|
@ -63,10 +81,10 @@ function EndpointServiceFactory($q, Endpoints, FileUploadService) {
|
|||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.createRemoteEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
service.createRemoteEndpoint = function(name, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
FileUploadService.createEndpoint(name, 'tcp://' + URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
.then(function success(response) {
|
||||
deferred.resolve(response.data);
|
||||
})
|
||||
|
|
45
app/portainer/services/api/groupService.js
Normal file
45
app/portainer/services/api/groupService.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
angular.module('portainer.app')
|
||||
.factory('GroupService', ['$q', 'EndpointGroups',
|
||||
function GroupService($q, EndpointGroups) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
service.group = function(groupId) {
|
||||
var deferred = $q.defer();
|
||||
|
||||
EndpointGroups.get({ id: groupId }).$promise
|
||||
.then(function success(data) {
|
||||
var group = new EndpointGroupModel(data);
|
||||
deferred.resolve(group);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to retrieve group', err: err });
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
service.groups = function() {
|
||||
return EndpointGroups.query({}).$promise;
|
||||
};
|
||||
|
||||
service.createGroup = function(model, endpoints) {
|
||||
var payload = new EndpointGroupCreateRequest(model, endpoints);
|
||||
return EndpointGroups.create(payload).$promise;
|
||||
};
|
||||
|
||||
service.updateGroup = function(model, endpoints) {
|
||||
var payload = new EndpointGroupUpdateRequest(model, endpoints);
|
||||
return EndpointGroups.update(payload).$promise;
|
||||
};
|
||||
|
||||
service.updateAccess = function(groupId, authorizedUserIDs, authorizedTeamIDs) {
|
||||
return EndpointGroups.updateAccess({ id: groupId }, { authorizedUsers: authorizedUserIDs, authorizedTeams: authorizedTeamIDs }).$promise;
|
||||
};
|
||||
|
||||
service.deleteGroup = function(groupId) {
|
||||
return EndpointGroups.remove({ id: groupId }).$promise;
|
||||
};
|
||||
|
||||
return service;
|
||||
}]);
|
|
@ -42,13 +42,14 @@ angular.module('portainer.app')
|
|||
});
|
||||
};
|
||||
|
||||
service.createEndpoint = function(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
service.createEndpoint = function(name, URL, PublicURL, groupID, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile) {
|
||||
return Upload.upload({
|
||||
url: 'api/endpoints',
|
||||
data: {
|
||||
Name: name,
|
||||
URL: URL,
|
||||
PublicURL: PublicURL,
|
||||
GroupID: groupID,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="endpoint">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plug" title="Endpoint"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
|
@ -25,11 +25,9 @@
|
|||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<span class="small text-muted">
|
||||
You can select which user or team can access this endpoint by moving them to the authorized accesses table. Simply click
|
||||
on a user or team entry to move it from one table to the other.
|
||||
</span>
|
||||
<td>Group</td>
|
||||
<td>
|
||||
<a ui-sref="portainer.groups.group({ id: group.Id })">{{ group.Name }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -39,5 +37,10 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<por-access-management ng-if="endpoint" access-controlled-entity="endpoint" update-access="updateAccess(userAccesses, teamAccesses)">
|
||||
</por-access-management>
|
||||
<div class="row" ng-if="endpoint && group">
|
||||
<div class="col-sm-12">
|
||||
<por-access-management
|
||||
access-controlled-entity="endpoint" entity-type="endpoint" inherit-from="group" update-access="updateAccess(userAccesses, teamAccesses)"
|
||||
></por-access-management>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'Notifications',
|
||||
function ($scope, $transition$, EndpointService, Notifications) {
|
||||
.controller('EndpointAccessController', ['$scope', '$transition$', 'EndpointService', 'GroupService', 'Notifications',
|
||||
function ($scope, $transition$, EndpointService, GroupService, Notifications) {
|
||||
|
||||
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
||||
return EndpointService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams);
|
||||
|
@ -9,7 +9,12 @@ function ($scope, $transition$, EndpointService, Notifications) {
|
|||
function initView() {
|
||||
EndpointService.endpoint($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
$scope.endpoint = data;
|
||||
var endpoint = data;
|
||||
$scope.endpoint = endpoint;
|
||||
return GroupService.group(endpoint.GroupId);
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.group = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
|
|
|
@ -43,6 +43,19 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-public-url-input -->
|
||||
<div class="col-sm-12 form-section-title">
|
||||
Grouping
|
||||
</div>
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Group
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select ng-options="group.Id as group.Name for group in groups" ng-model="endpoint.GroupId" id="endpoint_group" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
<!-- endpoint-security -->
|
||||
<div ng-if="endpointType === 'remote'">
|
||||
<div class="col-sm-12 form-section-title">
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('EndpointController', ['$scope', '$state', '$transition$', '$filter', 'EndpointService', 'Notifications',
|
||||
function ($scope, $state, $transition$, $filter, EndpointService, Notifications) {
|
||||
.controller('EndpointController', ['$q', '$scope', '$state', '$transition$', '$filter', 'EndpointService', 'GroupService', 'EndpointProvider', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, $filter, EndpointService, GroupService, EndpointProvider, Notifications) {
|
||||
|
||||
if (!$scope.applicationState.application.endpointManagement) {
|
||||
$state.go('portainer.endpoints');
|
||||
|
@ -27,6 +27,7 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
name: endpoint.Name,
|
||||
URL: endpoint.URL,
|
||||
PublicURL: endpoint.PublicURL,
|
||||
GroupId: endpoint.GroupId,
|
||||
TLS: TLS,
|
||||
TLSSkipVerify: TLSSkipVerify,
|
||||
TLSSkipClientVerify: TLSSkipClientVerify,
|
||||
|
@ -40,7 +41,8 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
EndpointService.updateEndpoint(endpoint.Id, endpointParams)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Endpoint updated', $scope.endpoint.Name);
|
||||
$state.go('portainer.endpoints');
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
$state.go('portainer.endpoints', {}, {reload: true});
|
||||
}, function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update endpoint');
|
||||
$scope.state.actionInProgress = false;
|
||||
|
@ -52,9 +54,12 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
};
|
||||
|
||||
function initView() {
|
||||
EndpointService.endpoint($transition$.params().id)
|
||||
$q.all({
|
||||
endpoint: EndpointService.endpoint($transition$.params().id),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoint = data;
|
||||
var endpoint = data.endpoint;
|
||||
if (endpoint.URL.indexOf('unix://') === 0) {
|
||||
$scope.endpointType = 'local';
|
||||
} else {
|
||||
|
@ -62,6 +67,7 @@ function ($scope, $state, $transition$, $filter, EndpointService, Notifications)
|
|||
}
|
||||
endpoint.URL = $filter('stripprotocol')(endpoint.URL);
|
||||
$scope.endpoint = endpoint;
|
||||
$scope.groups = data.groups;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint details');
|
||||
|
|
|
@ -59,6 +59,16 @@
|
|||
</div>
|
||||
</div>
|
||||
<!-- !endpoint-public-url-input -->
|
||||
<!-- group -->
|
||||
<div class="form-group">
|
||||
<label for="endpoint_group" class="col-sm-3 col-lg-2 control-label text-left">
|
||||
Group
|
||||
</label>
|
||||
<div class="col-sm-9 col-lg-10">
|
||||
<select ng-options="group.Id as group.Name for group in groups" ng-model="formValues.GroupId" id="endpoint_group" class="form-control"></select>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !group -->
|
||||
<!-- endpoint-security -->
|
||||
<por-endpoint-security form-data="formValues.SecurityFormData"></por-endpoint-security>
|
||||
<!-- !endpoint-security -->
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('EndpointsController', ['$scope', '$state', '$filter', 'EndpointService', 'Notifications',
|
||||
function ($scope, $state, $filter, EndpointService, Notifications) {
|
||||
.controller('EndpointsController', ['$q', '$scope', '$state', '$filter', 'EndpointService', 'GroupService', 'EndpointHelper', 'Notifications',
|
||||
function ($q, $scope, $state, $filter, EndpointService, GroupService, EndpointHelper, Notifications) {
|
||||
$scope.state = {
|
||||
uploadInProgress: false,
|
||||
actionInProgress: false
|
||||
|
@ -10,6 +10,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
Name: '',
|
||||
URL: '',
|
||||
PublicURL: '',
|
||||
GroupId: 1,
|
||||
SecurityFormData: new EndpointSecurityFormData()
|
||||
};
|
||||
|
||||
|
@ -20,6 +21,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
if (PublicURL === '') {
|
||||
PublicURL = URL.split(':')[0];
|
||||
}
|
||||
var groupId = $scope.formValues.GroupId;
|
||||
|
||||
var securityData = $scope.formValues.SecurityFormData;
|
||||
var TLS = securityData.TLS;
|
||||
|
@ -31,7 +33,7 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
var TLSKeyFile = TLSSkipClientVerify ? null : securityData.TLSKey;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
EndpointService.createRemoteEndpoint(name, URL, PublicURL, groupId, TLS, TLSSkipVerify, TLSSkipClientVerify, TLSCAFile, TLSCertFile, TLSKeyFile)
|
||||
.then(function success() {
|
||||
Notifications.success('Endpoint created', name);
|
||||
$state.reload();
|
||||
|
@ -65,16 +67,22 @@ function ($scope, $state, $filter, EndpointService, Notifications) {
|
|||
});
|
||||
};
|
||||
|
||||
function fetchEndpoints() {
|
||||
EndpointService.endpoints()
|
||||
function initView() {
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
$scope.endpoints = data;
|
||||
var endpoints = data.endpoints;
|
||||
var groups = data.groups;
|
||||
EndpointHelper.mapGroupNameToEndpoint(endpoints, groups);
|
||||
$scope.groups = groups;
|
||||
$scope.endpoints = endpoints;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
$scope.endpoints = [];
|
||||
Notifications.error('Failure', err, 'Unable to load view');
|
||||
});
|
||||
}
|
||||
|
||||
fetchEndpoints();
|
||||
initView();
|
||||
}]);
|
||||
|
|
34
app/portainer/views/groups/access/groupAccess.html
Normal file
34
app/portainer/views/groups/access/groupAccess.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Endpoint group access"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.groups">Groups</a> > <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a> > Access management
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row" ng-if="group">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-object-group" title="Group"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td>
|
||||
{{ group.Name }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="group">
|
||||
<div class="col-sm-12">
|
||||
<por-access-management
|
||||
ng-if="group" access-controlled-entity="group" entity-type="group" update-access="updateAccess(userAccesses, teamAccesses)"
|
||||
></por-access-management>
|
||||
</div>
|
||||
</div>
|
22
app/portainer/views/groups/access/groupAccessController.js
Normal file
22
app/portainer/views/groups/access/groupAccessController.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('GroupAccessController', ['$scope', '$transition$', 'GroupService', 'Notifications',
|
||||
function ($scope, $transition$, GroupService, Notifications) {
|
||||
|
||||
$scope.updateAccess = function(authorizedUsers, authorizedTeams) {
|
||||
return GroupService.updateAccess($transition$.params().id, authorizedUsers, authorizedTeams);
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var groupId = $transition$.params().id;
|
||||
|
||||
GroupService.group(groupId)
|
||||
.then(function success(data) {
|
||||
$scope.group = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load view');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
54
app/portainer/views/groups/create/createGroupController.js
Normal file
54
app/portainer/views/groups/create/createGroupController.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('CreateGroupController', ['$scope', '$state', 'GroupService', 'EndpointService', 'Notifications',
|
||||
function ($scope, $state, GroupService, EndpointService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.model.Labels.push({ name: '', value: '' });
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.model.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
$scope.create = function() {
|
||||
var model = $scope.model;
|
||||
|
||||
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.createGroup(model, associatedEndpoints)
|
||||
.then(function success() {
|
||||
Notifications.success('Group successfully created');
|
||||
$state.go('portainer.groups', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to create group');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
$scope.model = new EndpointGroupDefaultModel();
|
||||
|
||||
EndpointService.endpointsByGroup(1)
|
||||
.then(function success(data) {
|
||||
$scope.availableEndpoints = data;
|
||||
$scope.associatedEndpoints = [];
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
25
app/portainer/views/groups/create/creategroup.html
Normal file
25
app/portainer/views/groups/create/creategroup.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Create endpoint group"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.groups">Endpoint groups</a> > Add group
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<group-form
|
||||
model="model"
|
||||
available-endpoints="availableEndpoints"
|
||||
associated-endpoints="associatedEndpoints"
|
||||
add-label-action="addLabel"
|
||||
remove-label-action="removeLabel"
|
||||
form-action="create"
|
||||
form-action-label="Create the group"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></group-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
25
app/portainer/views/groups/edit/group.html
Normal file
25
app/portainer/views/groups/edit/group.html
Normal file
|
@ -0,0 +1,25 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Endpoint group details"></rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="portainer.groups">Groups</a> > <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<group-form
|
||||
model="group"
|
||||
available-endpoints="availableEndpoints"
|
||||
associated-endpoints="associatedEndpoints"
|
||||
add-label-action="addLabel"
|
||||
remove-label-action="removeLabel"
|
||||
form-action="update"
|
||||
form-action-label="Update the group"
|
||||
action-in-progress="state.actionInProgress"
|
||||
></group-form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
70
app/portainer/views/groups/edit/groupController.js
Normal file
70
app/portainer/views/groups/edit/groupController.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('GroupController', ['$q', '$scope', '$state', '$transition$', 'GroupService', 'EndpointService', 'Notifications',
|
||||
function ($q, $scope, $state, $transition$, GroupService, EndpointService, Notifications) {
|
||||
|
||||
$scope.state = {
|
||||
actionInProgress: false
|
||||
};
|
||||
|
||||
$scope.addLabel = function() {
|
||||
$scope.group.Labels.push({ name: '', value: '' });
|
||||
};
|
||||
|
||||
$scope.removeLabel = function(index) {
|
||||
$scope.group.Labels.splice(index, 1);
|
||||
};
|
||||
|
||||
$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)
|
||||
.then(function success(data) {
|
||||
Notifications.success('Group successfully updated');
|
||||
$state.go('portainer.groups', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to update group');
|
||||
})
|
||||
.finally(function final() {
|
||||
$scope.state.actionInProgress = false;
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
var groupId = $transition$.params().id;
|
||||
|
||||
$q.all({
|
||||
group: GroupService.group(groupId),
|
||||
endpoints: EndpointService.endpoints()
|
||||
})
|
||||
.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;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to load view');
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
20
app/portainer/views/groups/groups.html
Normal file
20
app/portainer/views/groups/groups.html
Normal file
|
@ -0,0 +1,20 @@
|
|||
<rd-header>
|
||||
<rd-header-title title="Endpoint groups">
|
||||
<a data-toggle="tooltip" title="Refresh" ui-sref="portainer.groups" ui-sref-opts="{reload: true}">
|
||||
<i class="fa fa-sync" aria-hidden="true"></i>
|
||||
</a>
|
||||
</rd-header-title>
|
||||
<rd-header-content>Endpoint group management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<groups-datatable
|
||||
title="Endpoint groups" title-icon="fa-object-group"
|
||||
dataset="groups" table-key="groups"
|
||||
order-by="Name" show-text-filter="true"
|
||||
access-management="applicationState.application.authentication"
|
||||
remove-action="removeAction"
|
||||
></groups-datatable>
|
||||
</div>
|
||||
</div>
|
38
app/portainer/views/groups/groupsController.js
Normal file
38
app/portainer/views/groups/groupsController.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('GroupsController', ['$scope', '$state', '$filter', 'GroupService', 'Notifications',
|
||||
function ($scope, $state, $filter, GroupService, Notifications) {
|
||||
|
||||
$scope.removeAction = function (selectedItems) {
|
||||
var actionCount = selectedItems.length;
|
||||
angular.forEach(selectedItems, function (group) {
|
||||
GroupService.deleteGroup(group.Id)
|
||||
.then(function success() {
|
||||
Notifications.success('Endpoint group successfully removed', group.Name);
|
||||
var index = $scope.groups.indexOf(group);
|
||||
$scope.groups.splice(index, 1);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove group');
|
||||
})
|
||||
.finally(function final() {
|
||||
--actionCount;
|
||||
if (actionCount === 0) {
|
||||
$state.reload();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
GroupService.groups()
|
||||
.then(function success(data) {
|
||||
$scope.groups = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoint groups');
|
||||
$scope.groups = [];
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
|
@ -31,7 +31,7 @@ function ($scope, $state, EndpointService, StateManager, EndpointProvider, Notif
|
|||
var endpointID = 1;
|
||||
|
||||
$scope.state.actionInProgress = true;
|
||||
EndpointService.createLocalEndpoint(name, URL, false, true)
|
||||
EndpointService.createLocalEndpoint()
|
||||
.then(function success(data) {
|
||||
endpointID = data.Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
</div>
|
||||
<div class="sidebar-content">
|
||||
<ul class="sidebar">
|
||||
<li class="sidebar-title"><span>Active endpoint</span></li>
|
||||
<li class="sidebar-title">
|
||||
<select class="select-endpoint form-control" ng-options="endpoint.Name for endpoint in endpoints" ng-model="activeEndpoint" ng-change="switchEndpoint(activeEndpoint)">
|
||||
</select>
|
||||
</li>
|
||||
<li class="sidebar-title"><span>Endpoint actions</span></li>
|
||||
<endpoint-selector ng-if="endpoints && groups"
|
||||
endpoints="endpoints"
|
||||
groups="groups"
|
||||
select-endpoint="switchEndpoint"
|
||||
></endpoint-selector>
|
||||
<li class="sidebar-title"><span>{{ activeEndpoint.Name }}</span></li>
|
||||
<docker-sidebar-content
|
||||
endpoint-api-version="applicationState.endpoint.apiVersion"
|
||||
swarm-management="applicationState.endpoint.mode.provider === 'DOCKER_SWARM_MODE' && applicationState.endpoint.mode.role === 'MANAGER'"
|
||||
|
@ -47,6 +47,9 @@
|
|||
</li>
|
||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<a ui-sref="portainer.endpoints" ui-sref-active="active">Endpoints <span class="menu-icon fa fa-plug fa-fw"></span></a>
|
||||
<div class="sidebar-sublist" ng-if="toggle && ($state.current.name === 'portainer.endpoints' || $state.current.name === 'portainer.endpoints.endpoint' || $state.current.name === 'portainer.endpoints.endpoint.access' || $state.current.name === 'portainer.groups' || $state.current.name === 'portainer.groups.group' || $state.current.name === 'portainer.groups.group.access' || $state.current.name === 'portainer.groups.new')">
|
||||
<a ui-sref="portainer.groups" ui-sref-active="active">Groups</a>
|
||||
</div>
|
||||
</li>
|
||||
<li class="sidebar-list" ng-if="!applicationState.application.authentication || isAdmin">
|
||||
<a ui-sref="portainer.registries" ui-sref-active="active">Registries <span class="menu-icon fa fa-database fa-fw"></span></a>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app')
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'Settings', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
||||
function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
||||
.controller('SidebarController', ['$q', '$scope', '$state', 'EndpointService', 'GroupService', 'StateManager', 'EndpointProvider', 'Notifications', 'Authentication', 'UserService', 'ExtensionManager',
|
||||
function ($q, $scope, $state, EndpointService, GroupService, StateManager, EndpointProvider, Notifications, Authentication, UserService, ExtensionManager) {
|
||||
|
||||
$scope.switchEndpoint = function(endpoint) {
|
||||
var activeEndpointID = EndpointProvider.endpointID();
|
||||
|
@ -25,16 +25,6 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
|
|||
});
|
||||
};
|
||||
|
||||
function setActiveEndpoint(endpoints) {
|
||||
var activeEndpointID = EndpointProvider.endpointID();
|
||||
angular.forEach(endpoints, function (endpoint) {
|
||||
if (endpoint.Id === activeEndpointID) {
|
||||
$scope.activeEndpoint = endpoint;
|
||||
EndpointProvider.setEndpointPublicURL(endpoint.PublicURL);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function checkPermissions(memberships) {
|
||||
var isLeader = false;
|
||||
angular.forEach(memberships, function(membership) {
|
||||
|
@ -49,13 +39,24 @@ function ($q, $scope, $state, Settings, EndpointService, StateManager, EndpointP
|
|||
$scope.uiVersion = StateManager.getState().application.version;
|
||||
$scope.displayExternalContributors = StateManager.getState().application.displayExternalContributors;
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
$scope.endpoints = [];
|
||||
|
||||
EndpointService.endpoints()
|
||||
$q.all({
|
||||
endpoints: EndpointService.endpoints(),
|
||||
groups: GroupService.groups()
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
$scope.endpoints = _.sortBy(endpoints, ['Name']);
|
||||
setActiveEndpoint(endpoints);
|
||||
var endpoints = data.endpoints;
|
||||
$scope.groups = data.groups;
|
||||
$scope.endpoints = endpoints;
|
||||
|
||||
var activeEndpointID = EndpointProvider.endpointID();
|
||||
for (var i = 0; i < endpoints.length; i++) {
|
||||
var endpoint = endpoints[i];
|
||||
if (endpoint.Id === activeEndpointID) {
|
||||
$scope.activeEndpoint = endpoint;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (StateManager.getState().application.authentication) {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue