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 2d50138af..6fe1c4042 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' @@ -205,3 +203,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 2fcbfcf68..f0d0c32fe 100644 --- a/data/model/legacy.py +++ b/data/model/legacy.py @@ -310,11 +310,54 @@ def _list_entity_robots(entity_name): .where(User.robot == True, User.username ** (entity_name + '+%'))) -def list_entity_robot_tuples(entity_name): - return (_list_entity_robots(entity_name) - .select(User.username, FederatedLogin.service_ident) - .tuples()) +class _TupleWrapper(object): + def __init__(self, data, fields): + self._data = data + self._fields = fields + def get(self, field): + return self._data[self._fields.index(field.name + ':' + field.model_class.__name__)] + + +class TupleSelector(object): + """ Helper class for selecting tuples from a peewee query and easily accessing + them as if they were objects. + """ + def __init__(self, query, fields): + self._query = query.select(*fields).tuples() + self._fields = [field.name + ':' + field.model_class.__name__ for field in fields] + + def __iter__(self): + return self._build_iterator() + + def _build_iterator(self): + for tuple_data in self._query: + yield _TupleWrapper(tuple_data, self._fields) + + + +def list_entity_robot_permission_teams(entity_name): + query = (_list_entity_robots(entity_name) + .join(RepositoryPermission, JOIN_LEFT_OUTER, + on=(RepositoryPermission.user == FederatedLogin.user)) + .join(Repository, JOIN_LEFT_OUTER) + .switch(User) + .join(TeamMember, JOIN_LEFT_OUTER) + .join(Team, JOIN_LEFT_OUTER)) + + fields = [User.username, FederatedLogin.service_ident, Repository.name, Team.name] + return TupleSelector(query, fields) + + +def list_robot_permissions(robot_name): + return (RepositoryPermission.select(RepositoryPermission, User, Repository) + .join(Repository) + .join(Visibility) + .switch(RepositoryPermission) + .join(Role) + .switch(RepositoryPermission) + .join(User) + .where(User.username == robot_name, User.robot == True)) def convert_user_to_organization(user, admin_user): # Change the user to an organization. @@ -654,13 +697,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 +712,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 @@ -1039,7 +1084,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 4302bd62f..3cb98fb84 100644 --- a/endpoints/api/organization.py +++ b/endpoints/api/organization.py @@ -24,16 +24,22 @@ logger = logging.getLogger(__name__) def org_view(o, teams): - admin_org = AdministerOrganizationPermission(o.username) - is_admin = admin_org.can() + is_admin = AdministerOrganizationPermission(o.username).can() + is_member = OrganizationMemberPermission(o.username).can() + view = { 'name': o.username, 'email': o.email if is_admin else '', - 'avatar': avatar.compute_hash(o.email, name=o.username), - 'teams': {t.name : team_view(o.username, t) for t in teams}, - 'is_admin': is_admin + '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 @@ -129,17 +135,17 @@ class Organization(ApiResource): @nickname('getOrganization') def get(self, orgname): """ Get the details for the specified organization """ - permission = OrganizationMemberPermission(orgname) - if permission.can(): - try: - org = model.get_organization(orgname) - except model.InvalidOrganizationException: - raise NotFound() + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + raise NotFound() + teams = None + if OrganizationMemberPermission(orgname).can(): teams = model.get_teams_within_org(org) - return org_view(org, teams) - raise Unauthorized() + return org_view(org, teams) + @require_scope(scopes.ORG_ADMIN) @nickname('changeOrganizationDetails') @@ -218,7 +224,7 @@ class OrgPrivateRepositories(ApiResource): @path_param('orgname', 'The name of the organization') class OrgnaizationMemberList(ApiResource): """ Resource for listing the members of an organization. """ - + @require_scope(scopes.ORG_ADMIN) @nickname('getOrganizationMembers') def get(self, orgname): @@ -297,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..4c0b62074 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 @@ -16,7 +17,10 @@ def role_view(repo_perm_obj): } def wrap_role_view_user(role_json, user): + role_json['name'] = user.username role_json['is_robot'] = user.robot + if not user.robot: + role_json['avatar'] = avatar.get_data_for_user(user) return role_json @@ -25,6 +29,12 @@ def wrap_role_view_org(role_json, user, org_members): return role_json +def wrap_role_view_team(role_json, team): + role_json['name'] = team.name + role_json['avatar'] = avatar.get_data_for_team(team) + return role_json + + @resource('/v1/repository//permissions/team/') @path_param('repository', 'The full path of the repository. e.g. namespace/name') class RepositoryTeamPermissionList(RepositoryParamResource): @@ -35,8 +45,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} } @@ -232,7 +245,7 @@ class RepositoryTeamPermission(RepositoryParamResource): 'role': new_permission['role']}, repo=model.get_repository(namespace, repository)) - return role_view(perm), 200 + return wrap_role_view_team(role_view(perm), perm.team), 200 @require_repo_admin @nickname('deleteTeamPermissions') diff --git a/endpoints/api/prototype.py b/endpoints/api/prototype.py index 343913c3a..de0c97483 100644 --- a/endpoints/api/prototype.py +++ b/endpoints/api/prototype.py @@ -7,6 +7,7 @@ from auth.permissions import AdministerOrganizationPermission from auth.auth_context import get_authenticated_user from auth import scopes from data import model +from app import avatar def prototype_view(proto, org_members): @@ -16,6 +17,7 @@ def prototype_view(proto, org_members): 'is_robot': user.robot, 'kind': 'user', 'is_org_member': user.robot or user.username in org_members, + 'avatar': avatar.get_data_for_user(user) } if proto.delegate_user: @@ -24,6 +26,7 @@ def prototype_view(proto, org_members): delegate_view = { 'name': proto.delegate_team.name, 'kind': 'team', + 'avatar': avatar.get_data_for_team(proto.delegate_team) } return { diff --git a/endpoints/api/robot.py b/endpoints/api/robot.py index b7614a356..ecb26e573 100644 --- a/endpoints/api/robot.py +++ b/endpoints/api/robot.py @@ -5,16 +5,63 @@ from auth.permissions import AdministerOrganizationPermission, OrganizationMembe from auth.auth_context import get_authenticated_user from auth import scopes from data import model +from data.database import User, Team, Repository, FederatedLogin from util.names import format_robot_username - +from flask import abort +from app import avatar def robot_view(name, token): return { 'name': name, - 'token': token, + 'token': token } +def permission_view(permission): + return { + 'repository': { + 'name': permission.repository.name, + 'is_public': permission.repository.visibility.name == 'public' + }, + 'role': permission.role.name + } + + +def robots_list(prefix): + tuples = model.list_entity_robot_permission_teams(prefix) + + robots = {} + robot_teams = set() + + for robot_tuple in tuples: + robot_name = robot_tuple.get(User.username) + if not robot_name in robots: + robots[robot_name] = { + 'name': robot_name, + 'token': robot_tuple.get(FederatedLogin.service_ident), + 'teams': [], + 'repositories': [] + } + + team_name = robot_tuple.get(Team.name) + repository_name = robot_tuple.get(Repository.name) + + if team_name is not None: + check_key = robot_name + ':' + team_name + if not check_key in robot_teams: + robot_teams.add(check_key) + + robots[robot_name]['teams'].append({ + 'name': team_name, + 'avatar': avatar.get_data(team_name, team_name, 'team') + }) + + if repository_name is not None: + if not repository_name in robots[robot_name]['repositories']: + robots[robot_name]['repositories'].append(repository_name) + + return {'robots': robots.values()} + @resource('/v1/user/robots') @internal_only class UserRobotList(ApiResource): @@ -24,10 +71,7 @@ class UserRobotList(ApiResource): def get(self): """ List the available robots for the user. """ user = get_authenticated_user() - robots = model.list_entity_robot_tuples(user.username) - return { - 'robots': [robot_view(name, password) for name, password in robots] - } + return robots_list(user.username) @resource('/v1/user/robots/') @@ -73,10 +117,7 @@ class OrgRobotList(ApiResource): """ List the organization's robots. """ permission = OrganizationMemberPermission(orgname) if permission.can(): - robots = model.list_entity_robot_tuples(orgname) - return { - 'robots': [robot_view(name, password) for name, password in robots] - } + return robots_list(orgname) raise Unauthorized() @@ -125,6 +166,47 @@ class OrgRobot(ApiResource): raise Unauthorized() +@resource('/v1/user/robots//permissions') +@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') +@internal_only +class UserRobotPermissions(ApiResource): + """ Resource for listing the permissions a user's robot has in the system. """ + @require_user_admin + @nickname('getUserRobotPermissions') + def get(self, robot_shortname): + """ Returns the list of repository permissions for the user's robot. """ + parent = get_authenticated_user() + robot, password = model.get_robot(robot_shortname, parent) + permissions = model.list_robot_permissions(robot.username) + + return { + 'permissions': [permission_view(permission) for permission in permissions] + } + + +@resource('/v1/organization//robots//permissions') +@path_param('orgname', 'The name of the organization') +@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') +@related_user_resource(UserRobotPermissions) +class OrgRobotPermissions(ApiResource): + """ Resource for listing the permissions an org's robot has in the system. """ + @require_user_admin + @nickname('getOrgRobotPermissions') + def get(self, orgname, robot_shortname): + """ Returns the list of repository permissions for the org's robot. """ + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + parent = model.get_organization(orgname) + robot, password = model.get_robot(robot_shortname, parent) + permissions = model.list_robot_permissions(robot.username) + + return { + 'permissions': [permission_view(permission) for permission in permissions] + } + + abort(403) + + @resource('/v1/user/robots//regenerate') @path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix') @internal_only 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 b5d260516..c3812e717 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -36,7 +36,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) @@ -62,7 +62,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) @@ -176,8 +176,8 @@ class User(ApiResource): 'description': 'The user\'s email address', }, 'avatar': { - 'type': 'string', - 'description': 'Avatar hash representing the user\'s icon' + 'type': 'object', + 'description': 'Avatar data representing the user\'s icon' }, 'organizations': { 'type': 'array', @@ -665,17 +665,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/core-ui.css b/static/css/core-ui.css index b74923d4d..37cc03d69 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -773,10 +773,17 @@ padding: 10px; } +.co-table.no-lines td { + border-bottom: 0px; + padding: 6px; +} + .co-table thead td { + color: #999; + font-size: 90%; text-transform: uppercase; - font-size: 16px; - color: #666; + font-weight: 300; + padding-top: 0px !important; } .co-table thead td a { @@ -813,11 +820,45 @@ width: 30px; } +.co-table td.caret-col { + width: 10px; + padding-left: 6px; + padding-right: 0px; + color: #aaa; +} + +.co-table td.caret-col i.fa { + cursor: pointer; +} + +.co-table .add-row-spacer td { + padding: 5px; +} + .co-table .add-row td { + padding-top: 10px; border-top: 2px solid #eee; border-bottom: none; } +.co-table tr.co-table-header-row td { + font-size: 12px; + text-transform: uppercase; + color: #ccc; + border-bottom: none; + padding-left: 10px; + padding-top: 10px; + padding-bottom: 4px; +} + +.co-table tr.co-table-header-row td i.fa { + margin-right: 4px; +} + +.co-table tr.indented-row td:first-child { + padding-left: 28px; +} + .cor-checkable-menu { display: inline-block; } diff --git a/static/css/directives/ui/application-manager.css b/static/css/directives/ui/application-manager.css new file mode 100644 index 000000000..8184f9183 --- /dev/null +++ b/static/css/directives/ui/application-manager.css @@ -0,0 +1,7 @@ +.application-manager-element .co-table { + margin-top: 20px; +} + +.application-manager-element i.fa { + margin-right: 4px; +} diff --git a/static/css/directives/ui/avatar.css b/static/css/directives/ui/avatar.css new file mode 100644 index 000000000..0ba53cad5 --- /dev/null +++ b/static/css/directives/ui/avatar.css @@ -0,0 +1,30 @@ +.avatar-element { + display: inline-block; + vertical-align: middle; + color: white !important; + text-align: center; + position: relative; + background: white; + overflow: hidden; +} + +.avatar-element.team { + border-radius: 50%; +} + +.avatar-element img { + position: absolute; + top: 0px; + left: 0px; +} + +.avatar-element .letter { + cursor: default !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; +} + +a .avatar-element .letter { + cursor: pointer !important; +} \ No newline at end of file diff --git a/static/css/directives/ui/billing-invoices.css b/static/css/directives/ui/billing-invoices.css new file mode 100644 index 000000000..10011237c --- /dev/null +++ b/static/css/directives/ui/billing-invoices.css @@ -0,0 +1,25 @@ + +.billing-invoices-element .invoice-title { + padding: 6px; + cursor: pointer; +} + +.billing-invoices-element .invoice-status .success { + color: green; +} + +.billing-invoices-element .invoice-status .pending { + color: steelblue; +} + +.billing-invoices-element .invoice-status .danger { + color: red; +} + +.billing-invoices-element .invoice-amount:before { + content: '$'; +} + +.billing-invoices-element .fa-download { + color: #aaa; +} \ 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/prototype-manager.css b/static/css/directives/ui/prototype-manager.css new file mode 100644 index 000000000..41ccb9824 --- /dev/null +++ b/static/css/directives/ui/prototype-manager.css @@ -0,0 +1,8 @@ +.prototype-manager-element i.fa { + margin-right: 4px; +} + +.prototype-manager-element td { + padding: 10px !important; + vertical-align: middle !important; +} \ No newline at end of file diff --git a/static/css/directives/ui/repository-permissions-table.css b/static/css/directives/ui/repository-permissions-table.css index 8e69007a2..437e6d57e 100644 --- a/static/css/directives/ui/repository-permissions-table.css +++ b/static/css/directives/ui/repository-permissions-table.css @@ -1,3 +1,15 @@ .repository-permissions-table #add-entity-permission { padding-left: 0px; +} + +.repository-permissions-table .user-permission-entity { + position: relative; +} + +.repository-permissions-table .outside-org { + position: absolute; + top: 15px; + left: -2px; + font-size: 16px; + color: #E8BB03; } \ No newline at end of file diff --git a/static/css/directives/ui/robots-manager.css b/static/css/directives/ui/robots-manager.css new file mode 100644 index 000000000..b2eb42bcf --- /dev/null +++ b/static/css/directives/ui/robots-manager.css @@ -0,0 +1,80 @@ +.robots-manager-element .robot a { + font-size: 16px; + cursor: pointer; +} + +.robots-manager-element .robot .prefix { + color: #aaa; +} + +.robots-manager-element .robot i { + margin-right: 10px; +} + +.robots-manager-element .popup-input-button i.fa { + margin-right: 4px; +} + +.robots-manager-element .empty { + color: #ccc; +} + +.robots-manager-element tr.open td { + border-bottom: 1px solid transparent; +} + +.robots-manager-element .permissions-table-wrapper { + margin-left: 0px; + border-left: 2px solid #ccc; + padding-left: 20px; +} + +.robots-manager-element .permissions-table tbody tr:last-child td { + border-bottom: 0px; +} + +.robots-manager-element .permissions-display-row { + position: relative; + padding-bottom: 20px; +} + +.robots-manager-element .permissions-display-row td:first-child { + min-width: 300px; +} + +.robots-manager-element .repo-circle { + color: #999; + display: inline-block; + position: relative; + background: #eee; + padding: 4px; + border-radius: 50%; + display: inline-block; + width: 46px; + height: 46px; + margin-right: 6px; +} + +.robots-manager-element .repo-circle .fa-hdd-o { + font-size: 1.7em; +} + +.robots-manager-element .repo-circle.no-background .fa-hdd-o { + font-size: 1.7em; +} + +.robots-manager-element .repo-circle .fa-lock { + width: 16px; + height: 16px; + line-height: 16px; + font-size: 12px !important; +} + +.robots-manager-element .repo-circle.no-background .fa-lock { + bottom: 5px; + right: 2px; +} + +.robots-manager-element .member-perm-summary { + margin-right: 14px; +} \ No newline at end of file diff --git a/static/css/directives/ui/teams-manager.css b/static/css/directives/ui/teams-manager.css new file mode 100644 index 000000000..a36edf5ce --- /dev/null +++ b/static/css/directives/ui/teams-manager.css @@ -0,0 +1,55 @@ +.teams-manager .popup-input-button { + float: right; +} + +.teams-manager .manager-header { + border-bottom: 1px solid #eee; + margin-bottom: 10px; +} + +.teams-manager .cor-options-menu { + display: inline-block; + margin-left: 10px; +} + +.teams-manager .header-col .info-icon { + font-size: 16px; +} + +.teams-manager .header-col .header-text { + text-transform: uppercase; + font-size: 14px; + color: #aaa !important; + display: inline-block; + padding-top: 4px; +} + +.teams-manager .control-col { + padding-top: 6px; +} + +.teams-manager .team-listing { + margin-bottom: 10px; +} + +.teams-manager .team-listing .avatar { + margin-right: 6px; +} + +.teams-manager .team-member-list .fa { + color: #ccc; +} + +.teams-manager .team-member-list { + position: relative; + min-height: 20px; + padding: 4px; + padding-left: 40px; +} + +.teams-manager .team-member-list .team-member-more { + vertical-align: middle; + padding-left: 6px; + color: #aaa; + font-size: 14px; +} diff --git a/static/css/pages/org-view.css b/static/css/pages/org-view.css new file mode 100644 index 000000000..1efd78fd1 --- /dev/null +++ b/static/css/pages/org-view.css @@ -0,0 +1,25 @@ +.org-view .organization-name { + vertical-align: middle; + margin-left: 6px; +} + +.org-view h3 { + margin-bottom: 20px; + margin-top: 0px; +} + +.org-view .section-description-header { + padding-left: 40px; + position: relative; + margin-bottom: 20px; +} + +.org-view .section-description-header:before { + font-family: FontAwesome; + content: "\f05a"; + position: absolute; + top: 2px; + left: 6px; + font-size: 27px; + color: #888; +} \ No newline at end of file diff --git a/static/css/pages/team-view.css b/static/css/pages/team-view.css new file mode 100644 index 000000000..26df5412d --- /dev/null +++ b/static/css/pages/team-view.css @@ -0,0 +1,51 @@ +.team-view .co-main-content-panel { + padding: 20px; +} + +.team-view .team-name { + vertical-align: middle; + margin-left: 6px; +} + +.team-view .team-view-header { + border-bottom: 1px solid #eee; + margin-bottom: 10px; + padding-bottom: 10px; +} + +.team-view .team-view-header > h3 { + margin-top: 10px; +} + +.team-view .team-view-header .popover { + max-width: none !important; +} + +.team-view .team-view-header .popover.bottom-right .arrow:after { + border-bottom-color: #f7f7f7; + top: 2px; +} + +.team-view .team-view-header .popover-content { + font-size: 14px; + padding-top: 6px; + min-width: 500px; +} + +.team-view .team-view-header .popover-content input { + background: white; +} + +.team-view .team-view-add-element .help-text { + font-size: 13px; + color: #ccc; + margin-top: 10px; +} + +.team-view .co-table-header-row td { + padding-top: 20px !important; +} + +.team-view .co-table-header-row:first-child td { + padding-top: 10px !important; +} \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index 9d10c888d..3d674254e 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; @@ -574,27 +525,6 @@ i.toggle-icon:hover { visibility: hidden; } -.robots-manager-element { - max-width: 800px; -} - -.robots-manager-element .alert { - margin-bottom: 20px; -} - -.robots-manager-element .robot a { - font-size: 16px; - cursor: pointer; -} - -.robots-manager-element .robot .prefix { - color: #aaa; -} - -.robots-manager-element .robot i { - margin-right: 10px; -} - .logs-view-element .header { padding-bottom: 10px; border-bottom: 1px solid #eee; @@ -2330,6 +2260,7 @@ p.editable:hover i { .copy-box-element input { border: 0px; padding-right: 32px; + cursor: pointer !important; } .copy-box-element .copy-container .copy-icon { @@ -3336,64 +3267,6 @@ p.editable:hover i { max-width: 100%; } -.billing-invoices-element .invoice-title { - padding: 6px; - cursor: pointer; -} - -.billing-invoices-element .invoice-status .success { - color: green; -} - -.billing-invoices-element .invoice-status .pending { - color: steelblue; -} - -.billing-invoices-element .invoice-status .danger { - color: red; -} - -.billing-invoices-element .invoice-amount:before { - content: '$'; -} - -.billing-invoices-element .invoice-details { - margin-left: 10px; - margin-bottom: 10px; - - padding: 4px; - padding-left: 6px; - border-left: 2px solid #eee !important; -} - -.billing-invoices-element .invoice-details td { - border: 0px solid transparent !important; -} - -.billing-invoices-element .invoice-details dl { - margin: 0px; -} - -.billing-invoices-element .invoice-details dd { - margin-left: 10px; - padding: 6px; - margin-bottom: 10px; -} - -.billing-invoices-element .invoice-title:hover { - color: steelblue; -} - -.prototype-manager-element thead th { - padding: 4px; - color: #666; -} - -.prototype-manager-element td { - padding: 10px !important; - vertical-align: middle !important; -} - .org-list h2 { margin-bottom: 20px; } @@ -5004,3 +4877,13 @@ i.rocket-icon { text-align: center; } +.manager-header { + margin-bottom: 10px; + padding-bottom: 10px; + border-bottom: 1px solid #eee; +} + +.manager-header h3 { + margin-bottom: 10px; +} + 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/application-manager.html b/static/directives/application-manager.html index ba7fb90ae..fa6eacb85 100644 --- a/static/directives/application-manager.html +++ b/static/directives/application-manager.html @@ -1,17 +1,24 @@
-
- -
-
- - Create New Application - +
+
+
+
+ + Create New Application + +
+

OAuth Applications

- +
+ The OAuth Applications panel allows organizations to define custom OAuth applications that can be used by internal or external customers to access data on behalf of the customers. More information about the API can be found by contacting support. +
+ +
- - + + 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/billing-invoices.html b/static/directives/billing-invoices.html index e022fef03..e4cf9cfe1 100644 --- a/static/directives/billing-invoices.html +++ b/static/directives/billing-invoices.html @@ -1,25 +1,23 @@
-
-
-
+
No invoices have been created
-
Application NameApplication URIApplication NameApplication URI
+
- - - - + + + + - - + + - - - - -
Billing Date/TimeAmount DueStatusBilling Date/TimeAmount DueStatus
{{ invoice.date * 1000 | date:'medium' }}{{ invoice.amount_due / 100 }}{{ invoice.date * 1000 | date:'medium' }}{{ invoice.amount_due / 100 }} Paid - Thank you! @@ -28,24 +26,12 @@ Payment pending +
-
-
Billing Period
-
- {{ invoice.period_start * 1000 | date:'mediumDate' }} - - {{ invoice.period_end * 1000 | date:'mediumDate' }} -
-
-
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 @@