Merge branch 'orgview'

This commit is contained in:
Joseph Schorr 2015-04-01 15:12:15 -04:00
commit 1deb5121b8
125 changed files with 2077 additions and 643 deletions

View file

@ -1,4 +1,5 @@
import hashlib import hashlib
import math
class Avatar(object): class Avatar(object):
def __init__(self, app=None): def __init__(self, app=None):
@ -7,8 +8,7 @@ class Avatar(object):
def _init_app(self, app): def _init_app(self, app):
return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')]( return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')](
app.config['SERVER_HOSTNAME'], app.config['PREFERRED_URL_SCHEME'], app.config['AVATAR_COLORS'], app.config['HTTPCLIENT'])
app.config['PREFERRED_URL_SCHEME'])
def __getattr__(self, name): def __getattr__(self, name):
return getattr(self.state, name, None) return getattr(self.state, name, None)
@ -16,48 +16,83 @@ class Avatar(object):
class BaseAvatar(object): class BaseAvatar(object):
""" Base class for all avatar implementations. """ """ Base class for all avatar implementations. """
def __init__(self, server_hostname, preferred_url_scheme): def __init__(self, preferred_url_scheme, colors, http_client):
self.server_hostname = server_hostname
self.preferred_url_scheme = preferred_url_scheme self.preferred_url_scheme = preferred_url_scheme
self.colors = colors
self.http_client = http_client
def get_url(self, email, size=16, name=None): def get_mail_html(self, name, email_or_id, size=16, kind='user'):
""" Returns the full URL for viewing the avatar of the given email address, with """ Returns the full HTML and CSS for viewing the avatar of the given name and email address,
an optional size. with an optional size.
""" """
raise NotImplementedError data = self.get_data(name, email_or_id, kind)
url = self._get_url(data['hash'], size) if kind != 'team' else None
font_size = size - 6
def compute_hash(self, email, name=None): if url is not None:
""" Computes the avatar hash for the given email address. If the name is given and a default # Try to load the gravatar. If we get a non-404 response, then we use it in place of
avatar is being computed, the name can be used in place of the email address. """ # the CSS avatar.
raise NotImplementedError response = self.http_client.get(url)
if response.status_code == 200:
return """<img src="%s" width="%s" height="%s" alt="%s"
style="vertical-align: middle;">""" % (url, size, size, kind)
radius = '50%' if kind == 'team' else '0%'
letter = '&Omega;' if kind == 'team' and data['name'] == 'owners' else data['name'].upper()[0]
return """
<span style="width: %spx; height: %spx; background-color: %s; font-size: %spx;
line-height: %spx; margin-left: 2px; margin-right: 2px; display: inline-block;
vertical-align: middle; text-align: center; color: white; border-radius: %s">
%s
</span>
""" % (size, size, data['color'], font_size, size, radius, letter)
def get_data_for_user(self, user):
return self.get_data(user.username, user.email, 'robot' if user.robot else 'user')
def get_data_for_team(self, team):
return self.get_data(team.name, team.name, 'team')
def get_data_for_org(self, org):
return self.get_data(org.username, org.email, 'org')
def get_data(self, name, email_or_id, kind='user'):
""" Computes and returns the full data block for the avatar:
{
'name': name,
'hash': The gravatar hash, if any.
'color': The color for the avatar
}
"""
colors = self.colors
hash_value = hashlib.md5(email_or_id.strip().lower()).hexdigest()
byte_count = int(math.ceil(math.log(len(colors), 16)))
byte_data = hash_value[0:byte_count]
hash_color = colors[int(byte_data, 16) % len(colors)]
return {
'name': name,
'hash': hash_value,
'color': hash_color,
'kind': kind
}
def _get_url(self, hash_value, size):
""" Returns the URL for displaying the overlay avatar. """
return None
class GravatarAvatar(BaseAvatar): class GravatarAvatar(BaseAvatar):
""" Avatar system that uses gravatar for generating avatars. """ """ Avatar system that uses gravatar for generating avatars. """
def compute_hash(self, email, name=None): def _get_url(self, hash_value, size=16):
email = email or "" return '%s://www.gravatar.com/avatar/%s?d=404&size=%s' % (self.preferred_url_scheme,
return hashlib.md5(email.strip().lower()).hexdigest() hash_value, size)
def get_url(self, email, size=16, name=None):
computed = self.compute_hash(email, name=name)
return '%s://www.gravatar.com/avatar/%s?d=identicon&size=%s' % (self.preferred_url_scheme,
computed, size)
class LocalAvatar(BaseAvatar): class LocalAvatar(BaseAvatar):
""" Avatar system that uses the local system for generating avatars. """ """ Avatar system that uses the local system for generating avatars. """
def compute_hash(self, email, name=None): pass
email = email or ""
if not name and not email:
return ''
prefix = name if name else email
return prefix[0] + hashlib.md5(email.strip().lower()).hexdigest()
def get_url(self, email, size=16, name=None):
computed = self.compute_hash(email, name=name)
return '%s://%s/avatar/%s?size=%s' % (self.preferred_url_scheme, self.server_hostname,
computed, size)
AVATAR_CLASSES = { AVATAR_CLASSES = {
'gravatar': GravatarAvatar, 'gravatar': GravatarAvatar,

View file

@ -45,8 +45,6 @@ class DefaultConfig(object):
PREFERRED_URL_SCHEME = 'http' PREFERRED_URL_SCHEME = 'http'
SERVER_HOSTNAME = 'localhost:5000' SERVER_HOSTNAME = 'localhost:5000'
AVATAR_KIND = 'local'
REGISTRY_TITLE = 'CoreOS Enterprise Registry' REGISTRY_TITLE = 'CoreOS Enterprise Registry'
REGISTRY_TITLE_SHORT = 'Enterprise Registry' REGISTRY_TITLE_SHORT = 'Enterprise Registry'
@ -205,3 +203,11 @@ class DefaultConfig(object):
# Signed registry grant token expiration in seconds # Signed registry grant token expiration in seconds
SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull
# The various avatar background colors.
AVATAR_KIND = 'local'
AVATAR_COLORS = ['#969696', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', '#d62728',
'#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', '#e377c2', '#f7b6d2',
'#7f7f7f', '#c7c7c7', '#bcbd22', '#1f77b4', '#17becf', '#9edae5', '#393b79',
'#5254a3', '#6b6ecf', '#9c9ede', '#9ecae1', '#31a354', '#b5cf6b', '#a1d99b',
'#8c6d31', '#ad494a', '#e7ba52', '#a55194']

View file

@ -310,11 +310,54 @@ def _list_entity_robots(entity_name):
.where(User.robot == True, User.username ** (entity_name + '+%'))) .where(User.robot == True, User.username ** (entity_name + '+%')))
def list_entity_robot_tuples(entity_name): class _TupleWrapper(object):
return (_list_entity_robots(entity_name) def __init__(self, data, fields):
.select(User.username, FederatedLogin.service_ident) self._data = data
.tuples()) self._fields = fields
def get(self, field):
return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)]
class TupleSelector(object):
""" Helper class for selecting tuples from a peewee query and easily accessing
them as if they were objects.
"""
def __init__(self, query, fields):
self._query = query.select(*fields).tuples()
self._fields = [field.name + ':' + field.model_class.__name__ for field in fields]
def __iter__(self):
return self._build_iterator()
def _build_iterator(self):
for tuple_data in self._query:
yield _TupleWrapper(tuple_data, self._fields)
def list_entity_robot_permission_teams(entity_name):
query = (_list_entity_robots(entity_name)
.join(RepositoryPermission, JOIN_LEFT_OUTER,
on=(RepositoryPermission.user == FederatedLogin.user))
.join(Repository, JOIN_LEFT_OUTER)
.switch(User)
.join(TeamMember, JOIN_LEFT_OUTER)
.join(Team, JOIN_LEFT_OUTER))
fields = [User.username, FederatedLogin.service_ident, Repository.name, Team.name]
return TupleSelector(query, fields)
def list_robot_permissions(robot_name):
return (RepositoryPermission.select(RepositoryPermission, User, Repository)
.join(Repository)
.join(Visibility)
.switch(RepositoryPermission)
.join(Role)
.switch(RepositoryPermission)
.join(User)
.where(User.username == robot_name, User.robot == True))
def convert_user_to_organization(user, admin_user): def convert_user_to_organization(user, admin_user):
# Change the user to an organization. # Change the user to an organization.
@ -654,13 +697,13 @@ def get_matching_users(username_prefix, robot_namespace=None,
(User.robot == True))) (User.robot == True)))
query = (User query = (User
.select(User.username, User.robot) .select(User.username, User.email, User.robot)
.group_by(User.username, User.robot) .group_by(User.username, User.email, User.robot)
.where(direct_user_query)) .where(direct_user_query))
if organization: if organization:
query = (query query = (query
.select(User.username, User.robot, fn.Sum(Team.id)) .select(User.username, User.email, User.robot, fn.Sum(Team.id))
.join(TeamMember, JOIN_LEFT_OUTER) .join(TeamMember, JOIN_LEFT_OUTER)
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) & .join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
(Team.organization == organization)))) (Team.organization == organization))))
@ -669,9 +712,11 @@ def get_matching_users(username_prefix, robot_namespace=None,
class MatchingUserResult(object): class MatchingUserResult(object):
def __init__(self, *args): def __init__(self, *args):
self.username = args[0] self.username = args[0]
self.is_robot = args[1] self.email = args[1]
self.robot = args[2]
if organization: if organization:
self.is_org_member = (args[2] != None) self.is_org_member = (args[3] != None)
else: else:
self.is_org_member = None self.is_org_member = None
@ -1039,7 +1084,8 @@ def get_all_repo_teams(namespace_name, repository_name):
def get_all_repo_users(namespace_name, repository_name): def get_all_repo_users(namespace_name, repository_name):
return (RepositoryPermission.select(User.username, User.robot, Role.name, RepositoryPermission) return (RepositoryPermission.select(User.username, User.email, User.robot, Role.name,
RepositoryPermission)
.join(User) .join(User)
.switch(RepositoryPermission) .switch(RepositoryPermission)
.join(Role) .join(Role)

View file

@ -4,7 +4,7 @@
<h3>Invitation to join team: {{ teamname }}</h3> <h3>Invitation to join team: {{ teamname }}</h3>
{{ inviter | user_reference }} has invited you to join the team <b>{{ teamname }}</b> under organization {{ organization | user_reference }}. {{ inviter | user_reference }} has invited you to join the team {{ teamname | team_reference }} under organization {{ organization | user_reference }}.
<br><br> <br><br>

View file

@ -24,16 +24,22 @@ logger = logging.getLogger(__name__)
def org_view(o, teams): def org_view(o, teams):
admin_org = AdministerOrganizationPermission(o.username) is_admin = AdministerOrganizationPermission(o.username).can()
is_admin = admin_org.can() is_member = OrganizationMemberPermission(o.username).can()
view = { view = {
'name': o.username, 'name': o.username,
'email': o.email if is_admin else '', 'email': o.email if is_admin else '',
'avatar': avatar.compute_hash(o.email, name=o.username), 'avatar': avatar.get_data_for_user(o),
'teams': {t.name : team_view(o.username, t) for t in teams}, 'is_admin': is_admin,
'is_admin': is_admin 'is_member': is_member
} }
if teams is not None:
teams = sorted(teams, key=lambda team:team.id)
view['teams'] = {t.name : team_view(o.username, t) for t in teams}
view['ordered_teams'] = [team.name for team in teams]
if is_admin: if is_admin:
view['invoice_email'] = o.invoice_email view['invoice_email'] = o.invoice_email
@ -129,17 +135,17 @@ class Organization(ApiResource):
@nickname('getOrganization') @nickname('getOrganization')
def get(self, orgname): def get(self, orgname):
""" Get the details for the specified organization """ """ Get the details for the specified organization """
permission = OrganizationMemberPermission(orgname)
if permission.can():
try: try:
org = model.get_organization(orgname) org = model.get_organization(orgname)
except model.InvalidOrganizationException: except model.InvalidOrganizationException:
raise NotFound() raise NotFound()
teams = None
if OrganizationMemberPermission(orgname).can():
teams = model.get_teams_within_org(org) teams = model.get_teams_within_org(org)
return org_view(org, teams) return org_view(org, teams)
raise Unauthorized()
@require_scope(scopes.ORG_ADMIN) @require_scope(scopes.ORG_ADMIN)
@nickname('changeOrganizationDetails') @nickname('changeOrganizationDetails')
@ -297,16 +303,14 @@ class ApplicationInformation(ApiResource):
if not application: if not application:
raise NotFound() raise NotFound()
org_hash = avatar.compute_hash(application.organization.email, app_email = application.avatar_email or application.organization.email
name=application.organization.username) app_data = avatar.get_data(application.name, app_email, 'app')
app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if
application.avatar_email else org_hash)
return { return {
'name': application.name, 'name': application.name,
'description': application.description, 'description': application.description,
'uri': application.application_uri, 'uri': application.application_uri,
'avatar': app_hash, 'avatar': app_data,
'organization': org_view(application.organization, []) 'organization': org_view(application.organization, [])
} }

View file

@ -2,6 +2,7 @@ import logging
from flask import request from flask import request
from app import avatar
from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource,
log_action, request_error, validate_json_request, path_param) log_action, request_error, validate_json_request, path_param)
from data import model from data import model
@ -16,7 +17,10 @@ def role_view(repo_perm_obj):
} }
def wrap_role_view_user(role_json, user): def wrap_role_view_user(role_json, user):
role_json['name'] = user.username
role_json['is_robot'] = user.robot role_json['is_robot'] = user.robot
if not user.robot:
role_json['avatar'] = avatar.get_data_for_user(user)
return role_json return role_json
@ -25,6 +29,12 @@ def wrap_role_view_org(role_json, user, org_members):
return role_json return role_json
def wrap_role_view_team(role_json, team):
role_json['name'] = team.name
role_json['avatar'] = avatar.get_data_for_team(team)
return role_json
@resource('/v1/repository/<repopath:repository>/permissions/team/') @resource('/v1/repository/<repopath:repository>/permissions/team/')
@path_param('repository', 'The full path of the repository. e.g. namespace/name') @path_param('repository', 'The full path of the repository. e.g. namespace/name')
class RepositoryTeamPermissionList(RepositoryParamResource): class RepositoryTeamPermissionList(RepositoryParamResource):
@ -35,8 +45,11 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
""" List all team permission. """ """ List all team permission. """
repo_perms = model.get_all_repo_teams(namespace, repository) repo_perms = model.get_all_repo_teams(namespace, repository)
def wrapped_role_view(repo_perm):
return wrap_role_view_team(role_view(repo_perm), repo_perm.team)
return { return {
'permissions': {repo_perm.team.name: role_view(repo_perm) 'permissions': {repo_perm.team.name: wrapped_role_view(repo_perm)
for repo_perm in repo_perms} for repo_perm in repo_perms}
} }
@ -232,7 +245,7 @@ class RepositoryTeamPermission(RepositoryParamResource):
'role': new_permission['role']}, 'role': new_permission['role']},
repo=model.get_repository(namespace, repository)) repo=model.get_repository(namespace, repository))
return role_view(perm), 200 return wrap_role_view_team(role_view(perm), perm.team), 200
@require_repo_admin @require_repo_admin
@nickname('deleteTeamPermissions') @nickname('deleteTeamPermissions')

View file

@ -7,6 +7,7 @@ from auth.permissions import AdministerOrganizationPermission
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
from app import avatar
def prototype_view(proto, org_members): def prototype_view(proto, org_members):
@ -16,6 +17,7 @@ def prototype_view(proto, org_members):
'is_robot': user.robot, 'is_robot': user.robot,
'kind': 'user', 'kind': 'user',
'is_org_member': user.robot or user.username in org_members, 'is_org_member': user.robot or user.username in org_members,
'avatar': avatar.get_data_for_user(user)
} }
if proto.delegate_user: if proto.delegate_user:
@ -24,6 +26,7 @@ def prototype_view(proto, org_members):
delegate_view = { delegate_view = {
'name': proto.delegate_team.name, 'name': proto.delegate_team.name,
'kind': 'team', 'kind': 'team',
'avatar': avatar.get_data_for_team(proto.delegate_team)
} }
return { return {

View file

@ -5,16 +5,63 @@ from auth.permissions import AdministerOrganizationPermission, OrganizationMembe
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
from data.database import User, Team, Repository, FederatedLogin
from util.names import format_robot_username from util.names import format_robot_username
from flask import abort
from app import avatar
def robot_view(name, token): def robot_view(name, token):
return { return {
'name': name, 'name': name,
'token': token, 'token': token
} }
def permission_view(permission):
return {
'repository': {
'name': permission.repository.name,
'is_public': permission.repository.visibility.name == 'public'
},
'role': permission.role.name
}
def robots_list(prefix):
tuples = model.list_entity_robot_permission_teams(prefix)
robots = {}
robot_teams = set()
for robot_tuple in tuples:
robot_name = robot_tuple.get(User.username)
if not robot_name in robots:
robots[robot_name] = {
'name': robot_name,
'token': robot_tuple.get(FederatedLogin.service_ident),
'teams': [],
'repositories': []
}
team_name = robot_tuple.get(Team.name)
repository_name = robot_tuple.get(Repository.name)
if team_name is not None:
check_key = robot_name + ':' + team_name
if not check_key in robot_teams:
robot_teams.add(check_key)
robots[robot_name]['teams'].append({
'name': team_name,
'avatar': avatar.get_data(team_name, team_name, 'team')
})
if repository_name is not None:
if not repository_name in robots[robot_name]['repositories']:
robots[robot_name]['repositories'].append(repository_name)
return {'robots': robots.values()}
@resource('/v1/user/robots') @resource('/v1/user/robots')
@internal_only @internal_only
class UserRobotList(ApiResource): class UserRobotList(ApiResource):
@ -24,10 +71,7 @@ class UserRobotList(ApiResource):
def get(self): def get(self):
""" List the available robots for the user. """ """ List the available robots for the user. """
user = get_authenticated_user() user = get_authenticated_user()
robots = model.list_entity_robot_tuples(user.username) return robots_list(user.username)
return {
'robots': [robot_view(name, password) for name, password in robots]
}
@resource('/v1/user/robots/<robot_shortname>') @resource('/v1/user/robots/<robot_shortname>')
@ -73,10 +117,7 @@ class OrgRobotList(ApiResource):
""" List the organization's robots. """ """ List the organization's robots. """
permission = OrganizationMemberPermission(orgname) permission = OrganizationMemberPermission(orgname)
if permission.can(): if permission.can():
robots = model.list_entity_robot_tuples(orgname) return robots_list(orgname)
return {
'robots': [robot_view(name, password) for name, password in robots]
}
raise Unauthorized() raise Unauthorized()
@ -125,6 +166,47 @@ class OrgRobot(ApiResource):
raise Unauthorized() raise Unauthorized()
@resource('/v1/user/robots/<robot_shortname>/permissions')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only
class UserRobotPermissions(ApiResource):
""" Resource for listing the permissions a user's robot has in the system. """
@require_user_admin
@nickname('getUserRobotPermissions')
def get(self, robot_shortname):
""" Returns the list of repository permissions for the user's robot. """
parent = get_authenticated_user()
robot, password = model.get_robot(robot_shortname, parent)
permissions = model.list_robot_permissions(robot.username)
return {
'permissions': [permission_view(permission) for permission in permissions]
}
@resource('/v1/organization/<orgname>/robots/<robot_shortname>/permissions')
@path_param('orgname', 'The name of the organization')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@related_user_resource(UserRobotPermissions)
class OrgRobotPermissions(ApiResource):
""" Resource for listing the permissions an org's robot has in the system. """
@require_user_admin
@nickname('getOrgRobotPermissions')
def get(self, orgname, robot_shortname):
""" Returns the list of repository permissions for the org's robot. """
permission = AdministerOrganizationPermission(orgname)
if permission.can():
parent = model.get_organization(orgname)
robot, password = model.get_robot(robot_shortname, parent)
permissions = model.list_robot_permissions(robot.username)
return {
'permissions': [permission_view(permission) for permission in permissions]
}
abort(403)
@resource('/v1/user/robots/<robot_shortname>/regenerate') @resource('/v1/user/robots/<robot_shortname>/regenerate')
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') @path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
@internal_only @internal_only

View file

@ -45,7 +45,7 @@ class EntitySearch(ApiResource):
'name': namespace_name, 'name': namespace_name,
'kind': 'org', 'kind': 'org',
'is_org_member': True, 'is_org_member': True,
'avatar': avatar.compute_hash(organization.email, name=organization.username), 'avatar': avatar.get_data_for_org(organization),
}] }]
except model.InvalidOrganizationException: except model.InvalidOrganizationException:
@ -63,7 +63,8 @@ class EntitySearch(ApiResource):
result = { result = {
'name': team.name, 'name': team.name,
'kind': 'team', 'kind': 'team',
'is_org_member': True 'is_org_member': True,
'avatar': avatar.get_data_for_team(team)
} }
return result return result
@ -71,11 +72,12 @@ class EntitySearch(ApiResource):
user_json = { user_json = {
'name': user.username, 'name': user.username,
'kind': 'user', 'kind': 'user',
'is_robot': user.is_robot, 'is_robot': user.robot,
'avatar': avatar.get_data_for_user(user)
} }
if organization is not None: if organization is not None:
user_json['is_org_member'] = user.is_robot or user.is_org_member user_json['is_org_member'] = user.robot or user.is_org_member
return user_json return user_json

View file

@ -108,7 +108,7 @@ def user_view(user):
'username': user.username, 'username': user.username,
'email': user.email, 'email': user.email,
'verified': user.verified, 'verified': user.verified,
'avatar': avatar.compute_hash(user.email, name=user.username), 'avatar': avatar.get_data_for_user(user),
'super_user': superusers.is_superuser(user.username) 'super_user': superusers.is_superuser(user.username)
} }

View file

@ -52,11 +52,11 @@ def team_view(orgname, team):
view_permission = ViewTeamPermission(orgname, team.name) view_permission = ViewTeamPermission(orgname, team.name)
role = model.get_team_org_role(team).name role = model.get_team_org_role(team).name
return { return {
'id': team.id,
'name': team.name, 'name': team.name,
'description': team.description, 'description': team.description,
'can_view': view_permission.can(), 'can_view': view_permission.can(),
'role': role 'role': role,
'avatar': avatar.get_data_for_team(team)
} }
def member_view(member, invited=False): def member_view(member, invited=False):
@ -64,7 +64,7 @@ def member_view(member, invited=False):
'name': member.username, 'name': member.username,
'kind': 'user', 'kind': 'user',
'is_robot': member.robot, 'is_robot': member.robot,
'avatar': avatar.compute_hash(member.email, name=member.username) if not member.robot else None, 'avatar': avatar.get_data_for_user(member),
'invited': invited, 'invited': invited,
} }
@ -76,7 +76,7 @@ def invite_view(invite):
return { return {
'email': invite.email, 'email': invite.email,
'kind': 'invite', 'kind': 'invite',
'avatar': avatar.compute_hash(invite.email), 'avatar': avatar.get_data(invite.email, invite.email, 'user'),
'invited': True 'invited': True
} }

View file

@ -36,7 +36,7 @@ def user_view(user):
admin_org = AdministerOrganizationPermission(o.username) admin_org = AdministerOrganizationPermission(o.username)
return { return {
'name': o.username, 'name': o.username,
'avatar': avatar.compute_hash(o.email, name=o.username), 'avatar': avatar.get_data_for_org(o),
'is_org_admin': admin_org.can(), 'is_org_admin': admin_org.can(),
'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(), 'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(),
'preferred_namespace': not (o.stripe_id is None) 'preferred_namespace': not (o.stripe_id is None)
@ -62,7 +62,7 @@ def user_view(user):
'verified': user.verified, 'verified': user.verified,
'anonymous': False, 'anonymous': False,
'username': user.username, 'username': user.username,
'avatar': avatar.compute_hash(user.email, name=user.username), 'avatar': avatar.get_data_for_user(user)
} }
user_admin = UserAdminPermission(user.username) user_admin = UserAdminPermission(user.username)
@ -176,8 +176,8 @@ class User(ApiResource):
'description': 'The user\'s email address', 'description': 'The user\'s email address',
}, },
'avatar': { 'avatar': {
'type': 'string', 'type': 'object',
'description': 'Avatar hash representing the user\'s icon' 'description': 'Avatar data representing the user\'s icon'
}, },
'organizations': { 'organizations': {
'type': 'array', 'type': 'array',
@ -665,17 +665,16 @@ class UserNotification(ApiResource):
def authorization_view(access_token): def authorization_view(access_token):
oauth_app = access_token.application oauth_app = access_token.application
app_email = oauth_app.avatar_email or oauth_app.organization.email
return { return {
'application': { 'application': {
'name': oauth_app.name, 'name': oauth_app.name,
'description': oauth_app.description, 'description': oauth_app.description,
'url': oauth_app.application_uri, 'url': oauth_app.application_uri,
'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email, 'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
name=oauth_app.name),
'organization': { 'organization': {
'name': oauth_app.organization.username, 'name': oauth_app.organization.username,
'avatar': avatar.compute_hash(oauth_app.organization.email, 'avatar': avatar.get_data_for_org(oauth_app.organization)
name=oauth_app.organization.username)
} }
}, },
'scopes': scopes.get_scope_information(access_token.scope), 'scopes': scopes.get_scope_information(access_token.scope),

View file

@ -3,7 +3,6 @@ import logging
from flask import (abort, redirect, request, url_for, make_response, Response, from flask import (abort, redirect, request, url_for, make_response, Response,
Blueprint, send_from_directory, jsonify, send_file) Blueprint, send_from_directory, jsonify, send_file)
from avatar_generator import Avatar
from flask.ext.login import current_user from flask.ext.login import current_user
from urlparse import urlparse from urlparse import urlparse
from health.healthcheck import get_healthchecker from health.healthcheck import get_healthchecker
@ -210,20 +209,6 @@ def endtoend_health():
return response return response
@app.route("/avatar/<avatar_hash>")
@set_cache_headers
def render_avatar(avatar_hash, headers):
try:
size = int(request.args.get('size', 16))
except ValueError:
size = 16
generated = Avatar.generate(size, avatar_hash, "PNG")
resp = make_response(generated, 200, {'Content-Type': 'image/png'})
resp.headers.extend(headers)
return resp
@web.route('/tos', methods=['GET']) @web.route('/tos', methods=['GET'])
@no_cache @no_cache
def tos(): def tos():
@ -449,15 +434,16 @@ def request_authorization_code():
# Load the application information. # Load the application information.
oauth_app = provider.get_application_for_client_id(client_id) oauth_app = provider.get_application_for_client_id(client_id)
app_email = oauth_app.email or organization.email
oauth_app_view = { oauth_app_view = {
'name': oauth_app.name, 'name': oauth_app.name,
'description': oauth_app.description, 'description': oauth_app.description,
'url': oauth_app.application_uri, 'url': oauth_app.application_uri,
'avatar': avatar.compute_hash(oauth_app.avatar_email, name=oauth_app.name), 'avatar': avatar.get_data(oauth_app.name, app_email, 'app'),
'organization': { 'organization': {
'name': oauth_app.organization.username, 'name': oauth_app.organization.username,
'avatar': avatar.compute_hash(oauth_app.organization.email, 'avatar': avatar.get_data_for_org(oauth_app.organization)
name=oauth_app.organization.username)
} }
} }

View file

@ -773,10 +773,17 @@
padding: 10px; padding: 10px;
} }
.co-table.no-lines td {
border-bottom: 0px;
padding: 6px;
}
.co-table thead td { .co-table thead td {
color: #999;
font-size: 90%;
text-transform: uppercase; text-transform: uppercase;
font-size: 16px; font-weight: 300;
color: #666; padding-top: 0px !important;
} }
.co-table thead td a { .co-table thead td a {
@ -813,11 +820,45 @@
width: 30px; width: 30px;
} }
.co-table td.caret-col {
width: 10px;
padding-left: 6px;
padding-right: 0px;
color: #aaa;
}
.co-table td.caret-col i.fa {
cursor: pointer;
}
.co-table .add-row-spacer td {
padding: 5px;
}
.co-table .add-row td { .co-table .add-row td {
padding-top: 10px;
border-top: 2px solid #eee; border-top: 2px solid #eee;
border-bottom: none; border-bottom: none;
} }
.co-table tr.co-table-header-row td {
font-size: 12px;
text-transform: uppercase;
color: #ccc;
border-bottom: none;
padding-left: 10px;
padding-top: 10px;
padding-bottom: 4px;
}
.co-table tr.co-table-header-row td i.fa {
margin-right: 4px;
}
.co-table tr.indented-row td:first-child {
padding-left: 28px;
}
.cor-checkable-menu { .cor-checkable-menu {
display: inline-block; display: inline-block;
} }

View file

@ -0,0 +1,7 @@
.application-manager-element .co-table {
margin-top: 20px;
}
.application-manager-element i.fa {
margin-right: 4px;
}

View file

@ -0,0 +1,30 @@
.avatar-element {
display: inline-block;
vertical-align: middle;
color: white !important;
text-align: center;
position: relative;
background: white;
overflow: hidden;
}
.avatar-element.team {
border-radius: 50%;
}
.avatar-element img {
position: absolute;
top: 0px;
left: 0px;
}
.avatar-element .letter {
cursor: default !important;
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
}
a .avatar-element .letter {
cursor: pointer !important;
}

View file

@ -0,0 +1,25 @@
.billing-invoices-element .invoice-title {
padding: 6px;
cursor: pointer;
}
.billing-invoices-element .invoice-status .success {
color: green;
}
.billing-invoices-element .invoice-status .pending {
color: steelblue;
}
.billing-invoices-element .invoice-status .danger {
color: red;
}
.billing-invoices-element .invoice-amount:before {
content: '$';
}
.billing-invoices-element .fa-download {
color: #aaa;
}

View file

@ -0,0 +1,7 @@
.entity-reference .new-entity-reference .entity-name {
margin-left: 6px;
}
.entity-reference .new-entity-reference .fa-wrench {
width: 16px;
}

View file

@ -0,0 +1,52 @@
.entity-search-element {
position: relative;
display: block;
}
.entity-search-element .entity-reference {
position: absolute !important;
top: 7px;
left: 8px;
right: 36px;
z-index: 0;
pointer-events: none;
}
.entity-search-element .entity-reference .entity-reference-element {
pointer-events: none;
}
.entity-search-element .entity-reference-element i.fa-exclamation-triangle {
pointer-events: all;
}
.entity-search-element .entity-reference .entity-name {
display: none;
}
.entity-search-element input {
vertical-align: middle;
width: 100%;
}
.entity-search-element.persistent input {
padding-left: 28px;
padding-right: 28px;
}
.entity-search-element .twitter-typeahead {
vertical-align: middle;
display: block !important;
margin-right: 36px;
}
.entity-search-element .dropdown {
vertical-align: middle;
position: absolute;
top: 0px;
right: 0px;
}
.entity-search-element .menuitem .avatar {
margin-right: 4px;
}

View file

@ -0,0 +1,8 @@
.prototype-manager-element i.fa {
margin-right: 4px;
}
.prototype-manager-element td {
padding: 10px !important;
vertical-align: middle !important;
}

View file

@ -1,3 +1,15 @@
.repository-permissions-table #add-entity-permission { .repository-permissions-table #add-entity-permission {
padding-left: 0px; padding-left: 0px;
} }
.repository-permissions-table .user-permission-entity {
position: relative;
}
.repository-permissions-table .outside-org {
position: absolute;
top: 15px;
left: -2px;
font-size: 16px;
color: #E8BB03;
}

View file

@ -0,0 +1,80 @@
.robots-manager-element .robot a {
font-size: 16px;
cursor: pointer;
}
.robots-manager-element .robot .prefix {
color: #aaa;
}
.robots-manager-element .robot i {
margin-right: 10px;
}
.robots-manager-element .popup-input-button i.fa {
margin-right: 4px;
}
.robots-manager-element .empty {
color: #ccc;
}
.robots-manager-element tr.open td {
border-bottom: 1px solid transparent;
}
.robots-manager-element .permissions-table-wrapper {
margin-left: 0px;
border-left: 2px solid #ccc;
padding-left: 20px;
}
.robots-manager-element .permissions-table tbody tr:last-child td {
border-bottom: 0px;
}
.robots-manager-element .permissions-display-row {
position: relative;
padding-bottom: 20px;
}
.robots-manager-element .permissions-display-row td:first-child {
min-width: 300px;
}
.robots-manager-element .repo-circle {
color: #999;
display: inline-block;
position: relative;
background: #eee;
padding: 4px;
border-radius: 50%;
display: inline-block;
width: 46px;
height: 46px;
margin-right: 6px;
}
.robots-manager-element .repo-circle .fa-hdd-o {
font-size: 1.7em;
}
.robots-manager-element .repo-circle.no-background .fa-hdd-o {
font-size: 1.7em;
}
.robots-manager-element .repo-circle .fa-lock {
width: 16px;
height: 16px;
line-height: 16px;
font-size: 12px !important;
}
.robots-manager-element .repo-circle.no-background .fa-lock {
bottom: 5px;
right: 2px;
}
.robots-manager-element .member-perm-summary {
margin-right: 14px;
}

View file

@ -0,0 +1,55 @@
.teams-manager .popup-input-button {
float: right;
}
.teams-manager .manager-header {
border-bottom: 1px solid #eee;
margin-bottom: 10px;
}
.teams-manager .cor-options-menu {
display: inline-block;
margin-left: 10px;
}
.teams-manager .header-col .info-icon {
font-size: 16px;
}
.teams-manager .header-col .header-text {
text-transform: uppercase;
font-size: 14px;
color: #aaa !important;
display: inline-block;
padding-top: 4px;
}
.teams-manager .control-col {
padding-top: 6px;
}
.teams-manager .team-listing {
margin-bottom: 10px;
}
.teams-manager .team-listing .avatar {
margin-right: 6px;
}
.teams-manager .team-member-list .fa {
color: #ccc;
}
.teams-manager .team-member-list {
position: relative;
min-height: 20px;
padding: 4px;
padding-left: 40px;
}
.teams-manager .team-member-list .team-member-more {
vertical-align: middle;
padding-left: 6px;
color: #aaa;
font-size: 14px;
}

View file

@ -0,0 +1,25 @@
.org-view .organization-name {
vertical-align: middle;
margin-left: 6px;
}
.org-view h3 {
margin-bottom: 20px;
margin-top: 0px;
}
.org-view .section-description-header {
padding-left: 40px;
position: relative;
margin-bottom: 20px;
}
.org-view .section-description-header:before {
font-family: FontAwesome;
content: "\f05a";
position: absolute;
top: 2px;
left: 6px;
font-size: 27px;
color: #888;
}

View file

@ -0,0 +1,51 @@
.team-view .co-main-content-panel {
padding: 20px;
}
.team-view .team-name {
vertical-align: middle;
margin-left: 6px;
}
.team-view .team-view-header {
border-bottom: 1px solid #eee;
margin-bottom: 10px;
padding-bottom: 10px;
}
.team-view .team-view-header > h3 {
margin-top: 10px;
}
.team-view .team-view-header .popover {
max-width: none !important;
}
.team-view .team-view-header .popover.bottom-right .arrow:after {
border-bottom-color: #f7f7f7;
top: 2px;
}
.team-view .team-view-header .popover-content {
font-size: 14px;
padding-top: 6px;
min-width: 500px;
}
.team-view .team-view-header .popover-content input {
background: white;
}
.team-view .team-view-add-element .help-text {
font-size: 13px;
color: #ccc;
margin-top: 10px;
}
.team-view .co-table-header-row td {
padding-top: 20px !important;
}
.team-view .co-table-header-row:first-child td {
padding-top: 10px !important;
}

View file

@ -372,55 +372,6 @@ nav.navbar-default .navbar-nav>li>a.active {
top: 70px; top: 70px;
} }
.entity-search-element {
position: relative;
display: block;
}
.entity-search-element .entity-reference {
position: absolute !important;
top: 7px;
left: 8px;
right: 36px;
z-index: 0;
pointer-events: none;
}
.entity-search-element .entity-reference .entity-reference-element {
pointer-events: none;
}
.entity-search-element .entity-reference-element i.fa-exclamation-triangle {
pointer-events: all;
}
.entity-search-element .entity-reference .entity-name {
display: none;
}
.entity-search-element input {
vertical-align: middle;
width: 100%;
}
.entity-search-element.persistent input {
padding-left: 28px;
padding-right: 28px;
}
.entity-search-element .twitter-typeahead {
vertical-align: middle;
display: block !important;
margin-right: 36px;
}
.entity-search-element .dropdown {
vertical-align: middle;
position: absolute;
top: 0px;
right: 0px;
}
.dropdown-menu i.fa { .dropdown-menu i.fa {
margin-right: 6px; margin-right: 6px;
position: relative; position: relative;
@ -574,27 +525,6 @@ i.toggle-icon:hover {
visibility: hidden; visibility: hidden;
} }
.robots-manager-element {
max-width: 800px;
}
.robots-manager-element .alert {
margin-bottom: 20px;
}
.robots-manager-element .robot a {
font-size: 16px;
cursor: pointer;
}
.robots-manager-element .robot .prefix {
color: #aaa;
}
.robots-manager-element .robot i {
margin-right: 10px;
}
.logs-view-element .header { .logs-view-element .header {
padding-bottom: 10px; padding-bottom: 10px;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
@ -2330,6 +2260,7 @@ p.editable:hover i {
.copy-box-element input { .copy-box-element input {
border: 0px; border: 0px;
padding-right: 32px; padding-right: 32px;
cursor: pointer !important;
} }
.copy-box-element .copy-container .copy-icon { .copy-box-element .copy-container .copy-icon {
@ -3336,64 +3267,6 @@ p.editable:hover i {
max-width: 100%; max-width: 100%;
} }
.billing-invoices-element .invoice-title {
padding: 6px;
cursor: pointer;
}
.billing-invoices-element .invoice-status .success {
color: green;
}
.billing-invoices-element .invoice-status .pending {
color: steelblue;
}
.billing-invoices-element .invoice-status .danger {
color: red;
}
.billing-invoices-element .invoice-amount:before {
content: '$';
}
.billing-invoices-element .invoice-details {
margin-left: 10px;
margin-bottom: 10px;
padding: 4px;
padding-left: 6px;
border-left: 2px solid #eee !important;
}
.billing-invoices-element .invoice-details td {
border: 0px solid transparent !important;
}
.billing-invoices-element .invoice-details dl {
margin: 0px;
}
.billing-invoices-element .invoice-details dd {
margin-left: 10px;
padding: 6px;
margin-bottom: 10px;
}
.billing-invoices-element .invoice-title:hover {
color: steelblue;
}
.prototype-manager-element thead th {
padding: 4px;
color: #666;
}
.prototype-manager-element td {
padding: 10px !important;
vertical-align: middle !important;
}
.org-list h2 { .org-list h2 {
margin-bottom: 20px; margin-bottom: 20px;
} }
@ -5004,3 +4877,13 @@ i.rocket-icon {
text-align: center; text-align: center;
} }
.manager-header {
margin-bottom: 10px;
padding-bottom: 10px;
border-bottom: 1px solid #eee;
}
.manager-header h3 {
margin-bottom: 10px;
}

View file

@ -0,0 +1,4 @@
<span class="anchor-element">
<a ng-href="{{ href }}" ng-show="href && !isOnlyText"><span ng-transclude></span></a>
<span ng-show="!href || isOnlyText"><span ng-transclude></span></span>
</span>

View file

@ -1,6 +1,6 @@
<div class="application-info-element" style="padding-bottom: 18px"> <div class="application-info-element" style="padding-bottom: 18px">
<div class="auth-header"> <div class="auth-header">
<span class="avatar" size="48" hash="application.avatar"></span> <span class="avatar" size="48" data="application.avatar"></span>
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2> <h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
<h4> <h4>
{{ application.organization.name }} {{ application.organization.name }}

View file

@ -1,17 +1,24 @@
<div class="application-manager-element"> <div class="application-manager-element">
<div class="quay-spinner" ng-show="loading"></div> <div class="cor-loader" ng-show="loading"></div>
<div ng-show="!loading">
<div class="cor-container" ng-show="!loading"> <div class="manager-header">
<div class="side-controls"> <div class="side-controls">
<span class="popup-input-button" placeholder="'Application Name'" submitted="createApplication(value)"> <span class="popup-input-button" placeholder="'Application Name'"
submitted="createApplication(value)">
<i class="fa fa-plus"></i> Create New Application <i class="fa fa-plus"></i> Create New Application
</span> </span>
</div> </div>
<h3>OAuth Applications</h3>
</div>
<table class="table"> <div class="manager-header section-description-header">
The OAuth Applications panel allows organizations to define custom OAuth applications that can be used by internal or external customers to access <span class="registry-name"></span> data on behalf of the customers. More information about the <span class="registry-name"></span> API can be found by contacting support.
</div>
<table class="co-table">
<thead> <thead>
<th>Application Name</th> <td>Application Name</td>
<th>Application URI</th> <td>Application URI</td>
</thead> </thead>
<tr ng-repeat="app in applications"> <tr ng-repeat="app in applications">

View file

@ -1 +1,12 @@
<img class="avatar-element" ng-src="{{ AvatarService.getAvatar(_hash, size) }}"> <span class="avatar-element"
ng-style="{'width': size, 'height': size, 'backgroundColor': data.color, 'fontSize': fontSize, 'lineHeight': lineHeight}"
ng-class="data.kind">
<img ng-src="//www.gravatar.com/avatar/{{ data.hash }}?d=404&size={{ size }}"
ng-if="loadGravatar"
ng-show="hasGravatar"
ng-image-watch="imageCallback">
<span class="default-avatar" ng-if="!isLoading && !hasGravatar">
<span class="letter" ng-if="data.kind != 'team' || data.name != 'owners'">{{ data.name.charAt(0).toUpperCase() }}</span>
<span class="letter" ng-if="data.kind == 'team' && data.name == 'owners'">&Omega;</span>
</span>
</span>

View file

@ -1,25 +1,23 @@
<div class="billing-invoices-element"> <div class="billing-invoices-element">
<div ng-show="loading"> <div class="cor-loader" ng-show="loading"></div>
<div class="quay-spinner"></div>
</div>
<div ng-show="!loading && !invoices"> <div ng-show="!loading && !invoices">
No invoices have been created No invoices have been created
</div> </div>
<div ng-show="!loading && invoices"> <div ng-show="!loading && invoices">
<table class="table"> <table class="co-table">
<thead> <thead>
<th>Billing Date/Time</th> <td>Billing Date/Time</td>
<th>Amount Due</th> <td>Amount Due</td>
<th>Status</th> <td>Status</td>
<th></th> <td class="options-col"></td>
</thead> </thead>
<tbody class="invoice" ng-repeat="invoice in invoices"> <tbody class="invoice" ng-repeat="invoice in invoices">
<tr class="invoice-title"> <tr class="invoice-title">
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td> <td><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td> <td><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
<td> <td>
<span class="invoice-status"> <span class="invoice-status">
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span> <span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
@ -28,24 +26,12 @@
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span> <span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
</span> </span>
</td> </td>
<td> <td class="options-col">
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new"> <a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
<i class="fa fa-download" data-title="Download Receipt" bs-tooltip="tooltip.title"></i> <i class="fa fa-download" data-title="Download Receipt" bs-tooltip="tooltip.title"></i>
</a> </a>
</td> </td>
</tr> </tr>
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
<td colspan="3">
<dl class="dl-normal">
<dt>Billing Period</dt>
<dd>
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
</dd>
</dl>
</td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View file

@ -1,38 +1,3 @@
<span class="entity-reference-element"> <span class="entity-reference-element">
<span ng-if="entity.kind == 'team'"> <span quay-include="{'Config.isNewLayout()': 'directives/new-entity-reference.html', '!Config.isNewLayout()': 'directives/old-entity-reference.html'}"></span>
<i class="fa fa-group" data-title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
<span class="entity-name">
<span ng-if="!getIsAdmin(namespace)">{{entity.name}}</span>
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
</span>
</span>
<span ng-if="entity.kind == 'org'">
<span class="avatar" size="avatarSize || 16" hash="entity.avatar"></span>
<span class="entity-name">
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
</span>
</span>
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
<span class="avatar" size="avatarSize || 16" hash="entity.avatar" ng-if="showAvatar == 'true' && entity.avatar"></span>
<span ng-if="showAvatar != 'true' || !entity.avatar">
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
</span>
<span class="entity-name" ng-if="entity.is_robot">
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
</a>
<span ng-if="!getIsAdmin(getPrefix(entity.name))">
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
</span>
</span>
<span class="entity-name" ng-if="!entity.is_robot">
<span>{{getShortenedName(entity.name)}}</span>
</span>
</span>
<i class="fa fa-exclamation-triangle" ng-if="entity.is_org_member === false"
data-title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
</i>
</span> </span>

View file

@ -6,12 +6,26 @@
<span class="caret"></span> <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" ng-class="pullRight == 'true' ? 'pull-right': ''" role="menu" aria-labelledby="entityDropdownMenu"> <ul class="dropdown-menu" ng-class="pullRight == 'true' ? 'pull-right': ''" role="menu" aria-labelledby="entityDropdownMenu">
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li> <li ng-show="lazyLoading" style="padding: 10px"><div class="cor-loader"></div></li>
<li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams"> <li role="presentation" class="dropdown-header" ng-show="!lazyLoading && !robots && !isAdmin && !teams">
You do not have permission to manage teams and robots for this organization You do not have permission to manage teams and robots for this organization
</li> </li>
<li role="presentation" ng-show="includeTeams && isOrganization && !lazyLoading && isAdmin">
<a role="menuitem" class="new-action" tabindex="-1" href="javascript:void(0)" ng-click="createTeam()">
<i class="fa fa-group"></i> Create team
</a>
</li>
<li role="presentation" ng-show="includeRobots && !lazyLoading && isAdmin">
<a role="menuitem" class="new-action" tabindex="-1" href="javascript:void(0)" ng-click="createRobot()">
<i class="fa fa-wrench"></i>
Create robot account
</a>
</li>
<li role="presentation" class="divider" ng-show="!lazyLoading && robots && isAdmin"></li>
<li role="presentation" class="dropdown-header" <li role="presentation" class="dropdown-header"
ng-show="!lazyLoading && !teams.length && !robots.length && !((includeTeams && isOrganization && isAdmin) || (includeRobots && isAdmin))"> ng-show="!lazyLoading && !teams.length && !robots.length && !((includeTeams && isOrganization && isAdmin) || (includeRobots && isAdmin))">
<span ng-if="includeRobots && includeTeams && isOrganization"> <span ng-if="includeRobots && includeTeams && isOrganization">
@ -35,34 +49,29 @@
</span> </span>
</li> </li>
<li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading" <li role="presentation" class="dropdown-header" ng-show="!lazyLoading && teams">Teams</li>
ng-click="setEntity(team.name, 'team', false)">
<li class="menuitem" role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading"
ng-click="setEntity(team.name, 'team', false, team.avatar)">
<a role="menuitem" tabindex="-1" href="javascript:void(0)"> <a role="menuitem" tabindex="-1" href="javascript:void(0)">
<span ng-if="!Config.isNewLayout()">
<i class="fa fa-group"></i> <span>{{ team.name }}</span> <i class="fa fa-group"></i> <span>{{ team.name }}</span>
</span>
<span ng-if="Config.isNewLayout()">
<span class="avatar" data="team.avatar" size="16"></span> <span>{{ team.name }}</span>
</span>
</a> </a>
</li> </li>
<li role="presentation" class="divider" ng-show="!lazyLoading && teams && (isAdmin || robots)"></li> <li role="presentation" class="divider" ng-show="!lazyLoading && teams && (isAdmin || robots)"></li>
<li role="presentation" ng-repeat="robot in robots" ng-show="!lazyLoading"> <li role="presentation" class="dropdown-header" ng-show="!lazyLoading && robots">Robot Accounts</li>
<li class="menuitem" role="presentation" ng-repeat="robot in robots" ng-show="!lazyLoading">
<a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="setEntity(robot.name, 'user', true)"> <a role="menuitem" tabindex="-1" href="javascript:void(0)" ng-click="setEntity(robot.name, 'user', true)">
<i class="fa fa-wrench"></i> <span>{{ robot.name }}</span> <i class="fa fa-wrench"></i> <span>{{ robot.name }}</span>
</a> </a>
</li> </li>
<li role="presentation" class="divider" ng-show="!lazyLoading && robots && isAdmin"></li>
<li role="presentation" ng-show="includeTeams && isOrganization && !lazyLoading && isAdmin">
<a role="menuitem" class="new-action" tabindex="-1" href="javascript:void(0)" ng-click="createTeam()">
<i class="fa fa-group"></i> Create team
</a>
</li>
<li role="presentation" ng-show="includeRobots && !lazyLoading && isAdmin">
<a role="menuitem" class="new-action" tabindex="-1" href="javascript:void(0)" ng-click="createRobot()">
<i class="fa fa-wrench"></i>
Create robot account
</a>
</li>
</ul> </ul>
</div> </div>
</span> </span>

View file

@ -23,7 +23,7 @@
<ul class="nav navbar-nav navbar-right visible-xs" ng-switch on="user.anonymous"> <ul class="nav navbar-nav navbar-right visible-xs" ng-switch on="user.anonymous">
<li ng-switch-when="false"> <li ng-switch-when="false">
<a href="/user/" class="user-view" target="{{ appLinkTarget() }}"> <a href="/user/" class="user-view" target="{{ appLinkTarget() }}">
<span class="avatar" size="32" hash="user.avatar"></span> <span class="avatar" size="32" data="user.avatar"></span>
{{ user.username }} {{ user.username }}
</a> </a>
</li> </li>
@ -48,7 +48,7 @@
<li class="dropdown" ng-switch-when="false"> <li class="dropdown" ng-switch-when="false">
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown"> <a href="javascript:void(0)" class="dropdown-toggle user-dropdown user-view" data-toggle="dropdown">
<span class="avatar" size="32" hash="user.avatar"></span> <span class="avatar" size="32" data="user.avatar"></span>
{{ user.username }} {{ user.username }}
<span class="notifications-bubble"></span> <span class="notifications-bubble"></span>
<b class="caret"></b> <b class="caret"></b>

View file

@ -1,19 +1,19 @@
<span class="namespace-selector-dropdown"> <span class="namespace-selector-dropdown">
<span ng-show="user.organizations.length == 0"> <span ng-show="user.organizations.length == 0">
<span class="avatar" size="24" hash="user.avatar"></span> <span class="avatar" size="24" data="user.avatar"></span>
<span class="namespace-name">{{user.username}}</span> <span class="namespace-name">{{user.username}}</span>
</span> </span>
<div class="btn-group" ng-show="user.organizations.length > 0"> <div class="btn-group" ng-show="user.organizations.length > 0">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span class="avatar" size="16" hash="namespaceObj.avatar"></span> <span class="avatar" size="16" data="namespaceObj.avatar"></span>
{{namespace}} <span class="caret"></span> {{namespace}} <span class="caret"></span>
</button> </button>
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<li class="namespace-item" ng-repeat="org in user.organizations" <li class="namespace-item" ng-repeat="org in user.organizations"
ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''"> ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''">
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)"> <a class="namespace" href="javascript:void(0)" ng-click="setNamespace(org)">
<span class="avatar" size="24" hash="org.avatar"></span> <span class="avatar" size="24" data="org.avatar"></span>
<span class="namespace-name">{{ org.name }}</span> <span class="namespace-name">{{ org.name }}</span>
</a> </a>
@ -25,7 +25,7 @@
<li class="divider"></li> <li class="divider"></li>
<li> <li>
<a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)"> <a class="namespace" href="javascript:void(0)" ng-click="setNamespace(user)">
<span class="avatar" size="24" hash="user.avatar"></span> <span class="avatar" size="24" data="user.avatar"></span>
<span class="namespace-name">{{ user.username }}</span> <span class="namespace-name">{{ user.username }}</span>
</a> </a>
</li> </li>

View file

@ -0,0 +1,40 @@
<span class="new-entity-reference" data-title="{{ getTitle(entity) }} {{ entity.name }}" bs-tooltip>
<span ng-switch on="entity.kind">
<!-- Team -->
<span ng-switch-when="team">
<span class="avatar" data="entity.avatar" size="avatarSize || 16"></span>
<span class="entity-name anchor"
href="/organization/{{ namespace }}/teams/{{ entity.name }}"
is-only-text="!getIsAdmin(namespace)">
{{ entity.name }}
</span>
</span>
<!-- Organization -->
<span ng-switch-when="org">
<span class="avatar" size="avatarSize || 16" data="entity.avatar"></span>
<span class="entity-name anchor" href="/organization/{{ entity.name }}"
is-only-text="!getIsAdmin(entity.name)">
</span>
</span>
<!-- User or Robot -->
<span ng-switch-when="user">
<!-- User -->
<span ng-if="!entity.is_robot">
<span class="avatar" size="avatarSize || 16" data="entity.avatar"></span>
<span class="entity-name">{{ entity.name }}</span>
</span>
<!-- Robot -->
<span ng-if="entity.is_robot">
<i class="fa fa-lg fa-wrench"></i>
<span class="entity-name anchor" href="{{ getRobotUrl(entity.name) }}"
is-only-text="!getIsAdmin(getPrefix(entity.name))">
<span class="prefix">{{ getPrefix(entity.name) }}+</span>
<span>{{ getShortenedName(entity.name) }}</span>
</span>
</span>
</span>
</span>
</span>

View file

@ -3,7 +3,7 @@
<div class="circle" ng-class="getClass(notification)"></div> <div class="circle" ng-class="getClass(notification)"></div>
<div class="message" ng-bind-html="getMessage(notification)"></div> <div class="message" ng-bind-html="getMessage(notification)"></div>
<div class="orginfo" ng-if="notification.organization"> <div class="orginfo" ng-if="notification.organization">
<span class="avatar" size="24" hash="getAvatar(notification.organization)"></span> <span class="avatar" size="24" data="getAvatar(notification.organization)"></span>
<span class="orgname">{{ notification.organization }}</span> <span class="orgname">{{ notification.organization }}</span>
</div> </div>
</div> </div>

View file

@ -0,0 +1,39 @@
<!-- DEPRECATED! -->
<span class="old-entity-reference">
<span ng-if="entity.kind == 'team'">
<i class="fa fa-group" data-title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
<span class="entity-name">
<span ng-if="!getIsAdmin(namespace)">{{entity.name}}</span>
<span ng-if="getIsAdmin(namespace)"><a href="/organization/{{ namespace }}/teams/{{ entity.name }}">{{entity.name}}</a></span>
</span>
</span>
<span ng-if="entity.kind == 'org'">
<span class="avatar" size="avatarSize || 16" data="entity.avatar"></span>
<span class="entity-name">
<span ng-if="!getIsAdmin(entity.name)">{{entity.name}}</span>
<span ng-if="getIsAdmin(entity.name)"><a href="/organization/{{ entity.name }}">{{entity.name}}</a></span>
</span>
</span>
<span ng-if="entity.kind != 'team' && entity.kind != 'org'">
<span class="avatar" size="avatarSize || 16" data="entity.avatar" ng-if="showAvatar == 'true' && entity.avatar"></span>
<span ng-if="showAvatar != 'true' || !entity.avatar">
<i class="fa fa-user" ng-show="!entity.is_robot" data-title="User" bs-tooltip="tooltip.title" data-container="body"></i>
<i class="fa fa-wrench" ng-show="entity.is_robot" data-title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
</span>
<span class="entity-name" ng-if="entity.is_robot">
<a href="{{ getRobotUrl(entity.name) }}" ng-if="getIsAdmin(getPrefix(entity.name))">
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
</a>
<span ng-if="!getIsAdmin(getPrefix(entity.name))">
<span class="prefix">{{ getPrefix(entity.name) }}+</span><span>{{ getShortenedName(entity.name) }}</span>
</span>
</span>
<span class="entity-name" ng-if="!entity.is_robot">
<span>{{getShortenedName(entity.name)}}</span>
</span>
</span>
<i class="fa fa-exclamation-triangle" ng-if="entity.is_org_member === false"
data-title="This user is not a member of the organization" bs-tooltip="tooltip.title" data-container="body">
</i>
</span>

View file

@ -1,5 +1,5 @@
<div class="organization-header-element"> <div class="organization-header-element">
<span class="avatar" size="24" hash="organization.avatar"></span> <span class="avatar" size="24" data="organization.avatar"></span>
<span class="organization-name" ng-show="teamName || clickable"> <span class="organization-name" ng-show="teamName || clickable">
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a> <a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</span> </span>

View file

@ -1,4 +1,4 @@
<button class="btn btn-success" data-trigger="click" <button class="btn btn-primary" data-trigger="click"
data-content-template="/static/directives/popup-input-dialog.html" data-content-template="/static/directives/popup-input-dialog.html"
data-placement="bottom" ng-click="popupShown()" bs-popover> data-placement="bottom" ng-click="popupShown()" bs-popover>
<span ng-transclude></span> <span ng-transclude></span>

View file

@ -1,54 +1,67 @@
<div class="prototype-manager-element"> <div class="prototype-manager-element">
<div class="quay-spinner" ng-show="loading"></div> <div class="cor-loader" ng-show="loading"></div>
<div ng-show="!loading">
<div class="cor-container" ng-show="!loading"> <div class="manager-header">
<div class="alert alert-info">
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository <strong>when it is created</strong>.
</div>
<div class="side-controls"> <div class="side-controls">
<button class="btn btn-success" ng-click="showAddDialog()"> <button class="btn btn-primary" ng-click="showAddDialog()">
<i class="fa fa-plus"></i> <i class="fa fa-plus"></i>
New Default Permission Create Default Permission
</button> </button>
</div> </div>
<h3>Default Permissions</h3>
</div>
<table class="table"> <div class="manager-header section-description-header">
The Default permissions panel defines permissions that should be granted automatically to a repository when it is created, in addition to the default of the repository's
creator. Permissions are assigned based on the user who created the repository.
<br><br>
<strong>Note:</strong> Permissions added here do <strong>not</strong> automatically get added to
existing repositories.
</div>
<table class="co-table">
<thead> <thead>
<th> <td>
<span class="context-tooltip" <span class="context-tooltip"
data-title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission." data-title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
bs-tooltip="tooltip.title" data-container="body"> data-container="body" bs-tooltip>
Repository Creator Repository Created By
</span> </span>
</th> </td>
<th> <td>
<span class="context-tooltip" data-title="The user, robot or team that is being granted the permission" <span class="context-tooltip"
bs-tooltip="tooltip.title" data-container="body"> data-title="The user, robot or team that is being granted the permission"
Applies To User/Robot/Team data-container="body" bs-tooltip>
Permission Applied To
</span> </span>
</th> </td>
<th>Permission</th> <td>Permission</td>
<th style="width: 150px"></th> <td class="options-col"></td>
</thead> </thead>
<tr ng-repeat="prototype in prototypes | orderBy:comparePrototypes"> <tr ng-repeat="prototype in prototypes | orderBy:comparePrototypes">
<td> <td>
<span class="entity-reference block-reference" entity="prototype.activating_user" <span class="entity-reference block-reference" entity="prototype.activating_user"
namespace="organization.name" ng-show="prototype.activating_user"></span> namespace="organization.name" ng-if="prototype.activating_user"
avatar-size="24"></span>
<span ng-show="!prototype.activating_user" style="font-variant: small-caps; font-weight: bold; font-size: 16px;"> <span ng-show="!prototype.activating_user"
style="font-variant: small-caps; font-weight: bold; font-size: 16px;">
(Organization Default) (Organization Default)
</span> </span>
</td> </td>
<td> <td>
<span class="entity-reference block-reference" entity="prototype.delegate" namespace="organization.name"></span> <span class="entity-reference block-reference" entity="prototype.delegate" namespace="organization.name" avatar-size="24"></span>
</td> </td>
<td> <td>
<span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span> <span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span>
</td> </td>
<td> <td class="options-col">
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deletePrototype(prototype)"></span> <span class="cor-options-menu">
<span class="cor-option" option-click="deletePrototype(prototype)">
<i class="fa fa-times"></i> Delete Permission
</span>
</span>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -2,16 +2,18 @@
<div class="new-repo-listing"> <div class="new-repo-listing">
<!-- Titles --> <!-- Titles -->
<div ng-if="!hideTitle">
<div ng-if="starred" class="repo-list-title"> <div ng-if="starred" class="repo-list-title">
<i class="fa fa-star starred"></i> <i class="fa fa-star starred"></i>
Starred Starred
</div> </div>
<div ng-if="!starred" class="repo-list-title"> <div ng-if="!starred" class="repo-list-title">
<span class="avatar" size="24" hash="namespace.avatar"></span> <span class="avatar" size="24" data="namespace.avatar"></span>
<span ng-if="!isOrganization(namespace.name)">{{ namespace.name }}</span> <span ng-if="!isOrganization(namespace.name)">{{ namespace.name }}</span>
<a ng-if="isOrganization(namespace.name)" <a ng-if="isOrganization(namespace.name)"
href="/organization/{{ namespace.name }}">{{ namespace.name }}</a> href="/organization/{{ namespace.name }}">{{ namespace.name }}</a>
</div> </div>
</div>
<!-- Repositories --> <!-- Repositories -->
<div class="resource-view" resource="repositoriesResource"> <div class="resource-view" resource="repositoriesResource">

View file

@ -4,7 +4,7 @@
<!-- User/Team Permissions --> <!-- User/Team Permissions -->
<div class="co-panel"> <div class="co-panel">
<div class="co-panel-heading"><i class="fa fa-key"></i> User and Robot Permissions</div> <div class="co-panel-heading"><i class="fa fa-key"></i> User and Robot Permissions</div>
<div class="panel-body"> <div class="panel-body" style="padding-top: 5px;">
<div class="repository-permissions-table" repository="repository"></div> <div class="repository-permissions-table" repository="repository"></div>
</div> </div>
</div> </div>
@ -12,7 +12,7 @@
<!-- Access Tokens (DEPRECATED) --> <!-- Access Tokens (DEPRECATED) -->
<div class="co-panel" ng-show="hasTokens"> <div class="co-panel" ng-show="hasTokens">
<div class="co-panel-heading"><i class="fa fa-key"></i> Access Token Permissions</div> <div class="co-panel-heading"><i class="fa fa-key"></i> Access Token Permissions</div>
<div class="panel-body"> <div class="panel-body" style="padding-top: 5px;">
<div class="repository-tokens-table" repository="repository" has-tokens="hasTokens"></div> <div class="repository-tokens-table" repository="repository" has-tokens="hasTokens"></div>
</div> </div>
</div> </div>

View file

@ -14,7 +14,7 @@
error-message="'Could not load repository events'"> error-message="'Could not load repository events'">
<div class="empty" ng-if="!notifications.length"> <div class="empty" ng-if="!notifications.length">
<div class="empty-primary-msg">No notification have been setup for this repository.</div> <div class="empty-primary-msg">No notifications have been setup for this repository.</div>
<div class="empty-secondary-msg" ng-if="repository.can_write"> <div class="empty-secondary-msg" ng-if="repository.can_write">
Click the "Create Notification" button above to add a new notification for a repository event. Click the "Create Notification" button above to add a new notification for a repository event.
</div> </div>

View file

@ -2,7 +2,7 @@
<div class="resource-view" <div class="resource-view"
resources="[permissionResources.team, permissionResources.user]" resources="[permissionResources.team, permissionResources.user]"
error-message="'Could not load repository permissions'"> error-message="'Could not load repository permissions'">
<table class="co-table permissions"> <table class="co-table no-lines permissions">
<thead> <thead>
<tr> <tr>
<td>Account Name</td> <td>Account Name</td>
@ -11,11 +11,27 @@
</tr> </tr>
</thead> </thead>
<tr ng-show="!hasPermissions(permissionResources.team, permissionResources.user)">
<td colspan="3">
<div class="empty">
<div class="empty-primary-msg">No permissions found.</div>
<div class="empty-secondary-msg">
To add a permission, enter the information below and click "Add Permission".
</div>
</div>
</td>
</tr>
<!-- Team Permissions --> <!-- Team Permissions -->
<tr ng-repeat="(name, permission) in permissionResources.team.value"> <tr class="co-table-header-row"
ng-if="(permissionResources.team.value | objectFilter:allEntries).length">
<td colspan="3"><i class="fa fa-group"></i> Team Permissions</td>
</tr>
<tr class="indented-row" ng-repeat="(name, permission) in permissionResources.team.value">
<td class="team entity"> <td class="team entity">
<span class="entity-reference" namespace="repository.namespace" <span class="entity-reference" namespace="repository.namespace"
entity="buildEntityForPermission(name, permission, 'team')"> entity="buildEntityForPermission(permission, 'team')"
avatar-size="24">
</span> </span>
</td> </td>
<td class="user-permissions"> <td class="user-permissions">
@ -32,26 +48,66 @@
</tr> </tr>
<!-- User Permissions --> <!-- User Permissions -->
<tr ng-repeat="(name, permission) in permissionResources.user.value"> <tr class="co-table-header-row"
<td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}"> ng-if="(permissionResources.user.value | objectFilter:onlyUser).length">
<td colspan="3"><i class="fa fa-user"></i> User Permissions</td>
</tr>
<tr class="indented-row"
ng-repeat="permission in (permissionResources.user.value | objectFilter:onlyUser)">
<td class="user-permission-entity">
<span class="entity-reference" namespace="repository.namespace" <span class="entity-reference" namespace="repository.namespace"
entity="buildEntityForPermission(name, permission, 'user')"> entity="buildEntityForPermission(permission, 'user')"
avatar-size="24">
</span> </span>
<i class="fa fa-exclamation-triangle outside-org"
ng-if="permission.is_org_member === false"
data-title="This user is not a member of the organization" bs-tooltip></i>
</td> </td>
<td class="user-permissions"> <td class="user-permissions">
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'user')" roles="roles"></span> <span class="role-group" current-role="permission.role"
role-changed="setRole(role, permission.name, 'user')" roles="roles"></span>
</div> </div>
</td> </td>
<td class="options-col"> <td class="options-col">
<span class="cor-options-menu"> <span class="cor-options-menu">
<span class="cor-option" option-click="deleteRole(name, 'user')"> <span class="cor-option" option-click="deleteRole(permission.name, 'user')">
<i class="fa fa-times"></i> Delete Permission <i class="fa fa-times"></i> Delete Permission
</span> </span>
</span> </span>
</td> </td>
</tr> </tr>
<!-- Robot Permissions -->
<tr class="co-table-header-row"
ng-if="(permissionResources.user.value | objectFilter:onlyRobot).length">
<td colspan="3"><i class="fa fa-wrench"></i> Robot Account Permissions</td>
</tr>
<tr class="indented-row"
ng-repeat="permission in (permissionResources.user.value | objectFilter:onlyRobot)">
<td class="user-permission-entity">
<span class="entity-reference" namespace="repository.namespace"
entity="buildEntityForPermission(permission, 'user')"
avatar-size="24">
</span>
</td>
<td class="user-permissions">
<div class="btn-group btn-group-sm">
<span class="role-group" current-role="permission.role"
role-changed="setRole(role, permission.name, 'user')" roles="roles"></span>
</div>
</td>
<td class="options-col">
<span class="cor-options-menu">
<span class="cor-option" option-click="deleteRole(permission.name, 'user')">
<i class="fa fa-times"></i> Delete Permission
</span>
</span>
</td>
</tr>
<tr class="add-row-spacer"><td colspan="3"></td></tr>
<tr class="add-row"> <tr class="add-row">
<td id="add-entity-permission" class="admin-search"> <td id="add-entity-permission" class="admin-search">
<span class="entity-search" namespace="repository.namespace" <span class="entity-search" namespace="repository.namespace"

View file

@ -1,32 +1,119 @@
<div class="robots-manager-element"> <div class="robots-manager-element">
<div class="quay-spinner" ng-show="loading"></div> <div class="cor-loader" ng-show="loading"></div>
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
<div ng-show="!loading"> <div ng-show="!loading">
<div class="manager-header">
<div class="side-controls"> <div class="side-controls">
<span class="popup-input-button" pattern="ROBOT_PATTERN" placeholder="'Robot Account Name'" <span class="popup-input-button" pattern="ROBOT_PATTERN"
placeholder="'Robot Account Name'"
submitted="createRobot(value)"> submitted="createRobot(value)">
<i class="fa fa-wrench"></i> Create Robot Account <i class="fa fa-plus"></i> Create Robot Account
</span> </span>
</div> </div>
<h3>Robot Accounts</h3>
</div>
<table class="table"> <div class="manager-header section-description-header">
Robot Accounts are named tokens that can be granted permissions on multiple repositories
under this <span ng-if="organization">organization</span><span ng-if="!organization">user namespace</span>. They are typically used in environments where credentials will
be shared, such as deployment systems.
</div>
<div class="empty" ng-if="!robots.length">
<div class="empty-primary-msg">No robot accounts defined.</div>
<div class="empty-secondary-msg">
Click the "Create Robot Account" button above to create a robot account.
</div>
</div>
<table class="co-table" ng-if="robots.length">
<thead> <thead>
<th>Robot Account Name</th> <td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()"></td>
<th style="width: 150px"></th> <td>Robot Account Name</td>
<td ng-if="organization && Config.isNewLayout()">Teams</td>
<td ng-if="Config.isNewLayout()">Repository Permissions</td>
<td class="options-col"></td>
</thead> </thead>
<tr ng-repeat="robotInfo in robots"> <tbody ng-repeat="robotInfo in robots">
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
<td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()">
<span ng-if="robotInfo.repositories.length > 0" 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>
</td>
<td class="robot"> <td class="robot">
<i class="fa fa-wrench"></i> <i class="fa fa-wrench"></i>
<a ng-click="showRobot(robotInfo)"> <a ng-click="showRobot(robotInfo)">
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }} <span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }}
</a> </a>
</td> </td>
<td> <td ng-if="organization && Config.isNewLayout()">
<span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span> <span class="empty" ng-if="robotInfo.teams.length == 0">
(Not a member of any team)
</span>
<span class="empty" ng-if="robotInfo.teams.length > 0">
<span ng-repeat="team in robotInfo.teams"
data-title="Team {{ team.name }}" bs-tooltip>
<span class="anchor" is-text-only="!organization.admin" href="/organization/{{ organization.name }}/teams/{{ team.name }}">
<span class="avatar" size="24" data="team.avatar"></span>
</span>
</span>
</span>
</td>
<td ng-if="Config.isNewLayout()">
<span class="empty" ng-if="robotInfo.repositories.length == 0">
(No permissions on any repositories)
</span>
<span class="member-perm-summary" ng-if="robotInfo.repositories.length > 0">
Permissions on
<span class="anchor" href="javascript:void(0)" is-text-only="!organization.is_admin" ng-click="showPermissions(robotInfo)">{{ robotInfo.repositories.length }}
<span ng-if="robotInfo.repositories.length == 1">repository</span>
<span ng-if="robotInfo.repositories.length > 1">repositories</span>
</span>
</span>
</td>
<td class="options-col">
<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="deleteRobot(robotInfo)">
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
</span>
</span>
</td> </td>
</tr> </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>
<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="roles"
read-only="true"></span>
</div>
</td>
</tr>
</table>
</div>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>

View file

@ -1,5 +1,6 @@
<div class="btn-group btn-group-sm"> <div class="btn-group btn-group-sm">
<button ng-repeat="role in roles" <button ng-repeat="role in roles"
type="button" class="btn" ng-click="setRole(role.id)" type="button" class="btn" ng-click="setRole(role.id)"
ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'">{{ role.title }}</button> ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'"
ng-disabled="readOnly">{{ role.title }}</button>
</div> </div>

View file

@ -0,0 +1,62 @@
<div class="teams-manager-element">
<div class="manager-header">
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)" ng-show="organization.is_admin">
<i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
</span>
<h3>Teams</h3>
</div>
<div class="row hidden-xs">
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
<span class="header-text">Team Permissions</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">
<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>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span>
</div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
<div class="team-member-list" 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 | limitTo: 25">
<span data-title="{{ member.name }}" bs-tooltip>
<span class="avatar" data="member.avatar" size="26" ng-if="!member.is_robot"></span>
<i class="fa fa-wrench fa-lg" ng-if="member.is_robot"></i>
</span>
</span>
<span class="team-member-more"
ng-if="members[team.name].members.length > 25">+ {{ members[team.name].members.length - 25 }} more team members.</span>
<span class="team-member-more"
ng-if="!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="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
<span class="cor-options-menu">
<span class="cor-option" option-click="askDeleteTeam(team.name)">
<i class="fa fa-times"></i> Delete Team {{ team.name }}
</span>
</span>
</div>
</div>
</div>
</div>

View file

@ -333,11 +333,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
}); });
var activeTab = $location.search()['tab']; var activeTab = $location.search()['tab'];
var checkTabs = function() {
var tabs = $('a[data-toggle="tab"]');
if (tabs.length == 0) {
$timeout(checkTabs, 50);
return;
}
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab tabs.on('shown.bs.tab', function (e) {
// is changed in the UI.
$timeout(function() {
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
var tabName = e.target.getAttribute('data-target').substr(1); var tabName = e.target.getAttribute('data-target').substr(1);
$rootScope.$apply(function() { $rootScope.$apply(function() {
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target; var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
@ -357,7 +360,11 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
if (activeTab) { if (activeTab) {
changeTab(activeTab); changeTab(activeTab);
} }
}, 400); // 400ms to make sure angular has rendered. };
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
// is changed in the UI.
$timeout(checkTabs, 50);
}); });
var initallyChecked = false; var initallyChecked = false;

View file

@ -0,0 +1,17 @@
/**
* Adds a ng-image-watch attribute, which is a callback invoked when the image is loaded or fails.
*/
angular.module('quay').directive('ngImageWatch', function () {
return {
restrict: 'A',
link: function postLink($scope, $element, $attr) {
$element.bind('error', function() {
$scope.$eval($attr.ngImageWatch)(false);
});
$element.bind('load', function() {
$scope.$eval($attr.ngImageWatch)(true);
});
}
};
});

View file

@ -120,6 +120,8 @@ angular.module('quay').directive('quayClasses', function(Features, Config) {
/** /**
* Adds a quay-include attribtue that adds a template solely if the expression evaluates to true. * Adds a quay-include attribtue that adds a template solely if the expression evaluates to true.
* Automatically adds the Features and Config services to the scope. * Automatically adds the Features and Config services to the scope.
*
Usage: quay-include="{'Features.BILLING': 'partials/landing-normal.html', '!Features.BILLING': 'partials/landing-login.html'}"
*/ */
angular.module('quay').directive('quayInclude', function($compile, $templateCache, $http, Features, Config) { angular.module('quay').directive('quayInclude', function($compile, $templateCache, $http, Features, Config) {
return { return {
@ -127,7 +129,7 @@ angular.module('quay').directive('quayInclude', function($compile, $templateCach
restrict: 'A', restrict: 'A',
link: function($scope, $element, $attr, ctrl) { link: function($scope, $element, $attr, ctrl) {
var getTemplate = function(templateName) { var getTemplate = function(templateName) {
var templateUrl = '/static/partials/' + templateName; var templateUrl = '/static/' + templateName;
return $http.get(templateUrl, {cache: $templateCache}); return $http.get(templateUrl, {cache: $templateCache});
}; };

View file

@ -0,0 +1,19 @@
/**
* An element which displays its contents wrapped in an <a> tag, but only if the href is not null.
*/
angular.module('quay').directive('anchor', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/anchor.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'href': '@href',
'isOnlyText': '=isOnlyText'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});

View file

@ -1,5 +1,5 @@
/** /**
* An element which displays an avatar for the given {email,name} or hash. * An element which displays an avatar for the given avatar data.
*/ */
angular.module('quay').directive('avatar', function () { angular.module('quay').directive('avatar', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
@ -9,25 +9,36 @@ angular.module('quay').directive('avatar', function () {
transclude: true, transclude: true,
restrict: 'C', restrict: 'C',
scope: { scope: {
'hash': '=hash', 'data': '=data',
'email': '=email',
'name': '=name',
'size': '=size' 'size': '=size'
}, },
controller: function($scope, $element, AvatarService) { controller: function($scope, $element, AvatarService, Config, UIService) {
$scope.AvatarService = AvatarService; $scope.AvatarService = AvatarService;
$scope.Config = Config;
$scope.isLoading = true;
$scope.hasGravatar = false;
$scope.loadGravatar = false;
var refreshHash = function() { $scope.imageCallback = function(r) {
if (!$scope.name && !$scope.email) { return; } $scope.isLoading = false;
$scope._hash = AvatarService.computeHash($scope.email, $scope.name); $scope.hasGravatar = r;
}; };
$scope.$watch('hash', function(hash) { $scope.$watch('size', function(size) {
$scope._hash = hash; size = size * 1 || 16;
$scope.fontSize = (size - 4) + 'px';
$scope.lineHeight = size + 'px';
}); });
$scope.$watch('name', refreshHash); $scope.$watch('data', function(data) {
$scope.$watch('email', refreshHash); if (!data) { return; }
$scope.loadGravatar = Config.AVATAR_KIND == 'gravatar' &&
(data.kind == 'user' || data.kind == 'org');
$scope.isLoading = $scope.loadGravatar;
$scope.hasGravatar = false;
});
} }
}; };
return directiveDefinitionObject; return directiveDefinitionObject;

View file

@ -15,11 +15,6 @@ angular.module('quay').directive('billingInvoices', function () {
}, },
controller: function($scope, $element, $sce, ApiService) { controller: function($scope, $element, $sce, ApiService) {
$scope.loading = false; $scope.loading = false;
$scope.invoiceExpanded = {};
$scope.toggleInvoice = function(id) {
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
};
var update = function() { var update = function() {
var hasValidUser = !!$scope.user; var hasValidUser = !!$scope.user;

View file

@ -39,6 +39,21 @@ angular.module('quay').directive('entityReference', function () {
return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name); return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name);
}; };
$scope.getTitle = function(entity) {
if (!entity) { return ''; }
switch (entity.kind) {
case 'org':
return 'Organization';
case 'team':
return 'Team';
case 'user':
return entity.is_robot ? 'Robot Account' : 'User';
}
};
$scope.getPrefix = function(name) { $scope.getPrefix = function(name) {
if (!name) { return ''; } if (!name) { return ''; }
var plus = name.indexOf('+'); var plus = name.indexOf('+');

View file

@ -56,6 +56,8 @@ angular.module('quay').directive('entitySearch', function () {
$scope.currentEntityInternal = $scope.currentEntity; $scope.currentEntityInternal = $scope.currentEntity;
$scope.Config = Config;
var isSupported = function(kind, opt_array) { var isSupported = function(kind, opt_array) {
return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0; return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
}; };
@ -102,7 +104,7 @@ angular.module('quay').directive('entitySearch', function () {
} }
CreateService.createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) { CreateService.createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) {
$scope.setEntity(created.name, 'team', false); $scope.setEntity(created.name, 'team', false, created.avatar);
$scope.teams[teamname] = created; $scope.teams[teamname] = created;
}); });
}); });
@ -121,17 +123,18 @@ angular.module('quay').directive('entitySearch', function () {
} }
CreateService.createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) { CreateService.createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) {
$scope.setEntity(created.name, 'user', true); $scope.setEntity(created.name, 'user', true, created.avatar);
$scope.robots.push(created); $scope.robots.push(created);
}); });
}); });
}; };
$scope.setEntity = function(name, kind, is_robot) { $scope.setEntity = function(name, kind, is_robot, avatar) {
var entity = { var entity = {
'name': name, 'name': name,
'kind': kind, 'kind': kind,
'is_robot': is_robot 'is_robot': is_robot,
'avatar': avatar
}; };
if ($scope.isOrganization) { if ($scope.isOrganization) {

View file

@ -11,9 +11,9 @@ angular.module('quay').directive('repoListGrid', function () {
scope: { scope: {
repositoriesResource: '=repositoriesResource', repositoriesResource: '=repositoriesResource',
starred: '=starred', starred: '=starred',
user: "=user",
namespace: '=namespace', namespace: '=namespace',
starToggled: '&starToggled' starToggled: '&starToggled',
hideTitle: '=hideTitle'
}, },
controller: function($scope, $element, UserService) { controller: function($scope, $element, UserService) {
$scope.isOrganization = function(namespace) { $scope.isOrganization = function(namespace) {

View file

@ -2,6 +2,21 @@
* An element which displays a table of permissions on a repository and allows them to be * An element which displays a table of permissions on a repository and allows them to be
* edited. * edited.
*/ */
angular.module('quay').filter('objectFilter', function() {
return function(obj, filterFn) {
if (!obj) { return []; }
var result = [];
angular.forEach(obj, function(value) {
if (filterFn(value)) {
result.push(value);
}
});
return result;
};
});
angular.module('quay').directive('repositoryPermissionsTable', function () { angular.module('quay').directive('repositoryPermissionsTable', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -13,6 +28,7 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
'repository': '=repository' 'repository': '=repository'
}, },
controller: function($scope, $element, ApiService, Restangular, UtilService) { controller: function($scope, $element, ApiService, Restangular, UtilService) {
// TODO(jschorr): move this to a service.
$scope.roles = [ $scope.roles = [
{ 'id': 'read', 'title': 'Read', 'kind': 'success' }, { 'id': 'read', 'title': 'Read', 'kind': 'success' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success' }, { 'id': 'write', 'title': 'Write', 'kind': 'success' },
@ -58,20 +74,49 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
return Restangular.one(url); return Restangular.one(url);
}; };
$scope.buildEntityForPermission = function(name, permission, kind) { $scope.buildEntityForPermission = function(permission, kind) {
var key = name + ':' + kind; var key = permission.name + ':' + kind;
if ($scope.permissionCache[key]) { if ($scope.permissionCache[key]) {
return $scope.permissionCache[key]; return $scope.permissionCache[key];
} }
return $scope.permissionCache[key] = { return $scope.permissionCache[key] = {
'kind': kind, 'kind': kind,
'name': name, 'name': permission.name,
'is_robot': permission.is_robot, 'is_robot': permission.is_robot,
'is_org_member': permission.is_org_member 'is_org_member': permission.is_org_member,
'avatar': permission.avatar
}; };
}; };
$scope.hasPermissions = function(teams, users) {
if (teams && teams.value) {
if (Object.keys(teams.value).length > 0) {
return true;
}
}
if (users && users.value) {
if (Object.keys(users.value).length > 0) {
return true;
}
}
return false;
};
$scope.allEntries = function() {
return true;
};
$scope.onlyRobot = function(permission) {
return permission.is_robot == true;
};
$scope.onlyUser = function(permission) {
return !permission.is_robot;
};
$scope.addPermission = function() { $scope.addPermission = function() {
$scope.addPermissionInfo['working'] = true; $scope.addPermissionInfo['working'] = true;
$scope.addNewPermission($scope.addPermissionInfo.entity, $scope.addPermissionInfo.role) $scope.addNewPermission($scope.addPermissionInfo.entity, $scope.addPermissionInfo.role)

View file

@ -12,12 +12,38 @@ angular.module('quay').directive('robotsManager', function () {
'organization': '=organization', 'organization': '=organization',
'user': '=user' 'user': '=user'
}, },
controller: function($scope, $element, ApiService, $routeParams, CreateService) { controller: function($scope, $element, ApiService, $routeParams, CreateService, Config) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN; $scope.ROBOT_PATTERN = ROBOT_PATTERN;
// TODO(jschorr): move this to a service.
$scope.roles = [
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
];
$scope.robots = null; $scope.robots = null;
$scope.loading = false; $scope.loading = false;
$scope.shownRobot = null; $scope.shownRobot = null;
$scope.showRobotCounter = 0; $scope.showRobotCounter = 0;
$scope.Config = Config;
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.showPermissions = function(robotInfo) {
robotInfo.showing_permissions = !robotInfo.showing_permissions;
if (robotInfo.showing_permissions) {
loadRobotPermissions(robotInfo);
}
};
$scope.regenerateToken = function(username) { $scope.regenerateToken = function(username) {
if (!username) { return; } if (!username) { return; }

View file

@ -12,6 +12,7 @@ angular.module('quay').directive('roleGroup', function () {
scope: { scope: {
'roles': '=roles', 'roles': '=roles',
'currentRole': '=currentRole', 'currentRole': '=currentRole',
'readOnly': '=readOnly',
'roleChanged': '&roleChanged' 'roleChanged': '&roleChanged'
}, },
controller: function($scope, $element) { controller: function($scope, $element) {

View file

@ -0,0 +1,131 @@
/**
* Element for managing the teams of an organization.
*/
angular.module('quay').directive('teamsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/teams-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization'
},
controller: function($scope, $element, ApiService, CreateService) {
$scope.TEAM_PATTERN = TEAM_PATTERN;
$scope.teamRoles = [
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
];
$scope.members = {};
$scope.orderedTeams = [];
var loadTeamMembers = function() {
if (!$scope.organization) { return; }
for (var name in $scope.organization.teams) {
if (!$scope.organization.teams.hasOwnProperty(name)) { continue; }
loadMembersOfTeam(name);
}
};
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() {
if (!$scope.organization) { return; }
$scope.orderedTeams = [];
$scope.organization.ordered_teams.map(function(name) {
$scope.orderedTeams.push($scope.organization.teams[name]);
});
};
$scope.$watch('organization', loadOrderedTeams);
$scope.$watch('organization', loadTeamMembers);
$scope.setRole = function(role, teamname) {
var previousRole = $scope.organization.teams[teamname].role;
$scope.organization.teams[teamname].role = role;
var params = {
'orgname': $scope.organization.name,
'teamname': teamname
};
var data = $scope.organization.teams[teamname];
var errorHandler = ApiService.errorDisplay('Cannot update team', function(resp) {
$scope.organization.teams[teamname].role = previousRole;
});
ApiService.updateOrganizationTeam(data, params).then(function(resp) {
}, errorHandler);
};
$scope.createTeam = function(teamname) {
if (!teamname) {
return;
}
if ($scope.organization.teams[teamname]) {
$('#team-' + teamname).removeClass('highlight');
setTimeout(function() {
$('#team-' + teamname).addClass('highlight');
}, 10);
return;
}
var orgname = $scope.organization.name;
CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) {
$scope.organization.teams[teamname] = created;
$scope.members[teamname] = {};
$scope.members[teamname].members = [];
$scope.organization.ordered_teams.push(teamname);
$scope.orderedTeams.push(created);
});
};
$scope.askDeleteTeam = function(teamname) {
bootbox.confirm('Are you sure you want to delete team ' + teamname + '?', function(resp) {
if (resp) {
$scope.deleteTeam(teamname);
}
});
};
$scope.deleteTeam = function(teamname) {
var params = {
'orgname': $scope.organization.name,
'teamname': teamname
};
ApiService.deleteOrganizationTeam(null, params).then(function() {
var index = $scope.organization.ordered_teams.indexOf(teamname);
if (index >= 0) {
$scope.organization.ordered_teams.splice(index, 1);
}
loadOrderedTeams();
delete $scope.organization.teams[teamname];
}, ApiService.errorDisplay('Cannot delete team'));
};
}
};
return directiveDefinitionObject;
});

View file

@ -14,6 +14,7 @@
$scope.setEnabled = function(value) { $scope.setEnabled = function(value) {
$scope.isEnabled = value; $scope.isEnabled = value;
CookieService.putPermanent('quay.exp-new-layout', value.toString()); CookieService.putPermanent('quay.exp-new-layout', value.toString());
document.location.reload();
}; };
} }
}()); }());

View file

@ -1,6 +1,6 @@
(function() { (function() {
/** /**
* Organization admin/settings page. * DEPRECATED: Organization admin/settings page.
*/ */
angular.module('quayPages').config(['pages', function(pages) { angular.module('quayPages').config(['pages', function(pages) {
pages.create('org-admin', 'org-admin.html', OrgAdminCtrl); pages.create('org-admin', 'org-admin.html', OrgAdminCtrl);

View file

@ -3,10 +3,97 @@
* Page that displays details about an organization, such as its teams. * Page that displays details about an organization, such as its teams.
*/ */
angular.module('quayPages').config(['pages', function(pages) { angular.module('quayPages').config(['pages', function(pages) {
pages.create('org-view', 'org-view.html', OrgViewCtrl); pages.create('org-view', 'org-view.html', OrgViewCtrl, {
'newLayout': true,
'title': 'Organization {{ organization.name }}',
'description': 'Organization {{ organization.name }}'
}, ['layout'])
pages.create('org-view', 'old-org-view.html', OldOrgViewCtrl, {
}, ['old-layout']);
}]); }]);
function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) { function OrgViewCtrl($scope, $routeParams, $timeout, ApiService, UIService, AvatarService) {
var orgname = $routeParams.orgname;
$scope.showLogsCounter = 0;
$scope.showApplicationsCounter = 0;
$scope.showInvoicesCounter = 0;
$scope.orgScope = {
'changingOrganization': false,
'organizationEmail': ''
};
$scope.$watch('orgScope.organizationEmail', function(e) {
UIService.hidePopover('#changeEmailForm input');
});
var loadRepositories = function() {
var options = {
'public': false,
'private': true,
'sort': true,
'namespace': orgname,
};
$scope.repositoriesResource = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
return resp.repositories;
});
};
var loadOrganization = function() {
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
$scope.organization = org;
$scope.orgScope.organizationEmail = org.email;
$scope.isAdmin = org.is_admin;
$scope.isMember = org.is_member;
// Load the repositories.
$timeout(function() {
loadRepositories();
}, 10);
});
};
// Load the organization.
loadOrganization();
$scope.showInvoices = function() {
$scope.showInvoicesCounter++;
};
$scope.showApplications = function() {
$scope.showApplicationsCounter++;
};
$scope.showLogs = function() {
$scope.showLogsCounter++;
};
$scope.changeEmail = function() {
UIService.hidePopover('#changeEmailForm input');
$scope.orgScope.changingOrganization = true;
var params = {
'orgname': orgname
};
var data = {
'email': $scope.orgScope.organizationEmail
};
ApiService.changeOrganizationDetails(data, params).then(function(org) {
$scope.orgScope.changingOrganization = false;
$scope.organization = org;
}, function(result) {
$scope.orgScope.changingOrganization = false;
UIService.showFormError('#changeEmailForm input', result, 'right');
});
};
}
function OldOrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) {
var orgname = $routeParams.orgname; var orgname = $routeParams.orgname;
$scope.TEAM_PATTERN = TEAM_PATTERN; $scope.TEAM_PATTERN = TEAM_PATTERN;

View file

@ -3,7 +3,14 @@
* Page to view the members of a team and add/remove them. * Page to view the members of a team and add/remove them.
*/ */
angular.module('quayPages').config(['pages', function(pages) { angular.module('quayPages').config(['pages', function(pages) {
pages.create('team-view', 'team-view.html', TeamViewCtrl); pages.create('team-view', 'team-view.html', TeamViewCtrl, {
'newLayout': true,
'title': 'Team {{ teamname }}',
'description': 'Team {{ teamname }}'
}, ['layout'])
pages.create('team-view', 'old-team-view.html', TeamViewCtrl, {
}, ['old-layout']);
}]); }]);
function TeamViewCtrl($rootScope, $scope, $timeout, Features, Restangular, ApiService, $routeParams) { function TeamViewCtrl($rootScope, $scope, $timeout, Features, Restangular, ApiService, $routeParams) {

View file

@ -14,7 +14,9 @@ angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5',
break; break;
case 'gravatar': case 'gravatar':
return '//www.gravatar.com/avatar/' + hash + '?d=identicon&size=' + size; // TODO(jschorr): Remove once the new layout is in place everywhere.
var default_kind = Config.isNewLayout() ? '404' : 'identicon';
return '//www.gravatar.com/avatar/' + hash + '?d=' + default_kind + '&size=' + size;
break; break;
} }
}; };

View file

@ -71,5 +71,10 @@ angular.module('quay').factory('Config', [function() {
return value; return value;
}; };
config.isNewLayout = function() {
// TODO(jschorr): Remove once new layout is in place for everyone.
return document.cookie.toString().indexOf('quay.exp-new-layout=true') >= 0;
};
return config; return config;
}]); }]);

View file

@ -66,10 +66,10 @@ angular.module('quay').factory('UIService', [function() {
} }
}; };
uiService.showPopover = function(elem, content) { uiService.showPopover = function(elem, content, opt_placement) {
var popover = $(elem).data('bs.popover'); var popover = $(elem).data('bs.popover');
if (!popover) { if (!popover) {
$(elem).popover({'content': '-', 'placement': 'left'}); $(elem).popover({'content': '-', 'placement': opt_placement || 'left'});
} }
setTimeout(function() { setTimeout(function() {
@ -79,10 +79,10 @@ angular.module('quay').factory('UIService', [function() {
}, 500); }, 500);
}; };
uiService.showFormError = function(elem, result) { uiService.showFormError = function(elem, result, opt_placement) {
var message = result.data['message'] || result.data['error_description'] || ''; var message = result.data['message'] || result.data['error_description'] || '';
if (message) { if (message) {
uiService.showPopover(elem, message); uiService.showPopover(elem, message, opt_placement);
} else { } else {
uiService.hidePopover(elem); uiService.hidePopover(elem);
} }

View file

@ -47,7 +47,7 @@
<div class="signup-form"></div> <div class="signup-form"></div>
</div> </div>
<div ng-show="!user.anonymous" class="user-welcome"> <div ng-show="!user.anonymous" class="user-welcome">
<span class="avatar" size="128" hash="user.avatar"></span> <span class="avatar" size="128" data="user.avatar"></span>
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div> <div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a> <a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a> <a class="btn btn-success" href="/new/">Create a new repository</a>

View file

@ -46,7 +46,7 @@
<div class="signup-form"></div> <div class="signup-form"></div>
</div> </div>
<div ng-show="!user.anonymous" class="user-welcome"> <div ng-show="!user.anonymous" class="user-welcome">
<span class="avatar" size="128" hash="user.avatar"></span> <span class="avatar" size="128" data="user.avatar"></span>
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div> <div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a> <a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
<a class="btn btn-success" href="/new/">Create a new repository</a> <a class="btn btn-success" href="/new/">Create a new repository</a>

View file

@ -1,3 +1,3 @@
<div quay-include="{'Features.BILLING': 'landing-normal.html', '!Features.BILLING': 'landing-login.html'}" onload="chromify()"> <div quay-include="{'Features.BILLING': 'partials/landing-normal.html', '!Features.BILLING': 'partials/landing-login.html'}" onload="chromify()">
<span class="quay-spinner"></span> <span class="quay-spinner"></span>
</div> </div>

View file

@ -10,7 +10,7 @@
<span class="avatar" size="48" email="application.avatar_email" name="application.name"></span> <span class="avatar" size="48" email="application.avatar_email" name="application.name"></span>
<h2>{{ application.name || '(Untitled)' }}</h2> <h2>{{ application.name || '(Untitled)' }}</h2>
<h4> <h4>
<span class="avatar" size="24" hash="organization.avatar" style="vertical-align: middle; margin-right: 4px;"></span> <span class="avatar" size="24" data="organization.avatar" style="vertical-align: middle; margin-right: 4px;"></span>
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span> <span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
</h4> </h4>
</div> </div>
@ -100,7 +100,7 @@
<div style="margin-bottom: 20px"> <div style="margin-bottom: 20px">
<strong>Note:</strong> The generated token will act on behalf of user <strong>Note:</strong> The generated token will act on behalf of user
<span class="avatar" hash="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;"></span> <span class="avatar" data="user.avatar" size="16" style="margin-left: 6px; margin-right: 4px;"></span>
{{ user.username }} {{ user.username }}
</div> </div>

View file

@ -0,0 +1,85 @@
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'">
<div class="org-view cor-container">
<div class="organization-header" organization="organization">
<div class="header-buttons" ng-show="organization.is_admin">
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)">
<i class="fa fa-group"></i> Create Team
</span>
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
</div>
</div>
<div class="row hidden-xs">
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
Team Permissions
<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>"></i>
</div>
</div>
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
<div id="team-{{name}}" class="row">
<div class="col-sm-7 col-md-8">
<div class="team-title">
<i class="fa fa-group"></i>
<span ng-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span>
</div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
</div>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeTeamModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change team</h4>
</div>
<div class="modal-body">
<span ng-show="!roleError">You do not have permission to change properties on teams.</span>
<span ng-show="roleError">{{ roleError }}</span>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="confirmdeleteModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete Team?</h4>
</div>
<div class="modal-body">
Are you sure you would like to delete this team? This <b>cannot be undone</b>.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteTeam()">Delete Team</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -0,0 +1,128 @@
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
<div class="team-view cor-container">
<div class="organization-header" organization="organization" team-name="teamname">
<div ng-show="canEditMembers" class="side-controls">
<div class="hidden-xs">
<button class="btn btn-success"
id="showAddMember"
data-title="Add Team Member"
data-content-template="/static/directives/team-view-add.html"
data-placement="bottom-right"
bs-popover="bs-popover">
<i class="fa fa-plus"></i>
Add Team Member
</button>
</div>
</div>
</div>
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
content-changed="updateForDescription" field-title="'team description'"></div>
<div class="empty-message" ng-if="!members.length">
This team has no members
</div>
<div class="empty-message" ng-if="members.length && !(members | filter:search).length">
No matching team members found
</div>
<table class="member-listing" style="margin-top: -20px" ng-show="members.length">
<!-- Members -->
<tr ng-if="(members | filter:search | filter: filterFunction(false, false)).length">
<td colspan="2"><div class="section-header">Team Members</div></td>
</tr>
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
<td class="user entity">
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
</td>
<td>
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
</td>
</tr>
<!-- Robots -->
<tr ng-if="(members | filter:search | filter: filterFunction(false, true)).length">
<td colspan="2"><div class="section-header">Robot Accounts</div></td>
</tr>
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, true) | orderBy: 'name'">
<td class="user entity">
<span class="entity-reference" entity="member" namespace="organization.name"></span>
</td>
<td>
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
</td>
</tr>
<!-- Invited -->
<tr ng-if="(members | filter:search | filter: filterFunction(true, false)).length">
<td colspan="2"><div class="section-header">Invited To Join</div></td>
</tr>
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
<td class="user entity">
<span ng-if="member.kind != 'invite'">
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
</span>
<span class="invite-listing" ng-if="member.kind == 'invite'">
<span class="avatar" size="32" data="member.avatar"></span>
{{ member.email }}
</span>
</td>
<td>
<span class="delete-ui" delete-title="'Revoke invite to join team'" button-title="'Revoke'"
perform-delete="revokeInvite(member)" ng-if="canEditMembers"></span>
</td>
</tr>
</table>
<div ng-show="canEditMembers">
<div ng-if-media="'(max-width: 767px)'">
<div ng-include="'/static/directives/team-view-add.html'"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeTeamModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change team</h4>
</div>
<div class="modal-body">
You do not have permission to change properties of this team.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="cannotChangeMembersModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change members</h4>
</div>
<div class="modal-body">
You do not have permission to change the members of this team.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -43,7 +43,6 @@
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;"> <div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual" <form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization"> data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization">
<span class="avatar" size="24" email="organizationEmail" name="orgname"></span>
<input type="email" class="form-control" ng-model="organizationEmail" <input type="email" class="form-control" ng-model="organizationEmail"
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required> style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required>
<button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email"> <button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">

View file

@ -1,85 +1,123 @@
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'"> <div class="resource-view org-view"
<div class="org-view cor-container"> resource="orgResource"
<div class="organization-header" organization="organization"> error-message="'Organization not found'">
<div class="header-buttons" ng-show="organization.is_admin"> <div class="page-content">
<div class="cor-title">
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'" <span class="cor-title-link"></span>
submitted="createTeam(value)"> <span class="cor-title-content">
<i class="fa fa-group"></i> Create Team <span class="avatar" size="32" data="organization.avatar"></span>
</span> <span class="organization-name">{{ organization.name }}</span>
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
</div>
</div>
<div class="row hidden-xs">
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
Team Permissions
<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>"></i>
</div>
</div>
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
<div id="team-{{name}}" class="row">
<div class="col-sm-7 col-md-8">
<div class="team-title">
<i class="fa fa-group"></i>
<span ng-show="team.can_view">
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
</span>
<span ng-show="!team.can_view">
{{ team.name }}
</span> </span>
</div> </div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div> <div class="cor-tab-panel">
</div> <div class="cor-tabs" quay-show="isMember">
<span class="cor-tab" tab-active="true" tab-title="Repositories" tab-target="#repos">
<i class="fa fa-hdd-o"></i>
</span>
<span class="cor-tab" tab-title="Teams" tab-target="#teams">
<i class="fa fa-users"></i>
</span>
<span class="cor-tab" tab-title="Robot Accounts" tab-target="#robots" ng-show="isAdmin">
<i class="fa fa-wrench"></i>
</span>
<span class="cor-tab" tab-title="Default Permissions" tab-target="#default" ng-show="isAdmin">
<i class="fa ci-stamp"></i>
</span>
<span class="cor-tab" tab-title="Billing" tab-target="#usage"
quay-show="isAdmin && Features.BILLING">
<i class="fa fa-credit-card"></i>
</span>
<span class="cor-tab" tab-title="Billing Invoices" tab-target="#invoices"
tab-init="showInvoices()" quay-show="isAdmin && Features.BILLING">
<i class="fa ci-invoice"></i>
</span>
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs"
tab-init="showLogs()" ng-show="isAdmin">
<i class="fa fa-bar-chart"></i>
</span>
<span class="cor-tab" tab-title="Applications" tab-target="#applications"
tab-init="showApplications()" ng-show="isAdmin">
<i class="fa ci-application"></i>
</span>
<span class="cor-tab" tab-title="Organization Settings" tab-target="#settings"
ng-show="isAdmin">
<i class="fa fa-gears"></i>
</span>
</div> <!-- /cor-tabs -->
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin"> <div class="cor-tab-content">
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span> <!-- Repositories -->
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button> <div id="repos" class="tab-pane active">
</div> <h3>Repositories</h3>
</div> <div class="repo-list-grid"
</div> repositories-resource="repositoriesResource"
starred="false"
namespace="namespace"
hide-title="true">
</div> </div>
</div> </div>
<!-- Modal message dialog --> <!-- Teams -->
<div class="modal fade" id="cannotChangeTeamModal"> <div id="teams" class="tab-pane">
<div class="modal-dialog"> <div class="teams-manager" organization="organization"></div>
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change team</h4>
</div> </div>
<div class="modal-body">
<span ng-show="!roleError">You do not have permission to change properties on teams.</span> <!-- Robot Accounts -->
<span ng-show="roleError">{{ roleError }}</span> <div id="robots" class="tab-pane">
<div class="robots-manager" organization="organization"></div>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button> <!-- Default Permissions -->
<div id="default" class="tab-pane">
<div class="prototype-manager" organization="organization"></div>
</div> </div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> <!-- Usage Logs -->
</div><!-- /.modal --> <div id="logs" class="tab-pane">
<div class="logs-view" organization="organization" makevisible="showLogsCounter"></div>
</div>
<!-- Applications -->
<div id="applications" class="tab-pane">
<div class="application-manager" organization="organization"
makevisible="showApplicationsCounter"></div>
</div>
<!-- Plan and Usage -->
<div id="usage" class="tab-pane" quay-require="['BILLING']">
<h3>Plan Usage and Billing</h3>
<div class="plan-manager" organization="organization.name"></div>
</div>
<!-- Billing Invoices -->
<div id="invoices" class="tab-pane" quay-require="['BILLING']">
<h3>Billing Invoices</h3>
<div class="billing-invoices" organization="organization"
makevisible="showInvoicesCounter"></div>
</div>
<!-- Settings -->
<div id="settings" class="tab-pane">
<h3>Organization Settings</h3>
<!-- Modal message dialog --> <div class="panel" ng-show="!orgScope.changingOrganization">
<div class="modal fade" id="confirmdeleteModal"> <div class="panel-title">Organization's e-mail address</div>
<div class="modal-dialog"> <div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
<div class="modal-content"> <form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()">
<div class="modal-header"> <input type="email" class="form-control" style="max-width: 500px;"
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button> ng-model="orgScope.organizationEmail" required>
<h4 class="modal-title">Delete Team?</h4> <button class="btn btn-primary" type="submit"
ng-disabled="changeEmailForm.$invalid || orgScope.organizationEmail == organization.email">
Save
</button>
</form>
</div>
</div>
</div>
</div> <!-- /cor-tab-content -->
</div> </div>
<div class="modal-body">
Are you sure you would like to delete this team? This <b>cannot be undone</b>.
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" ng-click="deleteTeam()">Delete Team</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div> </div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -23,7 +23,7 @@
<h2>Organizations</h2> <h2>Organizations</h2>
<div class="organization-listing" ng-repeat="organization in user.organizations"> <div class="organization-listing" ng-repeat="organization in user.organizations">
<span class="avatar" size="32" hash="organization.avatar"></span> <span class="avatar" size="32" data="organization.avatar"></span>
<a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a> <a class="org-title" href="/organization/{{ organization.name }}">{{ organization.name }}</a>
</div> </div>
</div> </div>

View file

@ -34,11 +34,11 @@
<ul class="namespaces-list"> <ul class="namespaces-list">
<li ng-repeat="namespace in namespaces"> <li ng-repeat="namespace in namespaces">
<span ng-if="!isOrganization(namespace.name)"> <span ng-if="!isOrganization(namespace.name)">
<span class="avatar" size="30" hash="namespace.avatar"></span> <span class="avatar" size="30" data="namespace.avatar"></span>
{{ namespace.name }} {{ namespace.name }}
</span> </span>
<a href="/organization/{{ namespace.name }}" ng-if="isOrganization(namespace.name)"> <a href="/organization/{{ namespace.name }}" ng-if="isOrganization(namespace.name)">
<span class="avatar" size="30" hash="namespace.avatar"></span> <span class="avatar" size="30" data="namespace.avatar"></span>
{{ namespace.name }} {{ namespace.name }}
</a> </a>
</li> </li>

View file

@ -37,12 +37,12 @@
<!-- Admin Only Tabs --> <!-- Admin Only Tabs -->
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()" <span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()"
ng-if="viewScope.repository.can_admin"> ng-show="viewScope.repository.can_admin">
<i class="fa fa-bar-chart"></i> <i class="fa fa-bar-chart"></i>
</span> </span>
<span class="cor-tab" tab-title="Settings" tab-target="#settings" <span class="cor-tab" tab-title="Settings" tab-target="#settings"
ng-if="viewScope.repository.can_admin"> ng-show="viewScope.repository.can_admin">
<i class="fa fa-gear"></i> <i class="fa fa-gear"></i>
</span> </span>
</div> <!-- /cor-tabs --> </div> <!-- /cor-tabs -->

View file

@ -139,7 +139,7 @@
<tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)" <tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)"
class="user-row"> class="user-row">
<td> <td>
<span class="avatar" hash="current_user.avatar" size="24"></span> <span class="avatar" data="current_user.avatar" size="24"></span>
</td> </td>
<td> <td>
<span class="labels"> <span class="labels">

View file

@ -1,9 +1,24 @@
<div class="resource-view" resource="orgResource" error-message="'No matching organization'"> <div class="resource-view team-view" resources="[orgResource, membersResource]"
<div class="team-view cor-container"> error-message="'No matching organization or team found'">
<div class="organization-header" organization="organization" team-name="teamname"> <div class="page-content">
<div class="cor-title">
<span class="cor-title-link">
<a class="back-link" href="/organization/{{ organization.name }}?tab=teams">
<span class="avatar" size="24" data="organization.avatar" style="margin-right: 4px"></span>
{{ organization.name }}
</a>
</span>
<span class="cor-title-content">
<span class="avatar" data="team.avatar" size="32"></span>
<span class="team-name">{{ teamname }}</span>
</span>
</div>
<div class="co-main-content-panel">
<div class="team-view-header">
<div ng-show="canEditMembers" class="side-controls"> <div ng-show="canEditMembers" class="side-controls">
<div class="hidden-xs"> <div class="hidden-xs">
<button class="btn btn-success" <button class="btn btn-primary"
id="showAddMember" id="showAddMember"
data-title="Add Team Member" data-title="Add Team Member"
data-content-template="/static/directives/team-view-add.html" data-content-template="/static/directives/team-view-add.html"
@ -14,69 +29,85 @@
</button> </button>
</div> </div>
</div> </div>
<div class="description markdown-input" content="team.description"
can-write="organization.is_admin"
content-changed="updateForDescription"
field-title="'team description'"></div>
</div> </div>
<div class="resource-view" resource="membersResource" error-message="'No matching team found'"> <div class="empty" ng-if="!members.length">
<div class="description markdown-input" content="team.description" can-write="organization.is_admin" <div class="empty-primary-msg">This team has no members.</div>
content-changed="updateForDescription" field-title="'team description'"></div> <div class="empty-secondary-msg">
Click the "Add Team Member" button above to add or invite team members.
<div class="empty-message" ng-if="!members.length"> </div>
This team has no members
</div> </div>
<div class="empty-message" ng-if="members.length && !(members | filter:search).length"> <table class="co-table no-lines" ng-if="members.length">
No matching team members found <!-- Team Members -->
</div> <tr class="co-table-header-row"
ng-if="(members | filter: filterFunction(false, false)).length">
<table class="member-listing" style="margin-top: -20px" ng-show="members.length"> <td colspan="3"><i class="fa fa-user"></i> Team Members</td>
<!-- Members -->
<tr ng-if="(members | filter:search | filter: filterFunction(false, false)).length">
<td colspan="2"><div class="section-header">Team Members</div></td>
</tr> </tr>
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'"> <tr class="indented-row"
ng-repeat="member in members | filter: filterFunction(false, false) | orderBy: 'name'">
<td class="user entity"> <td class="user entity">
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span> <span class="entity-reference" entity="member" namespace="organization.name"
show-avatar="true" avatar-size="24"></span>
</td> </td>
<td> <td>
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'" <span class="cor-options-menu" ng-if="canEditMembers">
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span> <span class="cor-option" option-click="removeMember(member.name)">
<i class="fa fa-times"></i> Remove {{ member.name }}
</span>
</span>
</td> </td>
</tr> </tr>
<!-- Robots --> <!-- Robot Accounts -->
<tr ng-if="(members | filter:search | filter: filterFunction(false, true)).length"> <tr class="co-table-header-row"
<td colspan="2"><div class="section-header">Robot Accounts</div></td> ng-if="(members | filter: filterFunction(false, true)).length">
<td colspan="3"><i class="fa fa-wrench"></i> Robot Accounts</td>
</tr> </tr>
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, true) | orderBy: 'name'"> <tr class="indented-row"
ng-repeat="member in members | filter: filterFunction(false, true) | orderBy: 'name'">
<td class="user entity"> <td class="user entity">
<span class="entity-reference" entity="member" namespace="organization.name"></span> <span class="entity-reference" entity="member" namespace="organization.name"></span>
</td> </td>
<td> <td>
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'" <span class="cor-options-menu" ng-if="canEditMembers">
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span> <span class="cor-option" option-click="removeMember(member.name)">
<i class="fa fa-times"></i> Remove {{ member.name }}
</span>
</span>
</td> </td>
</tr> </tr>
<!-- Invited --> <!-- Invitations -->
<tr ng-if="(members | filter:search | filter: filterFunction(true, false)).length"> <tr class="co-table-header-row"
<td colspan="2"><div class="section-header">Invited To Join</div></td> ng-if="(members | filter: filterFunction(true, false)).length">
<td colspan="3"><i class="fa ci-invite"></i> Invited to Join</td>
</tr> </tr>
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'"> <tr class="indented-row"
ng-repeat="member in members | filter: filterFunction(true, false) | orderBy: 'name'">
<td class="user entity"> <td class="user entity">
<span ng-if="member.kind != 'invite'"> <span ng-if="member.kind != 'invite'">
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span> <span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="24"></span>
</span> </span>
<span class="invite-listing" ng-if="member.kind == 'invite'"> <span class="invite-listing" ng-if="member.kind == 'invite'">
<span class="avatar" size="32" hash="member.avatar"></span> <span class="avatar" size="24" data="member.avatar" style="margin-right: 6px;"></span>
{{ member.email }} {{ member.email }}
</span> </span>
</td> </td>
<td> <td>
<span class="delete-ui" delete-title="'Revoke invite to join team'" button-title="'Revoke'" <span class="cor-options-menu" ng-if="canEditMembers">
perform-delete="revokeInvite(member)" ng-if="canEditMembers"></span> <span class="cor-option" option-click="revokeInvite(member)">
<i class="fa fa-times"></i> Revoke invite
</span>
</span>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -9,7 +9,7 @@
<div class="user-admin cor-container" ng-show="!user.anonymous"> <div class="user-admin cor-container" ng-show="!user.anonymous">
<div class="row"> <div class="row">
<div class="organization-header-element"> <div class="organization-header-element">
<span class="avatar" size="24" hash="user.avatar"></span> <span class="avatar" size="24" data="user.avatar"></span>
<span class="organization-name"> <span class="organization-name">
{{ user.username }} {{ user.username }}
</span> </span>
@ -72,7 +72,7 @@
<tr class="auth-info" ng-repeat="authInfo in authorizedApps"> <tr class="auth-info" ng-repeat="authInfo in authorizedApps">
<td> <td>
<span class="avatar" size="16" hash="authInfo.application.avatar"></span> <span class="avatar" size="16" data="authInfo.application.avatar"></span>
<a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank" <a href="{{ authInfo.application.url }}" ng-if="authInfo.application.url" target="_blank"
data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip> data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip>
{{ authInfo.application.name }} {{ authInfo.application.name }}
@ -317,7 +317,7 @@
<div class="form-group"> <div class="form-group">
<label for="orgName">Organization Name</label> <label for="orgName">Organization Name</label>
<div class="existing-data"> <div class="existing-data">
<span class="avatar" size="24" hash="user.avatar"></span> <span class="avatar" size="24" data="user.avatar"></span>
{{ user.username }}</div> {{ user.username }}</div>
<span class="description">This will continue to be the namespace for your repositories</span> <span class="description">This will continue to be the namespace for your repositories</span>
</div> </div>

View file

@ -17,7 +17,8 @@ from endpoints.api.image import RepositoryImageChanges, RepositoryImage, Reposit
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs, from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
RepositoryBuildList, RepositoryBuildResource) RepositoryBuildList, RepositoryBuildResource)
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot, from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
RegenerateOrgRobot, RegenerateUserRobot) RegenerateOrgRobot, RegenerateUserRobot, UserRobotPermissions,
OrgRobotPermissions)
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs, from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
TriggerBuildList, ActivateBuildTrigger, BuildTrigger, TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
@ -3356,10 +3357,28 @@ class TestRegenerateOrgRobot(ApiTestCase):
self._run_test('POST', 400, 'devtable', None) self._run_test('POST', 400, 'devtable', None)
class TestOrganizationBuynlarge(ApiTestCase): class TestUserRobotPermissions(ApiTestCase):
def setUp(self): def setUp(self):
ApiTestCase.setUp(self) ApiTestCase.setUp(self)
self._set_url(Organization, orgname="buynlarge") self._set_url(UserRobotPermissions, robot_shortname="robotname")
def test_get_anonymous(self):
self._run_test('GET', 401, None, None)
def test_get_freshuser(self):
self._run_test('GET', 400, 'freshuser', None)
def test_get_reader(self):
self._run_test('GET', 400, 'reader', None)
def test_get_devtable(self):
self._run_test('GET', 400, 'devtable', None)
class TestOrgRobotPermissions(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(OrgRobotPermissions, orgname="buynlarge", robot_shortname="robotname")
def test_get_anonymous(self): def test_get_anonymous(self):
self._run_test('GET', 401, None, None) self._run_test('GET', 401, None, None)
@ -3367,6 +3386,24 @@ class TestOrganizationBuynlarge(ApiTestCase):
def test_get_freshuser(self): def test_get_freshuser(self):
self._run_test('GET', 403, 'freshuser', None) 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', 400, 'devtable', None)
class TestOrganizationBuynlarge(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(Organization, orgname="buynlarge")
def test_get_anonymous(self):
self._run_test('GET', 200, None, None)
def test_get_freshuser(self):
self._run_test('GET', 200, 'freshuser', None)
def test_get_reader(self): def test_get_reader(self):
self._run_test('GET', 200, 'reader', None) self._run_test('GET', 200, 'reader', None)

View file

@ -552,12 +552,12 @@ class TestGetOrganization(ApiTestCase):
def test_unknownorg(self): def test_unknownorg(self):
self.login(ADMIN_ACCESS_USER) self.login(ADMIN_ACCESS_USER)
self.getResponse(Organization, params=dict(orgname='notvalid'), self.getResponse(Organization, params=dict(orgname='notvalid'),
expected_code=403) expected_code=404)
def test_cannotaccess(self): def test_cannotaccess(self):
self.login(NO_ACCESS_USER) self.login(NO_ACCESS_USER)
self.getResponse(Organization, params=dict(orgname=ORGANIZATION), self.getResponse(Organization, params=dict(orgname=ORGANIZATION),
expected_code=403) expected_code=200)
def test_getorganization(self): def test_getorganization(self):
self.login(READ_ACCESS_USER) self.login(READ_ACCESS_USER)

View file

@ -9,25 +9,29 @@ def icon_path(icon_name):
def icon_image(icon_name): def icon_image(icon_name):
return '<img src="%s" alt="%s">' % (icon_path(icon_name), icon_name) return '<img src="%s" alt="%s">' % (icon_path(icon_name), icon_name)
def team_reference(teamname):
avatar_html = avatar.get_mail_html(teamname, teamname, 24, 'team')
return "<span>%s <b>%s</b></span>" % (avatar_html, teamname)
def user_reference(username): def user_reference(username):
user = model.get_namespace_user(username) user = model.get_namespace_user(username)
if not user: if not user:
return username return username
is_robot = False
if user.robot: if user.robot:
parts = parse_robot_username(username) parts = parse_robot_username(username)
user = model.get_namespace_user(parts[0]) user = model.get_namespace_user(parts[0])
return """<span><img src="%s" alt="Robot"> <b>%s</b></span>""" % (icon_path('wrench'), username) return """<span><img src="%s" alt="Robot"> <b>%s</b></span>""" % (icon_path('wrench'), username)
alt = 'Organization' if user.organization else 'User' avatar_html = avatar.get_mail_html(user.username, user.email, 24,
'org' if user.organization else 'user')
return """ return """
<span> <span>
<img src="%s" %s
style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s">
<b>%s</b> <b>%s</b>
</span>""" % (avatar.get_url(user.email, 16), alt, username) </span>""" % (avatar_html, username)
def repository_tag_reference(repository_path_and_tag): def repository_tag_reference(repository_path_and_tag):
@ -52,12 +56,15 @@ def repository_reference(pair):
if not owner: if not owner:
return "%s/%s" % (namespace, repository) return "%s/%s" % (namespace, repository)
avatar_html = avatar.get_mail_html(owner.username, owner.email, 16,
'org' if owner.organization else 'user')
return """ return """
<span style="white-space: nowrap;"> <span style="white-space: nowrap;">
<img src="%s" style="vertical-align: middle; margin-left: 6px; margin-right: 4px;"> %s
<a href="%s/repository/%s/%s">%s/%s</a> <a href="%s/repository/%s/%s">%s/%s</a>
</span> </span>
""" % (avatar.get_url(owner.email, 16), get_app_url(), namespace, repository, namespace, repository) """ % (avatar_html, get_app_url(), namespace, repository, namespace, repository)
def admin_reference(username): def admin_reference(username):
@ -84,6 +91,7 @@ def get_template_env(searchpath):
def add_filters(template_env): def add_filters(template_env):
template_env.filters['icon_image'] = icon_image template_env.filters['icon_image'] = icon_image
template_env.filters['team_reference'] = team_reference
template_env.filters['user_reference'] = user_reference template_env.filters['user_reference'] = user_reference
template_env.filters['admin_reference'] = admin_reference template_env.filters['admin_reference'] = admin_reference
template_env.filters['repository_reference'] = repository_reference template_env.filters['repository_reference'] = repository_reference