1
0
Fork 0
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:
Anthony Lapenna 2018-04-26 18:08:46 +02:00 committed by GitHub
parent 2ffcb946b1
commit 1162549209
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1838 additions and 265 deletions

View file

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

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

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

View file

@ -3,6 +3,8 @@ angular.module('portainer.app').component('porAccessManagement', {
controller: 'porAccessManagementController',
bindings: {
accessControlledEntity: '<',
inheritFrom: '<',
entityType: '@',
updateAccess: '&'
}
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,9 @@
angular.module('portainer.app').component('endpointSelector', {
templateUrl: 'app/portainer/components/endpoint-selector/endpointSelector.html',
controller: 'EndpointSelectorController',
bindings: {
'endpoints': '<',
'groups': '<',
'selectEndpoint': '<'
}
});

View file

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

View file

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

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

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

View file

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

View file

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

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

View file

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

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

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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> &gt; <a ui-sref="portainer.groups.group({id: group.Id})">{{ group.Name }}</a> &gt; 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>

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

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

View 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> &gt; 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>

View 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> &gt; <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>

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

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

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

View file

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

View file

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

View file

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