New team view interface
This commit is contained in:
parent
56d7a3524d
commit
7d7cca39cc
8 changed files with 173 additions and 45 deletions
|
@ -8,6 +8,7 @@ from auth.auth_context import get_authenticated_user
|
|||
from auth import scopes
|
||||
from data import model
|
||||
from util.useremails import send_org_invite_email
|
||||
from util.gravatar import compute_hash
|
||||
|
||||
def add_or_invite_to_team(team, user=None, email=None, adder=None):
|
||||
invite = model.add_or_invite_to_team(team, user, email, adder)
|
||||
|
@ -44,6 +45,7 @@ def member_view(member, invited=False):
|
|||
'name': member.username,
|
||||
'kind': 'user',
|
||||
'is_robot': member.robot,
|
||||
'gravatar': compute_hash(member.email) if not member.robot else None,
|
||||
'invited': invited,
|
||||
}
|
||||
|
||||
|
@ -55,6 +57,7 @@ def invite_view(invite):
|
|||
return {
|
||||
'email': invite.email,
|
||||
'kind': 'invite',
|
||||
'gravatar': compute_hash(invite.email),
|
||||
'invited': True
|
||||
}
|
||||
|
||||
|
@ -162,14 +165,15 @@ class TeamMemberList(ApiResource):
|
|||
raise NotFound()
|
||||
|
||||
members = model.get_organization_team_members(team.id)
|
||||
data = {
|
||||
'members': {m.username : member_view(m) for m in members},
|
||||
'can_edit': edit_permission.can()
|
||||
}
|
||||
invites = []
|
||||
|
||||
if args['includePending'] and edit_permission.can():
|
||||
invites = model.get_organization_team_member_invites(team.id)
|
||||
data['pending'] = [invite_view(i) for i in invites]
|
||||
|
||||
data = {
|
||||
'members': [member_view(m) for m in members] + [invite_view(i) for i in invites],
|
||||
'can_edit': edit_permission.can()
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
|
|
|
@ -4593,4 +4593,48 @@ i.quay-icon {
|
|||
|
||||
.external-notification-view-element:hover .side-controls button {
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
}
|
||||
|
||||
.member-listing {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.member-listing .section-header {
|
||||
color: #ccc;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.member-listing .gravatar {
|
||||
vertical-align: middle;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.member-listing .entity-reference {
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.organization-header .popover {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.organization-header .popover.bottom-right .arrow:after {
|
||||
border-bottom-color: #f7f7f7;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.organization-header .popover-content {
|
||||
font-size: 14px;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.organization-header .popover-content input {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.organization-header .popover-content .help-text {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
|
|
@ -7,15 +7,19 @@
|
|||
</span>
|
||||
</span>
|
||||
<span ng-if="entity.kind == 'org'">
|
||||
<img src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s=16&d=identicon">
|
||||
<img ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||
<span class="entity-name">
|
||||
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
|
||||
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
|
||||
</span>
|
||||
</span>
|
||||
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<img class="gravatar" ng-if="showGravatar == 'true' && entity.gravatar" ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&d=identicon">
|
||||
<span ng-if="showGravatar != 'true' || !entity.gravatar">
|
||||
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
</span>
|
||||
|
||||
<span class="entity-name" ng-if="entity.is_robot">
|
||||
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
|
||||
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
ng-click="lazyLoad()">
|
||||
<span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu" role="menu" aria-labelledby="entityDropdownMenu">
|
||||
<ul class="dropdown-menu" ng-class="pullRight == 'true' ? 'pull-right': ''" role="menu" aria-labelledby="entityDropdownMenu">
|
||||
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
|
||||
|
||||
<li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams">
|
||||
|
|
14
static/directives/team-view-add.html
Normal file
14
static/directives/team-view-add.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<div style="width: 500px">
|
||||
<div class="entity-search"
|
||||
namespace="orgname" placeholder="'Add a registered user or robot...'"
|
||||
entity-selected="addNewMember(entity)"
|
||||
current-entity="selectedMember"
|
||||
auto-clear="true"
|
||||
allowed-entities="['user', 'robot']"
|
||||
pull-right="true"
|
||||
ng-show="!addingMember"></div>
|
||||
<div class="quay-spinner" ng-show="addingMember"></div>
|
||||
<div class="help-text" ng-show="!addingMember">
|
||||
Search by Quay.io username or robot account name
|
||||
</div>
|
||||
</div>
|
|
@ -1944,7 +1944,9 @@ quayApp.directive('entityReference', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'entity': '=entity',
|
||||
'namespace': '=namespace'
|
||||
'namespace': '=namespace',
|
||||
'showGravatar': '@showGravatar',
|
||||
'gravatarSize': '@gravatarSize'
|
||||
},
|
||||
controller: function($scope, $element, UserService, UtilService) {
|
||||
$scope.getIsAdmin = function(namespace) {
|
||||
|
@ -3423,6 +3425,8 @@ quayApp.directive('entitySearch', function () {
|
|||
|
||||
// Set this property to immediately clear the contents of the control.
|
||||
'clearValue': '=clearValue',
|
||||
|
||||
'pullRight': '@pullRight'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, Config) {
|
||||
$scope.lazyLoading = true;
|
||||
|
|
|
@ -2349,22 +2349,34 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
|
||||
$scope.orgname = orgname;
|
||||
$scope.teamname = teamname;
|
||||
$scope.addingMember = false;
|
||||
$scope.memberMap = null;
|
||||
|
||||
$rootScope.title = 'Loading...';
|
||||
|
||||
$scope.addNewMember = function(member) {
|
||||
if (!member || $scope.members[member.name]) { return; }
|
||||
$scope.filterFunction = function(invited, robots) {
|
||||
return function(item) {
|
||||
return item.is_robot == robots && item.invited == invited;
|
||||
};
|
||||
};
|
||||
|
||||
$scope.addNewMember = function(member) {
|
||||
if (!member || $scope.memberMap[member.name]) { return; }
|
||||
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname,
|
||||
'membername': member.name
|
||||
};
|
||||
|
||||
$scope.addingMember = true;
|
||||
ApiService.updateOrganizationTeamMember(null, params).then(function(resp) {
|
||||
$scope.members[member.name] = resp;
|
||||
$scope.members.push(resp);
|
||||
$scope.memberMap[resp.name] = resp;
|
||||
$scope.addingMember = false;
|
||||
}, function() {
|
||||
$('#cannotChangeMembersModal').modal({});
|
||||
$scope.addingMember = false;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -2376,7 +2388,14 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
};
|
||||
|
||||
ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) {
|
||||
delete $scope.members[username];
|
||||
for (var i = $scope.members.length - 1; i >= 0; --i) {
|
||||
var current = $scope.members[i];
|
||||
if (current.name == username) {
|
||||
$scope.members.splice(i, 1);
|
||||
delete $scope.memberMap[username];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, function() {
|
||||
$('#cannotChangeMembersModal').modal({});
|
||||
});
|
||||
|
@ -2424,6 +2443,12 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
'html': true
|
||||
});
|
||||
|
||||
$scope.memberMap = {};
|
||||
for (var i = 0; i < $scope.members.length; ++i) {
|
||||
var current = $scope.members[i];
|
||||
$scope.memberMap[current.name] = current;
|
||||
}
|
||||
|
||||
return resp.members;
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,41 +1,74 @@
|
|||
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
|
||||
<div class="team-view container">
|
||||
<div class="organization-header" organization="organization" team-name="teamname"></div>
|
||||
<div class="organization-header" organization="organization" team-name="teamname">
|
||||
<div ng-show="canEditMembers" class="side-controls">
|
||||
<button class="btn btn-success" data-trigger="click"
|
||||
data-title="Add Team Member"
|
||||
data-content-template="/static/directives/team-view-add.html"
|
||||
data-placement="bottom-right"
|
||||
bs-popover>
|
||||
<i class="fa fa-plus"></i>
|
||||
Add Team Member
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
|
||||
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
||||
content-changed="updateForDescription" field-title="'team description'"></div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Team Members
|
||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<table class="permissions">
|
||||
<tr ng-repeat="(name, member) in members">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name"></span>
|
||||
{{ member.invited }}
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove User From Team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-show="canEditMembers">
|
||||
<td colspan="3">
|
||||
<div class="entity-search" style="width: 100%"
|
||||
namespace="orgname" placeholder="'Add a registered user or robot...'"
|
||||
entity-selected="addNewMember(entity)"
|
||||
current-entity="selectedMember"
|
||||
auto-clear="true"
|
||||
allowed-entities="['user', 'robot']"></div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="empty-message" ng-if="!members.length">
|
||||
This team has no members
|
||||
</div>
|
||||
|
||||
<div class="empty-message" ng-if="members.length && !(members | filter:search).length">
|
||||
No matching team members found
|
||||
</div>
|
||||
|
||||
<table class="member-listing" style="margin-top: -20px" ng-show="members.length">
|
||||
<!-- Members -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(false, false)).length">
|
||||
<td colspan="2"><div class="section-header">Team Members</div></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' From Team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Robots -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(false, true)).length">
|
||||
<td colspan="2"><div class="section-header">Robot Accounts</div></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, true) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' From Team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Invited -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(true, false)).length">
|
||||
<td colspan="2"><div class="section-header">Invited To Join</div></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-gravatar="true" gravatar-size="32"></span>
|
||||
</td>
|
||||
<td>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
Reference in a new issue