Switch avatars to be built out of CSS and only overlayed with the gravatar when a non-default exists

This commit is contained in:
Joseph Schorr 2015-03-30 17:55:04 -04:00
parent 2d8d0c6fd3
commit 27a9b84587
94 changed files with 663 additions and 303 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'
@ -201,3 +199,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

@ -654,13 +654,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 +669,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
@ -1038,7 +1040,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

@ -30,13 +30,15 @@ def org_view(o, teams):
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),
'is_admin': is_admin, 'is_admin': is_admin,
'is_member': is_member 'is_member': is_member
} }
if teams is not None: 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['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
@ -301,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
@ -17,6 +18,8 @@ def role_view(repo_perm_obj):
def wrap_role_view_user(role_json, user): def wrap_role_view_user(role_json, user):
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 +28,11 @@ 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['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 +43,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}
} }

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

@ -35,7 +35,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)
@ -61,7 +61,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)
@ -621,17 +621,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

@ -0,0 +1,23 @@
.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;
}

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

@ -1,3 +1,55 @@
.teams-manager .popup-input-button { .teams-manager .popup-input-button {
float: right; 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

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

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

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

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

@ -8,7 +8,7 @@
Starred Starred
</div> </div>
<div ng-if="!starred" class="repo-list-title"> <div ng-if="!starred" class="repo-list-title">
<span class="avatar" size="24" hash="namespace.avatar"></span> <span class="avatar" size="24" data="namespace.avatar"></span>
<span ng-if="!isOrganization(namespace.name)">{{ namespace.name }}</span> <span ng-if="!isOrganization(namespace.name)">{{ namespace.name }}</span>
<a ng-if="isOrganization(namespace.name)" <a ng-if="isOrganization(namespace.name)"
href="/organization/{{ namespace.name }}">{{ namespace.name }}</a> href="/organization/{{ namespace.name }}">{{ namespace.name }}</a>

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

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

@ -15,7 +15,8 @@
<tr ng-repeat="(name, permission) in permissionResources.team.value"> <tr 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(name, permission, 'team')"
avatar-size="24">
</span> </span>
</td> </td>
<td class="user-permissions"> <td class="user-permissions">
@ -35,7 +36,8 @@
<tr ng-repeat="(name, permission) in permissionResources.user.value"> <tr ng-repeat="(name, permission) in permissionResources.user.value">
<td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}"> <td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}">
<span class="entity-reference" namespace="repository.namespace" <span class="entity-reference" namespace="repository.namespace"
entity="buildEntityForPermission(name, permission, 'user')"> entity="buildEntityForPermission(name, permission, 'user')"
avatar-size="24">
</span> </span>
</td> </td>
<td class="user-permissions"> <td class="user-permissions">
@ -79,4 +81,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,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

@ -84,4 +84,4 @@
The following images and any other images not referenced by a tag will be deleted: The following images and any other images not referenced by a tag will be deleted:
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,31 +1,62 @@
<div class="teams-manager-element"> <div class="teams-manager-element">
<div class="manager-header">
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'" <span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
submitted="createTeam(value)"> submitted="createTeam(value)" ng-show="organization.is_admin">
<i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team <i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
</span> </span>
<h3>Teams</h3> <h3>Teams</h3>
</div>
<div class="team-listing" ng-repeat="(name, team) in organization.teams"> <div class="row hidden-xs">
<div id="team-{{name}}" class="row"> <div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
<div class="col-sm-7 col-md-8"> <span class="header-text">Team Permissions</span>
<div class="team-title"> <i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
<i class="fa fa-group"></i> 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>"
<span ng-show="team.can_view"> data-html="true"
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a> data-trigger="hover"
</span> bs-popover></i>
<span ng-show="!team.can_view"> </div>
{{ team.name }} </div>
</span>
</div>
<div class="team-description markdown-view" content="team.description" first-line-only="true"></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>
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin"> <div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button> <div 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> </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> </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

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

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

@ -68,7 +68,8 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
'kind': kind, 'kind': kind,
'name': name, 'name': 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
}; };
}; };

View file

@ -19,6 +19,45 @@ angular.module('quay').directive('teamsManager', function () {
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' } { '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) { $scope.setRole = function(role, teamname) {
var previousRole = $scope.organization.teams[teamname].role; var previousRole = $scope.organization.teams[teamname].role;
$scope.organization.teams[teamname].role = role; $scope.organization.teams[teamname].role = role;
@ -54,6 +93,10 @@ angular.module('quay').directive('teamsManager', function () {
var orgname = $scope.organization.name; var orgname = $scope.organization.name;
CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) { CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) {
$scope.organization.teams[teamname] = created; $scope.organization.teams[teamname] = created;
$scope.members[teamname] = {};
$scope.members[teamname].members = [];
$scope.organization.ordered_teams.push(teamname);
$scope.orderedTeams.push(created);
}); });
}; };
@ -72,6 +115,12 @@ angular.module('quay').directive('teamsManager', function () {
}; };
ApiService.deleteOrganizationTeam(null, params).then(function() { 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]; delete $scope.organization.teams[teamname];
}, ApiService.errorDisplay('Cannot delete team')); }, ApiService.errorDisplay('Cannot delete team'));
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -5,7 +5,7 @@
<div class="cor-title"> <div class="cor-title">
<span class="cor-title-link"></span> <span class="cor-title-link"></span>
<span class="cor-title-content"> <span class="cor-title-content">
<span class="avatar" size="32" hash="organization.avatar"></span> <span class="avatar" size="32" data="organization.avatar"></span>
<span class="organization-name">{{ organization.name }}</span> <span class="organization-name">{{ organization.name }}</span>
</span> </span>
</div> </div>
@ -124,4 +124,4 @@
</div> <!-- /cor-tab-content --> </div> <!-- /cor-tab-content -->
</div> </div>
</div> </div>
</div> </div>

View file

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

View file

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

View file

@ -93,4 +93,4 @@
</div> <!-- /cor-tab-content --> </div> <!-- /cor-tab-content -->
</div> </div>
</div> </div>
</div> </div>

View file

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

View file

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

View file

@ -70,7 +70,7 @@
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span> <span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
</span> </span>
<span class="invite-listing" ng-if="member.kind == 'invite'"> <span class="invite-listing" ng-if="member.kind == 'invite'">
<span class="avatar" size="32" hash="member.avatar"></span> <span class="avatar" size="32" data="member.avatar"></span>
{{ member.email }} {{ member.email }}
</span> </span>
</td> </td>

View file

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

View file

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