Merge pull request #1754 from coreos-inc/team-add-perms
Better UI and permissions handling for robots and teams
This commit is contained in:
commit
cd8b45e25b
21 changed files with 895 additions and 458 deletions
|
@ -5,6 +5,17 @@ from data.database import (RepositoryPermission, User, Repository, Visibility, R
|
|||
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):
|
||||
return (RepositoryPermission
|
||||
.select(RepositoryPermission, User, Repository)
|
||||
|
|
|
@ -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,
|
||||
InvalidTeamMemberException, user, _basequery)
|
||||
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=''):
|
||||
|
@ -186,7 +188,50 @@ def get_matching_teams(team_prefix, 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):
|
||||
|
|
|
@ -11,10 +11,9 @@ from endpoints.api import (resource, nickname, ApiResource, validate_json_reques
|
|||
related_user_resource, internal_only, require_user_admin, log_action,
|
||||
show_if, path_param, require_scope)
|
||||
from endpoints.exception import Unauthorized, NotFound
|
||||
from endpoints.api.team import team_view
|
||||
from endpoints.api.user import User, PrivateRepositories
|
||||
from auth.permissions import (AdministerOrganizationPermission, OrganizationMemberPermission,
|
||||
CreateRepositoryPermission)
|
||||
CreateRepositoryPermission, ViewTeamPermission)
|
||||
from auth.auth_context import get_authenticated_user
|
||||
from auth import scopes
|
||||
from data import model
|
||||
|
@ -24,6 +23,18 @@ from data.billing import get_plan
|
|||
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):
|
||||
is_admin = AdministerOrganizationPermission(o.username).can()
|
||||
|
|
|
@ -15,6 +15,15 @@ from data import model
|
|||
from util.useremails import send_org_invite_email
|
||||
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):
|
||||
(team, inviter) = model.team.confirm_team_invite(code, user)
|
||||
|
@ -346,6 +355,30 @@ class InviteTeamMember(ApiResource):
|
|||
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>')
|
||||
@internal_only
|
||||
@show_if(features.MAILING)
|
||||
|
|
|
@ -15,39 +15,3 @@
|
|||
margin-left: 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;
|
||||
}
|
35
static/css/directives/ui/set-repo-permissions.css
Normal file
35
static/css/directives/ui/set-repo-permissions.css
Normal 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;
|
||||
}
|
|
@ -1,5 +1,19 @@
|
|||
.teams-manager .popup-input-button {
|
||||
.teams-manager .co-filter-box {
|
||||
display: block;
|
||||
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 {
|
||||
|
@ -20,6 +34,10 @@
|
|||
color: #ccc;
|
||||
}
|
||||
|
||||
.teams-manager .co-table .avatar {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.teams-manager .cor-confirm-dialog .entity-reference .avatar {
|
||||
margin-left: 4px;
|
||||
margin-right: 0px;
|
||||
|
@ -36,14 +54,20 @@
|
|||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.teams-manager .control-col {
|
||||
padding-left: 55px;
|
||||
padding-bottom: 10px;
|
||||
.teams-manager .co-filter-box {
|
||||
display: block;
|
||||
float: none;
|
||||
}
|
||||
}
|
||||
|
||||
.teams-manager .header-col .info-icon {
|
||||
.teams-manager .info-icon {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.teams-manager .popover-content {
|
||||
color: black;
|
||||
font-size: 16px;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.teams-manager .header-col .header-text {
|
||||
|
|
|
@ -634,13 +634,6 @@ i.toggle-icon:hover {
|
|||
padding: 6px;
|
||||
}
|
||||
|
||||
.info-icon {
|
||||
display: inline-block;
|
||||
float: right;
|
||||
vertical-align: middle;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.accordion-toggle {
|
||||
cursor: pointer;
|
||||
text-decoration: none !important;
|
||||
|
|
|
@ -8,81 +8,25 @@
|
|||
<i class="fa {{ entityIcon }}"></i>
|
||||
Create {{ entityTitle }}
|
||||
</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 }}
|
||||
</h4>
|
||||
</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>
|
||||
<div class="modal-body co-modal-body-scrollable" ng-show="view == 'addperms'">
|
||||
<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="namespace.avatar"></span>
|
||||
{{ info.namespace }}:
|
||||
</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 class="modal-body co-modal-body-scrollable" ng-show="view == 'setperms'">
|
||||
<div class="set-repo-permissions"
|
||||
namespace="info.namespace"
|
||||
entity-name="entity.name"
|
||||
entity-kind="entityKind"
|
||||
has-changed-repositories="context.hasChangedRepositories"
|
||||
has-checked-repositories="context.hasCheckedRepositories"
|
||||
repositories-loaded="repositoriesLoaded(repositories)"
|
||||
setting-permissions="settingPermissions()"
|
||||
permissions-set="permissionsSet(repositories)"
|
||||
set-permissions="context.setPermissionsCounter"
|
||||
ng-if="entity"></div>
|
||||
</div>
|
||||
<div class="modal-body" ng-show="view == 'enterName'">
|
||||
<form name="enterNameForm" ng-submit="createEntity()">
|
||||
|
@ -94,9 +38,9 @@
|
|||
</div>
|
||||
</form>
|
||||
</div> <!-- /.modal-body -->
|
||||
<div class="modal-footer" ng-show="view == 'addperms'">
|
||||
<button type="button" class="btn btn-primary" ng-click="addPermissions()"
|
||||
ng-show="checkedRepos.checked.length">Add permissions</button>
|
||||
<div class="modal-footer" ng-show="view == 'setperms'">
|
||||
<button type="button" class="btn btn-primary" ng-click="setPermissions()"
|
||||
ng-show="context.hasCheckedRepositories">Add permissions</button>
|
||||
<button type="button" class="btn btn-default" ng-click="hide()">Close</button>
|
||||
</div> <!-- /.footer-body -->
|
||||
<div class="modal-footer" ng-show="view == 'enterName'">
|
||||
|
|
|
@ -35,12 +35,11 @@
|
|||
<thead>
|
||||
<td>Robot Account Name</td>
|
||||
<td ng-if="organization">Teams</td>
|
||||
<td>Direct Repository Permissions</td>
|
||||
<td>Repositories</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<tbody ng-repeat="robotInfo in robots | filter:robotFilter | orderBy:getShortenedRobotName" bindonce>
|
||||
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
|
||||
<tr ng-repeat="robotInfo in robots | filter:robotFilter | orderBy:getShortenedRobotName" bindonce>
|
||||
<td class="robot">
|
||||
<i class="fa ci-robot hidden-xs"></i>
|
||||
<a ng-click="showRobot(robotInfo)">
|
||||
|
@ -61,28 +60,18 @@
|
|||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="empty" bo-if="robotInfo.repositories.length == 0">
|
||||
<span class="empty" ng-if="robotInfo.repositories.length == 0">
|
||||
<a is-only-text="!organization.is_admin" ng-click="setPermissions(robotInfo)">
|
||||
No repositories
|
||||
</span>
|
||||
|
||||
<span class="member-perm-summary" bo-if="robotInfo.repositories.length > 0">
|
||||
<span ng-click="showPermissions(robotInfo)">
|
||||
<i class="fa"
|
||||
ng-class="robotInfo.showing_permissions ? 'fa-caret-down' : 'fa-caret-right'"
|
||||
data-title="View Permissions List" bs-tooltip></i>
|
||||
</span>
|
||||
|
||||
<a class="hidden-xs" is-only-text="!organization.is_admin"
|
||||
ng-click="showPermissions(robotInfo)">
|
||||
<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>
|
||||
</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 class="member-perm-summary" ng-if="robotInfo.repositories.length > 0">
|
||||
<a is-only-text="!organization.is_admin" ng-click="setPermissions(robotInfo)">
|
||||
{{ robotInfo.repositories.length }}
|
||||
<span ng-if="robotInfo.repositories.length == 1">repository</span>
|
||||
<span ng-if="robotInfo.repositories.length > 1">repositories</span>
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
<td class="options-col">
|
||||
|
@ -90,43 +79,21 @@
|
|||
<span class="cor-option" option-click="showRobot(robotInfo)">
|
||||
<i class="fa fa-key"></i> View Credentials
|
||||
</span>
|
||||
<span class="cor-option" option-click="setPermissions(robotInfo)">
|
||||
<i class="fa fa-hdd-o"></i> Set Repository Permissions
|
||||
</span>
|
||||
<span class="cor-option" option-click="askDeleteRobot(robotInfo)">
|
||||
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="robotInfo.showing_permissions">
|
||||
<td class="permissions-display-row" colspan="4">
|
||||
<span class="cor-loader" ng-if="robotInfo.loading_permissions"></span>
|
||||
<div class="permissions-table-wrapper">
|
||||
<table class="permissions-table" ng-if="!robotInfo.loading_permissions">
|
||||
<thead>
|
||||
<td>Repository</td>
|
||||
<td>Permission</td>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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="robot-credentials-dialog" info="robotDisplayInfo"></div>
|
||||
|
|
35
static/directives/set-repo-permissions-dialog.html
Normal file
35
static/directives/set-repo-permissions-dialog.html
Normal 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">×</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>
|
69
static/directives/set-repo-permissions.html
Normal file
69
static/directives/set-repo-permissions.html
Normal 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>
|
|
@ -1,6 +1,14 @@
|
|||
<div class="teams-manager-element">
|
||||
<div class="feedback-bar" feedback="feedback"></div>
|
||||
<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="btn-group btn-group-sm" ng-show="organization.is_admin">
|
||||
<button class="btn"
|
||||
|
@ -17,76 +25,100 @@
|
|||
|
||||
<!-- Teams List -->
|
||||
<div ng-show="!showingMembers">
|
||||
<div class="row" style="margin-left: 0px; margin-right: 0px;">
|
||||
<button class="btn btn-primary hidden-xs"
|
||||
ng-show="organization.is_admin"
|
||||
style="margin-bottom: 10px; float: right;"
|
||||
style="margin-bottom: 10px; "
|
||||
ng-click="askCreateTeam()">
|
||||
<i class="fa fa-plus" style="margin-right: 4px;"></i> Create New Team
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="row hidden-xs">
|
||||
<div class="col-sm-7 col-md-8 header-col">
|
||||
<span class="header-text">Team Summary</span>
|
||||
</div>
|
||||
<div class="col-md-4 col-sm-5 header-col" ng-show="organization.is_admin">
|
||||
<span class="header-text">Team Permissions</span>
|
||||
<span class="co-filter-box">
|
||||
<span class="filter-message" ng-if="options.filter">
|
||||
Showing {{ orderedTeams.entries.length }} of {{ teams.length }} teams
|
||||
</span>
|
||||
<input class="form-control" type="text" ng-model="options.filter" placeholder="Filter Teams...">
|
||||
</span>
|
||||
|
||||
<table class="co-table" style="margin-top: 10px;">
|
||||
<thead>
|
||||
<td ng-class="TableService.tablePredicateClass('name', options.predicate, options.reverse)">
|
||||
<a ng-click="TableService.orderBy('name', options)">Team Name</a>
|
||||
</td>
|
||||
<td ng-class="TableService.tablePredicateClass('member_count', options.predicate, options.reverse)">
|
||||
<a ng-click="TableService.orderBy('member_count', options)">Members</a>
|
||||
</td>
|
||||
<td class="hidden-xs" ng-class="TableService.tablePredicateClass('repo_count', options.predicate, options.reverse)">
|
||||
<a ng-click="TableService.orderBy('repo_count', options)">Repositories</a>
|
||||
</td>
|
||||
<td ng-class="TableService.tablePredicateClass('role_index', options.predicate, options.reverse)">
|
||||
<a ng-click="TableService.orderBy('role_index', options)">Team Role</a>
|
||||
|
||||
<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>
|
||||
</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<div class="team-listing" ng-repeat="team in orderedTeams">
|
||||
<div id="team-{{team.name}}" class="row">
|
||||
<div class="col-sm-7 col-md-8">
|
||||
<div class="team-title">
|
||||
<span class="avatar" data="team.avatar" size="30"></span>
|
||||
<span ng-show="team.can_view">
|
||||
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
|
||||
<tr class="co-checkable-row"
|
||||
ng-repeat="team in orderedTeams.visibleEntries"
|
||||
bindonce>
|
||||
<td style="white-space: nowrap;">
|
||||
<span class="avatar" data="team.avatar" size="24"></span>
|
||||
<span bo-show="team.can_view">
|
||||
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}"><span bo-text="team.name"></span></a>
|
||||
</span>
|
||||
<span ng-show="!team.can_view">
|
||||
{{ team.name }}
|
||||
<span bo-show="!team.can_view" bo-text="team.name"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span bo-show="team.can_view">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
|
||||
|
||||
<div class="team-member-list hidden-xs" ng-if="members[team.name]">
|
||||
<div class="cor-loader" ng-if="!members[team.name].members"></div>
|
||||
<span class="team-member"
|
||||
ng-repeat="member in members[team.name].members | orderBy:'is_robot' | limitTo: 20">
|
||||
<span data-title="{{ member.name }}" bs-tooltip>
|
||||
<a href="/user/{{ member.name }}" ng-if="!member.is_robot">
|
||||
<span class="avatar" data="member.avatar" size="26"></span>
|
||||
<span bo-show="!team.can_view">
|
||||
<span bo-text="team.member_count"></span> <span class="hidden-xs">member<span bo-if="team.member_count != 1">s</span></span>
|
||||
</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>
|
||||
<i class="fa ci-robot fa-lg" ng-if="member.is_robot"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="team-member-more"
|
||||
ng-if="members[team.name].members.length > 20">+ {{ members[team.name].members.length - 20 }} more team members.</span>
|
||||
<span class="team-member-more"
|
||||
ng-if="members[team.name].members && !members[team.name].members.length">(Empty Team)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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"
|
||||
role-changed="setRole(role, team.name)" roles="teamRoles"></span>
|
||||
|
||||
<span class="cor-options-menu">
|
||||
</td>
|
||||
<td>
|
||||
<span class="cor-options-menu" ng-show="organization.is_admin">
|
||||
<span class="cor-option" option-click="viewTeam(team.name)">
|
||||
<i class="fa fa-user"></i> Manage Team Members
|
||||
</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)">
|
||||
<i class="fa fa-times"></i> Delete Team {{ team.name }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</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>
|
||||
|
||||
|
@ -149,8 +181,13 @@
|
|||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create team dialog -->
|
||||
<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 -->
|
||||
<div class="cor-confirm-dialog"
|
||||
dialog-context="removeMemberInfo"
|
||||
|
|
|
@ -20,25 +20,9 @@ angular.module('quay').directive('createEntityDialog', function () {
|
|||
'entityCreateCompleted': '&entityCreateCompleted'
|
||||
},
|
||||
|
||||
controller: function($scope, $element, ApiService, UIService, TableService, RolesService, UserService) {
|
||||
$scope.TableService = TableService;
|
||||
|
||||
$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';
|
||||
}
|
||||
});
|
||||
controller: function($scope, $element, ApiService, UIService, UserService) {
|
||||
$scope.context = {
|
||||
'setPermissionsCounter': 0
|
||||
};
|
||||
|
||||
$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() {
|
||||
$element.find('.modal').modal('hide');
|
||||
if ($scope.entity) {
|
||||
|
@ -68,6 +42,7 @@ angular.module('quay').directive('createEntityDialog', function () {
|
|||
$scope.show = function() {
|
||||
$scope.entityName = null;
|
||||
$scope.entity = null;
|
||||
$scope.entityForPermissions = null;
|
||||
$scope.creating = false;
|
||||
$scope.view = 'enterName';
|
||||
$scope.enterNameForm.$setPristine(true);
|
||||
|
@ -79,92 +54,13 @@ angular.module('quay').directive('createEntityDialog', function () {
|
|||
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) {
|
||||
$scope.entity = entity;
|
||||
|
||||
if (!entity || $scope.info.skip_permissions) {
|
||||
$scope.entity = entity;
|
||||
$scope.hide();
|
||||
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() {
|
||||
|
@ -175,21 +71,27 @@ angular.module('quay').directive('createEntityDialog', function () {
|
|||
});
|
||||
};
|
||||
|
||||
$scope.allRepositoriesFilter = function(item) {
|
||||
return true;
|
||||
$scope.permissionsSet = function(repositories) {
|
||||
$scope.entity['repo_count'] = repositories.length;
|
||||
$scope.hide();
|
||||
};
|
||||
|
||||
$scope.noRepositoriesFilter = function(item) {
|
||||
return false;
|
||||
$scope.settingPermissions = function() {
|
||||
$scope.view = 'settingperms';
|
||||
};
|
||||
|
||||
$scope.missingPermsRepositoriesFilter = function(item) {
|
||||
return !item.perm;
|
||||
$scope.setPermissions = function() {
|
||||
$scope.context.setPermissionsCounter++;
|
||||
};
|
||||
|
||||
$scope.$watch('options.predicate', setRepoState);
|
||||
$scope.$watch('options.reverse', setRepoState);
|
||||
$scope.$watch('options.filter', setRepoState);
|
||||
$scope.repositoriesLoaded = function(repositories) {
|
||||
if (repositories && !repositories.length) {
|
||||
$scope.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.view = 'setperms';
|
||||
};
|
||||
|
||||
$scope.$watch('entityNameRegex', function(r) {
|
||||
if (r) {
|
||||
|
|
|
@ -33,15 +33,6 @@ angular.module('quay').directive('robotsManager', function () {
|
|||
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) {
|
||||
if ($scope.robotFilter == robotName) {
|
||||
return;
|
||||
|
@ -56,14 +47,6 @@ angular.module('quay').directive('robotsManager', function () {
|
|||
$scope.robotFilter = robotName;
|
||||
};
|
||||
|
||||
$scope.showPermissions = function(robotInfo) {
|
||||
robotInfo.showing_permissions = !robotInfo.showing_permissions;
|
||||
|
||||
if (robotInfo.showing_permissions) {
|
||||
loadRobotPermissions(robotInfo);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.showRobot = function(info) {
|
||||
$scope.robotDisplayInfo = {
|
||||
'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() {
|
||||
update();
|
||||
};
|
||||
|
|
60
static/js/directives/ui/set-repo-permissions-dialog.js
Normal file
60
static/js/directives/ui/set-repo-permissions-dialog.js
Normal 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;
|
||||
});
|
235
static/js/directives/ui/set-repo-permissions.js
Normal file
235
static/js/directives/ui/set-repo-permissions.js
Normal 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;
|
||||
});
|
|
@ -12,7 +12,15 @@ angular.module('quay').directive('teamsManager', function () {
|
|||
'organization': '=organization',
|
||||
'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 = [
|
||||
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
|
||||
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
|
||||
|
@ -21,65 +29,42 @@ angular.module('quay').directive('teamsManager', function () {
|
|||
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.members = {};
|
||||
$scope.orderedTeams = [];
|
||||
$scope.teams = null;
|
||||
$scope.orderedTeams = null;
|
||||
$scope.showingMembers = false;
|
||||
$scope.fullMemberList = null;
|
||||
$scope.feedback = null;
|
||||
$scope.createTeamInfo = null;
|
||||
|
||||
var loadTeamMembers = function() {
|
||||
if (!$scope.organization || !$scope.isEnabled) { return; }
|
||||
|
||||
// Skip loading team members on mobile.
|
||||
if (!window.matchMedia('(min-width: 768px)').matches) {
|
||||
return;
|
||||
var getRoleIndex = function(name) {
|
||||
for (var i = 0; i < $scope.teamRoles.length; ++i) {
|
||||
if ($scope.teamRoles[i]['id'] == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
for (var name in $scope.organization.teams) {
|
||||
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);
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
var loadMembersOfTeam = function(name) {
|
||||
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() {
|
||||
var setTeamsState = function() {
|
||||
if (!$scope.organization || !$scope.organization.ordered_teams || !$scope.isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.orderedTeams = [];
|
||||
$scope.organization.ordered_teams.map(function(name) {
|
||||
$scope.orderedTeams.push($scope.organization.teams[name]);
|
||||
$scope.teams = [];
|
||||
$scope.organization.ordered_teams.map(function(name, index) {
|
||||
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) {
|
||||
var previousRole = $scope.organization.teams[teamname].role;
|
||||
$scope.organization.teams[teamname].role = role;
|
||||
|
@ -115,9 +100,9 @@ angular.module('quay').directive('teamsManager', function () {
|
|||
|
||||
$scope.handleTeamCreated = function(created) {
|
||||
var teamname = created.name;
|
||||
created['member_count'] = 0;
|
||||
|
||||
$scope.organization.teams[teamname] = created;
|
||||
$scope.members[teamname] = {};
|
||||
$scope.members[teamname].members = [];
|
||||
$scope.organization.ordered_teams.push(teamname);
|
||||
$scope.orderedTeams.push(created);
|
||||
|
||||
|
@ -150,8 +135,8 @@ angular.module('quay').directive('teamsManager', function () {
|
|||
$scope.organization.ordered_teams.splice(index, 1);
|
||||
}
|
||||
|
||||
loadOrderedTeams();
|
||||
delete $scope.organization.teams[teamname];
|
||||
setTeamsState();
|
||||
|
||||
$scope.feedback = {
|
||||
'kind': 'success',
|
||||
|
@ -192,12 +177,7 @@ angular.module('quay').directive('teamsManager', function () {
|
|||
|
||||
ApiService.removeOrganizationMember(null, params).then(function(resp) {
|
||||
// Reset the state of the directive.
|
||||
$scope.members = {};
|
||||
$scope.orderedTeams = [];
|
||||
$scope.fullMemberList = null;
|
||||
|
||||
loadOrderedTeams();
|
||||
loadTeamMembers();
|
||||
$scope.showMembers(true);
|
||||
|
||||
callback(true);
|
||||
|
@ -215,6 +195,27 @@ angular.module('quay').directive('teamsManager', function () {
|
|||
$scope.askRemoveMember = function(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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/**
|
||||
* 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 = {};
|
||||
|
||||
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' }
|
||||
];
|
||||
|
||||
var getPermissionEndpoint = function(repository, entityName, kind) {
|
||||
var getPermissionEndpoint = function(repository, entityName, entityKind) {
|
||||
if (entityKind == 'robot') {
|
||||
entityKind = 'user';
|
||||
}
|
||||
|
||||
var namespace = repository.namespace;
|
||||
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);
|
||||
};
|
||||
|
||||
roleService.deleteRepositoryRole = function(repository, entityKind, entityName, callback) {
|
||||
if (entityKind == 'robot') {
|
||||
entityKind = 'user';
|
||||
}
|
||||
|
||||
var errorDisplay = ApiService.errorDisplay('Cannot change permission', function(resp) {
|
||||
callback(false);
|
||||
});
|
||||
|
@ -39,6 +48,15 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
|
|||
};
|
||||
|
||||
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) {
|
||||
callback(false);
|
||||
});
|
||||
|
@ -53,5 +71,30 @@ angular.module('quay').factory('RolesService', ['UtilService', 'Restangular', 'A
|
|||
}, 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;
|
||||
}]);
|
||||
|
|
|
@ -10,7 +10,8 @@ from data import model
|
|||
from initdb import setup_database_for_testing, finished_database_for_testing
|
||||
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.search import EntitySearch
|
||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||
|
@ -679,6 +680,24 @@ class TestTeamMemberBuynlargeDevtableOwners(ApiTestCase):
|
|||
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):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
|
|
|
@ -28,7 +28,8 @@ from data import database, model
|
|||
from data.database import RepositoryActionCount, Repository as RepositoryTable
|
||||
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.search import EntitySearch, ConductSearch
|
||||
from endpoints.api.image import RepositoryImage, RepositoryImageList
|
||||
|
@ -1158,6 +1159,16 @@ class TestDeleteOrganizationTeam(ApiTestCase):
|
|||
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):
|
||||
def test_invalidteam(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
|
|
Reference in a new issue