mirror of
https://github.com/portainer/portainer.git
synced 2025-08-09 07:45:22 +02:00
feat(teamleader) EE-294 redesign team leader (#6973)
feat(teamleader) EE-294 redesign team leader (#6973)
This commit is contained in:
parent
bca1c6b9cf
commit
0522032515
29 changed files with 223 additions and 135 deletions
|
@ -4,7 +4,7 @@
|
|||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="actionBar" ng-show="$ctrl.isAdmin">
|
||||
<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>
|
||||
|
@ -25,7 +25,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
|
@ -43,7 +43,7 @@
|
|||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
|
|
|
@ -9,5 +9,6 @@ angular.module('portainer.app').component('teamsDatatable', {
|
|||
orderBy: '@',
|
||||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<div class="toolBar">
|
||||
<div class="toolBarTitle"> <i class="fa" ng-class="$ctrl.titleIcon" aria-hidden="true" style="margin-right: 2px"></i> {{ $ctrl.titleText }} </div>
|
||||
</div>
|
||||
<div class="actionBar">
|
||||
<div class="actionBar" ng-show="$ctrl.isAdmin">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-danger"
|
||||
|
@ -31,7 +31,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
|
||||
<label for="select_all"></label>
|
||||
</span>
|
||||
|
@ -63,11 +63,12 @@
|
|||
ng-class="{ active: item.Checked }"
|
||||
>
|
||||
<td>
|
||||
<span class="md-checkbox">
|
||||
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
|
||||
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
|
||||
<label for="select_{{ $index }}"></label>
|
||||
</span>
|
||||
<a ui-sref="portainer.users.user({id: item.Id})">{{ item.Username }}</a>
|
||||
<a ui-sref="portainer.users.user({id: item.Id})" ng-show="$ctrl.isAdmin">{{ item.Username }}</a>
|
||||
<span ng-show="!$ctrl.isAdmin">{{ item.Username }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<span>
|
||||
|
|
|
@ -13,5 +13,6 @@ angular.module('portainer.app').component('usersDatatable', {
|
|||
reverseOrder: '<',
|
||||
removeAction: '<',
|
||||
authenticationMethod: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,6 +24,7 @@ export function SettingsViewModel(data) {
|
|||
|
||||
export function PublicSettingsViewModel(settings) {
|
||||
this.AuthenticationMethod = settings.AuthenticationMethod;
|
||||
this.TeamSync = settings.TeamSync;
|
||||
this.RequiredPasswordLength = settings.RequiredPasswordLength;
|
||||
this.EnableEdgeComputeFeatures = settings.EnableEdgeComputeFeatures;
|
||||
this.EnforceEdgeID = settings.EnforceEdgeID;
|
||||
|
|
|
@ -33,10 +33,10 @@
|
|||
<td
|
||||
>{{ item.TeamName ? 'Team' : 'User' }} <code ng-if="item.TeamName">{{ item.TeamName }}</code> access defined on {{ item.AccessLocation }}
|
||||
<code ng-if="item.GroupName">{{ item.GroupName }}</code>
|
||||
<a ng-if="!item.GroupName" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
|
||||
<a ng-if="!item.GroupName && $ctrl.isAdmin" ui-sref="portainer.endpoints.endpoint.access({id: item.EndpointId})"
|
||||
><i style="margin-left: 5px" class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||
</a>
|
||||
<a ng-if="item.GroupName" ui-sref="portainer.groups.group.access({id: item.GroupId})"
|
||||
<a ng-if="item.GroupName && $ctrl.isAdmin" ui-sref="portainer.groups.group.access({id: item.GroupId})"
|
||||
><i style="margin-left: 5px" class="fa fa-users" aria-hidden="true"></i> Manage access
|
||||
</a>
|
||||
</td>
|
||||
|
|
|
@ -7,5 +7,6 @@ export const accessViewerDatatable = {
|
|||
tableKey: '@',
|
||||
orderBy: '@',
|
||||
dataset: '<',
|
||||
isAdmin: '<',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@ import AccessViewerPolicyModel from '../../models/access';
|
|||
|
||||
export default class AccessViewerController {
|
||||
/* @ngInject */
|
||||
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService) {
|
||||
constructor(Notifications, RoleService, UserService, EndpointService, GroupService, TeamService, TeamMembershipService, Authentication) {
|
||||
this.Notifications = Notifications;
|
||||
this.RoleService = RoleService;
|
||||
this.UserService = UserService;
|
||||
|
@ -13,6 +13,7 @@ export default class AccessViewerController {
|
|||
this.GroupService = GroupService;
|
||||
this.TeamService = TeamService;
|
||||
this.TeamMembershipService = TeamMembershipService;
|
||||
this.Authentication = Authentication;
|
||||
|
||||
this.limitedFeature = 'rbac-roles';
|
||||
this.users = [];
|
||||
|
@ -100,6 +101,33 @@ export default class AccessViewerController {
|
|||
return this.findLowestRole(policyRoles);
|
||||
}
|
||||
|
||||
// for admin, returns all users
|
||||
// for team leader, only return all his/her team member users
|
||||
async teamMemberUsers(users, teamMemberships) {
|
||||
if (this.isAdmin) {
|
||||
return users;
|
||||
}
|
||||
|
||||
const filteredUsers = [];
|
||||
const userId = this.Authentication.getUserDetails().ID;
|
||||
const leadingTeams = await this.UserService.userLeadingTeams(userId);
|
||||
|
||||
const isMember = (userId, teamId) => {
|
||||
return !!_.find(teamMemberships, { UserId: userId, TeamId: teamId });
|
||||
};
|
||||
|
||||
for (const user of users) {
|
||||
for (const leadingTeam of leadingTeams) {
|
||||
if (isMember(user.Id, leadingTeam.Id)) {
|
||||
filteredUsers.push(user);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredUsers;
|
||||
}
|
||||
|
||||
async $onInit() {
|
||||
try {
|
||||
const limitedToBE = isLimitedToBE(this.limitedFeature);
|
||||
|
@ -108,7 +136,8 @@ export default class AccessViewerController {
|
|||
return;
|
||||
}
|
||||
|
||||
this.users = await this.UserService.users();
|
||||
this.isAdmin = this.Authentication.isAdmin();
|
||||
this.allUsers = await this.UserService.users();
|
||||
this.endpoints = _.keyBy((await this.EndpointService.endpoints()).value, 'Id');
|
||||
const groups = await this.GroupService.groups();
|
||||
this.groupUserAccessPolicies = {};
|
||||
|
@ -121,6 +150,7 @@ export default class AccessViewerController {
|
|||
this.roles = _.keyBy(await this.RoleService.roles(), 'Id');
|
||||
this.teams = _.keyBy(await this.TeamService.teams(), 'Id');
|
||||
this.teamMemberships = await this.TeamMembershipService.memberships();
|
||||
this.users = await this.teamMemberUsers(this.allUsers, this.teamMemberships);
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Unable to retrieve accesses');
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
Effective role for each environment will be displayed for the selected user
|
||||
</div>
|
||||
</div>
|
||||
<access-viewer-datatable table-key="access_viewer" dataset="$ctrl.userRoles" order-by="EndpointName"> </access-viewer-datatable>
|
||||
<access-viewer-datatable table-key="access_viewer" dataset="$ctrl.userRoles" order-by="EndpointName" is-admin="$ctrl.isAdmin"> </access-viewer-datatable>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
|
|
@ -4,8 +4,7 @@ import { TeamMembershipModel } from '../../models/teamMembership';
|
|||
angular.module('portainer.app').factory('TeamService', [
|
||||
'$q',
|
||||
'Teams',
|
||||
'TeamMembershipService',
|
||||
function TeamServiceFactory($q, Teams, TeamMembershipService) {
|
||||
function TeamServiceFactory($q, Teams) {
|
||||
'use strict';
|
||||
var service = {};
|
||||
|
||||
|
@ -41,17 +40,11 @@ angular.module('portainer.app').factory('TeamService', [
|
|||
var deferred = $q.defer();
|
||||
var payload = {
|
||||
Name: name,
|
||||
TeamLeaders: leaderIds,
|
||||
};
|
||||
Teams.create({}, payload)
|
||||
.$promise.then(function success(data) {
|
||||
var teamId = data.Id;
|
||||
var teamMembershipQueries = [];
|
||||
angular.forEach(leaderIds, function (userId) {
|
||||
teamMembershipQueries.push(TeamMembershipService.createMembership(userId, teamId, 1));
|
||||
});
|
||||
$q.all(teamMembershipQueries).then(function success() {
|
||||
deferred.resolve();
|
||||
});
|
||||
.$promise.then(function success() {
|
||||
deferred.resolve();
|
||||
})
|
||||
.catch(function error(err) {
|
||||
deferred.reject({ msg: 'Unable to create team', err: err });
|
||||
|
|
|
@ -54,7 +54,7 @@
|
|||
>
|
||||
</sidebar-section>
|
||||
|
||||
<sidebar-section ng-if="isAdmin || isTeamLeader" title="Settings">
|
||||
<sidebar-section ng-if="showUsersSection" title="Settings">
|
||||
<sidebar-menu
|
||||
ng-show="display"
|
||||
icon-class="fa-users fa-fw"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
angular.module('portainer.app').controller('SidebarController', SidebarController);
|
||||
|
||||
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider) {
|
||||
function SidebarController($rootScope, $scope, $transitions, StateManager, Notifications, Authentication, UserService, EndpointProvider, SettingsService) {
|
||||
$scope.applicationState = StateManager.getState();
|
||||
$scope.endpointState = EndpointProvider.endpoint();
|
||||
$scope.display = !window.ddExtension;
|
||||
|
@ -29,11 +29,14 @@ function SidebarController($rootScope, $scope, $transitions, StateManager, Notif
|
|||
const userDetails = Authentication.getUserDetails();
|
||||
const isAdmin = isClusterAdmin();
|
||||
$scope.isAdmin = isAdmin;
|
||||
$scope.showUsersSection = isAdmin;
|
||||
|
||||
if (!isAdmin) {
|
||||
try {
|
||||
const memberships = await UserService.userMemberships(userDetails.ID);
|
||||
checkPermissions(memberships);
|
||||
const settings = await SettingsService.publicSettings();
|
||||
$scope.showUsersSection = $scope.isTeamLeader && !settings.TeamSync;
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve user memberships');
|
||||
}
|
||||
|
|
|
@ -36,6 +36,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="team && settings.TeamSync">
|
||||
<div class="col-sm-12 text-muted">
|
||||
<i class="fa fa-exclamation-circle orange-icon" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
The team leader feature is disabled as external authentication is currently enabled with team sync.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="team">
|
||||
<div class="col-sm-6">
|
||||
<rd-widget>
|
||||
|
@ -53,7 +60,7 @@
|
|||
</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="addAllUsers()" ng-if="isAdmin" ng-disabled="users.length === 0 || filteredUsers.length === 0"
|
||||
<button class="btn btn-primary btn-sm" ng-click="addAllUsers()" ng-if="isAdmin" ng-disabled="users.length === 0 || filteredUsers.length === 0 || settings.TeamSync"
|
||||
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Add all users</button
|
||||
>
|
||||
</div>
|
||||
|
@ -83,7 +90,7 @@
|
|||
<td>
|
||||
{{ user.Username }}
|
||||
<span style="margin-left: 5px">
|
||||
<a ng-click="addUser(user)"><i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add</a>
|
||||
<a ng-click="addUser(user)" ng-class="{ 'btn disabled py-0': settings.TeamSync }"> <i class="fa fa-plus-circle space-right" aria-hidden="true"></i>Add </a>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -118,7 +125,11 @@
|
|||
</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="removeAllUsers()" ng-if="isAdmin" ng-disabled="teamMembers.length === 0 || filteredGroupMembers.length === 0"
|
||||
<button
|
||||
class="btn btn-primary btn-sm"
|
||||
ng-click="removeAllUsers()"
|
||||
ng-if="isAdmin"
|
||||
ng-disabled="teamMembers.length === 0 || filteredGroupMembers.length === 0 || settings.TeamSync"
|
||||
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Remove all users</button
|
||||
>
|
||||
</div>
|
||||
|
@ -155,7 +166,9 @@
|
|||
<td>
|
||||
{{ user.Username }}
|
||||
<span style="margin-left: 5px" ng-if="isAdmin || user.TeamRole === 'Member'">
|
||||
<a ng-click="removeUser(user)"><i class="fa fa-minus-circle space-right" aria-hidden="true"></i>Remove</a>
|
||||
<a ng-click="removeUser(user)" ng-class="{ 'btn disabled py-0': settings.TeamSync }">
|
||||
<i class="fa fa-minus-circle space-right" aria-hidden="true"></i>Remove
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
|
@ -163,10 +176,10 @@
|
|||
<i ng-if="user.TeamRole === 'Member'" class="fa fa-user" aria-hidden="true" style="margin-right: 2px"></i>
|
||||
{{ user.TeamRole }}
|
||||
<span style="margin-left: 5px" ng-if="isAdmin">
|
||||
<a style="margin-left: 5px" ng-click="promoteToLeader(user)" ng-if="user.TeamRole === 'Member'"
|
||||
<a style="margin-left: 5px" ng-click="promoteToLeader(user)" ng-if="user.TeamRole === 'Member'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||
><i class="fa fa-user-plus space-right" aria-hidden="true"></i>Leader</a
|
||||
>
|
||||
<a style="margin-left: 5px" ng-click="demoteToMember(user)" ng-if="user.TeamRole === 'Leader'"
|
||||
<a style="margin-left: 5px" ng-click="demoteToMember(user)" ng-if="user.TeamRole === 'Leader'" ng-class="{ 'btn disabled py-0': settings.TeamSync }"
|
||||
><i class="fa fa-user-times space-right" aria-hidden="true"></i>Member</a
|
||||
>
|
||||
</span>
|
||||
|
|
|
@ -10,7 +10,8 @@ angular.module('portainer.app').controller('TeamController', [
|
|||
'Notifications',
|
||||
'PaginationService',
|
||||
'Authentication',
|
||||
function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMembershipService, ModalService, Notifications, PaginationService, Authentication) {
|
||||
'SettingsService',
|
||||
function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMembershipService, ModalService, Notifications, PaginationService, Authentication, SettingsService) {
|
||||
$scope.state = {
|
||||
pagination_count_users: PaginationService.getPaginationLimit('team_available_users'),
|
||||
pagination_count_members: PaginationService.getPaginationLimit('team_members'),
|
||||
|
@ -189,21 +190,23 @@ angular.module('portainer.app').controller('TeamController', [
|
|||
}
|
||||
}
|
||||
|
||||
function initView() {
|
||||
async function initView() {
|
||||
$scope.isAdmin = Authentication.isAdmin();
|
||||
$q.all({
|
||||
team: TeamService.team($transition$.params().id),
|
||||
users: UserService.users(false),
|
||||
memberships: TeamService.userMemberships($transition$.params().id),
|
||||
})
|
||||
.then(function success(data) {
|
||||
var users = data.users;
|
||||
$scope.team = data.team;
|
||||
assignUsersAndMembers(users, data.memberships);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
||||
|
||||
try {
|
||||
$scope.settings = await SettingsService.publicSettings();
|
||||
|
||||
const data = await $q.all({
|
||||
team: TeamService.team($transition$.params().id),
|
||||
users: UserService.users($scope.isAdmin && $scope.settings.TeamSync),
|
||||
memberships: TeamService.userMemberships($transition$.params().id),
|
||||
});
|
||||
|
||||
$scope.team = data.team;
|
||||
assignUsersAndMembers(data.users, data.memberships);
|
||||
} catch (err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve team details');
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
|
|
|
@ -11,6 +11,6 @@
|
|||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<teams-datatable title-text="Teams" title-icon="fa-users" dataset="teams" table-key="teams" order-by="Name" remove-action="removeAction"></teams-datatable>
|
||||
<teams-datatable title-text="Teams" title-icon="fa-users" dataset="teams" table-key="teams" order-by="Name" remove-action="removeAction" is-admin="isAdmin"></teams-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -89,6 +89,7 @@ angular.module('portainer.app').controller('TeamsController', [
|
|||
var teams = data.teams;
|
||||
$scope.teams = teams;
|
||||
$scope.users = _.orderBy(data.users, 'Username', 'asc');
|
||||
$scope.isTeamLeader = !!teams.length;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.teams = [];
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<rd-header-content>User management</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="row" ng-if="isAdmin">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-plus" title-text="Add a new user"> </rd-widget-header>
|
||||
|
@ -169,6 +169,7 @@
|
|||
order-by="Username"
|
||||
authentication-method="AuthenticationMethod"
|
||||
remove-action="removeAction"
|
||||
is-admin="isAdmin"
|
||||
></users-datatable>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue