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) try:
if permission.can(): org = model.get_organization(orgname)
try: except model.InvalidOrganizationException:
org = model.get_organization(orgname) raise NotFound()
except model.InvalidOrganizationException:
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)
raise Unauthorized() return org_view(org, teams)
@require_scope(scopes.ORG_ADMIN) @require_scope(scopes.ORG_ADMIN)
@nickname('changeOrganizationDetails') @nickname('changeOrganizationDetails')
@ -218,7 +224,7 @@ class OrgPrivateRepositories(ApiResource):
@path_param('orgname', 'The name of the organization') @path_param('orgname', 'The name of the organization')
class OrgnaizationMemberList(ApiResource): class OrgnaizationMemberList(ApiResource):
""" Resource for listing the members of an organization. """ """ Resource for listing the members of an organization. """
@require_scope(scopes.ORG_ADMIN) @require_scope(scopes.ORG_ADMIN)
@nickname('getOrganizationMembers') @nickname('getOrganizationMembers')
def get(self, orgname): def get(self, orgname):
@ -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'"
<i class="fa fa-plus"></i> Create New Application submitted="createApplication(value)">
</span> <i class="fa fa-plus"></i> Create New Application
</span>
</div>
<h3>OAuth Applications</h3>
</div> </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

@ -15,4 +15,4 @@
<div class="triggered-build-description" build="build" ng-if="build.trigger"></div> <div class="triggered-build-description" build="build" ng-if="build.trigger"></div>
<div ng-if="!build.trigger">Manually Started Build</div> <div ng-if="!build.trigger">Manually Started Build</div>
</div> </div>

View file

@ -36,4 +36,4 @@
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View file

@ -19,4 +19,4 @@
<div class="build-description triggered-build-description" build="build"></div> <div class="build-description triggered-build-description" build="build"></div>
</div> </div>
</span> </span>

View file

@ -1,3 +1,3 @@
<span class="co-checkable-item" ng-click="toggleItem()" <span class="co-checkable-item" ng-click="toggleItem()"
ng-class="controller.isChecked(item, controller.checked) ? 'checked': 'not-checked'"> ng-class="controller.isChecked(item, controller.checked) ? 'checked': 'not-checked'">
</span> </span>

View file

@ -1 +1 @@
<li><a href="javascript:void(0)" ng-click="selected()"><span ng-transclude/></a></li> <li><a href="javascript:void(0)" ng-click="selected()"><span ng-transclude/></a></li>

View file

@ -9,4 +9,4 @@
</span> </span>
<ul class="dropdown-menu" ng-transclude></ul> <ul class="dropdown-menu" ng-transclude></ul>
</span> </span>
</span> </span>

View file

@ -22,4 +22,4 @@
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div><!-- /.modal --> </div><!-- /.modal -->
</div> </div>

View file

@ -1,3 +1,3 @@
<div class="co-floating-bottom-bar"> <div class="co-floating-bottom-bar">
<span ng-transclude/> <span ng-transclude/>
</div> </div>

View file

@ -2,4 +2,4 @@
<div class="co-m-loader-dot__one"></div> <div class="co-m-loader-dot__one"></div>
<div class="co-m-loader-dot__two"></div> <div class="co-m-loader-dot__two"></div>
<div class="co-m-loader-dot__three"></div> <div class="co-m-loader-dot__three"></div>
</div> </div>

View file

@ -2,4 +2,4 @@
<div class="co-m-loader-dot__one"></div> <div class="co-m-loader-dot__one"></div>
<div class="co-m-loader-dot__two"></div> <div class="co-m-loader-dot__two"></div>
<div class="co-m-loader-dot__three"></div> <div class="co-m-loader-dot__three"></div>
</div> </div>

View file

@ -8,4 +8,4 @@
<div class="co-log-viewer-new-logs" ng-show="hasNewLogs" ng-click="moveToBottom()"> <div class="co-log-viewer-new-logs" ng-show="hasNewLogs" ng-click="moveToBottom()">
New Logs <i class="fa fa-lg fa-arrow-circle-down"></i> New Logs <i class="fa fa-lg fa-arrow-circle-down"></i>
</div> </div>
</div> </div>

View file

@ -1,3 +1,3 @@
<li> <li>
<a href="javascript:void(0)" ng-click="optionClick()" ng-transclude></a> <a href="javascript:void(0)" ng-click="optionClick()" ng-transclude></a>
</li> </li>

View file

@ -3,4 +3,4 @@
<i class="fa fa-gear fa-lg dropdown-toggle" data-toggle="dropdown" data-title="Options" bs-tooltip></i> <i class="fa fa-gear fa-lg dropdown-toggle" data-toggle="dropdown" data-title="Options" bs-tooltip></i>
<ul class="dropdown-menu pull-right" ng-transclude></ul> <ul class="dropdown-menu pull-right" ng-transclude></ul>
</div> </div>
</span> </span>

View file

@ -1,3 +1,3 @@
<div class="co-step-bar"> <div class="co-step-bar">
<span class="transclude" ng-transclude/> <span class="transclude" ng-transclude/>
</div> </div>

View file

@ -3,4 +3,4 @@
<span class="text" ng-if="text">{{ text }}</span> <span class="text" ng-if="text">{{ text }}</span>
<i class="fa fa-lg" ng-if="icon" ng-class="'fa-' + icon"></i> <i class="fa fa-lg" ng-if="icon" ng-class="'fa-' + icon"></i>
</span> </span>
</span> </span>

View file

@ -1 +1 @@
<div class="co-tab-content tab-content col-md-11" ng-transclude></div> <div class="co-tab-content tab-content col-md-11" ng-transclude></div>

View file

@ -1,3 +1,3 @@
<div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy"> <div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy">
<div class="co-tab-container" ng-transclude></div> <div class="co-tab-container" ng-transclude></div>
</div> </div>

View file

@ -10,4 +10,4 @@
<span ng-transclude/></span> <span ng-transclude/></span>
</span> </span>
</a> </a>
</li> </li>

View file

@ -1 +1 @@
<ul class="co-tabs col-md-1" ng-transclude></ul> <ul class="co-tabs col-md-1" ng-transclude></ul>

View file

@ -1,3 +1,3 @@
<div class="col-lg-3 col-md-3 col-sm-3 col-xs-1"> <div class="col-lg-3 col-md-3 col-sm-3 col-xs-1">
<span class="co-nav-title-action co-fx-text-shadow" ng-transclude></span> <span class="co-nav-title-action co-fx-text-shadow" ng-transclude></span>
</div> </div>

View file

@ -1,3 +1,3 @@
<div class="col-lg-6 col-md-6 col-sm-6 col-xs-12"> <div class="col-lg-6 col-md-6 col-sm-6 col-xs-12">
<h2 class="co-nav-title-content co-fx-text-shadow" ng-transclude></h2> <h2 class="co-nav-title-content co-fx-text-shadow" ng-transclude></h2>
</div> </div>

View file

@ -1 +1 @@
<div class="col-lg-3 col-md-3 hidden-sm hidden-xs" ng-transclude></div> <div class="col-lg-3 col-md-3 hidden-sm hidden-xs" ng-transclude></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)">
<i class="fa fa-group"></i> <span>{{ team.name }}</span> <span ng-if="!Config.isNewLayout()">
<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

@ -67,4 +67,4 @@
</div><!-- /.modal-content --> </div><!-- /.modal-content -->
</div><!-- /.modal-dialog --> </div><!-- /.modal-dialog -->
</div><!-- /.modal --> </div><!-- /.modal -->
</div> </div>

View file

@ -1,3 +1,3 @@
<span class="filter-control-element" ng-class="filter == value ? 'selected': 'not-selected'"> <span class="filter-control-element" ng-class="filter == value ? 'selected': 'not-selected'">
<a href="javascript:void(0)" ng-click="setFilter()"><span ng-transclude/></a> <a href="javascript:void(0)" ng-click="setFilter()"><span ng-transclude/></a>
</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

@ -30,4 +30,4 @@
</span> </span>
</div> </div>
</div> </div>
</div> </div>

View file

@ -102,4 +102,4 @@
has-changes="hasImageChanges"></div> has-changes="hasImageChanges"></div>
</span> </span>
</div> </div>
</div> </div>

View file

@ -12,4 +12,4 @@
</div> </div>
<div class="image-layer-dot"></div> <div class="image-layer-dot"></div>
<div class="image-layer-line"></div> <div class="image-layer-line"></div>
</div> </div>

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"> <div class="side-controls">
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>. <button class="btn btn-primary" ng-click="showAddDialog()">
<i class="fa fa-plus"></i>
Create Default Permission
</button>
</div>
<h3>Default Permissions</h3>
</div> </div>
<div class="side-controls"> <div class="manager-header section-description-header">
<button class="btn btn-success" ng-click="showAddDialog()"> 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
<i class="fa fa-plus"></i> creator. Permissions are assigned based on the user who created the repository.
New Default Permission <br><br>
</button> <strong>Note:</strong> Permissions added here do <strong>not</strong> automatically get added to
existing repositories.
</div> </div>
<table class="table"> <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

@ -72,4 +72,4 @@
<h4>Network Usage (Bytes)</h4> <h4>Network Usage (Bytes)</h4>
<div class="realtime-line-chart" data="data.count.network" labels="['Bytes In', 'Bytes Out']" counter="counter"></div> <div class="realtime-line-chart" data="data.count.network" labels="['Bytes In', 'Bytes Out']" counter="counter"></div>
</div> </div>
</div> </div>

View file

@ -10,4 +10,4 @@
<i class="fa fa-calendar" style="margin-right: 6px;"></i> <i class="fa fa-calendar" style="margin-right: 6px;"></i>
<a ng-href="{{ scheduled.shortlink }}" class="quay-service-status-description">{{ scheduled.name }}</a> <a ng-href="{{ scheduled.shortlink }}" class="quay-service-status-description">{{ scheduled.name }}</a>
</div> </div>
</div> </div>

View file

@ -3,4 +3,4 @@
ng-if="indicator != 'loading'"></span> ng-if="indicator != 'loading'"></span>
<span class="cor-loader-inline" ng-if="indicator == 'loading'"></span> <span class="cor-loader-inline" ng-if="indicator == 'loading'"></span>
<a href="http://status.quay.io" class="quay-service-status-description">{{ description }}</a> <a href="http://status.quay.io" class="quay-service-status-description">{{ description }}</a>
</span> </span>

View file

@ -3,4 +3,4 @@
<div class="chart"></div> <div class="chart"></div>
</div> </div>
<div class="cor-loader-inline" ng-if="counter < 1"></div> <div class="cor-loader-inline" ng-if="counter < 1"></div>
</div> </div>

View file

@ -3,4 +3,4 @@
<div class="chart"></div> <div class="chart"></div>
</div> </div>
<div class="cor-loader-inline" ng-if="counter < 1"></div> <div class="cor-loader-inline" ng-if="counter < 1"></div>
</div> </div>

View file

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

View file

@ -2,4 +2,4 @@
<i ng-class="repository.is_starred ? 'starred fa fa-star' : 'fa fa-star-o'" <i ng-class="repository.is_starred ? 'starred fa fa-star' : 'fa fa-star-o'"
class="star-icon" ng-click="toggleStar()"> class="star-icon" ng-click="toggleStar()">
</i> </i>
</span> </span>

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>
@ -72,4 +72,4 @@
repository="repository" repository="repository"
counter="showNewNotificationCounter" counter="showNewNotificationCounter"
notification-created="handleNotificationCreated(notification)"></div> notification-created="handleNotificationCreated(notification)"></div>
</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"
@ -79,4 +135,4 @@
dialog-action-title="Grant Permission"> dialog-action-title="Grant Permission">
The selected user is outside of your organization. Are you sure you want to grant the user access to this repository? The selected user is outside of your organization. Are you sure you want to grant the user access to this repository?
</div> </div>
</div> </div>

View file

@ -32,4 +32,4 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>

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="side-controls"> <div class="manager-header">
<span class="popup-input-button" pattern="ROBOT_PATTERN" placeholder="'Robot Account Name'" <div class="side-controls">
submitted="createRobot(value)"> <span class="popup-input-button" pattern="ROBOT_PATTERN"
<i class="fa fa-wrench"></i> Create Robot Account placeholder="'Robot Account Name'"
</span> submitted="createRobot(value)">
<i class="fa fa-plus"></i> Create Robot Account
</span>
</div>
<h3>Robot Accounts</h3>
</div> </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">
<td class="robot"> <tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
<i class="fa fa-wrench"></i> <td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()">
<a ng-click="showRobot(robotInfo)"> <span ng-if="robotInfo.repositories.length > 0" ng-click="showPermissions(robotInfo)">
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }} <i class="fa"
</a> ng-class="robotInfo.showing_permissions ? 'fa-caret-down' : 'fa-caret-right'"
</td> data-title="View Permissions List" bs-tooltip></i>
<td> </span>
<span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span> </td>
</td> <td class="robot">
</tr> <i class="fa fa-wrench"></i>
<a ng-click="showRobot(robotInfo)">
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }}
</a>
</td>
<td ng-if="organization && Config.isNewLayout()">
<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>
</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

@ -1,4 +1,4 @@
<span class="source-commit-link-elememt"> <span class="source-commit-link-elememt">
<i class="fa fa-dot-circle-o" data-title="Commit" data-container="body" bs-tooltip></i> <i class="fa fa-dot-circle-o" data-title="Commit" data-container="body" bs-tooltip></i>
<a ng-href="{{ getUrl(commitSha, urlTemplate) }}" target="_blank">{{ commitSha.substring(0, 8) }}</a> <a ng-href="{{ getUrl(commitSha, urlTemplate) }}" target="_blank">{{ commitSha.substring(0, 8) }}</a>
</span> </span>

View file

@ -12,4 +12,4 @@
<a ng-href="{{ getUrl(ref, tagTemplate, 'tag') }}" target="_blank">{{ getTitle(ref) }}</a> <a ng-href="{{ getUrl(ref, tagTemplate, 'tag') }}" target="_blank">{{ getTitle(ref) }}</a>
</span> </span>
</span> </span>
</span> </span>

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

@ -62,4 +62,4 @@
<!-- Unknown --> <!-- Unknown -->
<span ng-switch-default></span> <span ng-switch-default></span>
</span> </span>
</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,21 +74,50 @@ 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.addPermission = function() { $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.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);

Some files were not shown because too many files have changed in this diff Show more