Switch avatars to be built out of CSS and only overlayed with the gravatar when a non-default exists
This commit is contained in:
		
							parent
							
								
									2d8d0c6fd3
								
							
						
					
					
						commit
						27a9b84587
					
				
					 94 changed files with 663 additions and 303 deletions
				
			
		|  | @ -1,4 +1,5 @@ | |||
| import hashlib | ||||
| import math | ||||
| 
 | ||||
| class Avatar(object): | ||||
|   def __init__(self, app=None): | ||||
|  | @ -7,8 +8,7 @@ class Avatar(object): | |||
| 
 | ||||
|   def _init_app(self, app): | ||||
|     return AVATAR_CLASSES[app.config.get('AVATAR_KIND', 'Gravatar')]( | ||||
|       app.config['SERVER_HOSTNAME'], | ||||
|       app.config['PREFERRED_URL_SCHEME']) | ||||
|       app.config['PREFERRED_URL_SCHEME'], app.config['AVATAR_COLORS'], app.config['HTTPCLIENT']) | ||||
| 
 | ||||
|   def __getattr__(self, name): | ||||
|     return getattr(self.state, name, None) | ||||
|  | @ -16,48 +16,83 @@ class Avatar(object): | |||
| 
 | ||||
| class BaseAvatar(object): | ||||
|   """ Base class for all avatar implementations. """ | ||||
|   def __init__(self, server_hostname, preferred_url_scheme): | ||||
|     self.server_hostname = server_hostname | ||||
|   def __init__(self, preferred_url_scheme, colors, http_client): | ||||
|     self.preferred_url_scheme = preferred_url_scheme | ||||
|     self.colors = colors | ||||
|     self.http_client = http_client | ||||
| 
 | ||||
|   def get_url(self, email, size=16, name=None): | ||||
|     """ Returns the full URL for viewing the avatar of the given email address, with | ||||
|         an optional size. | ||||
|   def get_mail_html(self, name, email_or_id, size=16, kind='user'): | ||||
|     """ Returns the full HTML and CSS for viewing the avatar of the given name and email address, | ||||
|         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): | ||||
|     """ Computes the avatar hash for the given email address. If the name is given and a default | ||||
|         avatar is being computed, the name can be used in place of the email address. """ | ||||
|     raise NotImplementedError | ||||
|     if url is not None: | ||||
|       # Try to load the gravatar. If we get a non-404 response, then we use it in place of | ||||
|       # the CSS avatar. | ||||
|       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 = 'Ω' 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): | ||||
|   """ Avatar system that uses gravatar for generating avatars. """ | ||||
|   def compute_hash(self, email, name=None): | ||||
|     email = email or "" | ||||
|     return hashlib.md5(email.strip().lower()).hexdigest() | ||||
| 
 | ||||
|   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) | ||||
|   def _get_url(self, hash_value, size=16): | ||||
|     return '%s://www.gravatar.com/avatar/%s?d=404&size=%s' % (self.preferred_url_scheme, | ||||
|                                                               hash_value, size) | ||||
| 
 | ||||
| class LocalAvatar(BaseAvatar): | ||||
|   """ Avatar system that uses the local system for generating avatars. """ | ||||
|   def compute_hash(self, email, name=None): | ||||
|     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) | ||||
| 
 | ||||
|   pass | ||||
| 
 | ||||
| AVATAR_CLASSES = { | ||||
|   'gravatar': GravatarAvatar, | ||||
|  |  | |||
							
								
								
									
										10
									
								
								config.py
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								config.py
									
										
									
									
									
								
							|  | @ -45,8 +45,6 @@ class DefaultConfig(object): | |||
|   PREFERRED_URL_SCHEME = 'http' | ||||
|   SERVER_HOSTNAME = 'localhost:5000' | ||||
| 
 | ||||
|   AVATAR_KIND = 'local' | ||||
| 
 | ||||
|   REGISTRY_TITLE = 'CoreOS Enterprise Registry' | ||||
|   REGISTRY_TITLE_SHORT = 'Enterprise Registry' | ||||
| 
 | ||||
|  | @ -201,3 +199,11 @@ class DefaultConfig(object): | |||
| 
 | ||||
|   # Signed registry grant token expiration in seconds | ||||
|   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'] | ||||
|  |  | |||
|  | @ -654,13 +654,13 @@ def get_matching_users(username_prefix, robot_namespace=None, | |||
|                           (User.robot == True))) | ||||
| 
 | ||||
|   query = (User | ||||
|     .select(User.username, User.robot) | ||||
|     .group_by(User.username, User.robot) | ||||
|     .select(User.username, User.email, User.robot) | ||||
|     .group_by(User.username, User.email, User.robot) | ||||
|     .where(direct_user_query)) | ||||
| 
 | ||||
|   if organization: | ||||
|     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(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) & | ||||
|                                        (Team.organization == organization)))) | ||||
|  | @ -669,9 +669,11 @@ def get_matching_users(username_prefix, robot_namespace=None, | |||
|   class MatchingUserResult(object): | ||||
|     def __init__(self, *args): | ||||
|       self.username = args[0] | ||||
|       self.is_robot = args[1] | ||||
|       self.email = args[1] | ||||
|       self.robot = args[2] | ||||
| 
 | ||||
|       if organization: | ||||
|         self.is_org_member = (args[2] != None) | ||||
|         self.is_org_member = (args[3] != None) | ||||
|       else: | ||||
|         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): | ||||
|   return (RepositoryPermission.select(User.username, User.robot, Role.name, RepositoryPermission) | ||||
|   return (RepositoryPermission.select(User.username, User.email, User.robot, Role.name, | ||||
|                                       RepositoryPermission) | ||||
|           .join(User) | ||||
|           .switch(RepositoryPermission) | ||||
|           .join(Role) | ||||
|  |  | |||
|  | @ -4,7 +4,7 @@ | |||
| 
 | ||||
| <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> | ||||
| 
 | ||||
|  |  | |||
|  | @ -30,13 +30,15 @@ def org_view(o, teams): | |||
|   view = { | ||||
|     'name': o.username, | ||||
|     '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_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: | ||||
|     view['invoice_email'] = o.invoice_email | ||||
|  | @ -301,16 +303,14 @@ class ApplicationInformation(ApiResource): | |||
|     if not application: | ||||
|       raise NotFound() | ||||
| 
 | ||||
|     org_hash = avatar.compute_hash(application.organization.email, | ||||
|                                    name=application.organization.username) | ||||
|     app_hash = (avatar.compute_hash(application.avatar_email, name=application.name) if | ||||
|                 application.avatar_email else org_hash) | ||||
|     app_email = application.avatar_email or application.organization.email | ||||
|     app_data = avatar.get_data(application.name, app_email, 'app') | ||||
| 
 | ||||
|     return { | ||||
|       'name': application.name, | ||||
|       'description': application.description, | ||||
|       'uri': application.application_uri, | ||||
|       'avatar': app_hash, | ||||
|       'avatar': app_data, | ||||
|       'organization': org_view(application.organization, []) | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ import logging | |||
| 
 | ||||
| from flask import request | ||||
| 
 | ||||
| from app import avatar | ||||
| from endpoints.api import (resource, nickname, require_repo_admin, RepositoryParamResource, | ||||
|                            log_action, request_error, validate_json_request, path_param) | ||||
| from data import model | ||||
|  | @ -17,6 +18,8 @@ def role_view(repo_perm_obj): | |||
| 
 | ||||
| def wrap_role_view_user(role_json, user): | ||||
|   role_json['is_robot'] = user.robot | ||||
|   if not user.robot: | ||||
|     role_json['avatar'] =  avatar.get_data_for_user(user) | ||||
|   return role_json | ||||
| 
 | ||||
| 
 | ||||
|  | @ -25,6 +28,11 @@ def wrap_role_view_org(role_json, user, org_members): | |||
|   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/') | ||||
| @path_param('repository', 'The full path of the repository. e.g. namespace/name') | ||||
| class RepositoryTeamPermissionList(RepositoryParamResource): | ||||
|  | @ -35,8 +43,11 @@ class RepositoryTeamPermissionList(RepositoryParamResource): | |||
|     """ List all team permission. """ | ||||
|     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 { | ||||
|       '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} | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -45,7 +45,7 @@ class EntitySearch(ApiResource): | |||
|             'name': namespace_name, | ||||
|             'kind': 'org', | ||||
|             'is_org_member': True, | ||||
|             'avatar': avatar.compute_hash(organization.email, name=organization.username), | ||||
|             'avatar': avatar.get_data_for_org(organization), | ||||
|           }] | ||||
| 
 | ||||
|     except model.InvalidOrganizationException: | ||||
|  | @ -63,7 +63,8 @@ class EntitySearch(ApiResource): | |||
|       result = { | ||||
|         'name': team.name, | ||||
|         'kind': 'team', | ||||
|         'is_org_member': True | ||||
|         'is_org_member': True, | ||||
|         'avatar': avatar.get_data_for_team(team) | ||||
|       } | ||||
|       return result | ||||
| 
 | ||||
|  | @ -71,11 +72,12 @@ class EntitySearch(ApiResource): | |||
|       user_json = { | ||||
|         'name': user.username, | ||||
|         'kind': 'user', | ||||
|         'is_robot': user.is_robot, | ||||
|         'is_robot': user.robot, | ||||
|         'avatar': avatar.get_data_for_user(user) | ||||
|       } | ||||
| 
 | ||||
|       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 | ||||
| 
 | ||||
|  |  | |||
|  | @ -108,7 +108,7 @@ def user_view(user): | |||
|     'username': user.username, | ||||
|     'email': user.email, | ||||
|     '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) | ||||
|   } | ||||
| 
 | ||||
|  |  | |||
|  | @ -52,11 +52,11 @@ def team_view(orgname, team): | |||
|   view_permission = ViewTeamPermission(orgname, team.name) | ||||
|   role = model.get_team_org_role(team).name | ||||
|   return { | ||||
|     'id': team.id, | ||||
|     'name': team.name, | ||||
|     'description': team.description, | ||||
|     'can_view': view_permission.can(), | ||||
|     'role': role | ||||
|     'role': role, | ||||
|     'avatar': avatar.get_data_for_team(team) | ||||
|   } | ||||
| 
 | ||||
| def member_view(member, invited=False): | ||||
|  | @ -64,7 +64,7 @@ def member_view(member, invited=False): | |||
|     'name': member.username, | ||||
|     'kind': 'user', | ||||
|     '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, | ||||
|   } | ||||
| 
 | ||||
|  | @ -76,7 +76,7 @@ def invite_view(invite): | |||
|     return { | ||||
|       'email': invite.email, | ||||
|       'kind': 'invite', | ||||
|       'avatar': avatar.compute_hash(invite.email), | ||||
|       'avatar': avatar.get_data(invite.email, invite.email, 'user'), | ||||
|       'invited': True | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,7 +35,7 @@ def user_view(user): | |||
|     admin_org = AdministerOrganizationPermission(o.username) | ||||
|     return { | ||||
|       '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(), | ||||
|       'can_create_repo': admin_org.can() or CreateRepositoryPermission(o.username).can(), | ||||
|       'preferred_namespace': not (o.stripe_id is None) | ||||
|  | @ -61,7 +61,7 @@ def user_view(user): | |||
|     'verified': user.verified, | ||||
|     'anonymous': False, | ||||
|     'username': user.username, | ||||
|     'avatar': avatar.compute_hash(user.email, name=user.username), | ||||
|     'avatar': avatar.get_data_for_user(user) | ||||
|   } | ||||
| 
 | ||||
|   user_admin = UserAdminPermission(user.username) | ||||
|  | @ -621,17 +621,16 @@ class UserNotification(ApiResource): | |||
| 
 | ||||
| def authorization_view(access_token): | ||||
|   oauth_app = access_token.application | ||||
|   app_email = oauth_app.avatar_email or oauth_app.organization.email | ||||
|   return { | ||||
|     'application': { | ||||
|       'name': oauth_app.name, | ||||
|       'description': oauth_app.description, | ||||
|       'url': oauth_app.application_uri, | ||||
|       'avatar': avatar.compute_hash(oauth_app.avatar_email or oauth_app.organization.email, | ||||
|                                     name=oauth_app.name), | ||||
|       'avatar': avatar.get_data(oauth_app.name, app_email, 'app'), | ||||
|       'organization': { | ||||
|         'name': oauth_app.organization.username, | ||||
|         'avatar': avatar.compute_hash(oauth_app.organization.email, | ||||
|                                       name=oauth_app.organization.username) | ||||
|         'avatar': avatar.get_data_for_org(oauth_app.organization) | ||||
|       } | ||||
|     }, | ||||
|     'scopes': scopes.get_scope_information(access_token.scope), | ||||
|  |  | |||
|  | @ -3,7 +3,6 @@ import logging | |||
| from flask import (abort, redirect, request, url_for, make_response, Response, | ||||
|                    Blueprint, send_from_directory, jsonify, send_file) | ||||
| 
 | ||||
| from avatar_generator import Avatar | ||||
| from flask.ext.login import current_user | ||||
| from urlparse import urlparse | ||||
| from health.healthcheck import get_healthchecker | ||||
|  | @ -210,20 +209,6 @@ def endtoend_health(): | |||
|   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']) | ||||
| @no_cache | ||||
| def tos(): | ||||
|  | @ -449,15 +434,16 @@ def request_authorization_code(): | |||
| 
 | ||||
|     # Load the application information. | ||||
|     oauth_app = provider.get_application_for_client_id(client_id) | ||||
|     app_email = oauth_app.email or organization.email | ||||
| 
 | ||||
|     oauth_app_view = { | ||||
|       'name': oauth_app.name, | ||||
|       'description': oauth_app.description, | ||||
|       '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': { | ||||
|         'name': oauth_app.organization.username, | ||||
|         'avatar': avatar.compute_hash(oauth_app.organization.email, | ||||
|                                       name=oauth_app.organization.username) | ||||
|         'avatar': avatar.get_data_for_org(oauth_app.organization) | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										23
									
								
								static/css/directives/ui/avatar.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								static/css/directives/ui/avatar.css
									
										
									
									
									
										Normal 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; | ||||
| } | ||||
							
								
								
									
										7
									
								
								static/css/directives/ui/entity-reference.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								static/css/directives/ui/entity-reference.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| .entity-reference .new-entity-reference .entity-name { | ||||
|   margin-left: 6px; | ||||
| } | ||||
| 
 | ||||
| .entity-reference .new-entity-reference .fa-wrench { | ||||
|   width: 16px; | ||||
| } | ||||
							
								
								
									
										52
									
								
								static/css/directives/ui/entity-search.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								static/css/directives/ui/entity-search.css
									
										
									
									
									
										Normal 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; | ||||
| } | ||||
|  | @ -1,3 +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; | ||||
| } | ||||
|  |  | |||
|  | @ -372,55 +372,6 @@ nav.navbar-default .navbar-nav>li>a.active { | |||
|   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 { | ||||
|   margin-right: 6px; | ||||
|   position: relative; | ||||
|  |  | |||
							
								
								
									
										4
									
								
								static/directives/anchor.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								static/directives/anchor.html
									
										
									
									
									
										Normal 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> | ||||
|  | @ -1,6 +1,6 @@ | |||
| <div class="application-info-element" style="padding-bottom: 18px"> | ||||
|   <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> | ||||
|     <h4> | ||||
|       {{ application.organization.name }} | ||||
|  |  | |||
|  | @ -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'">Ω</span> | ||||
|   </span> | ||||
| </span> | ||||
|  | @ -15,4 +15,4 @@ | |||
| 
 | ||||
|   <div class="triggered-build-description" build="build" ng-if="build.trigger"></div> | ||||
|   <div ng-if="!build.trigger">Manually Started Build</div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -36,4 +36,4 @@ | |||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -19,4 +19,4 @@ | |||
| 
 | ||||
|    <div class="build-description triggered-build-description" build="build"></div> | ||||
|   </div> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <span class="co-checkable-item" ng-click="toggleItem()" | ||||
|       ng-class="controller.isChecked(item, controller.checked) ? 'checked': 'not-checked'"> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -9,4 +9,4 @@ | |||
|     </span> | ||||
|     <ul class="dropdown-menu" ng-transclude></ul> | ||||
|   </span> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -22,4 +22,4 @@ | |||
|       </div><!-- /.modal-content --> | ||||
|     </div><!-- /.modal-dialog --> | ||||
|   </div><!-- /.modal --> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <div class="co-floating-bottom-bar"> | ||||
|     <span ng-transclude/> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -2,4 +2,4 @@ | |||
|   <div class="co-m-loader-dot__one"></div> | ||||
|   <div class="co-m-loader-dot__two"></div> | ||||
|   <div class="co-m-loader-dot__three"></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -2,4 +2,4 @@ | |||
|   <div class="co-m-loader-dot__one"></div> | ||||
|   <div class="co-m-loader-dot__two"></div> | ||||
|   <div class="co-m-loader-dot__three"></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -8,4 +8,4 @@ | |||
|   <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> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <li> | ||||
|     <a href="javascript:void(0)" ng-click="optionClick()" ng-transclude></a> | ||||
| </li> | ||||
| </li> | ||||
|  |  | |||
|  | @ -3,4 +3,4 @@ | |||
|     <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> | ||||
|   </div> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <div class="co-step-bar"> | ||||
|     <span class="transclude" ng-transclude/> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -3,4 +3,4 @@ | |||
|       <span class="text" ng-if="text">{{ text }}</span> | ||||
|       <i class="fa fa-lg" ng-if="icon" ng-class="'fa-' + icon"></i> | ||||
|   </span> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <div class="co-main-content-panel co-tab-panel co-fx-box-shadow-heavy"> | ||||
|     <div class="co-tab-container" ng-transclude></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -10,4 +10,4 @@ | |||
|              <span ng-transclude/></span> | ||||
|         </span> | ||||
|     </a> | ||||
| </li> | ||||
| </li> | ||||
|  |  | |||
|  | @ -1 +1 @@ | |||
| <ul class="co-tabs col-md-1" ng-transclude></ul> | ||||
| <ul class="co-tabs col-md-1" ng-transclude></ul> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <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> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <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> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -1,38 +1,3 @@ | |||
| <span class="entity-reference-element"> | ||||
|   <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" 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 quay-include="{'Config.isNewLayout()': 'directives/new-entity-reference.html', '!Config.isNewLayout()': 'directives/old-entity-reference.html'}"></span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -6,12 +6,26 @@ | |||
|       <span class="caret"></span> | ||||
|     </button> | ||||
|     <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"> | ||||
|         You do not have permission to manage teams and robots for this organization | ||||
|       </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" | ||||
|           ng-show="!lazyLoading && !teams.length && !robots.length && !((includeTeams && isOrganization && isAdmin) || (includeRobots && isAdmin))"> | ||||
|         <span ng-if="includeRobots && includeTeams && isOrganization"> | ||||
|  | @ -35,34 +49,29 @@ | |||
|         </span> | ||||
|       </li> | ||||
| 
 | ||||
|       <li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading" | ||||
|           ng-click="setEntity(team.name, 'team', false)"> | ||||
|       <li role="presentation" class="dropdown-header" ng-show="!lazyLoading && teams">Teams</li> | ||||
| 
 | ||||
|       <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)"> | ||||
|           <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> | ||||
|       </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)"> | ||||
|           <i class="fa fa-wrench"></i> <span>{{ robot.name }}</span> | ||||
|         </a> | ||||
|       </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> | ||||
|   </div> | ||||
| </span> | ||||
|  |  | |||
|  | @ -67,4 +67,4 @@ | |||
|       </div><!-- /.modal-content --> | ||||
|     </div><!-- /.modal-dialog --> | ||||
|   </div><!-- /.modal --> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,3 +1,3 @@ | |||
| <span class="filter-control-element" ng-class="filter == value ? 'selected': 'not-selected'"> | ||||
|   <a href="javascript:void(0)" ng-click="setFilter()"><span ng-transclude/></a> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ | |||
|   <ul class="nav navbar-nav navbar-right visible-xs" ng-switch on="user.anonymous"> | ||||
|     <li ng-switch-when="false"> | ||||
|       <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 }} | ||||
|       </a> | ||||
|     </li> | ||||
|  | @ -48,7 +48,7 @@ | |||
| 
 | ||||
|     <li class="dropdown" ng-switch-when="false"> | ||||
|       <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 }} | ||||
|         <span class="notifications-bubble"></span> | ||||
|         <b class="caret"></b> | ||||
|  |  | |||
|  | @ -30,4 +30,4 @@ | |||
|       </span> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -102,4 +102,4 @@ | |||
|            has-changes="hasImageChanges"></div> | ||||
|     </span> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -12,4 +12,4 @@ | |||
|   </div> | ||||
|   <div class="image-layer-dot"></div> | ||||
|   <div class="image-layer-line"></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,19 +1,19 @@ | |||
| <span class="namespace-selector-dropdown"> | ||||
|   <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> | ||||
| 
 | ||||
|   <div class="btn-group" ng-show="user.organizations.length > 0"> | ||||
|   <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> | ||||
|   </button> | ||||
|   <ul class="dropdown-menu" role="menu"> | ||||
|     <li class="namespace-item" ng-repeat="org in user.organizations" | ||||
|         ng-class="(requireCreate && !namespaces[org.name].can_create_repo) ? 'disabled' : ''"> | ||||
|       <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> | ||||
|       </a> | ||||
| 
 | ||||
|  | @ -25,7 +25,7 @@ | |||
|     <li class="divider"></li> | ||||
|     <li> | ||||
|       <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> | ||||
|       </a> | ||||
|     </li> | ||||
|  |  | |||
							
								
								
									
										40
									
								
								static/directives/new-entity-reference.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								static/directives/new-entity-reference.html
									
										
									
									
									
										Normal 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> | ||||
|  | @ -3,7 +3,7 @@ | |||
|     <div class="circle" ng-class="getClass(notification)"></div> | ||||
|     <div class="message" ng-bind-html="getMessage(notification)"></div> | ||||
|     <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> | ||||
|     </div> | ||||
|   </div> | ||||
|  |  | |||
							
								
								
									
										39
									
								
								static/directives/old-entity-reference.html
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								static/directives/old-entity-reference.html
									
										
									
									
									
										Normal 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> | ||||
|  | @ -1,5 +1,5 @@ | |||
| <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"> | ||||
|     <a href="/organization/{{ organization.name }}">{{ organization.name }}</a> | ||||
|   </span> | ||||
|  |  | |||
|  | @ -72,4 +72,4 @@ | |||
|     <h4>Network Usage (Bytes)</h4> | ||||
|     <div class="realtime-line-chart" data="data.count.network" labels="['Bytes In', 'Bytes Out']" counter="counter"></div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -10,4 +10,4 @@ | |||
|      <i class="fa fa-calendar" style="margin-right: 6px;"></i> | ||||
|      <a ng-href="{{ scheduled.shortlink }}" class="quay-service-status-description">{{ scheduled.name }}</a> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -3,4 +3,4 @@ | |||
|         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> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -3,4 +3,4 @@ | |||
|         <div class="chart"></div> | ||||
|     </div> | ||||
|     <div class="cor-loader-inline" ng-if="counter < 1"></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -3,4 +3,4 @@ | |||
|         <div class="chart"></div> | ||||
|     </div> | ||||
|     <div class="cor-loader-inline" ng-if="counter < 1"></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|         Starred | ||||
|       </div> | ||||
|       <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> | ||||
|         <a ng-if="isOrganization(namespace.name)" | ||||
|            href="/organization/{{ namespace.name }}">{{ namespace.name }}</a> | ||||
|  |  | |||
|  | @ -2,4 +2,4 @@ | |||
|   <i ng-class="repository.is_starred ? 'starred fa fa-star' : 'fa fa-star-o'" | ||||
|      class="star-icon" ng-click="toggleStar()"> | ||||
|   </i> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -72,4 +72,4 @@ | |||
|        repository="repository" | ||||
|        counter="showNewNotificationCounter" | ||||
|        notification-created="handleNotificationCreated(notification)"></div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -15,7 +15,8 @@ | |||
|         <tr ng-repeat="(name, permission) in permissionResources.team.value"> | ||||
|           <td class="team entity"> | ||||
|             <span class="entity-reference" namespace="repository.namespace" | ||||
|                   entity="buildEntityForPermission(name, permission, 'team')"> | ||||
|                   entity="buildEntityForPermission(name, permission, 'team')" | ||||
|                   avatar-size="24"> | ||||
|             </span> | ||||
|           </td> | ||||
|           <td class="user-permissions"> | ||||
|  | @ -35,7 +36,8 @@ | |||
|         <tr ng-repeat="(name, permission) in permissionResources.user.value"> | ||||
|           <td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}"> | ||||
|             <span class="entity-reference" namespace="repository.namespace" | ||||
|                   entity="buildEntityForPermission(name, permission, 'user')"> | ||||
|                   entity="buildEntityForPermission(name, permission, 'user')" | ||||
|                   avatar-size="24"> | ||||
|             </span> | ||||
|           </td> | ||||
|           <td class="user-permissions"> | ||||
|  | @ -79,4 +81,4 @@ | |||
|        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? | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -32,4 +32,4 @@ | |||
|         </tbody> | ||||
|       </table> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| <span class="source-commit-link-elememt"> | ||||
|   <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> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -12,4 +12,4 @@ | |||
|       <a ng-href="{{ getUrl(ref, tagTemplate, 'tag') }}" target="_blank">{{ getTitle(ref) }}</a> | ||||
|     </span> | ||||
|   </span> | ||||
| </span> | ||||
| </span> | ||||
|  |  | |||
|  | @ -84,4 +84,4 @@ | |||
|       The following images and any other images not referenced by a tag will be deleted: | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -1,31 +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)"> | ||||
|           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="team-listing" ng-repeat="(name, team) in organization.teams"> | ||||
|       <div id="team-{{name}}" class="row"> | ||||
|         <div class="col-sm-7 col-md-8"> | ||||
|           <div class="team-title"> | ||||
|             <i class="fa fa-group"></i> | ||||
|             <span ng-show="team.can_view"> | ||||
|               <a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a> | ||||
|             </span> | ||||
|             <span ng-show="!team.can_view"> | ||||
|               {{ team.name }} | ||||
|             </span> | ||||
|           </div> | ||||
|   <div class="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-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 class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin"> | ||||
|           <span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span> | ||||
|           <button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button> | ||||
|         <div 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> | ||||
| </div> | ||||
|  |  | |||
|  | @ -62,4 +62,4 @@ | |||
|   <!-- Unknown --> | ||||
|   <span ng-switch-default></span> | ||||
| </span> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
							
								
								
									
										17
									
								
								static/js/directives/ng-image-watch.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								static/js/directives/ng-image-watch.js
									
										
									
									
									
										Normal 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); | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| }); | ||||
|  | @ -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. | ||||
|  * 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) { | ||||
|   return { | ||||
|  | @ -127,7 +129,7 @@ angular.module('quay').directive('quayInclude', function($compile, $templateCach | |||
|     restrict: 'A', | ||||
|     link: function($scope, $element, $attr, ctrl) { | ||||
|       var getTemplate = function(templateName) { | ||||
|         var templateUrl = '/static/partials/' + templateName; | ||||
|         var templateUrl = '/static/' + templateName; | ||||
|         return $http.get(templateUrl, {cache: $templateCache}); | ||||
|       }; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										19
									
								
								static/js/directives/ui/anchor.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								static/js/directives/ui/anchor.js
									
										
									
									
									
										Normal 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; | ||||
| }); | ||||
|  | @ -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 () { | ||||
|   var directiveDefinitionObject = { | ||||
|  | @ -9,25 +9,36 @@ angular.module('quay').directive('avatar', function () { | |||
|     transclude: true, | ||||
|     restrict: 'C', | ||||
|     scope: { | ||||
|       'hash': '=hash', | ||||
|       'email': '=email', | ||||
|       'name': '=name', | ||||
|       'data': '=data', | ||||
|       'size': '=size' | ||||
|     }, | ||||
|     controller: function($scope, $element, AvatarService) { | ||||
|     controller: function($scope, $element, AvatarService, Config, UIService) { | ||||
|       $scope.AvatarService = AvatarService; | ||||
|       $scope.Config = Config; | ||||
|       $scope.isLoading = true; | ||||
|       $scope.hasGravatar = false; | ||||
|       $scope.loadGravatar = false; | ||||
| 
 | ||||
|       var refreshHash = function() { | ||||
|         if (!$scope.name && !$scope.email) { return; } | ||||
|         $scope._hash = AvatarService.computeHash($scope.email, $scope.name); | ||||
|       $scope.imageCallback = function(r) { | ||||
|         $scope.isLoading = false; | ||||
|         $scope.hasGravatar = r; | ||||
|       }; | ||||
| 
 | ||||
|       $scope.$watch('hash', function(hash) { | ||||
|         $scope._hash = hash; | ||||
|       $scope.$watch('size', function(size) { | ||||
|         size = size * 1 || 16; | ||||
|         $scope.fontSize = (size - 4) + 'px'; | ||||
|         $scope.lineHeight = size + 'px'; | ||||
|       }); | ||||
| 
 | ||||
|       $scope.$watch('name', refreshHash); | ||||
|       $scope.$watch('email', refreshHash); | ||||
|       $scope.$watch('data', function(data) { | ||||
|         if (!data) { return; } | ||||
| 
 | ||||
|         $scope.loadGravatar = Config.AVATAR_KIND == 'gravatar' && | ||||
|           (data.kind == 'user' || data.kind == 'org'); | ||||
| 
 | ||||
|         $scope.isLoading = $scope.loadGravatar; | ||||
|         $scope.hasGravatar = false; | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|   return directiveDefinitionObject; | ||||
|  |  | |||
|  | @ -39,6 +39,21 @@ angular.module('quay').directive('entityReference', function () { | |||
|         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) { | ||||
|         if (!name) { return ''; } | ||||
|         var plus = name.indexOf('+'); | ||||
|  |  | |||
|  | @ -56,6 +56,8 @@ angular.module('quay').directive('entitySearch', function () { | |||
| 
 | ||||
|       $scope.currentEntityInternal = $scope.currentEntity; | ||||
| 
 | ||||
|       $scope.Config = Config; | ||||
| 
 | ||||
|       var isSupported = function(kind, opt_array) { | ||||
|         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) { | ||||
|             $scope.setEntity(created.name, 'team', false); | ||||
|             $scope.setEntity(created.name, 'team', false, created.avatar); | ||||
|             $scope.teams[teamname] = created; | ||||
|           }); | ||||
|         }); | ||||
|  | @ -121,17 +123,18 @@ angular.module('quay').directive('entitySearch', function () { | |||
|           } | ||||
| 
 | ||||
|           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.setEntity = function(name, kind, is_robot) { | ||||
|       $scope.setEntity = function(name, kind, is_robot, avatar) { | ||||
|         var entity = { | ||||
|           'name': name, | ||||
|           'kind': kind, | ||||
|           'is_robot': is_robot | ||||
|           'is_robot': is_robot, | ||||
|           'avatar': avatar | ||||
|         }; | ||||
| 
 | ||||
|         if ($scope.isOrganization) { | ||||
|  |  | |||
|  | @ -68,7 +68,8 @@ angular.module('quay').directive('repositoryPermissionsTable', function () { | |||
|           'kind': kind, | ||||
|           'name': name, | ||||
|           'is_robot': permission.is_robot, | ||||
|           'is_org_member': permission.is_org_member | ||||
|           'is_org_member': permission.is_org_member, | ||||
|           'avatar': permission.avatar | ||||
|         }; | ||||
|       }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -19,6 +19,45 @@ angular.module('quay').directive('teamsManager', function () { | |||
|           { '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; | ||||
|  | @ -54,6 +93,10 @@ angular.module('quay').directive('teamsManager', function () { | |||
|         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); | ||||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|  | @ -72,6 +115,12 @@ angular.module('quay').directive('teamsManager', 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]; | ||||
|         }, ApiService.errorDisplay('Cannot delete team')); | ||||
|       }; | ||||
|  |  | |||
|  | @ -14,7 +14,9 @@ angular.module('quay').factory('AvatarService', ['Config', '$sanitize', 'md5', | |||
|         break; | ||||
| 
 | ||||
|       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; | ||||
|     } | ||||
|   }; | ||||
|  |  | |||
|  | @ -71,5 +71,10 @@ angular.module('quay').factory('Config', [function() { | |||
|     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; | ||||
| }]); | ||||
|  | @ -61,4 +61,4 @@ | |||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -56,4 +56,4 @@ | |||
|      </div> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -47,7 +47,7 @@ | |||
|             <div class="signup-form"></div> | ||||
|           </div> | ||||
|           <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> | ||||
|             <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> | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ | |||
|             <div class="signup-form"></div> | ||||
|           </div> | ||||
|           <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> | ||||
|             <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> | ||||
|  |  | |||
|  | @ -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> | ||||
| </div> | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ | |||
|           <span class="avatar" size="48" email="application.avatar_email" name="application.name"></span> | ||||
|           <h2>{{ application.name || '(Untitled)' }}</h2> | ||||
|           <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> | ||||
|           </h4> | ||||
|         </div> | ||||
|  | @ -100,7 +100,7 @@ | |||
| 
 | ||||
|             <div style="margin-bottom: 20px"> | ||||
|               <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 }} | ||||
|             </div> | ||||
| 
 | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ | |||
|     <div class="cor-title"> | ||||
|       <span class="cor-title-link"></span> | ||||
|       <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> | ||||
|     </div> | ||||
|  | @ -124,4 +124,4 @@ | |||
|       </div> <!-- /cor-tab-content --> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ | |||
|     <h2>Organizations</h2> | ||||
| 
 | ||||
|     <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> | ||||
|     </div> | ||||
|   </div> | ||||
|  |  | |||
|  | @ -34,11 +34,11 @@ | |||
|         <ul class="namespaces-list"> | ||||
|           <li ng-repeat="namespace in namespaces"> | ||||
|             <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 }} | ||||
|             </span> | ||||
|             <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 }} | ||||
|             </a> | ||||
|           </li> | ||||
|  |  | |||
|  | @ -93,4 +93,4 @@ | |||
|       </div> <!-- /cor-tab-content --> | ||||
|     </div> | ||||
|   </div> | ||||
| </div> | ||||
| </div> | ||||
|  |  | |||
|  | @ -296,4 +296,4 @@ | |||
|       </div> | ||||
|     </div><!-- /.modal-content --> | ||||
|   </div><!-- /.modal-dialog --> | ||||
| </div><!-- /.modal --> | ||||
| </div><!-- /.modal --> | ||||
|  |  | |||
|  | @ -139,7 +139,7 @@ | |||
|               <tr ng-repeat="current_user in (users | filter:search | orderBy:'username' | limitTo:100)" | ||||
|                   class="user-row"> | ||||
|                 <td> | ||||
|                   <span class="avatar" hash="current_user.avatar" size="24"></span> | ||||
|                   <span class="avatar" data="current_user.avatar" size="24"></span> | ||||
|                 </td> | ||||
|                 <td> | ||||
|                   <span class="labels"> | ||||
|  |  | |||
|  | @ -70,7 +70,7 @@ | |||
|               <span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span> | ||||
|             </span> | ||||
|             <span class="invite-listing" ng-if="member.kind == 'invite'"> | ||||
|               <span class="avatar" size="32" hash="member.avatar"></span> | ||||
|               <span class="avatar" size="32" data="member.avatar"></span> | ||||
|               {{ member.email }} | ||||
|             </span> | ||||
|           </td> | ||||
|  |  | |||
|  | @ -9,7 +9,7 @@ | |||
| <div class="user-admin cor-container" ng-show="!user.anonymous"> | ||||
|   <div class="row"> | ||||
|     <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"> | ||||
|         {{ user.username }} | ||||
|       </span> | ||||
|  | @ -72,7 +72,7 @@ | |||
| 
 | ||||
|                 <tr class="auth-info" ng-repeat="authInfo in authorizedApps"> | ||||
|                   <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" | ||||
|                        data-title="{{ authInfo.application.description || authInfo.application.name }}" bs-tooltip> | ||||
|                       {{ authInfo.application.name }} | ||||
|  | @ -291,7 +291,7 @@ | |||
|               <div class="form-group"> | ||||
|                 <label for="orgName">Organization Name</label> | ||||
|                 <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> | ||||
|                 <span class="description">This will continue to be the namespace for your repositories</span> | ||||
|               </div> | ||||
|  |  | |||
|  | @ -9,25 +9,29 @@ def icon_path(icon_name): | |||
| def icon_image(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): | ||||
|   user = model.get_namespace_user(username) | ||||
|   if not user: | ||||
|     return username | ||||
| 
 | ||||
|   is_robot = False | ||||
|   if user.robot: | ||||
|     parts = parse_robot_username(username) | ||||
|     user = model.get_namespace_user(parts[0]) | ||||
| 
 | ||||
|     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 """ | ||||
|   <span> | ||||
|   <img src="%s" | ||||
|       style="vertical-align: middle; margin-left: 6px; margin-right: 4px;" alt="%s"> | ||||
|   %s | ||||
|   <b>%s</b> | ||||
|   </span>""" % (avatar.get_url(user.email, 16), alt, username) | ||||
|   </span>""" % (avatar_html, username) | ||||
| 
 | ||||
| 
 | ||||
| def repository_tag_reference(repository_path_and_tag): | ||||
|  | @ -52,12 +56,15 @@ def repository_reference(pair): | |||
|   if not owner: | ||||
|     return "%s/%s" % (namespace, repository) | ||||
| 
 | ||||
|   avatar_html = avatar.get_mail_html(owner.username, owner.email, 16, | ||||
|                                      'org' if owner.organization else 'user') | ||||
| 
 | ||||
|   return """ | ||||
|   <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> | ||||
|   </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): | ||||
|  | @ -84,6 +91,7 @@ def get_template_env(searchpath): | |||
| 
 | ||||
| def add_filters(template_env): | ||||
|   template_env.filters['icon_image'] = icon_image | ||||
|   template_env.filters['team_reference'] = team_reference | ||||
|   template_env.filters['user_reference'] = user_reference | ||||
|   template_env.filters['admin_reference'] = admin_reference | ||||
|   template_env.filters['repository_reference'] = repository_reference | ||||
|  |  | |||
		Reference in a new issue