New team view interface

This commit is contained in:
Joseph Schorr 2014-08-15 20:51:31 -04:00
parent 56d7a3524d
commit 7d7cca39cc
8 changed files with 173 additions and 45 deletions

View file

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

View file

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

View file

@ -7,15 +7,19 @@
</span>
</span>
<span ng-if="entity.kind == 'org'">
<img src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s=16&amp;d=identicon">
<img ng-src="//www.gravatar.com/avatar/{{ entity.gravatar }}?s={{ gravatarSize || '16' }}&amp;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' }}&amp;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>

View file

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

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

View file

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

View file

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

View file

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