Merge branch 'orgview'
This commit is contained in:
commit
1deb5121b8
125 changed files with 2077 additions and 643 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'
|
||||
|
||||
|
@ -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']
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
||||
teams = None
|
||||
if OrganizationMemberPermission(orgname).can():
|
||||
teams = model.get_teams_within_org(org)
|
||||
|
||||
return org_view(org, teams)
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
@nickname('changeOrganizationDetails')
|
||||
|
@ -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, [])
|
||||
}
|
||||
|
||||
|
|
|
@ -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/<repopath: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')
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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/<robot_shortname>')
|
||||
|
@ -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/<robot_shortname>/permissions')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@internal_only
|
||||
class UserRobotPermissions(ApiResource):
|
||||
""" Resource for listing the permissions a user's robot has in the system. """
|
||||
@require_user_admin
|
||||
@nickname('getUserRobotPermissions')
|
||||
def get(self, robot_shortname):
|
||||
""" Returns the list of repository permissions for the user's robot. """
|
||||
parent = get_authenticated_user()
|
||||
robot, password = model.get_robot(robot_shortname, parent)
|
||||
permissions = model.list_robot_permissions(robot.username)
|
||||
|
||||
return {
|
||||
'permissions': [permission_view(permission) for permission in permissions]
|
||||
}
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/robots/<robot_shortname>/permissions')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@related_user_resource(UserRobotPermissions)
|
||||
class OrgRobotPermissions(ApiResource):
|
||||
""" Resource for listing the permissions an org's robot has in the system. """
|
||||
@require_user_admin
|
||||
@nickname('getOrgRobotPermissions')
|
||||
def get(self, orgname, robot_shortname):
|
||||
""" Returns the list of repository permissions for the org's robot. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
parent = model.get_organization(orgname)
|
||||
robot, password = model.get_robot(robot_shortname, parent)
|
||||
permissions = model.list_robot_permissions(robot.username)
|
||||
|
||||
return {
|
||||
'permissions': [permission_view(permission) for permission in permissions]
|
||||
}
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@resource('/v1/user/robots/<robot_shortname>/regenerate')
|
||||
@path_param('robot_shortname', 'The short name for the robot, without any user or organization prefix')
|
||||
@internal_only
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
7
static/css/directives/ui/application-manager.css
Normal file
7
static/css/directives/ui/application-manager.css
Normal file
|
@ -0,0 +1,7 @@
|
|||
.application-manager-element .co-table {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.application-manager-element i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
30
static/css/directives/ui/avatar.css
Normal file
30
static/css/directives/ui/avatar.css
Normal file
|
@ -0,0 +1,30 @@
|
|||
.avatar-element {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
color: white !important;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-element.team {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.avatar-element img {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.avatar-element .letter {
|
||||
cursor: default !important;
|
||||
font-style: normal !important;
|
||||
font-weight: normal !important;
|
||||
font-variant: normal !important;
|
||||
}
|
||||
|
||||
a .avatar-element .letter {
|
||||
cursor: pointer !important;
|
||||
}
|
25
static/css/directives/ui/billing-invoices.css
Normal file
25
static/css/directives/ui/billing-invoices.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
|
||||
.billing-invoices-element .invoice-title {
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-status .success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-status .pending {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-status .danger {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.billing-invoices-element .invoice-amount:before {
|
||||
content: '$';
|
||||
}
|
||||
|
||||
.billing-invoices-element .fa-download {
|
||||
color: #aaa;
|
||||
}
|
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;
|
||||
}
|
8
static/css/directives/ui/prototype-manager.css
Normal file
8
static/css/directives/ui/prototype-manager.css
Normal file
|
@ -0,0 +1,8 @@
|
|||
.prototype-manager-element i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.prototype-manager-element td {
|
||||
padding: 10px !important;
|
||||
vertical-align: middle !important;
|
||||
}
|
|
@ -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;
|
||||
}
|
80
static/css/directives/ui/robots-manager.css
Normal file
80
static/css/directives/ui/robots-manager.css
Normal file
|
@ -0,0 +1,80 @@
|
|||
.robots-manager-element .robot a {
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot .prefix {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.robots-manager-element .robot i {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.robots-manager-element .popup-input-button i.fa {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.robots-manager-element .empty {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.robots-manager-element tr.open td {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-table-wrapper {
|
||||
margin-left: 0px;
|
||||
border-left: 2px solid #ccc;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-table tbody tr:last-child td {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-display-row {
|
||||
position: relative;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.robots-manager-element .permissions-display-row td:first-child {
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle {
|
||||
color: #999;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
background: #eee;
|
||||
padding: 4px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle .fa-hdd-o {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle.no-background .fa-hdd-o {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle .fa-lock {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.robots-manager-element .repo-circle.no-background .fa-lock {
|
||||
bottom: 5px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.robots-manager-element .member-perm-summary {
|
||||
margin-right: 14px;
|
||||
}
|
55
static/css/directives/ui/teams-manager.css
Normal file
55
static/css/directives/ui/teams-manager.css
Normal file
|
@ -0,0 +1,55 @@
|
|||
.teams-manager .popup-input-button {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.teams-manager .manager-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teams-manager .cor-options-menu {
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.teams-manager .header-col .info-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.teams-manager .header-col .header-text {
|
||||
text-transform: uppercase;
|
||||
font-size: 14px;
|
||||
color: #aaa !important;
|
||||
display: inline-block;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.teams-manager .control-col {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.teams-manager .team-listing {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.teams-manager .team-listing .avatar {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list .fa {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list {
|
||||
position: relative;
|
||||
min-height: 20px;
|
||||
padding: 4px;
|
||||
padding-left: 40px;
|
||||
}
|
||||
|
||||
.teams-manager .team-member-list .team-member-more {
|
||||
vertical-align: middle;
|
||||
padding-left: 6px;
|
||||
color: #aaa;
|
||||
font-size: 14px;
|
||||
}
|
25
static/css/pages/org-view.css
Normal file
25
static/css/pages/org-view.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
.org-view .organization-name {
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.org-view h3 {
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.org-view .section-description-header {
|
||||
padding-left: 40px;
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.org-view .section-description-header:before {
|
||||
font-family: FontAwesome;
|
||||
content: "\f05a";
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 6px;
|
||||
font-size: 27px;
|
||||
color: #888;
|
||||
}
|
51
static/css/pages/team-view.css
Normal file
51
static/css/pages/team-view.css
Normal file
|
@ -0,0 +1,51 @@
|
|||
.team-view .co-main-content-panel {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.team-view .team-name {
|
||||
vertical-align: middle;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header {
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header > h3 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover {
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover.bottom-right .arrow:after {
|
||||
border-bottom-color: #f7f7f7;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover-content {
|
||||
font-size: 14px;
|
||||
padding-top: 6px;
|
||||
min-width: 500px;
|
||||
}
|
||||
|
||||
.team-view .team-view-header .popover-content input {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.team-view .team-view-add-element .help-text {
|
||||
font-size: 13px;
|
||||
color: #ccc;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.team-view .co-table-header-row td {
|
||||
padding-top: 20px !important;
|
||||
}
|
||||
|
||||
.team-view .co-table-header-row:first-child td {
|
||||
padding-top: 10px !important;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
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,17 +1,24 @@
|
|||
<div class="application-manager-element">
|
||||
<div class="quay-spinner" ng-show="loading"></div>
|
||||
|
||||
<div class="cor-container" ng-show="!loading">
|
||||
<div class="cor-loader" ng-show="loading"></div>
|
||||
<div ng-show="!loading">
|
||||
<div class="manager-header">
|
||||
<div class="side-controls">
|
||||
<span class="popup-input-button" placeholder="'Application Name'" submitted="createApplication(value)">
|
||||
<span class="popup-input-button" placeholder="'Application Name'"
|
||||
submitted="createApplication(value)">
|
||||
<i class="fa fa-plus"></i> Create New Application
|
||||
</span>
|
||||
</div>
|
||||
<h3>OAuth Applications</h3>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<div class="manager-header section-description-header">
|
||||
The OAuth Applications panel allows organizations to define custom OAuth applications that can be used by internal or external customers to access <span class="registry-name"></span> data on behalf of the customers. More information about the <span class="registry-name"></span> API can be found by contacting support.
|
||||
</div>
|
||||
|
||||
<table class="co-table">
|
||||
<thead>
|
||||
<th>Application Name</th>
|
||||
<th>Application URI</th>
|
||||
<td>Application Name</td>
|
||||
<td>Application URI</td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="app in applications">
|
||||
|
|
|
@ -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>
|
|
@ -1,25 +1,23 @@
|
|||
<div class="billing-invoices-element">
|
||||
<div ng-show="loading">
|
||||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
<div class="cor-loader" ng-show="loading"></div>
|
||||
|
||||
<div ng-show="!loading && !invoices">
|
||||
No invoices have been created
|
||||
</div>
|
||||
|
||||
<div ng-show="!loading && invoices">
|
||||
<table class="table">
|
||||
<table class="co-table">
|
||||
<thead>
|
||||
<th>Billing Date/Time</th>
|
||||
<th>Amount Due</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
<td>Billing Date/Time</td>
|
||||
<td>Amount Due</td>
|
||||
<td>Status</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<tbody class="invoice" ng-repeat="invoice in invoices">
|
||||
<tr class="invoice-title">
|
||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
||||
<td><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
||||
<td><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
||||
<td>
|
||||
<span class="invoice-status">
|
||||
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
|
||||
|
@ -28,24 +26,12 @@
|
|||
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<td class="options-col">
|
||||
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
||||
<i class="fa fa-download" data-title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
|
||||
<td colspan="3">
|
||||
<dl class="dl-normal">
|
||||
<dt>Billing Period</dt>
|
||||
<dd>
|
||||
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
|
||||
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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)">
|
||||
<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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<button class="btn btn-success" data-trigger="click"
|
||||
<button class="btn btn-primary" data-trigger="click"
|
||||
data-content-template="/static/directives/popup-input-dialog.html"
|
||||
data-placement="bottom" ng-click="popupShown()" bs-popover>
|
||||
<span ng-transclude></span>
|
||||
|
|
|
@ -1,54 +1,67 @@
|
|||
<div class="prototype-manager-element">
|
||||
<div class="quay-spinner" ng-show="loading"></div>
|
||||
|
||||
<div class="cor-container" ng-show="!loading">
|
||||
<div class="alert alert-info">
|
||||
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository <strong>when it is created</strong>.
|
||||
</div>
|
||||
|
||||
<div class="cor-loader" ng-show="loading"></div>
|
||||
<div ng-show="!loading">
|
||||
<div class="manager-header">
|
||||
<div class="side-controls">
|
||||
<button class="btn btn-success" ng-click="showAddDialog()">
|
||||
<button class="btn btn-primary" ng-click="showAddDialog()">
|
||||
<i class="fa fa-plus"></i>
|
||||
New Default Permission
|
||||
Create Default Permission
|
||||
</button>
|
||||
</div>
|
||||
<h3>Default Permissions</h3>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<div class="manager-header section-description-header">
|
||||
The Default permissions panel defines permissions that should be granted automatically to a repository when it is created, in addition to the default of the repository's
|
||||
creator. Permissions are assigned based on the user who created the repository.
|
||||
<br><br>
|
||||
<strong>Note:</strong> Permissions added here do <strong>not</strong> automatically get added to
|
||||
existing repositories.
|
||||
</div>
|
||||
|
||||
<table class="co-table">
|
||||
<thead>
|
||||
<th>
|
||||
<td>
|
||||
<span class="context-tooltip"
|
||||
data-title="The user or robot that is creating a repository. If '(Organization Default)', then any repository created in this organization will be granted the permission."
|
||||
bs-tooltip="tooltip.title" data-container="body">
|
||||
Repository Creator
|
||||
data-container="body" bs-tooltip>
|
||||
Repository Created By
|
||||
</span>
|
||||
</th>
|
||||
<th>
|
||||
<span class="context-tooltip" data-title="The user, robot or team that is being granted the permission"
|
||||
bs-tooltip="tooltip.title" data-container="body">
|
||||
Applies To User/Robot/Team
|
||||
</td>
|
||||
<td>
|
||||
<span class="context-tooltip"
|
||||
data-title="The user, robot or team that is being granted the permission"
|
||||
data-container="body" bs-tooltip>
|
||||
Permission Applied To
|
||||
</span>
|
||||
</th>
|
||||
<th>Permission</th>
|
||||
<th style="width: 150px"></th>
|
||||
</td>
|
||||
<td>Permission</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="prototype in prototypes | orderBy:comparePrototypes">
|
||||
<td>
|
||||
<span class="entity-reference block-reference" entity="prototype.activating_user"
|
||||
namespace="organization.name" ng-show="prototype.activating_user"></span>
|
||||
namespace="organization.name" ng-if="prototype.activating_user"
|
||||
avatar-size="24"></span>
|
||||
|
||||
<span ng-show="!prototype.activating_user" style="font-variant: small-caps; font-weight: bold; font-size: 16px;">
|
||||
<span ng-show="!prototype.activating_user"
|
||||
style="font-variant: small-caps; font-weight: bold; font-size: 16px;">
|
||||
(Organization Default)
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="entity-reference block-reference" entity="prototype.delegate" namespace="organization.name"></span>
|
||||
<span class="entity-reference block-reference" entity="prototype.delegate" namespace="organization.name" avatar-size="24"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deletePrototype(prototype)"></span>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="deletePrototype(prototype)">
|
||||
<i class="fa fa-times"></i> Delete Permission
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -2,16 +2,18 @@
|
|||
<div class="new-repo-listing">
|
||||
|
||||
<!-- Titles -->
|
||||
<div ng-if="!hideTitle">
|
||||
<div ng-if="starred" class="repo-list-title">
|
||||
<i class="fa fa-star starred"></i>
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Repositories -->
|
||||
<div class="resource-view" resource="repositoriesResource">
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<!-- User/Team Permissions -->
|
||||
<div class="co-panel">
|
||||
<div class="co-panel-heading"><i class="fa fa-key"></i> User and Robot Permissions</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body" style="padding-top: 5px;">
|
||||
<div class="repository-permissions-table" repository="repository"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -12,7 +12,7 @@
|
|||
<!-- Access Tokens (DEPRECATED) -->
|
||||
<div class="co-panel" ng-show="hasTokens">
|
||||
<div class="co-panel-heading"><i class="fa fa-key"></i> Access Token Permissions</div>
|
||||
<div class="panel-body">
|
||||
<div class="panel-body" style="padding-top: 5px;">
|
||||
<div class="repository-tokens-table" repository="repository" has-tokens="hasTokens"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
error-message="'Could not load repository events'">
|
||||
|
||||
<div class="empty" ng-if="!notifications.length">
|
||||
<div class="empty-primary-msg">No notification have been setup for this repository.</div>
|
||||
<div class="empty-primary-msg">No notifications have been setup for this repository.</div>
|
||||
<div class="empty-secondary-msg" ng-if="repository.can_write">
|
||||
Click the "Create Notification" button above to add a new notification for a repository event.
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="resource-view"
|
||||
resources="[permissionResources.team, permissionResources.user]"
|
||||
error-message="'Could not load repository permissions'">
|
||||
<table class="co-table permissions">
|
||||
<table class="co-table no-lines permissions">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Account Name</td>
|
||||
|
@ -11,11 +11,27 @@
|
|||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr ng-show="!hasPermissions(permissionResources.team, permissionResources.user)">
|
||||
<td colspan="3">
|
||||
<div class="empty">
|
||||
<div class="empty-primary-msg">No permissions found.</div>
|
||||
<div class="empty-secondary-msg">
|
||||
To add a permission, enter the information below and click "Add Permission".
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Team Permissions -->
|
||||
<tr ng-repeat="(name, permission) in permissionResources.team.value">
|
||||
<tr class="co-table-header-row"
|
||||
ng-if="(permissionResources.team.value | objectFilter:allEntries).length">
|
||||
<td colspan="3"><i class="fa fa-group"></i> Team Permissions</td>
|
||||
</tr>
|
||||
<tr class="indented-row" ng-repeat="(name, permission) in permissionResources.team.value">
|
||||
<td class="team entity">
|
||||
<span class="entity-reference" namespace="repository.namespace"
|
||||
entity="buildEntityForPermission(name, permission, 'team')">
|
||||
entity="buildEntityForPermission(permission, 'team')"
|
||||
avatar-size="24">
|
||||
</span>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
|
@ -32,26 +48,66 @@
|
|||
</tr>
|
||||
|
||||
<!-- User Permissions -->
|
||||
<tr ng-repeat="(name, permission) in permissionResources.user.value">
|
||||
<td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}">
|
||||
<tr class="co-table-header-row"
|
||||
ng-if="(permissionResources.user.value | objectFilter:onlyUser).length">
|
||||
<td colspan="3"><i class="fa fa-user"></i> User Permissions</td>
|
||||
</tr>
|
||||
<tr class="indented-row"
|
||||
ng-repeat="permission in (permissionResources.user.value | objectFilter:onlyUser)">
|
||||
<td class="user-permission-entity">
|
||||
<span class="entity-reference" namespace="repository.namespace"
|
||||
entity="buildEntityForPermission(name, permission, 'user')">
|
||||
entity="buildEntityForPermission(permission, 'user')"
|
||||
avatar-size="24">
|
||||
</span>
|
||||
<i class="fa fa-exclamation-triangle outside-org"
|
||||
ng-if="permission.is_org_member === false"
|
||||
data-title="This user is not a member of the organization" bs-tooltip></i>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'user')" roles="roles"></span>
|
||||
<span class="role-group" current-role="permission.role"
|
||||
role-changed="setRole(role, permission.name, 'user')" roles="roles"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="deleteRole(name, 'user')">
|
||||
<span class="cor-option" option-click="deleteRole(permission.name, 'user')">
|
||||
<i class="fa fa-times"></i> Delete Permission
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Robot Permissions -->
|
||||
<tr class="co-table-header-row"
|
||||
ng-if="(permissionResources.user.value | objectFilter:onlyRobot).length">
|
||||
<td colspan="3"><i class="fa fa-wrench"></i> Robot Account Permissions</td>
|
||||
</tr>
|
||||
<tr class="indented-row"
|
||||
ng-repeat="permission in (permissionResources.user.value | objectFilter:onlyRobot)">
|
||||
<td class="user-permission-entity">
|
||||
<span class="entity-reference" namespace="repository.namespace"
|
||||
entity="buildEntityForPermission(permission, 'user')"
|
||||
avatar-size="24">
|
||||
</span>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
<div class="btn-group btn-group-sm">
|
||||
<span class="role-group" current-role="permission.role"
|
||||
role-changed="setRole(role, permission.name, 'user')" roles="roles"></span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="deleteRole(permission.name, 'user')">
|
||||
<i class="fa fa-times"></i> Delete Permission
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="add-row-spacer"><td colspan="3"></td></tr>
|
||||
|
||||
<tr class="add-row">
|
||||
<td id="add-entity-permission" class="admin-search">
|
||||
<span class="entity-search" namespace="repository.namespace"
|
||||
|
|
|
@ -1,32 +1,119 @@
|
|||
<div class="robots-manager-element">
|
||||
<div class="quay-spinner" ng-show="loading"></div>
|
||||
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
|
||||
<div class="cor-loader" ng-show="loading"></div>
|
||||
|
||||
<div ng-show="!loading">
|
||||
<div class="manager-header">
|
||||
<div class="side-controls">
|
||||
<span class="popup-input-button" pattern="ROBOT_PATTERN" placeholder="'Robot Account Name'"
|
||||
<span class="popup-input-button" pattern="ROBOT_PATTERN"
|
||||
placeholder="'Robot Account Name'"
|
||||
submitted="createRobot(value)">
|
||||
<i class="fa fa-wrench"></i> Create Robot Account
|
||||
<i class="fa fa-plus"></i> Create Robot Account
|
||||
</span>
|
||||
</div>
|
||||
<h3>Robot Accounts</h3>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<div class="manager-header section-description-header">
|
||||
Robot Accounts are named tokens that can be granted permissions on multiple repositories
|
||||
under this <span ng-if="organization">organization</span><span ng-if="!organization">user namespace</span>. They are typically used in environments where credentials will
|
||||
be shared, such as deployment systems.
|
||||
</div>
|
||||
|
||||
<div class="empty" ng-if="!robots.length">
|
||||
<div class="empty-primary-msg">No robot accounts defined.</div>
|
||||
<div class="empty-secondary-msg">
|
||||
Click the "Create Robot Account" button above to create a robot account.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="co-table" ng-if="robots.length">
|
||||
<thead>
|
||||
<th>Robot Account Name</th>
|
||||
<th style="width: 150px"></th>
|
||||
<td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()"></td>
|
||||
<td>Robot Account Name</td>
|
||||
<td ng-if="organization && Config.isNewLayout()">Teams</td>
|
||||
<td ng-if="Config.isNewLayout()">Repository Permissions</td>
|
||||
<td class="options-col"></td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="robotInfo in robots">
|
||||
<tbody ng-repeat="robotInfo in robots">
|
||||
<tr ng-class="robotInfo.showing_permissions ? 'open' : 'closed'">
|
||||
<td class="caret-col" ng-if="organization.is_admin && Config.isNewLayout()">
|
||||
<span ng-if="robotInfo.repositories.length > 0" ng-click="showPermissions(robotInfo)">
|
||||
<i class="fa"
|
||||
ng-class="robotInfo.showing_permissions ? 'fa-caret-down' : 'fa-caret-right'"
|
||||
data-title="View Permissions List" bs-tooltip></i>
|
||||
</span>
|
||||
</td>
|
||||
<td class="robot">
|
||||
<i class="fa fa-wrench"></i>
|
||||
<a ng-click="showRobot(robotInfo)">
|
||||
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }}
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span>
|
||||
<td ng-if="organization && Config.isNewLayout()">
|
||||
<span class="empty" ng-if="robotInfo.teams.length == 0">
|
||||
(Not a member of any team)
|
||||
</span>
|
||||
<span class="empty" ng-if="robotInfo.teams.length > 0">
|
||||
<span ng-repeat="team in robotInfo.teams"
|
||||
data-title="Team {{ team.name }}" bs-tooltip>
|
||||
<span class="anchor" is-text-only="!organization.admin" href="/organization/{{ organization.name }}/teams/{{ team.name }}">
|
||||
<span class="avatar" size="24" data="team.avatar"></span>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td ng-if="Config.isNewLayout()">
|
||||
<span class="empty" ng-if="robotInfo.repositories.length == 0">
|
||||
(No permissions on any repositories)
|
||||
</span>
|
||||
|
||||
<span class="member-perm-summary" ng-if="robotInfo.repositories.length > 0">
|
||||
Permissions on
|
||||
<span class="anchor" href="javascript:void(0)" is-text-only="!organization.is_admin" ng-click="showPermissions(robotInfo)">{{ robotInfo.repositories.length }}
|
||||
<span ng-if="robotInfo.repositories.length == 1">repository</span>
|
||||
<span ng-if="robotInfo.repositories.length > 1">repositories</span>
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
<td class="options-col">
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="showRobot(robotInfo)">
|
||||
<i class="fa fa-key"></i> View Credentials
|
||||
</span>
|
||||
<span class="cor-option" option-click="deleteRobot(robotInfo)">
|
||||
<i class="fa fa-times"></i> Delete Robot {{ robotInfo.name }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="robotInfo.showing_permissions">
|
||||
<td class="permissions-display-row" colspan="4">
|
||||
<span class="cor-loader" ng-if="robotInfo.loading_permissions"></span>
|
||||
<div class="permissions-table-wrapper">
|
||||
<table class="permissions-table" ng-if="!robotInfo.loading_permissions">
|
||||
<thead>
|
||||
<td>Repository</td>
|
||||
<td>Permission</td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="permission in robotInfo.permissions">
|
||||
<td>
|
||||
<span class="repo-icon repo-circle no-background" repo="permission.repository"></span>
|
||||
<a ng-href="/repository/{{ getPrefix(robotInfo.name) }}/{{ permission.repository.name }}?tab=settings">{{ getPrefix(robotInfo.name) }}/{{ permission.repository.name }}</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<span class="role-group" current-role="permission.role" roles="roles"
|
||||
read-only="true"></span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<div class="btn-group btn-group-sm">
|
||||
<button ng-repeat="role in roles"
|
||||
type="button" class="btn" ng-click="setRole(role.id)"
|
||||
ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'">{{ role.title }}</button>
|
||||
ng-class="(currentRole == role.id) ? ('active btn-' + role.kind) : 'btn-default'"
|
||||
ng-disabled="readOnly">{{ role.title }}</button>
|
||||
</div>
|
||||
|
|
62
static/directives/teams-manager.html
Normal file
62
static/directives/teams-manager.html
Normal file
|
@ -0,0 +1,62 @@
|
|||
<div class="teams-manager-element">
|
||||
<div class="manager-header">
|
||||
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
|
||||
submitted="createTeam(value)" ng-show="organization.is_admin">
|
||||
<i class="fa fa-plus" style="margin-right: 6px;"></i> Create New Team
|
||||
</span>
|
||||
<h3>Teams</h3>
|
||||
</div>
|
||||
|
||||
<div class="row hidden-xs">
|
||||
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
|
||||
<span class="header-text">Team Permissions</span>
|
||||
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" data-title=""
|
||||
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"
|
||||
data-html="true"
|
||||
data-trigger="hover"
|
||||
bs-popover></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="team-listing" ng-repeat="team in orderedTeams">
|
||||
<div id="team-{{team.name}}" class="row">
|
||||
<div class="col-sm-7 col-md-8">
|
||||
<div class="team-title">
|
||||
<span class="avatar" data="team.avatar" size="30"></span>
|
||||
<span ng-show="team.can_view">
|
||||
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
|
||||
</span>
|
||||
<span ng-show="!team.can_view">
|
||||
{{ team.name }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
|
||||
|
||||
<div class="team-member-list" ng-if="members[team.name]">
|
||||
<div class="cor-loader" ng-if="!members[team.name].members"></div>
|
||||
<span class="team-member" ng-repeat="member in members[team.name].members | limitTo: 25">
|
||||
<span data-title="{{ member.name }}" bs-tooltip>
|
||||
<span class="avatar" data="member.avatar" size="26" ng-if="!member.is_robot"></span>
|
||||
<i class="fa fa-wrench fa-lg" ng-if="member.is_robot"></i>
|
||||
</span>
|
||||
</span>
|
||||
<span class="team-member-more"
|
||||
ng-if="members[team.name].members.length > 25">+ {{ members[team.name].members.length - 25 }} more team members.</span>
|
||||
<span class="team-member-more"
|
||||
ng-if="!members[team.name].members.length">(Empty Team)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
|
||||
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
|
||||
|
||||
<span class="cor-options-menu">
|
||||
<span class="cor-option" option-click="askDeleteTeam(team.name)">
|
||||
<i class="fa fa-times"></i> Delete Team {{ team.name }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -333,11 +333,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
});
|
||||
|
||||
var activeTab = $location.search()['tab'];
|
||||
var checkTabs = function() {
|
||||
var tabs = $('a[data-toggle="tab"]');
|
||||
if (tabs.length == 0) {
|
||||
$timeout(checkTabs, 50);
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
||||
// is changed in the UI.
|
||||
$timeout(function() {
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
tabs.on('shown.bs.tab', function (e) {
|
||||
var tabName = e.target.getAttribute('data-target').substr(1);
|
||||
$rootScope.$apply(function() {
|
||||
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
|
||||
|
@ -357,7 +360,11 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
if (activeTab) {
|
||||
changeTab(activeTab);
|
||||
}
|
||||
}, 400); // 400ms to make sure angular has rendered.
|
||||
};
|
||||
|
||||
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
||||
// is changed in the UI.
|
||||
$timeout(checkTabs, 50);
|
||||
});
|
||||
|
||||
var initallyChecked = false;
|
||||
|
|
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;
|
||||
|
|
|
@ -15,11 +15,6 @@ angular.module('quay').directive('billingInvoices', function () {
|
|||
},
|
||||
controller: function($scope, $element, $sce, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.invoiceExpanded = {};
|
||||
|
||||
$scope.toggleInvoice = function(id) {
|
||||
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
|
@ -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) {
|
||||
|
|
|
@ -11,9 +11,9 @@ angular.module('quay').directive('repoListGrid', function () {
|
|||
scope: {
|
||||
repositoriesResource: '=repositoriesResource',
|
||||
starred: '=starred',
|
||||
user: "=user",
|
||||
namespace: '=namespace',
|
||||
starToggled: '&starToggled'
|
||||
starToggled: '&starToggled',
|
||||
hideTitle: '=hideTitle'
|
||||
},
|
||||
controller: function($scope, $element, UserService) {
|
||||
$scope.isOrganization = function(namespace) {
|
||||
|
|
|
@ -2,6 +2,21 @@
|
|||
* An element which displays a table of permissions on a repository and allows them to be
|
||||
* edited.
|
||||
*/
|
||||
angular.module('quay').filter('objectFilter', function() {
|
||||
return function(obj, filterFn) {
|
||||
if (!obj) { return []; }
|
||||
|
||||
var result = [];
|
||||
angular.forEach(obj, function(value) {
|
||||
if (filterFn(value)) {
|
||||
result.push(value);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
});
|
||||
|
||||
angular.module('quay').directive('repositoryPermissionsTable', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -13,6 +28,7 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
|
|||
'repository': '=repository'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, Restangular, UtilService) {
|
||||
// TODO(jschorr): move this to a service.
|
||||
$scope.roles = [
|
||||
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
|
||||
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
|
||||
|
@ -58,20 +74,49 @@ angular.module('quay').directive('repositoryPermissionsTable', function () {
|
|||
return Restangular.one(url);
|
||||
};
|
||||
|
||||
$scope.buildEntityForPermission = function(name, permission, kind) {
|
||||
var key = name + ':' + kind;
|
||||
$scope.buildEntityForPermission = function(permission, kind) {
|
||||
var key = permission.name + ':' + kind;
|
||||
if ($scope.permissionCache[key]) {
|
||||
return $scope.permissionCache[key];
|
||||
}
|
||||
|
||||
return $scope.permissionCache[key] = {
|
||||
'kind': kind,
|
||||
'name': name,
|
||||
'name': permission.name,
|
||||
'is_robot': permission.is_robot,
|
||||
'is_org_member': permission.is_org_member
|
||||
'is_org_member': permission.is_org_member,
|
||||
'avatar': permission.avatar
|
||||
};
|
||||
};
|
||||
|
||||
$scope.hasPermissions = function(teams, users) {
|
||||
if (teams && teams.value) {
|
||||
if (Object.keys(teams.value).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (users && users.value) {
|
||||
if (Object.keys(users.value).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.allEntries = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.onlyRobot = function(permission) {
|
||||
return permission.is_robot == true;
|
||||
};
|
||||
|
||||
$scope.onlyUser = function(permission) {
|
||||
return !permission.is_robot;
|
||||
};
|
||||
|
||||
$scope.addPermission = function() {
|
||||
$scope.addPermissionInfo['working'] = true;
|
||||
$scope.addNewPermission($scope.addPermissionInfo.entity, $scope.addPermissionInfo.role)
|
||||
|
|
|
@ -12,12 +12,38 @@ angular.module('quay').directive('robotsManager', function () {
|
|||
'organization': '=organization',
|
||||
'user': '=user'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, $routeParams, CreateService) {
|
||||
controller: function($scope, $element, ApiService, $routeParams, CreateService, Config) {
|
||||
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
|
||||
|
||||
// TODO(jschorr): move this to a service.
|
||||
$scope.roles = [
|
||||
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
|
||||
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
|
||||
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
|
||||
];
|
||||
|
||||
$scope.robots = null;
|
||||
$scope.loading = false;
|
||||
$scope.shownRobot = null;
|
||||
$scope.showRobotCounter = 0;
|
||||
$scope.Config = Config;
|
||||
|
||||
var loadRobotPermissions = function(info) {
|
||||
var shortName = $scope.getShortenedName(info.name);
|
||||
info.loading_permissions = true;
|
||||
ApiService.getRobotPermissions($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
|
||||
info.permissions = resp.permissions;
|
||||
info.loading_permissions = false;
|
||||
}, ApiService.errorDisplay('Could not load robot permissions'));
|
||||
};
|
||||
|
||||
$scope.showPermissions = function(robotInfo) {
|
||||
robotInfo.showing_permissions = !robotInfo.showing_permissions;
|
||||
|
||||
if (robotInfo.showing_permissions) {
|
||||
loadRobotPermissions(robotInfo);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.regenerateToken = function(username) {
|
||||
if (!username) { return; }
|
||||
|
|
|
@ -12,6 +12,7 @@ angular.module('quay').directive('roleGroup', function () {
|
|||
scope: {
|
||||
'roles': '=roles',
|
||||
'currentRole': '=currentRole',
|
||||
'readOnly': '=readOnly',
|
||||
'roleChanged': '&roleChanged'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
|
|
131
static/js/directives/ui/teams-manager.js
Normal file
131
static/js/directives/ui/teams-manager.js
Normal file
|
@ -0,0 +1,131 @@
|
|||
/**
|
||||
* Element for managing the teams of an organization.
|
||||
*/
|
||||
angular.module('quay').directive('teamsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/teams-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, CreateService) {
|
||||
$scope.TEAM_PATTERN = TEAM_PATTERN;
|
||||
$scope.teamRoles = [
|
||||
{ 'id': 'member', 'title': 'Member', 'kind': 'default' },
|
||||
{ 'id': 'creator', 'title': 'Creator', 'kind': 'success' },
|
||||
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
|
||||
];
|
||||
|
||||
$scope.members = {};
|
||||
$scope.orderedTeams = [];
|
||||
|
||||
var loadTeamMembers = function() {
|
||||
if (!$scope.organization) { return; }
|
||||
|
||||
for (var name in $scope.organization.teams) {
|
||||
if (!$scope.organization.teams.hasOwnProperty(name)) { continue; }
|
||||
loadMembersOfTeam(name);
|
||||
}
|
||||
};
|
||||
|
||||
var loadMembersOfTeam = function(name) {
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'teamname': name
|
||||
};
|
||||
|
||||
$scope.members[name] = {};
|
||||
|
||||
ApiService.getOrganizationTeamMembers(null, params).then(function(resp) {
|
||||
$scope.members[name].members = resp.members;
|
||||
}, function() {
|
||||
delete $scope.members[name];
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrderedTeams = function() {
|
||||
if (!$scope.organization) { return; }
|
||||
|
||||
$scope.orderedTeams = [];
|
||||
$scope.organization.ordered_teams.map(function(name) {
|
||||
$scope.orderedTeams.push($scope.organization.teams[name]);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', loadOrderedTeams);
|
||||
$scope.$watch('organization', loadTeamMembers);
|
||||
|
||||
$scope.setRole = function(role, teamname) {
|
||||
var previousRole = $scope.organization.teams[teamname].role;
|
||||
$scope.organization.teams[teamname].role = role;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
var data = $scope.organization.teams[teamname];
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot update team', function(resp) {
|
||||
$scope.organization.teams[teamname].role = previousRole;
|
||||
});
|
||||
|
||||
ApiService.updateOrganizationTeam(data, params).then(function(resp) {
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.createTeam = function(teamname) {
|
||||
if (!teamname) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.organization.teams[teamname]) {
|
||||
$('#team-' + teamname).removeClass('highlight');
|
||||
setTimeout(function() {
|
||||
$('#team-' + teamname).addClass('highlight');
|
||||
}, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
var orgname = $scope.organization.name;
|
||||
CreateService.createOrganizationTeam(ApiService, orgname, teamname, function(created) {
|
||||
$scope.organization.teams[teamname] = created;
|
||||
$scope.members[teamname] = {};
|
||||
$scope.members[teamname].members = [];
|
||||
$scope.organization.ordered_teams.push(teamname);
|
||||
$scope.orderedTeams.push(created);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.askDeleteTeam = function(teamname) {
|
||||
bootbox.confirm('Are you sure you want to delete team ' + teamname + '?', function(resp) {
|
||||
if (resp) {
|
||||
$scope.deleteTeam(teamname);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteTeam = function(teamname) {
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
||||
var index = $scope.organization.ordered_teams.indexOf(teamname);
|
||||
if (index >= 0) {
|
||||
$scope.organization.ordered_teams.splice(index, 1);
|
||||
}
|
||||
|
||||
loadOrderedTeams();
|
||||
delete $scope.organization.teams[teamname];
|
||||
}, ApiService.errorDisplay('Cannot delete team'));
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -14,6 +14,7 @@
|
|||
$scope.setEnabled = function(value) {
|
||||
$scope.isEnabled = value;
|
||||
CookieService.putPermanent('quay.exp-new-layout', value.toString());
|
||||
document.location.reload();
|
||||
};
|
||||
}
|
||||
}());
|
|
@ -1,6 +1,6 @@
|
|||
(function() {
|
||||
/**
|
||||
* Organization admin/settings page.
|
||||
* DEPRECATED: Organization admin/settings page.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('org-admin', 'org-admin.html', OrgAdminCtrl);
|
||||
|
|
|
@ -3,10 +3,97 @@
|
|||
* Page that displays details about an organization, such as its teams.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('org-view', 'org-view.html', OrgViewCtrl);
|
||||
pages.create('org-view', 'org-view.html', OrgViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Organization {{ organization.name }}',
|
||||
'description': 'Organization {{ organization.name }}'
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('org-view', 'old-org-view.html', OldOrgViewCtrl, {
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) {
|
||||
function OrgViewCtrl($scope, $routeParams, $timeout, ApiService, UIService, AvatarService) {
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
$scope.showLogsCounter = 0;
|
||||
$scope.showApplicationsCounter = 0;
|
||||
$scope.showInvoicesCounter = 0;
|
||||
|
||||
$scope.orgScope = {
|
||||
'changingOrganization': false,
|
||||
'organizationEmail': ''
|
||||
};
|
||||
|
||||
$scope.$watch('orgScope.organizationEmail', function(e) {
|
||||
UIService.hidePopover('#changeEmailForm input');
|
||||
});
|
||||
|
||||
var loadRepositories = function() {
|
||||
var options = {
|
||||
'public': false,
|
||||
'private': true,
|
||||
'sort': true,
|
||||
'namespace': orgname,
|
||||
};
|
||||
|
||||
$scope.repositoriesResource = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories;
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
$scope.orgScope.organizationEmail = org.email;
|
||||
$scope.isAdmin = org.is_admin;
|
||||
$scope.isMember = org.is_member;
|
||||
|
||||
// Load the repositories.
|
||||
$timeout(function() {
|
||||
loadRepositories();
|
||||
}, 10);
|
||||
});
|
||||
};
|
||||
|
||||
// Load the organization.
|
||||
loadOrganization();
|
||||
|
||||
$scope.showInvoices = function() {
|
||||
$scope.showInvoicesCounter++;
|
||||
};
|
||||
|
||||
$scope.showApplications = function() {
|
||||
$scope.showApplicationsCounter++;
|
||||
};
|
||||
|
||||
$scope.showLogs = function() {
|
||||
$scope.showLogsCounter++;
|
||||
};
|
||||
|
||||
$scope.changeEmail = function() {
|
||||
UIService.hidePopover('#changeEmailForm input');
|
||||
|
||||
$scope.orgScope.changingOrganization = true;
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
};
|
||||
|
||||
var data = {
|
||||
'email': $scope.orgScope.organizationEmail
|
||||
};
|
||||
|
||||
ApiService.changeOrganizationDetails(data, params).then(function(org) {
|
||||
$scope.orgScope.changingOrganization = false;
|
||||
$scope.organization = org;
|
||||
}, function(result) {
|
||||
$scope.orgScope.changingOrganization = false;
|
||||
UIService.showFormError('#changeEmailForm input', result, 'right');
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
function OldOrgViewCtrl($rootScope, $scope, ApiService, $routeParams, CreateService) {
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
$scope.TEAM_PATTERN = TEAM_PATTERN;
|
||||
|
|
|
@ -3,7 +3,14 @@
|
|||
* Page to view the members of a team and add/remove them.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('team-view', 'team-view.html', TeamViewCtrl);
|
||||
pages.create('team-view', 'team-view.html', TeamViewCtrl, {
|
||||
'newLayout': true,
|
||||
'title': 'Team {{ teamname }}',
|
||||
'description': 'Team {{ teamname }}'
|
||||
}, ['layout'])
|
||||
|
||||
pages.create('team-view', 'old-team-view.html', TeamViewCtrl, {
|
||||
}, ['old-layout']);
|
||||
}]);
|
||||
|
||||
function TeamViewCtrl($rootScope, $scope, $timeout, Features, Restangular, ApiService, $routeParams) {
|
||||
|
|
|
@ -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;
|
||||
}]);
|
|
@ -66,10 +66,10 @@ angular.module('quay').factory('UIService', [function() {
|
|||
}
|
||||
};
|
||||
|
||||
uiService.showPopover = function(elem, content) {
|
||||
uiService.showPopover = function(elem, content, opt_placement) {
|
||||
var popover = $(elem).data('bs.popover');
|
||||
if (!popover) {
|
||||
$(elem).popover({'content': '-', 'placement': 'left'});
|
||||
$(elem).popover({'content': '-', 'placement': opt_placement || 'left'});
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
|
@ -79,10 +79,10 @@ angular.module('quay').factory('UIService', [function() {
|
|||
}, 500);
|
||||
};
|
||||
|
||||
uiService.showFormError = function(elem, result) {
|
||||
uiService.showFormError = function(elem, result, opt_placement) {
|
||||
var message = result.data['message'] || result.data['error_description'] || '';
|
||||
if (message) {
|
||||
uiService.showPopover(elem, message);
|
||||
uiService.showPopover(elem, message, opt_placement);
|
||||
} else {
|
||||
uiService.hidePopover(elem);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
85
static/partials/old-org-view.html
Normal file
85
static/partials/old-org-view.html
Normal file
|
@ -0,0 +1,85 @@
|
|||
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'">
|
||||
<div class="org-view cor-container">
|
||||
<div class="organization-header" organization="organization">
|
||||
<div class="header-buttons" ng-show="organization.is_admin">
|
||||
|
||||
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
|
||||
submitted="createTeam(value)">
|
||||
<i class="fa fa-group"></i> Create Team
|
||||
</span>
|
||||
|
||||
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
|
||||
</div>
|
||||
</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">
|
||||
Team Permissions
|
||||
<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>"></i>
|
||||
</div>
|
||||
</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="team-description markdown-view" content="team.description" first-line-only="true"></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>
|
||||
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotChangeTeamModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot change team</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span ng-show="!roleError">You do not have permission to change properties on teams.</span>
|
||||
<span ng-show="roleError">{{ roleError }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmdeleteModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Delete Team?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you would like to delete this team? This <b>cannot be undone</b>.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteTeam()">Delete Team</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
128
static/partials/old-team-view.html
Normal file
128
static/partials/old-team-view.html
Normal file
|
@ -0,0 +1,128 @@
|
|||
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
|
||||
<div class="team-view cor-container">
|
||||
<div class="organization-header" organization="organization" team-name="teamname">
|
||||
<div ng-show="canEditMembers" class="side-controls">
|
||||
<div class="hidden-xs">
|
||||
<button class="btn btn-success"
|
||||
id="showAddMember"
|
||||
data-title="Add Team Member"
|
||||
data-content-template="/static/directives/team-view-add.html"
|
||||
data-placement="bottom-right"
|
||||
bs-popover="bs-popover">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add Team Member
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
|
||||
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
||||
content-changed="updateForDescription" field-title="'team description'"></div>
|
||||
|
||||
<div class="empty-message" ng-if="!members.length">
|
||||
This team has no members
|
||||
</div>
|
||||
|
||||
<div class="empty-message" ng-if="members.length && !(members | filter:search).length">
|
||||
No matching team members found
|
||||
</div>
|
||||
|
||||
<table class="member-listing" style="margin-top: -20px" ng-show="members.length">
|
||||
<!-- Members -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(false, false)).length">
|
||||
<td colspan="2"><div class="section-header">Team Members</div></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Robots -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(false, true)).length">
|
||||
<td colspan="2"><div class="section-header">Robot Accounts</div></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, true) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Invited -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(true, false)).length">
|
||||
<td colspan="2"><div class="section-header">Invited To Join</div></td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span ng-if="member.kind != 'invite'">
|
||||
<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" data="member.avatar"></span>
|
||||
{{ member.email }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Revoke invite to join team'" button-title="'Revoke'"
|
||||
perform-delete="revokeInvite(member)" ng-if="canEditMembers"></span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div ng-show="canEditMembers">
|
||||
<div ng-if-media="'(max-width: 767px)'">
|
||||
<div ng-include="'/static/directives/team-view-add.html'"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotChangeTeamModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot change team</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
You do not have permission to change properties of this team.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotChangeMembersModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot change members</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
You do not have permission to change the members of this team.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
|
@ -43,7 +43,6 @@
|
|||
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
|
||||
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()" data-trigger="manual"
|
||||
data-content="{{ changeEmailError }}" data-placement="bottom" ng-show="!updatingOrganization">
|
||||
<span class="avatar" size="24" email="organizationEmail" name="orgname"></span>
|
||||
<input type="email" class="form-control" ng-model="organizationEmail"
|
||||
style="margin-left: 10px; margin-right: 10px; width: 400px; display: inline-block;" required>
|
||||
<button class="btn btn-primary" type="submit" ng-disabled="changeEmailForm.$invalid || organizationEmail == organization.email">
|
||||
|
|
|
@ -1,85 +1,123 @@
|
|||
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'">
|
||||
<div class="org-view cor-container">
|
||||
<div class="organization-header" organization="organization">
|
||||
<div class="header-buttons" ng-show="organization.is_admin">
|
||||
|
||||
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
|
||||
submitted="createTeam(value)">
|
||||
<i class="fa fa-group"></i> Create Team
|
||||
</span>
|
||||
|
||||
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
|
||||
</div>
|
||||
</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">
|
||||
Team Permissions
|
||||
<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>"></i>
|
||||
</div>
|
||||
</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 }}
|
||||
<div class="resource-view org-view"
|
||||
resource="orgResource"
|
||||
error-message="'Organization not found'">
|
||||
<div class="page-content">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link"></span>
|
||||
<span class="cor-title-content">
|
||||
<span class="avatar" size="32" data="organization.avatar"></span>
|
||||
<span class="organization-name">{{ organization.name }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
|
||||
<div class="cor-tab-panel">
|
||||
<div class="cor-tabs" quay-show="isMember">
|
||||
<span class="cor-tab" tab-active="true" tab-title="Repositories" tab-target="#repos">
|
||||
<i class="fa fa-hdd-o"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Teams" tab-target="#teams">
|
||||
<i class="fa fa-users"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Robot Accounts" tab-target="#robots" ng-show="isAdmin">
|
||||
<i class="fa fa-wrench"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Default Permissions" tab-target="#default" ng-show="isAdmin">
|
||||
<i class="fa ci-stamp"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Billing" tab-target="#usage"
|
||||
quay-show="isAdmin && Features.BILLING">
|
||||
<i class="fa fa-credit-card"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Billing Invoices" tab-target="#invoices"
|
||||
tab-init="showInvoices()" quay-show="isAdmin && Features.BILLING">
|
||||
<i class="fa ci-invoice"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs"
|
||||
tab-init="showLogs()" ng-show="isAdmin">
|
||||
<i class="fa fa-bar-chart"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Applications" tab-target="#applications"
|
||||
tab-init="showApplications()" ng-show="isAdmin">
|
||||
<i class="fa ci-application"></i>
|
||||
</span>
|
||||
<span class="cor-tab" tab-title="Organization Settings" tab-target="#settings"
|
||||
ng-show="isAdmin">
|
||||
<i class="fa fa-gears"></i>
|
||||
</span>
|
||||
</div> <!-- /cor-tabs -->
|
||||
|
||||
<div class="cor-tab-content">
|
||||
<!-- Repositories -->
|
||||
<div id="repos" class="tab-pane active">
|
||||
<h3>Repositories</h3>
|
||||
<div class="repo-list-grid"
|
||||
repositories-resource="repositoriesResource"
|
||||
starred="false"
|
||||
namespace="namespace"
|
||||
hide-title="true">
|
||||
</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>
|
||||
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
|
||||
<!-- Teams -->
|
||||
<div id="teams" class="tab-pane">
|
||||
<div class="teams-manager" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Robot Accounts -->
|
||||
<div id="robots" class="tab-pane">
|
||||
<div class="robots-manager" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Default Permissions -->
|
||||
<div id="default" class="tab-pane">
|
||||
<div class="prototype-manager" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Usage Logs -->
|
||||
<div id="logs" class="tab-pane">
|
||||
<div class="logs-view" organization="organization" makevisible="showLogsCounter"></div>
|
||||
</div>
|
||||
|
||||
<!-- Applications -->
|
||||
<div id="applications" class="tab-pane">
|
||||
<div class="application-manager" organization="organization"
|
||||
makevisible="showApplicationsCounter"></div>
|
||||
</div>
|
||||
|
||||
<!-- Plan and Usage -->
|
||||
<div id="usage" class="tab-pane" quay-require="['BILLING']">
|
||||
<h3>Plan Usage and Billing</h3>
|
||||
<div class="plan-manager" organization="organization.name"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Invoices -->
|
||||
<div id="invoices" class="tab-pane" quay-require="['BILLING']">
|
||||
<h3>Billing Invoices</h3>
|
||||
<div class="billing-invoices" organization="organization"
|
||||
makevisible="showInvoicesCounter"></div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div id="settings" class="tab-pane">
|
||||
<h3>Organization Settings</h3>
|
||||
|
||||
|
||||
<div class="panel" ng-show="!orgScope.changingOrganization">
|
||||
<div class="panel-title">Organization's e-mail address</div>
|
||||
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
|
||||
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()">
|
||||
<input type="email" class="form-control" style="max-width: 500px;"
|
||||
ng-model="orgScope.organizationEmail" required>
|
||||
<button class="btn btn-primary" type="submit"
|
||||
ng-disabled="changeEmailForm.$invalid || orgScope.organizationEmail == organization.email">
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div> <!-- /cor-tab-content -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="cannotChangeTeamModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Cannot change team</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span ng-show="!roleError">You do not have permission to change properties on teams.</span>
|
||||
<span ng-show="roleError">{{ roleError }}</span>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="confirmdeleteModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Delete Team?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you sure you would like to delete this team? This <b>cannot be undone</b>.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteTeam()">Delete Team</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -37,12 +37,12 @@
|
|||
|
||||
<!-- Admin Only Tabs -->
|
||||
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()"
|
||||
ng-if="viewScope.repository.can_admin">
|
||||
ng-show="viewScope.repository.can_admin">
|
||||
<i class="fa fa-bar-chart"></i>
|
||||
</span>
|
||||
|
||||
<span class="cor-tab" tab-title="Settings" tab-target="#settings"
|
||||
ng-if="viewScope.repository.can_admin">
|
||||
ng-show="viewScope.repository.can_admin">
|
||||
<i class="fa fa-gear"></i>
|
||||
</span>
|
||||
</div> <!-- /cor-tabs -->
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
|
||||
<div class="team-view cor-container">
|
||||
<div class="organization-header" organization="organization" team-name="teamname">
|
||||
<div class="resource-view team-view" resources="[orgResource, membersResource]"
|
||||
error-message="'No matching organization or team found'">
|
||||
<div class="page-content">
|
||||
<div class="cor-title">
|
||||
<span class="cor-title-link">
|
||||
<a class="back-link" href="/organization/{{ organization.name }}?tab=teams">
|
||||
<span class="avatar" size="24" data="organization.avatar" style="margin-right: 4px"></span>
|
||||
{{ organization.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="cor-title-content">
|
||||
<span class="avatar" data="team.avatar" size="32"></span>
|
||||
<span class="team-name">{{ teamname }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="co-main-content-panel">
|
||||
<div class="team-view-header">
|
||||
<div ng-show="canEditMembers" class="side-controls">
|
||||
<div class="hidden-xs">
|
||||
<button class="btn btn-success"
|
||||
<button class="btn btn-primary"
|
||||
id="showAddMember"
|
||||
data-title="Add Team Member"
|
||||
data-content-template="/static/directives/team-view-add.html"
|
||||
|
@ -14,69 +29,85 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="description markdown-input" content="team.description"
|
||||
can-write="organization.is_admin"
|
||||
content-changed="updateForDescription"
|
||||
field-title="'team description'"></div>
|
||||
</div>
|
||||
|
||||
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
|
||||
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
||||
content-changed="updateForDescription" field-title="'team description'"></div>
|
||||
|
||||
<div class="empty-message" ng-if="!members.length">
|
||||
This team has no members
|
||||
<div class="empty" ng-if="!members.length">
|
||||
<div class="empty-primary-msg">This team has no members.</div>
|
||||
<div class="empty-secondary-msg">
|
||||
Click the "Add Team Member" button above to add or invite team members.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-message" ng-if="members.length && !(members | filter:search).length">
|
||||
No matching team members found
|
||||
</div>
|
||||
|
||||
<table class="member-listing" style="margin-top: -20px" ng-show="members.length">
|
||||
<!-- Members -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(false, false)).length">
|
||||
<td colspan="2"><div class="section-header">Team Members</div></td>
|
||||
<table class="co-table no-lines" ng-if="members.length">
|
||||
<!-- Team Members -->
|
||||
<tr class="co-table-header-row"
|
||||
ng-if="(members | filter: filterFunction(false, false)).length">
|
||||
<td colspan="3"><i class="fa fa-user"></i> Team Members</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||
<tr class="indented-row"
|
||||
ng-repeat="member in members | filter: filterFunction(false, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||
<span class="entity-reference" entity="member" namespace="organization.name"
|
||||
show-avatar="true" avatar-size="24"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
<span class="cor-options-menu" ng-if="canEditMembers">
|
||||
<span class="cor-option" option-click="removeMember(member.name)">
|
||||
<i class="fa fa-times"></i> Remove {{ member.name }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Robots -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(false, true)).length">
|
||||
<td colspan="2"><div class="section-header">Robot Accounts</div></td>
|
||||
<!-- Robot Accounts -->
|
||||
<tr class="co-table-header-row"
|
||||
ng-if="(members | filter: filterFunction(false, true)).length">
|
||||
<td colspan="3"><i class="fa fa-wrench"></i> Robot Accounts</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(false, true) | orderBy: 'name'">
|
||||
<tr class="indented-row"
|
||||
ng-repeat="member in members | filter: filterFunction(false, true) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Remove ' + member.name + ' from team'" button-title="'Remove'"
|
||||
perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
|
||||
<span class="cor-options-menu" ng-if="canEditMembers">
|
||||
<span class="cor-option" option-click="removeMember(member.name)">
|
||||
<i class="fa fa-times"></i> Remove {{ member.name }}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- Invited -->
|
||||
<tr ng-if="(members | filter:search | filter: filterFunction(true, false)).length">
|
||||
<td colspan="2"><div class="section-header">Invited To Join</div></td>
|
||||
<!-- Invitations -->
|
||||
<tr class="co-table-header-row"
|
||||
ng-if="(members | filter: filterFunction(true, false)).length">
|
||||
<td colspan="3"><i class="fa ci-invite"></i> Invited to Join</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-repeat="member in members | filter:search | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||
<tr class="indented-row"
|
||||
ng-repeat="member in members | filter: filterFunction(true, false) | orderBy: 'name'">
|
||||
<td class="user entity">
|
||||
<span ng-if="member.kind != 'invite'">
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="32"></span>
|
||||
<span class="entity-reference" entity="member" namespace="organization.name" show-avatar="true" avatar-size="24"></span>
|
||||
</span>
|
||||
<span class="invite-listing" ng-if="member.kind == 'invite'">
|
||||
<span class="avatar" size="32" hash="member.avatar"></span>
|
||||
<span class="avatar" size="24" data="member.avatar" style="margin-right: 6px;"></span>
|
||||
{{ member.email }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" delete-title="'Revoke invite to join team'" button-title="'Revoke'"
|
||||
perform-delete="revokeInvite(member)" ng-if="canEditMembers"></span>
|
||||
<span class="cor-options-menu" ng-if="canEditMembers">
|
||||
<span class="cor-option" option-click="revokeInvite(member)">
|
||||
<i class="fa fa-times"></i> Revoke invite
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
@ -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 }}
|
||||
|
@ -317,7 +317,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>
|
||||
|
|
|
@ -17,7 +17,8 @@ from endpoints.api.image import RepositoryImageChanges, RepositoryImage, Reposit
|
|||
from endpoints.api.build import (FileDropResource, RepositoryBuildStatus, RepositoryBuildLogs,
|
||||
RepositoryBuildList, RepositoryBuildResource)
|
||||
from endpoints.api.robot import (UserRobotList, OrgRobot, OrgRobotList, UserRobot,
|
||||
RegenerateOrgRobot, RegenerateUserRobot)
|
||||
RegenerateOrgRobot, RegenerateUserRobot, UserRobotPermissions,
|
||||
OrgRobotPermissions)
|
||||
|
||||
from endpoints.api.trigger import (BuildTriggerActivate, BuildTriggerSources, BuildTriggerSubdirs,
|
||||
TriggerBuildList, ActivateBuildTrigger, BuildTrigger,
|
||||
|
@ -3356,10 +3357,28 @@ class TestRegenerateOrgRobot(ApiTestCase):
|
|||
self._run_test('POST', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestOrganizationBuynlarge(ApiTestCase):
|
||||
class TestUserRobotPermissions(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(Organization, orgname="buynlarge")
|
||||
self._set_url(UserRobotPermissions, robot_shortname="robotname")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 400, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 400, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestOrgRobotPermissions(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(OrgRobotPermissions, orgname="buynlarge", robot_shortname="robotname")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 401, None, None)
|
||||
|
@ -3367,6 +3386,24 @@ class TestOrganizationBuynlarge(ApiTestCase):
|
|||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 403, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 403, 'reader', None)
|
||||
|
||||
def test_get_devtable(self):
|
||||
self._run_test('GET', 400, 'devtable', None)
|
||||
|
||||
|
||||
class TestOrganizationBuynlarge(ApiTestCase):
|
||||
def setUp(self):
|
||||
ApiTestCase.setUp(self)
|
||||
self._set_url(Organization, orgname="buynlarge")
|
||||
|
||||
def test_get_anonymous(self):
|
||||
self._run_test('GET', 200, None, None)
|
||||
|
||||
def test_get_freshuser(self):
|
||||
self._run_test('GET', 200, 'freshuser', None)
|
||||
|
||||
def test_get_reader(self):
|
||||
self._run_test('GET', 200, 'reader', None)
|
||||
|
||||
|
|
|
@ -552,12 +552,12 @@ class TestGetOrganization(ApiTestCase):
|
|||
def test_unknownorg(self):
|
||||
self.login(ADMIN_ACCESS_USER)
|
||||
self.getResponse(Organization, params=dict(orgname='notvalid'),
|
||||
expected_code=403)
|
||||
expected_code=404)
|
||||
|
||||
def test_cannotaccess(self):
|
||||
self.login(NO_ACCESS_USER)
|
||||
self.getResponse(Organization, params=dict(orgname=ORGANIZATION),
|
||||
expected_code=403)
|
||||
expected_code=200)
|
||||
|
||||
def test_getorganization(self):
|
||||
self.login(READ_ACCESS_USER)
|
||||
|
|
|
@ -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