From 27a9b8458718999ae4cd1c5904fbbb41772c9dc2 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 30 Mar 2015 17:55:04 -0400 Subject: [PATCH] Switch avatars to be built out of CSS and only overlayed with the gravatar when a non-default exists --- avatars/avatars.py | 101 ++++++++++++------ config.py | 10 +- data/model/legacy.py | 15 +-- emails/teaminvite.html | 2 +- endpoints/api/organization.py | 12 +-- endpoints/api/permission.py | 13 ++- endpoints/api/search.py | 10 +- endpoints/api/superuser.py | 2 +- endpoints/api/team.py | 8 +- endpoints/api/user.py | 11 +- endpoints/web.py | 22 +--- static/css/directives/ui/avatar.css | 23 ++++ static/css/directives/ui/entity-reference.css | 7 ++ static/css/directives/ui/entity-search.css | 52 +++++++++ static/css/directives/ui/teams-manager.css | 54 +++++++++- static/css/quay.css | 49 --------- static/directives/anchor.html | 4 + static/directives/application-info.html | 2 +- static/directives/avatar.html | 13 ++- static/directives/build-info-bar.html | 2 +- static/directives/build-logs-view.html | 2 +- static/directives/build-mini-status.html | 2 +- static/directives/cor-checkable-item.html | 2 +- .../directives/cor-checkable-menu-item.html | 2 +- static/directives/cor-checkable-menu.html | 2 +- static/directives/cor-confirm-dialog.html | 2 +- .../directives/cor-floating-bottom-bar.html | 2 +- static/directives/cor-loader-inline.html | 2 +- static/directives/cor-loader.html | 2 +- static/directives/cor-log-box.html | 2 +- static/directives/cor-option.html | 2 +- static/directives/cor-options-menu.html | 2 +- static/directives/cor-step-bar.html | 2 +- static/directives/cor-step.html | 2 +- static/directives/cor-tab-content.html | 2 +- static/directives/cor-tab-panel.html | 2 +- static/directives/cor-tab.html | 2 +- static/directives/cor-tabs.html | 2 +- static/directives/cor-title-action.html | 2 +- static/directives/cor-title-content.html | 2 +- static/directives/cor-title-link.html | 2 +- static/directives/entity-reference.html | 37 +------ static/directives/entity-search.html | 47 ++++---- static/directives/fetch-tag-dialog.html | 2 +- static/directives/filter-control.html | 2 +- static/directives/header-bar.html | 4 +- static/directives/image-changes-view.html | 2 +- static/directives/image-info-sidebar.html | 2 +- static/directives/image-view-layer.html | 2 +- static/directives/namespace-selector.html | 8 +- static/directives/new-entity-reference.html | 40 +++++++ static/directives/notification-view.html | 2 +- static/directives/old-entity-reference.html | 39 +++++++ static/directives/organization-header.html | 2 +- static/directives/ps-usage-graph.html | 2 +- .../directives/quay-service-status-bar.html | 2 +- static/directives/quay-service-status.html | 2 +- static/directives/realtime-area-chart.html | 2 +- static/directives/realtime-line-chart.html | 2 +- static/directives/repo-list-grid.html | 2 +- static/directives/repo-star.html | 2 +- .../directives/repository-events-table.html | 2 +- .../repository-permissions-table.html | 8 +- .../directives/repository-tokens-table.html | 2 +- static/directives/source-commit-link.html | 2 +- static/directives/source-ref-link.html | 2 +- static/directives/tag-operations-dialog.html | 2 +- static/directives/teams-manager.html | 67 ++++++++---- .../triggered-build-description.html | 2 +- static/js/directives/ng-image-watch.js | 17 +++ static/js/directives/quay-layout.js | 4 +- static/js/directives/ui/anchor.js | 19 ++++ static/js/directives/ui/avatar.js | 35 +++--- static/js/directives/ui/entity-reference.js | 15 +++ static/js/directives/ui/entity-search.js | 11 +- .../ui/repository-permissions-table.js | 3 +- static/js/directives/ui/teams-manager.js | 49 +++++++++ static/js/services/avatar-service.js | 4 +- static/js/services/features-config.js | 5 + static/partials/build-view.html | 2 +- static/partials/image-view.html | 2 +- static/partials/landing-login.html | 2 +- static/partials/landing-normal.html | 2 +- static/partials/landing.html | 2 +- static/partials/manage-application.html | 4 +- static/partials/org-view.html | 4 +- static/partials/organizations.html | 2 +- static/partials/repo-list.html | 4 +- static/partials/repo-view.html | 2 +- static/partials/setup.html | 2 +- static/partials/super-user.html | 2 +- static/partials/team-view.html | 2 +- static/partials/user-admin.html | 6 +- util/jinjautil.py | 22 ++-- 94 files changed, 663 insertions(+), 303 deletions(-) create mode 100644 static/css/directives/ui/avatar.css create mode 100644 static/css/directives/ui/entity-reference.css create mode 100644 static/css/directives/ui/entity-search.css create mode 100644 static/directives/anchor.html create mode 100644 static/directives/new-entity-reference.html create mode 100644 static/directives/old-entity-reference.html create mode 100644 static/js/directives/ng-image-watch.js create mode 100644 static/js/directives/ui/anchor.js diff --git a/avatars/avatars.py b/avatars/avatars.py index 40935df10..220cae9cb 100644 --- a/avatars/avatars.py +++ b/avatars/avatars.py @@ -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 """%s""" % (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 """ + + %s + +""" % (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, diff --git a/config.py b/config.py index 339ffca34..e33c8bc46 100644 --- a/config.py +++ b/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'] diff --git a/data/model/legacy.py b/data/model/legacy.py index aa968408c..8d5a7d656 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -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) diff --git a/emails/teaminvite.html b/emails/teaminvite.html index 3d8ff9c14..128bbe00f 100644 --- a/emails/teaminvite.html +++ b/emails/teaminvite.html @@ -4,7 +4,7 @@

Invitation to join team: {{ teamname }}

-{{ inviter | user_reference }} has invited you to join the team {{ teamname }} under organization {{ organization | user_reference }}. +{{ inviter | user_reference }} has invited you to join the team {{ teamname | team_reference }} under organization {{ organization | user_reference }}.

diff --git a/endpoints/api/organization.py b/endpoints/api/organization.py index 115644789..3cb98fb84 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -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, []) } diff --git a/endpoints/api/permission.py b/endpoints/api/permission.py index c8a473d9c..f855a99f8 100644 --- a/endpoints/api/permission.py +++ b/endpoints/api/permission.py @@ -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//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} } diff --git a/endpoints/api/search.py b/endpoints/api/search.py index 0e3561745..76223ac1c 100644 --- a/endpoints/api/search.py +++ b/endpoints/api/search.py @@ -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 diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py index 2c7daf633..01dbc5cb2 100644 --- a/endpoints/api/superuser.py +++ b/endpoints/api/superuser.py @@ -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) } diff --git a/endpoints/api/team.py b/endpoints/api/team.py index 91f225fa1..ce42f5e94 100644 --- a/endpoints/api/team.py +++ b/endpoints/api/team.py @@ -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 } diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 6c3cafd63..6766c0fe9 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -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), diff --git a/endpoints/web.py b/endpoints/web.py index c3af01e44..1359fd3bc 100644 --- a/endpoints/web.py +++ b/endpoints/web.py @@ -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/") -@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) } } diff --git a/static/css/directives/ui/avatar.css b/static/css/directives/ui/avatar.css new file mode 100644 index 000000000..97a15dc31 --- /dev/null +++ b/static/css/directives/ui/avatar.css @@ -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; +} \ No newline at end of file diff --git a/static/css/directives/ui/entity-reference.css b/static/css/directives/ui/entity-reference.css new file mode 100644 index 000000000..5885f0d8c --- /dev/null +++ b/static/css/directives/ui/entity-reference.css @@ -0,0 +1,7 @@ +.entity-reference .new-entity-reference .entity-name { + margin-left: 6px; +} + +.entity-reference .new-entity-reference .fa-wrench { + width: 16px; +} diff --git a/static/css/directives/ui/entity-search.css b/static/css/directives/ui/entity-search.css new file mode 100644 index 000000000..eaab63120 --- /dev/null +++ b/static/css/directives/ui/entity-search.css @@ -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; +} \ No newline at end of file diff --git a/static/css/directives/ui/teams-manager.css b/static/css/directives/ui/teams-manager.css index 24de3e908..a36edf5ce 100644 --- a/static/css/directives/ui/teams-manager.css +++ b/static/css/directives/ui/teams-manager.css @@ -1,3 +1,55 @@ .teams-manager .popup-input-button { float: right; -} \ No newline at end of file +} + +.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; +} diff --git a/static/css/quay.css b/static/css/quay.css index 03452682b..70714d7e0 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -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; diff --git a/static/directives/anchor.html b/static/directives/anchor.html new file mode 100644 index 000000000..563fbe581 --- /dev/null +++ b/static/directives/anchor.html @@ -0,0 +1,4 @@ + + + + diff --git a/static/directives/application-info.html b/static/directives/application-info.html index 241fec279..960bee7ed 100644 --- a/static/directives/application-info.html +++ b/static/directives/application-info.html @@ -1,6 +1,6 @@
- +

{{ application.name }}

{{ application.organization.name }} diff --git a/static/directives/avatar.html b/static/directives/avatar.html index 46c56afe5..866992232 100644 --- a/static/directives/avatar.html +++ b/static/directives/avatar.html @@ -1 +1,12 @@ - \ No newline at end of file + + + + {{ data.name.charAt(0).toUpperCase() }} + Ω + + \ No newline at end of file diff --git a/static/directives/build-info-bar.html b/static/directives/build-info-bar.html index 45b17d47c..a25a056c6 100644 --- a/static/directives/build-info-bar.html +++ b/static/directives/build-info-bar.html @@ -15,4 +15,4 @@
Manually Started Build
-

\ No newline at end of file +
diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html index 696fd4b93..074ccd5b0 100644 --- a/static/directives/build-logs-view.html +++ b/static/directives/build-logs-view.html @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/static/directives/build-mini-status.html b/static/directives/build-mini-status.html index ec743ce94..590a857b5 100644 --- a/static/directives/build-mini-status.html +++ b/static/directives/build-mini-status.html @@ -19,4 +19,4 @@
- \ No newline at end of file + diff --git a/static/directives/cor-checkable-item.html b/static/directives/cor-checkable-item.html index f3e65e39b..4dde44d92 100644 --- a/static/directives/cor-checkable-item.html +++ b/static/directives/cor-checkable-item.html @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/static/directives/cor-checkable-menu-item.html b/static/directives/cor-checkable-menu-item.html index 452e37ea7..3fc5f7c25 100644 --- a/static/directives/cor-checkable-menu-item.html +++ b/static/directives/cor-checkable-menu-item.html @@ -1 +1 @@ -
  • \ No newline at end of file +
  • diff --git a/static/directives/cor-checkable-menu.html b/static/directives/cor-checkable-menu.html index 2c6fce8f4..74c626ff1 100644 --- a/static/directives/cor-checkable-menu.html +++ b/static/directives/cor-checkable-menu.html @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/static/directives/cor-confirm-dialog.html b/static/directives/cor-confirm-dialog.html index 330729390..c6aa9d2fd 100644 --- a/static/directives/cor-confirm-dialog.html +++ b/static/directives/cor-confirm-dialog.html @@ -22,4 +22,4 @@ - \ No newline at end of file + diff --git a/static/directives/cor-floating-bottom-bar.html b/static/directives/cor-floating-bottom-bar.html index 2e5337fd2..11615e6a8 100644 --- a/static/directives/cor-floating-bottom-bar.html +++ b/static/directives/cor-floating-bottom-bar.html @@ -1,3 +1,3 @@
    -
    \ No newline at end of file + diff --git a/static/directives/cor-loader-inline.html b/static/directives/cor-loader-inline.html index 39ffb5b99..3a2c42c1d 100644 --- a/static/directives/cor-loader-inline.html +++ b/static/directives/cor-loader-inline.html @@ -2,4 +2,4 @@
    - \ No newline at end of file + diff --git a/static/directives/cor-loader.html b/static/directives/cor-loader.html index 112680a22..f0aab7afc 100644 --- a/static/directives/cor-loader.html +++ b/static/directives/cor-loader.html @@ -2,4 +2,4 @@
    - \ No newline at end of file + diff --git a/static/directives/cor-log-box.html b/static/directives/cor-log-box.html index c5442d0f7..6d3157db3 100644 --- a/static/directives/cor-log-box.html +++ b/static/directives/cor-log-box.html @@ -8,4 +8,4 @@
    New Logs
    - \ No newline at end of file + diff --git a/static/directives/cor-option.html b/static/directives/cor-option.html index 0eb57170b..727e3dda3 100644 --- a/static/directives/cor-option.html +++ b/static/directives/cor-option.html @@ -1,3 +1,3 @@
  • -
  • \ No newline at end of file + diff --git a/static/directives/cor-options-menu.html b/static/directives/cor-options-menu.html index 8b6cf1e26..7e5f43cc3 100644 --- a/static/directives/cor-options-menu.html +++ b/static/directives/cor-options-menu.html @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/static/directives/cor-step-bar.html b/static/directives/cor-step-bar.html index 274a2c924..117f8185d 100644 --- a/static/directives/cor-step-bar.html +++ b/static/directives/cor-step-bar.html @@ -1,3 +1,3 @@
    -
    \ No newline at end of file + diff --git a/static/directives/cor-step.html b/static/directives/cor-step.html index 5339db30e..acc9baee4 100644 --- a/static/directives/cor-step.html +++ b/static/directives/cor-step.html @@ -3,4 +3,4 @@ {{ text }} - \ No newline at end of file + diff --git a/static/directives/cor-tab-content.html b/static/directives/cor-tab-content.html index 997ae5af1..747ccb2c8 100644 --- a/static/directives/cor-tab-content.html +++ b/static/directives/cor-tab-content.html @@ -1 +1 @@ -
    \ No newline at end of file +
    diff --git a/static/directives/cor-tab-panel.html b/static/directives/cor-tab-panel.html index f92d683ab..d041c9466 100644 --- a/static/directives/cor-tab-panel.html +++ b/static/directives/cor-tab-panel.html @@ -1,3 +1,3 @@
    -
    \ No newline at end of file + diff --git a/static/directives/cor-tab.html b/static/directives/cor-tab.html index 61c8b327f..07d4e0e92 100644 --- a/static/directives/cor-tab.html +++ b/static/directives/cor-tab.html @@ -10,4 +10,4 @@ - \ No newline at end of file + diff --git a/static/directives/cor-tabs.html b/static/directives/cor-tabs.html index 1a965932e..5ab85ecb1 100644 --- a/static/directives/cor-tabs.html +++ b/static/directives/cor-tabs.html @@ -1 +1 @@ -
      \ No newline at end of file +
        diff --git a/static/directives/cor-title-action.html b/static/directives/cor-title-action.html index f06f9b78d..807fe1bab 100644 --- a/static/directives/cor-title-action.html +++ b/static/directives/cor-title-action.html @@ -1,3 +1,3 @@
        -
        \ No newline at end of file + diff --git a/static/directives/cor-title-content.html b/static/directives/cor-title-content.html index 6acbe47b3..5b2077d08 100644 --- a/static/directives/cor-title-content.html +++ b/static/directives/cor-title-content.html @@ -1,3 +1,3 @@

        -
        \ No newline at end of file + diff --git a/static/directives/cor-title-link.html b/static/directives/cor-title-link.html index f400b8741..428671f86 100644 --- a/static/directives/cor-title-link.html +++ b/static/directives/cor-title-link.html @@ -1 +1 @@ - \ No newline at end of file + diff --git a/static/directives/entity-reference.html b/static/directives/entity-reference.html index 0252515dc..2229bcb0c 100644 --- a/static/directives/entity-reference.html +++ b/static/directives/entity-reference.html @@ -1,38 +1,3 @@ - - - - {{entity.name}} - {{entity.name}} - - - - - - {{entity.name}} - {{entity.name}} - - - - - - - - - - - - {{ getPrefix(entity.name) }}+{{ getShortenedName(entity.name) }} - - - {{ getPrefix(entity.name) }}+{{ getShortenedName(entity.name) }} - - - - {{getShortenedName(entity.name)}} - - - - + diff --git a/static/directives/entity-search.html b/static/directives/entity-search.html index 80114df88..ae181735d 100644 --- a/static/directives/entity-search.html +++ b/static/directives/entity-search.html @@ -6,12 +6,26 @@ diff --git a/static/directives/fetch-tag-dialog.html b/static/directives/fetch-tag-dialog.html index befe6cedc..54480562c 100644 --- a/static/directives/fetch-tag-dialog.html +++ b/static/directives/fetch-tag-dialog.html @@ -67,4 +67,4 @@ - \ No newline at end of file + diff --git a/static/directives/filter-control.html b/static/directives/filter-control.html index 9f0a5e140..c8afc88fe 100644 --- a/static/directives/filter-control.html +++ b/static/directives/filter-control.html @@ -1,3 +1,3 @@ - \ No newline at end of file + diff --git a/static/directives/header-bar.html b/static/directives/header-bar.html index bac17c19c..3fb263d4b 100644 --- a/static/directives/header-bar.html +++ b/static/directives/header-bar.html @@ -23,7 +23,7 @@