Merge pull request #1754 from coreos-inc/team-add-perms

Better UI and permissions handling for robots and teams
This commit is contained in:
josephschorr 2016-09-06 17:21:19 -04:00 committed by GitHub
commit cd8b45e25b
21 changed files with 895 additions and 458 deletions

View file

@ -5,6 +5,17 @@ from data.database import (RepositoryPermission, User, Repository, Visibility, R
from data.model import DataModelException, _basequery from data.model import DataModelException, _basequery
def list_team_permissions(team):
return (RepositoryPermission
.select(RepositoryPermission)
.join(Repository)
.join(Visibility)
.switch(RepositoryPermission)
.join(Role)
.switch(RepositoryPermission)
.where(RepositoryPermission.team == team))
def list_robot_permissions(robot_name): def list_robot_permissions(robot_name):
return (RepositoryPermission return (RepositoryPermission
.select(RepositoryPermission, User, Repository) .select(RepositoryPermission, User, Repository)

View file

@ -1,7 +1,9 @@
from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite from data.database import Team, TeamMember, TeamRole, User, TeamMemberInvite, RepositoryPermission
from data.model import (DataModelException, InvalidTeamException, UserAlreadyInTeam, from data.model import (DataModelException, InvalidTeamException, UserAlreadyInTeam,
InvalidTeamMemberException, user, _basequery) InvalidTeamMemberException, user, _basequery)
from util.validation import validate_username from util.validation import validate_username
from peewee import fn, JOIN_LEFT_OUTER
from util.morecollections import AttrDict
def create_team(name, org_obj, team_role_name, description=''): def create_team(name, org_obj, team_role_name, description=''):
@ -186,7 +188,50 @@ def get_matching_teams(team_prefix, organization):
def get_teams_within_org(organization): def get_teams_within_org(organization):
return Team.select().where(Team.organization == organization) """ Returns a AttrDict of team info (id, name, description), its role under the org,
the number of repositories on which it has permission, and the number of members.
"""
query = (Team.select()
.where(Team.organization == organization)
.join(TeamRole))
def _team_view(team):
return {
'id': team.id,
'name': team.name,
'description': team.description,
'role_name': team.role.name,
'repo_count': 0,
'member_count': 0,
}
teams = {team.id: _team_view(team) for team in query}
if not teams:
# Just in case. Should ideally never happen.
return []
# Add repository permissions count.
permission_tuples = (RepositoryPermission.select(RepositoryPermission.team,
fn.Count(RepositoryPermission.id))
.where(RepositoryPermission.team << teams.keys())
.group_by(RepositoryPermission.team)
.tuples())
for perm_tuple in permission_tuples:
teams[perm_tuple[0]]['repo_count'] = perm_tuple[1]
# Add the member count.
members_tuples = (TeamMember.select(TeamMember.team,
fn.Count(TeamMember.id))
.where(TeamMember.team << teams.keys())
.group_by(TeamMember.team)
.tuples())
for member_tuple in members_tuples:
teams[member_tuple[0]]['member_count'] = member_tuple[1]
return [AttrDict(team_info) for team_info in teams.values()]
def get_user_teams_within_org(username, organization): def get_user_teams_within_org(username, organization):

View file

@ -8,13 +8,12 @@ import features
from app import billing as stripe, avatar from app import billing as stripe, avatar
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
related_user_resource, internal_only, require_user_admin, log_action, related_user_resource, internal_only, require_user_admin, log_action,
show_if, path_param, require_scope) show_if, path_param, require_scope)
from endpoints.exception import Unauthorized, NotFound from endpoints.exception import Unauthorized, NotFound
from endpoints.api.team import team_view
from endpoints.api.user import User, PrivateRepositories from endpoints.api.user import User, PrivateRepositories
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission, from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
CreateRepositoryPermission) CreateRepositoryPermission, ViewTeamPermission)
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
from auth import scopes from auth import scopes
from data import model from data import model
@ -24,6 +23,18 @@ from data.billing import get_plan
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def team_view(orgname, team):
return {
'name': team.name,
'description': team.description,
'role': team.role_name,
'avatar': avatar.get_data_for_team(team),
'can_view': ViewTeamPermission(orgname, team.name).can(),
'repo_count': team.repo_count,
'member_count': team.member_count,
}
def org_view(o, teams): def org_view(o, teams):
is_admin = AdministerOrganizationPermission(o.username).can() is_admin = AdministerOrganizationPermission(o.username).can()

View file

@ -5,7 +5,7 @@ from flask import request
import features import features
from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error, from endpoints.api import (resource, nickname, ApiResource, validate_json_request, request_error,
log_action, internal_only, require_scope, path_param, query_param, log_action, internal_only, require_scope, path_param, query_param,
truthy_bool, parse_args, require_user_admin, show_if) truthy_bool, parse_args, require_user_admin, show_if)
from endpoints.exception import Unauthorized, NotFound from endpoints.exception import Unauthorized, NotFound
from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission from auth.permissions import AdministerOrganizationPermission, ViewTeamPermission
@ -15,6 +15,15 @@ from data import model
from util.useremails import send_org_invite_email from util.useremails import send_org_invite_email
from app import avatar from app import avatar
def permission_view(permission):
return {
'repository': {
'name': permission.repository.name,
'is_public': permission.repository.visibility.name == 'public'
},
'role': permission.role.name
}
def try_accept_invite(code, user): def try_accept_invite(code, user):
(team, inviter) = model.team.confirm_team_invite(code, user) (team, inviter) = model.team.confirm_team_invite(code, user)
@ -346,6 +355,30 @@ class InviteTeamMember(ApiResource):
raise Unauthorized() raise Unauthorized()
@resource('/v1/organization/<orgname>/team/<teamname>/permissions')
@path_param('orgname', 'The name of the organization')
@path_param('teamname', 'The name of the team')
class TeamPermissions(ApiResource):
""" Resource for listing the permissions an org's team has in the system. """
@nickname('getTeamPermissions')
def get(self, orgname, teamname):
""" Returns the list of repository permissions for the org's team. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
try:
team = model.team.get_organization_team(orgname, teamname)
except model.InvalidTeamException:
raise NotFound()
permissions = model.permission.list_team_permissions(team)
return {
'permissions': [permission_view(permission) for permission in permissions]
}
raise Unauthorized()
@resource('/v1/teaminvite/<code>') @resource('/v1/teaminvite/<code>')
@internal_only @internal_only
@show_if(features.MAILING) @show_if(features.MAILING)

View file

@ -15,39 +15,3 @@
margin-left: 4px; margin-left: 4px;
margin-right: 4px; margin-right: 4px;
} }
.create-entity-dialog-element label {
margin-top: 4px;
}
.create-entity-dialog-element .co-table {
margin-top: 20px;
}
.create-entity-dialog-element .fa-hdd-o {
margin-right: 4px;
vertical-align: middle;
}
.create-entity-dialog-element .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.create-entity-dialog-element .co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.create-entity-dialog-element .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.create-entity-dialog-element label .avatar {
vertical-align: text-bottom;
margin-left: 4px;
}

View file

@ -0,0 +1,35 @@
.set-repo-permissions-element label {
margin-top: 4px;
}
.set-repo-permissions-element .co-table {
margin-top: 20px;
}
.set-repo-permissions-element .fa-hdd-o {
margin-right: 4px;
vertical-align: middle;
}
.set-repo-permissions-element .co-filter-box {
display: block;
float: right;
margin-bottom: 20px;
}
.set-repo-permissions-element .co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.set-repo-permissions-element .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
}
.set-repo-permissions-element label .avatar {
vertical-align: text-bottom;
margin-left: 4px;
}

View file

@ -1,5 +1,19 @@
.teams-manager .popup-input-button { .teams-manager .co-filter-box {
display: block;
float: right; float: right;
margin-bottom: 20px;
}
.teams-manager.co-filter-box .filter-message {
left: -180px;
top: 4px;
}
.teams-manager .co-filter-box input {
width: 100%;
padding-top: 2px;
padding-bottom: 2px;
height: 28px;
} }
.teams-manager .manager-header { .teams-manager .manager-header {
@ -20,6 +34,10 @@
color: #ccc; color: #ccc;
} }
.teams-manager .co-table .avatar {
margin-right: 6px;
}
.teams-manager .cor-confirm-dialog .entity-reference .avatar { .teams-manager .cor-confirm-dialog .entity-reference .avatar {
margin-left: 4px; margin-left: 4px;
margin-right: 0px; margin-right: 0px;
@ -36,14 +54,20 @@
} }
@media (max-width: 767px) { @media (max-width: 767px) {
.teams-manager .control-col { .teams-manager .co-filter-box {
padding-left: 55px; display: block;
padding-bottom: 10px; float: none;
} }
} }
.teams-manager .header-col .info-icon { .teams-manager .info-icon {
margin-left: 4px;
}
.teams-manager .popover-content {
color: black;
font-size: 16px; font-size: 16px;
text-transform: none;
} }
.teams-manager .header-col .header-text { .teams-manager .header-col .header-text {

View file

@ -634,13 +634,6 @@ i.toggle-icon:hover {
padding: 6px; padding: 6px;
} }
.info-icon {
display: inline-block;
float: right;
vertical-align: middle;
font-size: 20px;
}
.accordion-toggle { .accordion-toggle {
cursor: pointer; cursor: pointer;
text-decoration: none !important; text-decoration: none !important;

View file

@ -8,81 +8,25 @@
<i class="fa {{ entityIcon }}"></i> <i class="fa {{ entityIcon }}"></i>
Create {{ entityTitle }} Create {{ entityTitle }}
</h4> </h4>
<h4 class="modal-title" ng-show="view == 'addperms' || view == 'addingperms'"> <h4 class="modal-title" ng-show="view == 'setperms' || view == 'settingperms'">
Add permissions for <i class="fa {{ entityIcon }}"></i> {{ entity.name }} Add permissions for <i class="fa {{ entityIcon }}"></i> {{ entity.name }}
</h4> </h4>
</div> <!-- /.model-header --> </div> <!-- /.model-header -->
<div class="modal-body" ng-show="view == 'creating' || view == 'addingperms'"> <div class="modal-body" ng-show="view == 'creating' || view == 'settingperms'">
<div class="cor-loader"></div> <div class="cor-loader"></div>
</div> </div>
<div class="modal-body co-modal-body-scrollable" ng-show="view == 'addperms'"> <div class="modal-body co-modal-body-scrollable" ng-show="view == 'setperms'">
<span class="co-filter-box"> <div class="set-repo-permissions"
<span class="filter-message" ng-if="options.filter"> namespace="info.namespace"
Showing {{ orderedRepositories.entries.length }} of {{ repositories.length }} repositories entity-name="entity.name"
</span> entity-kind="entityKind"
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Repositories..."> has-changed-repositories="context.hasChangedRepositories"
</span> has-checked-repositories="context.hasCheckedRepositories"
repositories-loaded="repositoriesLoaded(repositories)"
<label> setting-permissions="settingPermissions()"
Select repositories in permissions-set="permissionsSet(repositories)"
<span class="avatar" size="16" data="namespace.avatar"></span> set-permissions="context.setPermissionsCounter"
{{ info.namespace }}: ng-if="entity"></div>
</label>
<table class="co-table" style="margin-bottom: 210px;">
<thead>
<td class="checkbox-col checkbox-menu-col">
<span class="cor-checkable-menu" controller="checkedRepos">
<div class="cor-checkable-menu-item" item-filter="allRepositoriesFilter(item)">
<i class="fa fa-check-square-o"></i>All Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="noRepositoriesFilter(item)">
<i class="fa fa-square-o"></i>No Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="missingPermsRepositoriesFilter(item)">
<i class="fa fa-circle-o"></i>Missing Permissions
</div>
</span>
</td>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('name', options)">Repository Name</a>
</td>
<td>Permission</td>
<td ng-class="TableService.tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('last_modified_datetime', options)">Last Updated</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="repo in orderedRepositories.visibleEntries"
ng-class="checkedRepos.isChecked(repo, checkedRepos.checked) ? 'checked' : ''"
bindonce>
<td class="offset-check-col">
<span class="cor-checkable-item" controller="checkedRepos" item="repo"></span>
</td>
<td>
<i class="fa fa-hdd-o"></i>
<span bo-text="repo.name"></span>
</td>
<td>
<span class="role-group small" current-role="repo.permission"
roles="repoRolesOrNone"
role-changed="setRole(role, repo)"></span>
</td>
<td>
<span ng-if="repo.last_modified">
{{ repo.last_modified * 1000 | amCalendar }}
</span>
<span class="empty" ng-if="!repo.last_modified">(Empty Repository)</span>
</td>
</tr>
</table>
<div class="empty" ng-if="!orderedRepositories.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching repositories found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div> </div>
<div class="modal-body" ng-show="view == 'enterName'"> <div class="modal-body" ng-show="view == 'enterName'">
<form name="enterNameForm" ng-submit="createEntity()"> <form name="enterNameForm" ng-submit="createEntity()">
@ -94,9 +38,9 @@
</div> </div>
</form> </form>
</div> <!-- /.modal-body --> </div> <!-- /.modal-body -->
<div class="modal-footer" ng-show="view == 'addperms'"> <div class="modal-footer" ng-show="view == 'setperms'">
<button type="button" class="btn btn-primary" ng-click="addPermissions()" <button type="button" class="btn btn-primary" ng-click="setPermissions()"
ng-show="checkedRepos.checked.length">Add permissions</button> ng-show="context.hasCheckedRepositories">Add permissions</button>
<button type="button" class="btn btn-default" ng-click="hide()">Close</button> <button type="button" class="btn btn-default" ng-click="hide()">Close</button>
</div> <!-- /.footer-body --> </div> <!-- /.footer-body -->
<div class="modal-footer" ng-show="view == 'enterName'"> <div class="modal-footer" ng-show="view == 'enterName'">

View file

@ -35,99 +35,66 @@
<thead> <thead>
<td>Robot Account Name</td> <td>Robot Account Name</td>
<td ng-if="organization">Teams</td> <td ng-if="organization">Teams</td>
<td>Direct Repository Permissions</td> <td>Repositories</td>
<td class="options-col"></td> <td class="options-col"></td>
</thead> </thead>
<tbody ng-repeat="robotInfo in robots | filter:robotFilter | orderBy:getShortenedRobotName" bindonce> <tr ng-repeat="robotInfo in robots | filter:robotFilter | orderBy:getShortenedRobotName" bindonce>
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'"> <td class="robot">
<td class="robot"> <i class="fa ci-robot hidden-xs"></i>
<i class="fa ci-robot hidden-xs"></i> <a ng-click="showRobot(robotInfo)">
<a ng-click="showRobot(robotInfo)"> <span class="prefix" bo-text="getPrefix(robotInfo.name) + '+'"></span><span bo-text="getShortenedName(robotInfo.name)"></span>
<span class="prefix" bo-text="getPrefix(robotInfo.name) + '+'"></span><span bo-text="getShortenedName(robotInfo.name)"></span> </a>
</a> </td>
</td> <td bo-if="organization">
<td bo-if="organization"> <span class="empty" bo-if="robotInfo.teams.length == 0">
<span class="empty" bo-if="robotInfo.teams.length == 0"> No teams
No teams </span>
</span> <span class="empty" bo-if="robotInfo.teams.length > 0">
<span class="empty" bo-if="robotInfo.teams.length > 0"> <span ng-repeat="team in robotInfo.teams"
<span ng-repeat="team in robotInfo.teams" data-title="Team {{ team.name }}" bs-tooltip>
data-title="Team {{ team.name }}" bs-tooltip> <span class="anchor" is-only-text="!organization.is_admin" href="/organization/{{ organization.name }}/teams/{{ team.name }}">
<span class="anchor" is-only-text="!organization.is_admin" href="/organization/{{ organization.name }}/teams/{{ team.name }}"> <span class="avatar" size="24" data="team.avatar"></span>
<span class="avatar" size="24" data="team.avatar"></span>
</span>
</span> </span>
</span> </span>
</td> </span>
<td> </td>
<span class="empty" bo-if="robotInfo.repositories.length == 0"> <td>
<span class="empty" ng-if="robotInfo.repositories.length == 0">
<a is-only-text="!organization.is_admin" ng-click="setPermissions(robotInfo)">
No repositories No repositories
</span> </a>
</span>
<span class="member-perm-summary" bo-if="robotInfo.repositories.length > 0"> <span class="member-perm-summary" ng-if="robotInfo.repositories.length > 0">
<span ng-click="showPermissions(robotInfo)"> <a is-only-text="!organization.is_admin" ng-click="setPermissions(robotInfo)">
<i class="fa" {{ robotInfo.repositories.length }}
ng-class="robotInfo.showing_permissions ? 'fa-caret-down' : 'fa-caret-right'" <span ng-if="robotInfo.repositories.length == 1">repository</span>
data-title="View Permissions List" bs-tooltip></i> <span ng-if="robotInfo.repositories.length > 1">repositories</span>
</span> </a>
</span>
<a class="hidden-xs" is-only-text="!organization.is_admin" </td>
ng-click="showPermissions(robotInfo)"> <td class="options-col">
<span bo-text="robotInfo.repositories.length"></span> <span class="cor-options-menu">
<span bo-if="robotInfo.repositories.length == 1">repository</span> <span class="cor-option" option-click="showRobot(robotInfo)">
<span bo-if="robotInfo.repositories.length > 1">repositories</span> <i class="fa fa-key"></i> View Credentials
</a>
<span class="visible-xs">
<span bo-text="robotInfo.repositories.length"></span>
<span bo-if="robotInfo.repositories.length == 1">repository</span>
<span bo-if="robotInfo.repositories.length > 1">repositories</span>
</span>
</span> </span>
</td> <span class="cor-option" option-click="setPermissions(robotInfo)">
<td class="options-col"> <i class="fa fa-hdd-o"></i> Set Repository Permissions
<span class="cor-options-menu">
<span class="cor-option" option-click="showRobot(robotInfo)">
<i class="fa fa-key"></i> View Credentials
</span>
<span class="cor-option" option-click="askDeleteRobot(robotInfo)">
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
</span>
</span> </span>
</td> <span class="cor-option" option-click="askDeleteRobot(robotInfo)">
</tr> <i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
<tr ng-if="robotInfo.showing_permissions"> </span>
<td class="permissions-display-row" colspan="4"> </span>
<span class="cor-loader" ng-if="robotInfo.loading_permissions"></span> </td>
<div class="permissions-table-wrapper"> </tr>
<table class="permissions-table" ng-if="!robotInfo.loading_permissions">
<thead>
<td>Repository</td>
<td>Permission</td>
</thead>
<tr ng-repeat="permission in robotInfo.permissions">
<td>
<span class="repo-icon repo-circle no-background" repo="permission.repository"></span>
<a ng-href="/repository/{{ getPrefix(robotInfo.name) }}/{{ permission.repository.name }}?tab=settings">{{ getPrefix(robotInfo.name) }}/{{ permission.repository.name }}</a>
</td>
<td>
<div class="btn-group btn-group-sm">
<span class="role-group"
current-role="permission.role"
roles="repoRoles"
read-only="true"></span>
</div>
</td>
</tr>
</table>
</div>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
<!-- Set repo permissions dialog -->
<div class="set-repo-permissions-dialog" info="setRepoPermissionsInfo"
permissions-set="handlePermissionsSet(info, repositories)"></div>
<div class="create-robot-dialog" info="createRobotInfo" robot-created="robotCreated()"></div> <div class="create-robot-dialog" info="createRobotInfo" robot-created="robotCreated()"></div>
<div class="robot-credentials-dialog" info="robotDisplayInfo"></div> <div class="robot-credentials-dialog" info="robotDisplayInfo"></div>
</div> </div>

View file

@ -0,0 +1,35 @@
<div class="set-repo-permissions-dialog-element">
<div class="modal fade co-dialog wider">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" ng-click="hide()" aria-hidden="true">&times;</button>
<h4 class="modal-title">
Set permissions for <i class="fa {{ info.entityIcon }}"></i> {{ info.entityName }}
</h4>
</div> <!-- /.model-header -->
<div class="modal-body" ng-show="working">
<div class="cor-loader"></div>
</div>
<div class="modal-body co-modal-body-scrollable" ng-show="!working" style="padding-bottom: 210px;">
<div class="set-repo-permissions"
namespace="context.info.namespace"
entity-name="context.info.entityName"
entity-kind="context.info.entityKind"
has-changed-repositories="context.hasChangedRepositories"
has-checked-repositories="context.hasCheckedRepositories"
setting-permissions="settingPermissions()"
permissions-set="permissionsSetComplete(repositories)"
set-permissions="setPermissionsCounter"
ng-if="context.info.namespace && context.info.entityName && context.info.entityKind">
</div>
</div> <!-- /.modal-body -->
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-primary" ng-click="setPermissions()"
ng-show="context.hasChangedRepositories">Update permissions</button>
<button type="button" class="btn btn-default" ng-click="hide()">Close</button>
</div> <!-- /.footer-body -->
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,69 @@
<div class="set-repo-permissions-element">
<span class="co-filter-box">
<span class="filter-message" ng-if="options.filter">
Showing {{ orderedRepositories.entries.length }} of {{ repositories.length }} repositories
</span>
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Repositories...">
</span>
<label>
Select repositories in
<span class="avatar" size="16" data="namespaceInfo.avatar"></span>
{{ namespace }}:
</label>
<table class="co-table">
<thead>
<td class="checkbox-col checkbox-menu-col">
<span class="cor-checkable-menu" controller="checkedRepos">
<div class="cor-checkable-menu-item" item-filter="allRepositoriesFilter(item)">
<i class="fa fa-check-square-o"></i>All Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="noRepositoriesFilter(item)">
<i class="fa fa-square-o"></i>No Repositories
</div>
<div class="cor-checkable-menu-item" item-filter="missingPermsRepositoriesFilter(item)">
<i class="fa fa-circle-o"></i>Missing Permissions
</div>
</span>
</td>
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('name', options)">Repository Name</a>
</td>
<td>Permission</td>
<td ng-class="TableService.tablePredicateClass('last_modified_datetime', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('last_modified_datetime', options)">Last Updated</a>
</td>
</thead>
<tr class="co-checkable-row"
ng-repeat="repo in orderedRepositories.visibleEntries"
ng-class="checkedRepos.isChecked(repo, checkedRepos.checked) ? 'checked' : ''"
bindonce>
<td class="offset-check-col">
<span class="cor-checkable-item" controller="checkedRepos" item="repo"></span>
</td>
<td>
<i class="fa fa-hdd-o"></i>
<span bo-text="repo.name"></span>
</td>
<td>
<span class="role-group small" current-role="repo.permission"
roles="repoRolesOrNone"
role-changed="setRole(role, repo)"></span>
</td>
<td>
<span ng-if="repo.last_modified">
{{ repo.last_modified * 1000 | amCalendar }}
</span>
<span class="empty" ng-if="!repo.last_modified">(Empty Repository)</span>
</td>
</tr>
</table>
<div class="empty" ng-if="!orderedRepositories.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching repositories found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div>
</div>

View file

@ -1,6 +1,14 @@
<div class="teams-manager-element"> <div class="teams-manager-element">
<div class="feedback-bar" feedback="feedback"></div> <div class="feedback-bar" feedback="feedback"></div>
<div class="manager-header" header-title="Teams and Membership"> <div class="manager-header" header-title="Teams and Membership">
<div class="tab-header-controls visible-xs">
<button class="btn btn-primary"
ng-show="organization.is_admin"
ng-click="askCreateTeam()">
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
</button>
</div>
<div class="tab-header-controls hidden-xs"> <div class="tab-header-controls hidden-xs">
<div class="btn-group btn-group-sm" ng-show="organization.is_admin"> <div class="btn-group btn-group-sm" ng-show="organization.is_admin">
<button class="btn" <button class="btn"
@ -17,76 +25,100 @@
<!-- Teams List --> <!-- Teams List -->
<div ng-show="!showingMembers"> <div ng-show="!showingMembers">
<div class="row" style="margin-left: 0px; margin-right: 0px;"> <button class="btn btn-primary hidden-xs"
<button class="btn btn-primary hidden-xs" ng-show="organization.is_admin"
ng-show="organization.is_admin" style="margin-bottom: 10px; "
style="margin-bottom: 10px; float: right;" ng-click="askCreateTeam()">
ng-click="askCreateTeam()"> <i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team </button>
</button>
</div>
<div class="row hidden-xs"> <span class="co-filter-box">
<div class="col-sm-7 col-md-8 header-col"> <span class="filter-message" ng-if="options.filter">
<span class="header-text">Team Summary</span> Showing {{ orderedTeams.entries.length }} of {{ teams.length }} teams
</div> </span>
<div class="col-md-4 col-sm-5 header-col" ng-show="organization.is_admin"> <input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Teams...">
<span class="header-text">Team Permissions</span> </span>
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"
data-html="true"
data-trigger="hover"
bs-popover></i>
</div>
</div>
<div class="team-listing" ng-repeat="team in orderedTeams"> <table class="co-table" style="margin-top: 10px;">
<div id="team-{{team.name}}" class="row"> <thead>
<div class="col-sm-7 col-md-8"> <td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
<div class="team-title"> <a ng-click="TableService.orderBy('name', options)">Team Name</a>
<span class="avatar" data="team.avatar" size="30"></span> </td>
<span ng-show="team.can_view"> <td ng-class="TableService.tablePredicateClass('member_count', options.predicate, options.reverse)">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a> <a ng-click="TableService.orderBy('member_count', options)">Members</a>
</span> </td>
<span ng-show="!team.can_view"> <td class="hidden-xs" ng-class="TableService.tablePredicateClass('repo_count', options.predicate, options.reverse)">
{{ team.name }} <a ng-click="TableService.orderBy('repo_count', options)">Repositories</a>
</span> </td>
</div> <td ng-class="TableService.tablePredicateClass('role_index', options.predicate, options.reverse)">
<a ng-click="TableService.orderBy('role_index', options)">Team Role</a>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div> <i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"
data-html="true"
data-trigger="hover"
bs-popover></i>
</td>
<td class="options-col"></td>
</thead>
<div class="team-member-list hidden-xs" ng-if="members[team.name]"> <tr class="co-checkable-row"
<div class="cor-loader" ng-if="!members[team.name].members"></div> ng-repeat="team in orderedTeams.visibleEntries"
<span class="team-member" bindonce>
ng-repeat="member in members[team.name].members | orderBy:'is_robot' | limitTo: 20"> <td style="white-space: nowrap;">
<span data-title="{{ member.name }}" bs-tooltip> <span class="avatar" data="team.avatar" size="24"></span>
<a href="/user/{{ member.name }}" ng-if="!member.is_robot"> <span bo-show="team.can_view">
<span class="avatar" data="member.avatar" size="26"></span> <a href="/organization/{{ organization.name }}/teams/{{ team.name }}"><span bo-text="team.name"></span></a>
</a> </span>
<i class="fa ci-robot fa-lg" ng-if="member.is_robot"></i> <span bo-show="!team.can_view" bo-text="team.name"></span>
</span> </td>
</span> <td>
<span class="team-member-more" <span bo-show="team.can_view">
ng-if="members[team.name].members.length > 20">+ {{ members[team.name].members.length - 20 }} more team members.</span> <a href="/organization/{{ organization.name }}/teams/{{ team.name }}"><span bo-text="team.member_count"></span> <span class="hidden-xs">member<span bo-if="team.member_count != 1">s</span></span></a>
<span class="team-member-more" </span>
ng-if="members[team.name].members && !members[team.name].members.length">(Empty Team)</span> <span bo-show="!team.can_view">
</div> <span bo-text="team.member_count"></span> <span class="hidden-xs">member<span bo-if="team.member_count != 1">s</span></span>
</div> </span>
</td>
<td class="hidden-xs">
<span class="empty" ng-if="team.repo_count == 0">
<a is-only-text="!organization.is_admin" ng-click="setRepoPermissions(team.name)">
No repositories
</a>
</span>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin"> <span class="member-perm-summary" ng-if="team.repo_count > 0">
<a is-only-text="!organization.is_admin" ng-click="setRepoPermissions(team.name)">
{{ team.repo_count }}
<span ng-if="team.repo_count == 1">repository</span>
<span ng-if="team.repo_count > 1">repositories</span>
</a>
</span>
</td>
<td>
<span class="role-group" current-role="team.role" pull-left="true" <span class="role-group" current-role="team.role" pull-left="true"
role-changed="setRole(role, team.name)" roles="teamRoles"></span> role-changed="setRole(role, team.name)" roles="teamRoles"></span>
</td>
<span class="cor-options-menu"> <td>
<span class="cor-options-menu" ng-show="organization.is_admin">
<span class="cor-option" option-click="viewTeam(team.name)"> <span class="cor-option" option-click="viewTeam(team.name)">
<i class="fa fa-user"></i> Manage Team Members <i class="fa fa-user"></i> Manage Team Members
</span> </span>
<span class="cor-option" option-click="setRepoPermissions(team.name)">
<i class="fa fa-hdd-o"></i> Set Repository Permissions
</span>
<span class="cor-option" option-click="askDeleteTeam(team.name)"> <span class="cor-option" option-click="askDeleteTeam(team.name)">
<i class="fa fa-times"></i> Delete Team {{ team.name }} <i class="fa fa-times"></i> Delete Team {{ team.name }}
</span> </span>
</span> </span>
</div> </td>
</div> </tr>
</table>
<div class="empty" ng-if="!orderedTeams.entries.length"
style="margin-top: 20px;">
<div class="empty-primary-msg">No matching teams found.</div>
<div class="empty-secondary-msg">Try expanding your filtering terms.</div>
</div> </div>
</div> </div>
@ -149,8 +181,13 @@
</table> </table>
</div> </div>
<!-- Create team dialog -->
<div class="create-team-dialog" info="createTeamInfo" team-created="handleTeamCreated(team)"></div> <div class="create-team-dialog" info="createTeamInfo" team-created="handleTeamCreated(team)"></div>
<!-- Set repo permissions dialog -->
<div class="set-repo-permissions-dialog" info="setRepoPermissionsInfo"
permissions-set="handlePermissionsSet(info, repositories)"></div>
<!-- Remove member confirm --> <!-- Remove member confirm -->
<div class="cor-confirm-dialog" <div class="cor-confirm-dialog"
dialog-context="removeMemberInfo" dialog-context="removeMemberInfo"

View file

@ -20,25 +20,9 @@ angular.module('quay').directive('createEntityDialog', function () {
'entityCreateCompleted': '&entityCreateCompleted' 'entityCreateCompleted': '&entityCreateCompleted'
}, },
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) { controller: function($scope, $element, ApiService, UIService, UserService) {
$scope.TableService = TableService; $scope.context = {
'setPermissionsCounter': 0
$scope.options = {
'predicate': 'last_modified_datetime',
'reverse': false,
'filter': ''
};
var handleRepoCheckChange = function() {
$scope.repositories.forEach(function(repo) {
if ($scope.checkedRepos.isChecked(repo)) {
if (repo['permission'] == 'none') {
repo['permission'] = 'read';
}
} else {
repo['permission'] = 'none';
}
});
}; };
$scope.$on('$destroy', function() { $scope.$on('$destroy', function() {
@ -47,16 +31,6 @@ angular.module('quay').directive('createEntityDialog', function () {
} }
}); });
$scope.setRole = function(role, repo) {
repo['permission'] = role;
if (role == 'none') {
$scope.checkedRepos.uncheckItem(repo);
} else {
$scope.checkedRepos.checkItem(repo);
}
};
$scope.hide = function() { $scope.hide = function() {
$element.find('.modal').modal('hide'); $element.find('.modal').modal('hide');
if ($scope.entity) { if ($scope.entity) {
@ -68,6 +42,7 @@ angular.module('quay').directive('createEntityDialog', function () {
$scope.show = function() { $scope.show = function() {
$scope.entityName = null; $scope.entityName = null;
$scope.entity = null; $scope.entity = null;
$scope.entityForPermissions = null;
$scope.creating = false; $scope.creating = false;
$scope.view = 'enterName'; $scope.view = 'enterName';
$scope.enterNameForm.$setPristine(true); $scope.enterNameForm.$setPristine(true);
@ -79,92 +54,13 @@ angular.module('quay').directive('createEntityDialog', function () {
document.body.appendChild($element[0]); document.body.appendChild($element[0]);
}; };
var setRepoState = function() {
if (!$scope.repositories) {
return;
}
$scope.orderedRepositories = TableService.buildOrderedItems($scope.repositories, $scope.options,
['name', 'permission'],
['last_modified_datetime']);
};
var entityCreateCallback = function(entity) { var entityCreateCallback = function(entity) {
$scope.entity = entity;
if (!entity || $scope.info.skip_permissions) { if (!entity || $scope.info.skip_permissions) {
$scope.entity = entity;
$scope.hide(); $scope.hide();
return; return;
} }
// Load the repositories under the entity's namespace.
var params = {
'namespace': $scope.info.namespace,
'last_modified': true
};
ApiService.listRepos(null, params).then(function(resp) {
$scope.view = 'addperms';
$scope.entity = entity;
var repos = [];
resp['repositories'].forEach(function(repo) {
repos.push({
'namespace': repo.namespace,
'name': repo.name,
'last_modified': repo.last_modified,
'last_modified_datetime': TableService.getReversedTimestamp(repo.last_modified),
'permission': 'none'
});
});
if (repos.length == 0) {
$scope.hide();
return;
}
$scope.repositories = repos;
$scope.checkedRepos = UIService.createCheckStateController($scope.repositories, 'name');
$scope.checkedRepos.listen(handleRepoCheckChange);
if ($scope.info.repository) {
repos.forEach(function(repo) {
if (repo['namespace'] == $scope.info.repository.namespace &&
repo['name'] == $scope.info.repository.name) {
$scope.checkedRepos.checkItem(repo);
$scope.options.filter = $scope.info.repository.name;
}
});
}
setRepoState();
}, ApiService.errorDisplay('Could not load repositories'));
};
$scope.addPermissions = function() {
$scope.view = 'addingperms';
var repos = $scope.checkedRepos.checked;
var counter = 0;
var addPerm = function() {
if (counter >= repos.length) {
$scope.hide();
return;
}
var repo = repos[counter];
RolesService.setRepositoryRole(repo, repo.permission, $scope.entityKind, $scope.entity.name,
function(status) {
if (status) {
counter++;
addPerm();
} else {
$scope.hide();
}
});
};
addPerm();
}; };
$scope.createEntity = function() { $scope.createEntity = function() {
@ -175,21 +71,27 @@ angular.module('quay').directive('createEntityDialog', function () {
}); });
}; };
$scope.allRepositoriesFilter = function(item) { $scope.permissionsSet = function(repositories) {
return true; $scope.entity['repo_count'] = repositories.length;
$scope.hide();
}; };
$scope.noRepositoriesFilter = function(item) { $scope.settingPermissions = function() {
return false; $scope.view = 'settingperms';
}; };
$scope.missingPermsRepositoriesFilter = function(item) { $scope.setPermissions = function() {
return !item.perm; $scope.context.setPermissionsCounter++;
}; };
$scope.$watch('options.predicate', setRepoState); $scope.repositoriesLoaded = function(repositories) {
$scope.$watch('options.reverse', setRepoState); if (repositories && !repositories.length) {
$scope.$watch('options.filter', setRepoState); $scope.hide();
return;
}
$scope.view = 'setperms';
};
$scope.$watch('entityNameRegex', function(r) { $scope.$watch('entityNameRegex', function(r) {
if (r) { if (r) {

View file

@ -33,15 +33,6 @@ angular.module('quay').directive('robotsManager', function () {
locationListener && locationListener(); locationListener && locationListener();
}); });
var loadRobotPermissions = function(info) {
var shortName = $scope.getShortenedName(info.name);
info.loading_permissions = true;
ApiService.getRobotPermissions($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
info.permissions = resp.permissions;
info.loading_permissions = false;
}, ApiService.errorDisplay('Could not load robot permissions'));
};
$scope.filterToRobot = function(robotName) { $scope.filterToRobot = function(robotName) {
if ($scope.robotFilter == robotName) { if ($scope.robotFilter == robotName) {
return; return;
@ -56,14 +47,6 @@ angular.module('quay').directive('robotsManager', function () {
$scope.robotFilter = robotName; $scope.robotFilter = robotName;
}; };
$scope.showPermissions = function(robotInfo) {
robotInfo.showing_permissions = !robotInfo.showing_permissions;
if (robotInfo.showing_permissions) {
loadRobotPermissions(robotInfo);
}
};
$scope.showRobot = function(info) { $scope.showRobot = function(info) {
$scope.robotDisplayInfo = { $scope.robotDisplayInfo = {
'name': info.name 'name': info.name
@ -126,6 +109,21 @@ angular.module('quay').directive('robotsManager', function () {
}); });
}; };
$scope.setPermissions = function(info) {
var namespace = $scope.organization ? $scope.organization.name : $scope.user.username;
$scope.setRepoPermissionsInfo = {
'namespace': namespace,
'entityName': info.name,
'entityKind': 'robot',
'entityIcon': 'ci-robot'
};
};
$scope.handlePermissionsSet = function(info, repositories) {
var index = $scope.findRobotIndexByName(info.entityName);
$scope.robots[index]['repositories'] = repositories;
};
$scope.robotCreated = function() { $scope.robotCreated = function() {
update(); update();
}; };

View file

@ -0,0 +1,60 @@
/**
* An element which displays a dialog for setting permissions for an entity to repositories under
* a namespace.
*/
angular.module('quay').directive('setRepoPermissionsDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/set-repo-permissions-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'info': '=info',
'permissionsSet': '&permissionsSet',
},
controller: function($scope, $element) {
$scope.setPermissionsCounter = 0;
$scope.loading = false;
$scope.context = {};
$scope.setPermissions = function() {
$scope.setPermissionsCounter++;
};
$scope.settingPermissions = function() {
$scope.working = true;
};
$scope.show = function() {
$scope.setPermissionsCounter = 0;
$scope.working = false;
$element.find('.modal').modal({});
};
$scope.hide = function() {
$scope.working = false;
$scope.context.info = null;
$scope.context.hasChangedRepositories = false;
$scope.context.hasCheckedRepositories = false;
$element.find('.modal').modal('hide');
};
$scope.permissionsSetComplete = function(repositories) {
$scope.hide();
$scope.permissionsSet({'repositories': repositories, 'info': $scope.info});
};
$scope.$watch('info', function(info) {
if (info) {
$scope.context.info = info;
$scope.show();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -0,0 +1,235 @@
/**
* An element which displays a table for setting permissions for an entity to repositories under
* a namespace.
*/
angular.module('quay').directive('setRepoPermissions', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/set-repo-permissions.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'namespace': '=namespace',
'entityName': '=entityName',
'entityKind': '=entityKind',
'setPermissions': '=setPermissions',
'hasCheckedRepositories': '=hasCheckedRepositories',
'hasChangedRepositories': '=hasChangedRepositories',
'repositoriesLoaded': '&repositoriesLoaded',
'settingPermissions': '&settingPermissions',
'permissionsSet': '&permissionsSet',
},
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
$scope.TableService = TableService;
$scope.options = {
'predicate': 'last_modified_datetime',
'reverse': false,
'filter': ''
};
$scope.repositories = null;
$scope.currentNamespace = null;
$scope.currentEntityName = null;
var checkForChanges = function() {
var hasChanges = false;
$scope.repositories.forEach(function(repo) {
if (repo['permission'] != repo['original_permission']) {
hasChanges = true;
}
});
$scope.hasCheckedRepositories = !!$scope.checkedRepos.checked.length;
$scope.hasChangedRepositories = hasChanges;
};
var handleRepoCheckChange = function() {
$scope.repositories.forEach(function(repo) {
if ($scope.checkedRepos.isChecked(repo)) {
if (repo['permission'] == 'none') {
repo['permission'] = 'read';
}
} else {
repo['permission'] = 'none';
}
});
checkForChanges();
};
var setRepoState = function() {
if (!$scope.repositories) {
return;
}
$scope.orderedRepositories = TableService.buildOrderedItems(
$scope.repositories, $scope.options,
['name', 'permission'],
['last_modified_datetime']);
};
var loadRepositoriesAndPermissions = function() {
if (!$scope.namespace || !$scope.entityName || !$scope.entityKind) {
return;
}
if (($scope.entityName == $scope.currentEntityName) &&
($scope.namespace == $scope.currentNamespace)) {
return;
}
$scope.currentNamespace = $scope.namespace;
$scope.currentEntityName = $scope.entityName;
// Load the repository permissions for the entity first. We then load the full repo list
// and compare.
RolesService.getRepoPermissions($scope.namespace, $scope.entityKind, $scope.entityName,
function(permissions) {
if (permissions == null) {
$scope.currentNamespace = null;
$scope.currentEntityName = null;
return;
}
var existingPermissionsMap = {};
permissions.forEach(function(existingPermission) {
existingPermissionsMap[existingPermission.repository.name] = existingPermission.role;
});
loadRepositories(existingPermissionsMap);
});
};
var loadRepositories = function(existingPermissionsMap) {
$scope.namespaceInfo = UserService.getNamespace($scope.namespace);
// Load the repositories under the entity's namespace, along with the current repo
// permissions for the entity.
var params = {
'namespace': $scope.namespace,
'last_modified': true
};
ApiService.listRepos(null, params).then(function(resp) {
$scope.currentNamespace = $scope.namespace;
var repos = [];
resp['repositories'].forEach(function(repo) {
var existingPermission = existingPermissionsMap[repo.name] || 'none';
repos.push({
'namespace': repo.namespace,
'name': repo.name,
'last_modified': repo.last_modified,
'last_modified_datetime': TableService.getReversedTimestamp(repo.last_modified),
'permission': existingPermission,
'original_permission': existingPermission
});
});
if (repos.length == 0) {
$scope.repositoriesLoaded({'repositories': repos});
return;
}
$scope.repositories = repos;
$scope.checkedRepos = UIService.createCheckStateController($scope.repositories, 'name');
repos.forEach(function(repo) {
if (repo.permission != 'none') {
$scope.checkedRepos.checkItem(repo);
}
});
$scope.checkedRepos.listen(handleRepoCheckChange);
setRepoState();
$scope.repositoriesLoaded({'repositories': repos});
}, ApiService.errorDisplay('Could not load repositories'));
};
var setPermissions = function() {
if (!$scope.checkedRepos || !$scope.namespace || !$scope.repositories) {
return;
}
$scope.settingPermissions();
var repos = $scope.repositories;
var counter = 0;
var setPerm = function() {
if (counter >= repos.length) {
$scope.permissionsSet({'repositories': $scope.checkedRepos.checked});
$scope.checkedRepos.setChecked([]);
return;
}
var repo = repos[counter];
if (repo['permission'] == repo['original_permission']) {
// Skip changing it.
counter++;
setPerm();
return;
}
RolesService.setRepositoryRole(repo, repo.permission, $scope.entityKind,
$scope.entityName, function(status) {
if (status) {
counter++;
setPerm();
}
});
};
setPerm();
};
$scope.setRole = function(role, repo) {
repo['permission'] = role;
if (role == 'none') {
$scope.checkedRepos.uncheckItem(repo);
} else {
$scope.checkedRepos.checkItem(repo);
}
checkForChanges();
};
$scope.allRepositoriesFilter = function(item) {
return true;
};
$scope.noRepositoriesFilter = function(item) {
return false;
};
$scope.missingPermsRepositoriesFilter = function(item) {
return !item.perm;
};
$scope.$watch('options.predicate', setRepoState);
$scope.$watch('options.reverse', setRepoState);
$scope.$watch('options.filter', setRepoState);
$scope.$watch('namespace', loadRepositoriesAndPermissions);
$scope.$watch('entityName', loadRepositoriesAndPermissions);
$scope.$watch('entityKind', loadRepositoriesAndPermissions);
$scope.$watch('setPermissions', function(value) {
if (value) {
setPermissions();
}
});
}
};
return directiveDefinitionObject;
});

View file

@ -12,7 +12,15 @@ angular.module('quay').directive('teamsManager', function () {
'organization': '=organization', 'organization': '=organization',
'isEnabled': '=isEnabled' 'isEnabled': '=isEnabled'
}, },
controller: function($scope, $element, ApiService, $timeout, UserService) { controller: function($scope, $element, ApiService, $timeout, UserService, TableService, UIService) {
$scope.TableService = TableService;
$scope.options = {
'predicate': 'ordered_team_index',
'reverse': false,
'filter': ''
};
$scope.teamRoles = [ $scope.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default' }, { 'id': 'member', 'title': 'Member', 'kind': 'default' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' }, { 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
@ -21,65 +29,42 @@ angular.module('quay').directive('teamsManager', function () {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
$scope.members = {}; $scope.teams = null;
$scope.orderedTeams = []; $scope.orderedTeams = null;
$scope.showingMembers = false; $scope.showingMembers = false;
$scope.fullMemberList = null; $scope.fullMemberList = null;
$scope.feedback = null; $scope.feedback = null;
$scope.createTeamInfo = null; $scope.createTeamInfo = null;
var loadTeamMembers = function() { var getRoleIndex = function(name) {
if (!$scope.organization || !$scope.isEnabled) { return; } for (var i = 0; i < $scope.teamRoles.length; ++i) {
if ($scope.teamRoles[i]['id'] == name) {
// Skip loading team members on mobile. return i;
if (!window.matchMedia('(min-width: 768px)').matches) { }
return;
} }
for (var name in $scope.organization.teams) { return -1;
if (!$scope.organization.teams.hasOwnProperty(name) || $scope.members[name]) { continue; }
// Load fully async to prevent it from blocking the UI.
(function(teamname) {
$timeout(function() {
loadMembersOfTeam(teamname);
}, 1);
})(name);
}
}; };
var loadMembersOfTeam = function(name) { var setTeamsState = function() {
var params = {
'orgname': $scope.organization.name,
'teamname': name
};
$scope.members[name] = {};
ApiService.getOrganizationTeamMembers(null, params).then(function(resp) {
$scope.members[name].members = resp.members;
}, function() {
delete $scope.members[name];
});
};
var loadOrderedTeams = function() {
if (!$scope.organization || !$scope.organization.ordered_teams || !$scope.isEnabled) { if (!$scope.organization || !$scope.organization.ordered_teams || !$scope.isEnabled) {
return; return;
} }
$scope.orderedTeams = []; $scope.teams = [];
$scope.organization.ordered_teams.map(function(name) { $scope.organization.ordered_teams.map(function(name, index) {
$scope.orderedTeams.push($scope.organization.teams[name]); var team = $scope.organization.teams[name];
team['ordered_team_index'] = $scope.organization.ordered_teams.length - index;
team['role_index'] = getRoleIndex(team['role']);
$scope.teams.push(team);
}); });
$scope.orderedTeams = TableService.buildOrderedItems(
$scope.teams, $scope.options,
['name'],
['ordered_team_index', 'member_count', 'repo_count', 'role_index']);
}; };
$scope.$watch('organization', loadOrderedTeams);
$scope.$watch('organization', loadTeamMembers);
$scope.$watch('isEnabled', loadOrderedTeams);
$scope.$watch('isEnabled', loadTeamMembers);
$scope.setRole = function(role, teamname) { $scope.setRole = function(role, teamname) {
var previousRole = $scope.organization.teams[teamname].role; var previousRole = $scope.organization.teams[teamname].role;
$scope.organization.teams[teamname].role = role; $scope.organization.teams[teamname].role = role;
@ -115,9 +100,9 @@ angular.module('quay').directive('teamsManager', function () {
$scope.handleTeamCreated = function(created) { $scope.handleTeamCreated = function(created) {
var teamname = created.name; var teamname = created.name;
created['member_count'] = 0;
$scope.organization.teams[teamname] = created; $scope.organization.teams[teamname] = created;
$scope.members[teamname] = {};
$scope.members[teamname].members = [];
$scope.organization.ordered_teams.push(teamname); $scope.organization.ordered_teams.push(teamname);
$scope.orderedTeams.push(created); $scope.orderedTeams.push(created);
@ -150,8 +135,8 @@ angular.module('quay').directive('teamsManager', function () {
$scope.organization.ordered_teams.splice(index, 1); $scope.organization.ordered_teams.splice(index, 1);
} }
loadOrderedTeams();
delete $scope.organization.teams[teamname]; delete $scope.organization.teams[teamname];
setTeamsState();
$scope.feedback = { $scope.feedback = {
'kind': 'success', 'kind': 'success',
@ -192,12 +177,7 @@ angular.module('quay').directive('teamsManager', function () {
ApiService.removeOrganizationMember(null, params).then(function(resp) { ApiService.removeOrganizationMember(null, params).then(function(resp) {
// Reset the state of the directive. // Reset the state of the directive.
$scope.members = {};
$scope.orderedTeams = [];
$scope.fullMemberList = null; $scope.fullMemberList = null;
loadOrderedTeams();
loadTeamMembers();
$scope.showMembers(true); $scope.showMembers(true);
callback(true); callback(true);
@ -215,6 +195,27 @@ angular.module('quay').directive('teamsManager', function () {
$scope.askRemoveMember = function(memberInfo) { $scope.askRemoveMember = function(memberInfo) {
$scope.removeMemberInfo = $.extend({}, memberInfo); $scope.removeMemberInfo = $.extend({}, memberInfo);
}; };
$scope.setRepoPermissions = function(teamName) {
$scope.setRepoPermissionsInfo = {
'namespace': $scope.organization.name,
'entityName': teamName,
'entityKind': 'team',
'entityIcon': 'fa-group'
};
};
$scope.handlePermissionsSet = function(info, repositories) {
var team = $scope.organization.teams[info.entityName];
team['repo_count'] = repositories.length;
};
$scope.$watch('organization', setTeamsState);
$scope.$watch('isEnabled', setTeamsState);
$scope.$watch('options.predicate', setTeamsState);
$scope.$watch('options.reverse', setTeamsState);
$scope.$watch('options.filter', setTeamsState);
} }
}; };

View file

@ -1,7 +1,8 @@
/** /**
* Service which defines the various role groups. * Service which defines the various role groups.
*/ */
angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', function(UtilService, Restangular, ApiService) { angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'ApiService', 'UserService',
function(UtilService, Restangular, ApiService, UserService) {
var roleService = {}; var roleService = {};
roleService.repoRolesOrNone = [ roleService.repoRolesOrNone = [
@ -20,14 +21,22 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access to the organization' } { 'id': 'admin', 'title': 'Admin', 'kind': 'primary', 'description': 'Full admin access to the organization' }
]; ];
var getPermissionEndpoint = function(repository, entityName, kind) { var getPermissionEndpoint = function(repository, entityName, entityKind) {
if (entityKind == 'robot') {
entityKind = 'user';
}
var namespace = repository.namespace; var namespace = repository.namespace;
var name = repository.name; var name = repository.name;
var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', kind, entityName); var url = UtilService.getRestUrl('repository', namespace, name, 'permissions', entityKind, entityName);
return Restangular.one(url); return Restangular.one(url);
}; };
roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) { roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) {
if (entityKind == 'robot') {
entityKind = 'user';
}
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) { var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false); callback(false);
}); });
@ -39,6 +48,15 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
}; };
roleService.setRepositoryRole = function(repository, role, entityKind, entityName, callback) { roleService.setRepositoryRole = function(repository, role, entityKind, entityName, callback) {
if (role == 'none') {
roleService.deleteRepositoryRole(repository, entityKind, entityName, callback);
return;
}
if (entityKind == 'robot') {
entityKind = 'user';
}
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) { var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
callback(false); callback(false);
}); });
@ -53,5 +71,30 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
}, errorDisplay); }, errorDisplay);
}; };
roleService.getRepoPermissions = function(namespace, entityKind, entityName, callback) {
var errorHandler = ApiService.errorDisplay('Could not load permissions', callback);
if (entityKind == 'team') {
var params = {
'orgname': namespace,
'teamname': entityName
};
ApiService.getTeamPermissions(null, params).then(function(resp) {
callback(resp.permissions);
}, errorHandler);
} else if (entityKind == 'robot') {
var parts = entityName.split('+');
var shortName = parts[1];
var orgname = UserService.isOrganization(namespace) ? namespace : null;
ApiService.getRobotPermissions(orgname, null, {'robot_shortname': shortName}).then(function(resp) {
callback(resp.permissions);
}, errorHandler);
} else {
throw Error('Unknown entity kind ' + entityKind);
}
};
return roleService; return roleService;
}]); }]);

View file

@ -10,7 +10,8 @@ from data import model
from initdb import setup_database_for_testing, finished_database_for_testing from initdb import setup_database_for_testing, finished_database_for_testing
from endpoints.api import api_bp, api from endpoints.api import api_bp, api
from endpoints.api.team import TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite from endpoints.api.team import (TeamMember, TeamMemberList, OrganizationTeam, TeamMemberInvite,
TeamPermissions)
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RevertTag from endpoints.api.tag import RepositoryTagImages, RepositoryTag, ListRepositoryTags, RevertTag
from endpoints.api.search import EntitySearch from endpoints.api.search import EntitySearch
from endpoints.api.image import RepositoryImage, RepositoryImageList from endpoints.api.image import RepositoryImage, RepositoryImageList
@ -679,6 +680,24 @@ class TestTeamMemberBuynlargeDevtableOwners(ApiTestCase):
self._run_test('DELETE', 400, 'devtable', None) self._run_test('DELETE', 400, 'devtable', None)
class TestTeamPermissionsBuynlarge(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(TeamPermissions, orgname="buynlarge", teamname="readers")
def test_get_anonymous(self):
self._run_test('GET', 401, None, None)
def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 403, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 200, 'devtable', None)
class TestTeamMemberListBuynlargeReaders(ApiTestCase): class TestTeamMemberListBuynlargeReaders(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)

View file

@ -28,7 +28,8 @@ from data import database, model
from data.database import RepositoryActionCount, Repository as RepositoryTable from data.database import RepositoryActionCount, Repository as RepositoryTable
from test.helpers import assert_action_logged from test.helpers import assert_action_logged
from endpoints.api.team import TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam from endpoints.api.team import (TeamMember, TeamMemberList, TeamMemberInvite, OrganizationTeam,
TeamPermissions)
from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags from endpoints.api.tag import RepositoryTagImages, RepositoryTag, RevertTag, ListRepositoryTags
from endpoints.api.search import EntitySearch, ConductSearch from endpoints.api.search import EntitySearch, ConductSearch
from endpoints.api.image import RepositoryImage, RepositoryImageList from endpoints.api.image import RepositoryImage, RepositoryImageList
@ -1158,6 +1159,16 @@ class TestDeleteOrganizationTeam(ApiTestCase):
self.assertEquals(msg, data['message']) self.assertEquals(msg, data['message'])
class TestTeamPermissions(ApiTestCase):
def test_team_permissions(self):
self.login(ADMIN_ACCESS_USER)
resp = self.getJsonResponse(TeamPermissions,
params=dict(orgname=ORGANIZATION, teamname='readers'))
self.assertEquals(1, len(resp['permissions']))
class TestGetOrganizationTeamMembers(ApiTestCase): class TestGetOrganizationTeamMembers(ApiTestCase):
def test_invalidteam(self): def test_invalidteam(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)