Merge remote-tracking branch 'origin/master' into swaggerlikeus

Conflicts:
	data/database.py
	endpoints/api.py
	endpoints/common.py
	templates/base.html
	test/data/test.db
	test/specs.py
This commit is contained in:
jakedt 2014-03-19 15:39:44 -04:00
commit c93c62600d
59 changed files with 4636 additions and 216 deletions

View file

@ -299,8 +299,21 @@ class OAuthAccessToken(BaseModel):
data = CharField() # What the hell is this field for? data = CharField() # What the hell is this field for?
class NotificationKind(BaseModel):
name = CharField(index=True)
class Notification(BaseModel):
uuid = CharField(default=uuid_generator, index=True)
kind = ForeignKeyField(NotificationKind, index=True)
target = ForeignKeyField(User, index=True)
metadata_json = TextField(default='{}')
created = DateTimeField(default=datetime.now, index=True)
all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility, all_models = [User, Repository, Image, AccessToken, Role, RepositoryPermission, Visibility,
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem, RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
RepositoryBuild, Team, TeamMember, TeamRole, Webhook, LogEntryKind, LogEntry, RepositoryBuild, Team, TeamMember, TeamRole, Webhook, LogEntryKind, LogEntry,
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger, PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken] OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
Notification]

View file

@ -58,7 +58,7 @@ class InvalidBuildTriggerException(DataModelException):
pass pass
def create_user(username, password, email): def create_user(username, password, email, is_organization=False):
if not validate_email(email): if not validate_email(email):
raise InvalidEmailAddressException('Invalid email address: %s' % email) raise InvalidEmailAddressException('Invalid email address: %s' % email)
if not validate_username(username): if not validate_username(username):
@ -92,6 +92,12 @@ def create_user(username, password, email):
new_user = User.create(username=username, password_hash=pw_hash, new_user = User.create(username=username, password_hash=pw_hash,
email=email) email=email)
# If the password is None, then add a notification for the user to change
# their password ASAP.
if not pw_hash and not is_organization:
create_notification('password_required', new_user)
return new_user return new_user
except Exception as ex: except Exception as ex:
raise DataModelException(ex.message) raise DataModelException(ex.message)
@ -100,7 +106,7 @@ def create_user(username, password, email):
def create_organization(name, email, creating_user): def create_organization(name, email, creating_user):
try: try:
# Create the org # Create the org
new_org = create_user(name, None, email) new_org = create_user(name, None, email, is_organization=True)
new_org.organization = True new_org.organization = True
new_org.save() new_org.save()
@ -661,6 +667,9 @@ def change_password(user, new_password):
user.password_hash = pw_hash user.password_hash = pw_hash
user.save() user.save()
# Remove any password required notifications for the user.
delete_notifications_by_kind(user, 'password_required')
def change_invoice_email(user, invoice_email): def change_invoice_email(user, invoice_email):
user.invoice_email = invoice_email user.invoice_email = invoice_email
@ -1537,3 +1546,46 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
limit): limit):
return (list_repository_builds(namespace_name, repository_name, limit) return (list_repository_builds(namespace_name, repository_name, limit)
.where(RepositoryBuildTrigger.uuid == trigger_uuid)) .where(RepositoryBuildTrigger.uuid == trigger_uuid))
def create_notification(kind, target, metadata={}):
kind_ref = NotificationKind.get(name=kind)
notification = Notification.create(kind=kind_ref, target=target,
metadata_json=json.dumps(metadata))
return notification
def list_notifications(user, kind=None):
Org = User.alias()
AdminTeam = Team.alias()
AdminTeamMember = TeamMember.alias()
AdminUser = User.alias()
query = (Notification.select()
.join(User)
.switch(Notification)
.join(Org, JOIN_LEFT_OUTER, on=(Org.id == Notification.target))
.join(AdminTeam, JOIN_LEFT_OUTER, on=(Org.id ==
AdminTeam.organization))
.join(TeamRole, JOIN_LEFT_OUTER, on=(AdminTeam.role == TeamRole.id))
.switch(AdminTeam)
.join(AdminTeamMember, JOIN_LEFT_OUTER, on=(AdminTeam.id ==
AdminTeamMember.team))
.join(AdminUser, JOIN_LEFT_OUTER, on=(AdminTeamMember.user ==
AdminUser.id)))
where_clause = ((Notification.target == user) |
((AdminUser.id == user) &
(TeamRole.name == 'admin')))
if kind:
where_clause = where_clause & (NotificationKind.name == kind)
return query.where(where_clause).order_by(Notification.created).desc()
def delete_notifications_by_kind(target, kind):
kind_ref = NotificationKind.get(name=kind)
Notification.delete().where(Notification.target == target,
Notification.kind == kind_ref).execute()

View file

@ -18,9 +18,23 @@ user_files = app.config['USERFILES']
build_logs = app.config['BUILDLOGS'] build_logs = app.config['BUILDLOGS']
def get_trigger_config(trigger):
try:
return json.loads(trigger.config)
except:
return {}
def get_job_config(build_obj):
try:
return json.loads(build_obj.job_config)
except:
return None
def trigger_view(trigger): def trigger_view(trigger):
if trigger and trigger.uuid: if trigger and trigger.uuid:
config_dict = json.loads(trigger.config) config_dict = get_trigger_config(trigger)
build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name) build_trigger = BuildTrigger.get_trigger_for_service(trigger.service.name)
return { return {
'service': trigger.service.name, 'service': trigger.service.name,
@ -42,7 +56,7 @@ def build_status_view(build_obj, can_write=False):
'started': format_date(build_obj.started), 'started': format_date(build_obj.started),
'display_name': build_obj.display_name, 'display_name': build_obj.display_name,
'status': status, 'status': status,
'job_config': json.loads(build_obj.job_config) if can_write else None, 'job_config': get_job_config(build_obj) if can_write else None,
'is_writer': can_write, 'is_writer': can_write,
'trigger': trigger_view(build_obj.trigger), 'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key, 'resource_key': build_obj.resource_key,
@ -54,7 +68,7 @@ def build_status_view(build_obj, can_write=False):
return resp return resp
@resource('/v1/repository/<path:repository>/build/') @resource('/v1/repository/<repopath:repository>/build/')
class RepositoryBuildList(RepositoryParamResource): class RepositoryBuildList(RepositoryParamResource):
""" Resource related to creating and listing repository builds. """ """ Resource related to creating and listing repository builds. """
schemas = { schemas = {
@ -127,7 +141,7 @@ class RepositoryBuildList(RepositoryParamResource):
return resp, 201, headers return resp, 201, headers
@resource('/v1/repository/<path:repository>/build/<build_uuid>/status') @resource('/v1/repository/<repopath:repository>/build/<build_uuid>/status')
class RepositoryBuildStatus(RepositoryParamResource): class RepositoryBuildStatus(RepositoryParamResource):
""" Resource for dealing with repository build status. """ """ Resource for dealing with repository build status. """
@require_repo_read @require_repo_read
@ -142,7 +156,7 @@ class RepositoryBuildStatus(RepositoryParamResource):
return build_status_view(build, can_write) return build_status_view(build, can_write)
@resource('/v1/repository/<path:repository>/build/<build_uuid>/logs') @resource('/v1/repository/<repopath:repository>/build/<build_uuid>/logs')
class RepositoryBuildLogs(RepositoryParamResource): class RepositoryBuildLogs(RepositoryParamResource):
""" Resource for loading repository build logs. """ """ Resource for loading repository build logs. """
@require_repo_write @require_repo_write

View file

@ -29,7 +29,7 @@ def image_view(image):
} }
@resource('/v1/repository/<path:repository>/image/') @resource('/v1/repository/<repopath:repository>/image/')
class RepositoryImageList(RepositoryParamResource): class RepositoryImageList(RepositoryParamResource):
""" Resource for listing repository images. """ """ Resource for listing repository images. """
@require_repo_read @require_repo_read
@ -54,7 +54,7 @@ class RepositoryImageList(RepositoryParamResource):
} }
@resource('/v1/repository/<path:repository>/image/<image_id>') @resource('/v1/repository/<repopath:repository>/image/<image_id>')
class RepositoryImage(RepositoryParamResource): class RepositoryImage(RepositoryParamResource):
""" Resource for handling repository images. """ """ Resource for handling repository images. """
@require_repo_read @require_repo_read
@ -68,7 +68,7 @@ class RepositoryImage(RepositoryParamResource):
return image_view(image) return image_view(image)
@resource('/v1/repository/<path:repository>/image/<image_id>/changes') @resource('/v1/repository/<repopath:repository>/image/<image_id>/changes')
class RepositoryImageChanges(RepositoryParamResource): class RepositoryImageChanges(RepositoryParamResource):
""" Resource for handling repository image change lists. """ """ Resource for handling repository image change lists. """

View file

@ -63,7 +63,7 @@ def get_logs(namespace, start_time, end_time, performer_name=None,
} }
@resource('/v1/repository/<path:repository>/logs') @resource('/v1/repository/<repopath:repository>/logs')
@internal_only @internal_only
class RepositoryLogs(RepositoryParamResource): class RepositoryLogs(RepositoryParamResource):
""" Resource for fetching logs for the specific repository. """ """ Resource for fetching logs for the specific repository. """

View file

@ -25,7 +25,7 @@ def wrap_role_view_org(role_json, user, org_members):
return role_json return role_json
@resource('/v1/repository/<path:repository>/permissions/team/') @resource('/v1/repository/<repopath:repository>/permissions/team/')
class RepositoryTeamPermissionList(RepositoryParamResource): class RepositoryTeamPermissionList(RepositoryParamResource):
""" Resource for repository team permissions. """ """ Resource for repository team permissions. """
@require_repo_admin @require_repo_admin
@ -40,7 +40,7 @@ class RepositoryTeamPermissionList(RepositoryParamResource):
} }
@resource('/v1/repository/<path:repository>/permissions/user/') @resource('/v1/repository/<repopath:repository>/permissions/user/')
class RepositoryUserPermissionList(RepositoryParamResource): class RepositoryUserPermissionList(RepositoryParamResource):
""" Resource for repository user permissions. """ """ Resource for repository user permissions. """
@require_repo_admin @require_repo_admin
@ -79,7 +79,7 @@ class RepositoryUserPermissionList(RepositoryParamResource):
} }
@resource('/v1/repository/<path:repository>/permissions/user/<username>') @resource('/v1/repository/<repopath:repository>/permissions/user/<username>')
class RepositoryUserPermission(RepositoryParamResource): class RepositoryUserPermission(RepositoryParamResource):
""" Resource for managing individual user permissions. """ """ Resource for managing individual user permissions. """
schemas = { schemas = {
@ -174,7 +174,7 @@ class RepositoryUserPermission(RepositoryParamResource):
return 'Deleted', 204 return 'Deleted', 204
@resource('/v1/repository/<path:repository>/permissions/team/<teamname>') @resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
class RepositoryTeamPermission(RepositoryParamResource): class RepositoryTeamPermission(RepositoryParamResource):
""" Resource for managing individual team permissions. """ """ Resource for managing individual team permissions. """
schemas = { schemas = {

View file

@ -152,7 +152,7 @@ def image_view(image):
'size': extended_props.image_size, 'size': extended_props.image_size,
} }
@resource('/v1/repository/<path:repository>') @resource('/v1/repository/<repopath:repository>')
class Repository(RepositoryParamResource): class Repository(RepositoryParamResource):
"""Operations for managing a specific repository.""" """Operations for managing a specific repository."""
schemas = { schemas = {
@ -248,7 +248,7 @@ class Repository(RepositoryParamResource):
return 'Deleted', 204 return 'Deleted', 204
@resource('/v1/repository/<path:repository>/changevisibility') @resource('/v1/repository/<repopath:repository>/changevisibility')
class RepositoryVisibility(RepositoryParamResource): class RepositoryVisibility(RepositoryParamResource):
""" Custom verb for changing the visibility of the repository. """ """ Custom verb for changing the visibility of the repository. """
schemas = { schemas = {

View file

@ -18,7 +18,7 @@ def token_view(token_obj):
} }
@resource('/v1/repository/<path:repository>/tokens/') @resource('/v1/repository/<repopath:repository>/tokens/')
class RepositoryTokenList(RepositoryParamResource): class RepositoryTokenList(RepositoryParamResource):
""" Resource for creating and listing repository tokens. """ """ Resource for creating and listing repository tokens. """
schemas = { schemas = {
@ -65,7 +65,7 @@ class RepositoryTokenList(RepositoryParamResource):
return token_view(token), 201 return token_view(token), 201
@resource('/v1/repository/<path:repository>/tokens/<code>') @resource('/v1/repository/<repopath:repository>/tokens/<code>')
class RepositoryToken(RepositoryParamResource): class RepositoryToken(RepositoryParamResource):
""" Resource for managing individual tokens. """ """ Resource for managing individual tokens. """
schemas = { schemas = {

View file

@ -2,6 +2,7 @@ import logging
import stripe import stripe
from endpoints.api import request_error, log_action, NotFound from endpoints.api import request_error, log_action, NotFound
from endpoints.common import check_repository_usage
from data import model from data import model
from data.plans import PLANS from data.plans import PLANS
@ -58,6 +59,7 @@ def subscribe(user, plan, token, require_business_plan):
cus = stripe.Customer.create(email=user.email, plan=plan, card=card) cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
user.stripe_id = cus.id user.stripe_id = cus.id
user.save() user.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan}) log_action('account_change_plan', user.username, {'plan': plan})
except stripe.CardError as e: except stripe.CardError as e:
return carderror_response(e) return carderror_response(e)
@ -74,6 +76,7 @@ def subscribe(user, plan, token, require_business_plan):
# We only have to cancel the subscription if they actually have one # We only have to cancel the subscription if they actually have one
cus.cancel_subscription() cus.cancel_subscription()
cus.save() cus.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan}) log_action('account_change_plan', user.username, {'plan': plan})
else: else:
@ -89,6 +92,7 @@ def subscribe(user, plan, token, require_business_plan):
return carderror_response(e) return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos) response_json = subscription_view(cus.subscription, private_repos)
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan}) log_action('account_change_plan', user.username, {'plan': plan})
return response_json, status_code return response_json, status_code

View file

@ -5,7 +5,7 @@ from data import model
from auth.auth_context import get_authenticated_user from auth.auth_context import get_authenticated_user
@resource('/v1/repository/<path:repository>/tag/<tag>') @resource('/v1/repository/<repopath:repository>/tag/<tag>')
class RepositoryTag(RepositoryParamResource): class RepositoryTag(RepositoryParamResource):
""" Resource for managing repository tags. """ """ Resource for managing repository tags. """
@ -24,7 +24,7 @@ class RepositoryTag(RepositoryParamResource):
return 'Deleted', 204 return 'Deleted', 204
@resource('/v1/repository/<path:repository>/tag/<tag>/images') @resource('/v1/repository/<repopath:repository>/tag/<tag>/images')
class RepositoryTagImages(RepositoryParamResource): class RepositoryTagImages(RepositoryParamResource):
""" Resource for listing the images in a specific repository tag. """ """ Resource for listing the images in a specific repository tag. """
@require_repo_read @require_repo_read

View file

@ -9,7 +9,8 @@ from app import app
from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin,
log_action, request_error, query_param, parse_args, internal_only, log_action, request_error, query_param, parse_args, internal_only,
validate_json_request, api, Unauthorized, NotFound, InvalidRequest) validate_json_request, api, Unauthorized, NotFound, InvalidRequest)
from endpoints.api.build import build_status_view, trigger_view, RepositoryBuildStatus from endpoints.api.build import (build_status_view, trigger_view, RepositoryBuildStatus,
get_trigger_config)
from endpoints.common import start_build from endpoints.common import start_build
from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException, from endpoints.trigger import (BuildTrigger as BuildTriggerBase, TriggerDeactivationException,
TriggerActivationException, EmptyRepositoryException) TriggerActivationException, EmptyRepositoryException)
@ -25,7 +26,7 @@ def _prepare_webhook_url(scheme, username, password, hostname, path):
return urlunparse((scheme, auth_hostname, path, '', '', '')) return urlunparse((scheme, auth_hostname, path, '', '', ''))
@resource('/v1/repository/<path:repository>/trigger/') @resource('/v1/repository/<repopath:repository>/trigger/')
class BuildTriggerList(RepositoryParamResource): class BuildTriggerList(RepositoryParamResource):
""" Resource for listing repository build triggers. """ """ Resource for listing repository build triggers. """
@ -39,7 +40,7 @@ class BuildTriggerList(RepositoryParamResource):
} }
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>') @resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>')
class BuildTrigger(RepositoryParamResource): class BuildTrigger(RepositoryParamResource):
""" Resource for managing specific build triggers. """ """ Resource for managing specific build triggers. """
@ -64,7 +65,7 @@ class BuildTrigger(RepositoryParamResource):
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
config_dict = json.loads(trigger.config) config_dict = get_trigger_config(trigger)
if handler.is_active(config_dict): if handler.is_active(config_dict):
try: try:
handler.deactivate(trigger.auth_token, config_dict) handler.deactivate(trigger.auth_token, config_dict)
@ -81,7 +82,7 @@ class BuildTrigger(RepositoryParamResource):
return 'No Content', 204 return 'No Content', 204
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/subdir') @resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/subdir')
@internal_only @internal_only
class BuildTriggerSubdirs(RepositoryParamResource): class BuildTriggerSubdirs(RepositoryParamResource):
""" Custom verb for fetching the subdirs which are buildable for a trigger. """ """ Custom verb for fetching the subdirs which are buildable for a trigger. """
@ -123,7 +124,7 @@ class BuildTriggerSubdirs(RepositoryParamResource):
raise Unauthorized() raise Unauthorized()
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/activate') @resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/activate')
@internal_only @internal_only
class BuildTriggerActivate(RepositoryParamResource): class BuildTriggerActivate(RepositoryParamResource):
""" Custom verb for activating a build trigger once all required information has been collected. """ Custom verb for activating a build trigger once all required information has been collected.
@ -147,7 +148,7 @@ class BuildTriggerActivate(RepositoryParamResource):
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
existing_config_dict = json.loads(trigger.config) existing_config_dict = get_trigger_config(trigger)
if handler.is_active(existing_config_dict): if handler.is_active(existing_config_dict):
raise InvalidRequest('Trigger config is not sufficient for activation.') raise InvalidRequest('Trigger config is not sufficient for activation.')
@ -191,7 +192,7 @@ class BuildTriggerActivate(RepositoryParamResource):
raise Unauthorized() raise Unauthorized()
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/start') @resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/start')
class ActivateBuildTrigger(RepositoryParamResource): class ActivateBuildTrigger(RepositoryParamResource):
""" Custom verb to manually activate a build trigger. """ """ Custom verb to manually activate a build trigger. """
@ -205,11 +206,11 @@ class ActivateBuildTrigger(RepositoryParamResource):
raise NotFound() raise NotFound()
handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name) handler = BuildTriggerBase.get_trigger_for_service(trigger.service.name)
existing_config_dict = json.loads(trigger.config) config_dict = get_trigger_config(trigger)
if not handler.is_active(existing_config_dict): if not handler.is_active(config_dict):
raise InvalidRequest('Trigger is not active.') raise InvalidRequest('Trigger is not active.')
specs = handler.manual_start(trigger.auth_token, json.loads(trigger.config)) specs = handler.manual_start(trigger.auth_token, config_dict)
dockerfile_id, tags, name, subdir = specs dockerfile_id, tags, name, subdir = specs
repo = model.get_repository(namespace, repository) repo = model.get_repository(namespace, repository)
@ -225,7 +226,7 @@ class ActivateBuildTrigger(RepositoryParamResource):
return resp, 201, headers return resp, 201, headers
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/builds') @resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/builds')
class TriggerBuildList(RepositoryParamResource): class TriggerBuildList(RepositoryParamResource):
""" Resource to represent builds that were activated from the specified trigger. """ """ Resource to represent builds that were activated from the specified trigger. """
@require_repo_admin @require_repo_admin
@ -242,7 +243,7 @@ class TriggerBuildList(RepositoryParamResource):
} }
@resource('/v1/repository/<path:repository>/trigger/<trigger_uuid>/sources') @resource('/v1/repository/<repopath:repository>/trigger/<trigger_uuid>/sources')
@internal_only @internal_only
class BuildTriggerSources(RepositoryParamResource): class BuildTriggerSources(RepositoryParamResource):
""" Custom verb to fetch the list of build sources for the trigger config. """ """ Custom verb to fetch the list of build sources for the trigger config. """

View file

@ -1,5 +1,6 @@
import logging import logging
import stripe import stripe
import json
from flask import request from flask import request
from flask.ext.login import logout_user from flask.ext.login import logout_user
@ -8,7 +9,7 @@ from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app from app import app
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error, from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
log_action, internal_only, NotFound, Unauthorized, require_user_admin, log_action, internal_only, NotFound, Unauthorized, require_user_admin,
require_user_read, InvalidToken, require_scope) require_user_read, InvalidToken, require_scope, format_date)
from endpoints.api.subscribe import subscribe from endpoints.api.subscribe import subscribe
from endpoints.common import common_login from endpoints.common import common_login
from data import model from data import model
@ -60,6 +61,15 @@ def user_view(user):
} }
def notification_view(notification):
return {
'organization': notification.target.username if notification.target.organization else None,
'kind': notification.kind.name,
'created': format_date(notification.created),
'metadata': json.loads(notification.metadata_json),
}
@resource('/v1/user/') @resource('/v1/user/')
class User(ApiResource): class User(ApiResource):
""" Operations related to users. """ """ Operations related to users. """
@ -364,3 +374,15 @@ class Recovery(ApiResource):
code = model.create_reset_password_email_code(email) code = model.create_reset_password_email_code(email)
send_recovery_email(email, code.code) send_recovery_email(email, code.code)
return 'Created', 201 return 'Created', 201
@resource('/v1/user/notifications')
@internal_only
class UserNotificationList(ApiResource):
@require_user_admin
@nickname('listUserNotifications')
def get(self):
notifications = model.list_notifications(get_authenticated_user())
return {
'notifications': [notification_view(notification) for notification in notifications]
}

View file

@ -14,7 +14,7 @@ def webhook_view(webhook):
} }
@resource('/v1/repository/<path:repository>/webhook/') @resource('/v1/repository/<repopath:repository>/webhook/')
class WebhookList(RepositoryParamResource): class WebhookList(RepositoryParamResource):
""" Resource for dealing with listing and creating webhooks. """ """ Resource for dealing with listing and creating webhooks. """
schemas = { schemas = {
@ -52,7 +52,7 @@ class WebhookList(RepositoryParamResource):
} }
@resource('/v1/repository/<path:repository>/webhook/<public_id>') @resource('/v1/repository/<repopath:repository>/webhook/<public_id>')
class Webhook(RepositoryParamResource): class Webhook(RepositoryParamResource):
""" Resource for dealing with specific webhooks. """ """ Resource for dealing with specific webhooks. """
@require_repo_admin @require_repo_admin

View file

@ -5,7 +5,7 @@ import urlparse
import json import json
from flask import session, make_response, render_template, request from flask import session, make_response, render_template, request
from flask.ext.login import login_user, UserMixin from flask.ext.login import login_user, UserMixin, current_user
from flask.ext.principal import identity_changed from flask.ext.principal import identity_changed
from data import model from data import model
@ -13,13 +13,20 @@ from data.queue import dockerfile_build_queue
from app import app, login_manager from app import app, login_manager
from auth.permissions import QuayDeferredPermissionUser from auth.permissions import QuayDeferredPermissionUser
from endpoints.api.discovery import swagger_route_data from endpoints.api.discovery import swagger_route_data
from werkzeug.routing import BaseConverter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
route_data = None route_data = None
class RepoPathConverter(BaseConverter):
regex = '[\.a-zA-Z0-9_\-]+/[\.a-zA-Z0-9_\-]+'
weight = 200
app.url_map.converters['repopath'] = RepoPathConverter
def get_route_data(): def get_route_data():
global route_data global route_data
if route_data: if route_data:
@ -87,13 +94,21 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token
def render_page_template(name, **kwargs): def render_page_template(name, **kwargs):
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()), **kwargs))
resp = make_response(render_template(name, route_data=json.dumps(get_route_data()),
**kwargs))
resp.headers['X-FRAME-OPTIONS'] = 'DENY' resp.headers['X-FRAME-OPTIONS'] = 'DENY'
return resp return resp
def check_repository_usage(user_or_org, plan_found):
private_repos = model.get_private_repo_count(user_or_org.username)
repos_allowed = plan_found['privateRepos']
if private_repos > repos_allowed:
model.create_notification('over_private_usage', user_or_org, {'namespace': user_or_org.username})
else:
model.delete_notifications_by_kind(user_or_org, 'over_private_usage')
def start_build(repository, dockerfile_id, tags, build_name, subdir, manual, def start_build(repository, dockerfile_id, tags, build_name, subdir, manual,
trigger=None): trigger=None):
host = urlparse.urlparse(request.url).netloc host = urlparse.urlparse(request.url).netloc

View file

@ -203,7 +203,7 @@ class GithubBuildTrigger(BuildTrigger):
try: try:
repo = gh_client.get_repo(source) repo = gh_client.get_repo(source)
default_commit = repo.get_branch(repo.master_branch).commit default_commit = repo.get_branch(repo.master_branch or 'master').commit
commit_tree = repo.get_git_tree(default_commit.sha, recursive=True) commit_tree = repo.get_git_tree(default_commit.sha, recursive=True)
return [os.path.dirname(elem.path) for elem in commit_tree.tree return [os.path.dirname(elem.path) for elem in commit_tree.tree
@ -283,4 +283,4 @@ class GithubBuildTrigger(BuildTrigger):
short_sha = GithubBuildTrigger.get_display_name(master_sha) short_sha = GithubBuildTrigger.get_display_name(master_sha)
ref = 'refs/heads/%s' % repo.master_branch ref = 'refs/heads/%s' % repo.master_branch
return self._prepare_build(config, repo, master_sha, short_sha, ref) return self._prepare_build(config, repo, master_sha, short_sha, ref)

View file

@ -25,6 +25,7 @@ web = Blueprint('web', __name__)
STATUS_TAGS = app.config['STATUS_TAGS'] STATUS_TAGS = app.config['STATUS_TAGS']
@web.route('/', methods=['GET'], defaults={'path': ''}) @web.route('/', methods=['GET'], defaults={'path': ''})
@web.route('/organization/<path:path>', methods=['GET']) @web.route('/organization/<path:path>', methods=['GET'])
@no_cache @no_cache
@ -32,6 +33,11 @@ def index(path):
return render_page_template('index.html') return render_page_template('index.html')
@web.route('/500', methods=['GET'])
def internal_error_display():
return render_page_template('500.html')
@web.route('/snapshot', methods=['GET']) @web.route('/snapshot', methods=['GET'])
@web.route('/snapshot/', methods=['GET']) @web.route('/snapshot/', methods=['GET'])
@web.route('/snapshot/<path:path>', methods=['GET']) @web.route('/snapshot/<path:path>', methods=['GET'])

View file

@ -225,6 +225,11 @@ def initialize_database():
LogEntryKind.create(name='setup_repo_trigger') LogEntryKind.create(name='setup_repo_trigger')
LogEntryKind.create(name='delete_repo_trigger') LogEntryKind.create(name='delete_repo_trigger')
NotificationKind.create(name='password_required')
NotificationKind.create(name='over_private_usage')
NotificationKind.create(name='test_notification')
def wipe_database(): def wipe_database():
logger.debug('Wiping all data from the DB.') logger.debug('Wiping all data from the DB.')
@ -262,6 +267,9 @@ def populate_database():
new_user_4.verified = True new_user_4.verified = True
new_user_4.save() new_user_4.save()
new_user_5 = model.create_user('unverified', 'password', 'no5@thanks.com')
new_user_5.save()
reader = model.create_user('reader', 'password', 'no1@thanks.com') reader = model.create_user('reader', 'password', 'no1@thanks.com')
reader.verified = True reader.verified = True
reader.save() reader.save()
@ -270,6 +278,8 @@ def populate_database():
outside_org.verified = True outside_org.verified = True
outside_org.save() outside_org.save()
model.create_notification('test_notification', new_user_1, metadata={'some': 'value'})
__generate_repository(new_user_4, 'randomrepo', 'Random repo repository.', False, __generate_repository(new_user_4, 'randomrepo', 'Random repo repository.', False,
[], (4, [], ['latest', 'prod'])) [], (4, [], ['latest', 'prod']))

View file

@ -15,8 +15,9 @@ var isDebug = !!options['d'];
var rootUrl = isDebug ? 'http://localhost:5000/' : 'https://quay.io/'; var rootUrl = isDebug ? 'http://localhost:5000/' : 'https://quay.io/';
var repo = isDebug ? 'complex' : 'r0'; var repo = isDebug ? 'complex' : 'r0';
var org = isDebug ? 'buynlarge' : 'quay' var org = isDebug ? 'buynlarge' : 'devtable'
var orgrepo = 'orgrepo' var orgrepo = isDebug ? 'buynlarge/orgrepo' : 'quay/testconnect2';
var buildrepo = isDebug ? 'devtable/building' : 'quay/testconnect2';
var outputDir = "screenshots/"; var outputDir = "screenshots/";
@ -32,8 +33,16 @@ casper.on("page.error", function(msg, trace) {
}); });
casper.start(rootUrl + 'signin', function () { casper.start(rootUrl + 'signin', function () {
this.wait(1000);
});
casper.thenClick('.accordion-toggle[data-target="#collapseSignin"]', function() {
this.wait(1000);
});
casper.then(function () {
this.fill('.form-signin', { this.fill('.form-signin', {
'username': 'devtable', 'username': isDebug ? 'devtable' : 'quaydemo',
'password': isDebug ? 'password': 'C>K98%y"_=54x"<', 'password': isDebug ? 'password': 'C>K98%y"_=54x"<',
}, false); }, false);
}); });
@ -43,6 +52,7 @@ casper.thenClick('.form-signin button[type=submit]', function() {
}); });
casper.then(function() { casper.then(function() {
this.waitForSelector('.fa-lock');
this.log('Generating user home screenshot.'); this.log('Generating user home screenshot.');
}); });
@ -150,12 +160,25 @@ casper.then(function() {
this.log('Generating oganization repository admin screenshot.'); this.log('Generating oganization repository admin screenshot.');
}); });
casper.thenOpen(rootUrl + 'repository/' + org + '/' + orgrepo + '/admin', function() { casper.thenOpen(rootUrl + 'repository/' + orgrepo + '/admin', function() {
this.waitForText('outsideorg') this.waitForText('Robot Account')
}); });
casper.then(function() { casper.then(function() {
this.capture(outputDir + 'org-repo-admin.png'); this.capture(outputDir + 'org-repo-admin.png');
}); });
casper.then(function() {
this.log('Generating build history screenshot.');
});
casper.thenOpen(rootUrl + 'repository/' + buildrepo + '/build', function() {
this.waitForText('Starting');
});
casper.then(function() {
this.capture(outputDir + 'build-history.png');
});
casper.run(); casper.run();

View file

@ -9,6 +9,57 @@
} }
} }
.notification-view-element {
cursor: pointer;
margin-bottom: 10px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
position: relative;
max-width: 320px;
}
.notification-view-element .orginfo {
margin-top: 8px;
float: left;
}
.notification-view-element .orginfo .orgname {
font-size: 12px;
color: #aaa;
}
.notification-view-element .circle {
position: absolute;
top: 14px;
left: 0px;
width: 12px;
height: 12px;
display: inline-block;
border-radius: 50%;
}
.notification-view-element .datetime {
margin-top: 16px;
font-size: 12px;
color: #aaa;
text-align: right;
}
.notification-view-element .message {
margin-bottom: 4px;
}
.notification-view-element .container {
padding: 10px;
border-radius: 6px;
margin-left: 16px;
}
.notification-view-element .container:hover {
background: rgba(66, 139, 202, 0.1);
}
.dockerfile-path { .dockerfile-path {
margin-top: 10px; margin-top: 10px;
padding: 20px; padding: 20px;
@ -507,7 +558,22 @@ i.toggle-icon:hover {
min-width: 200px; min-width: 200px;
} }
.user-notification { .notification-primary {
background: #428bca;
color: white;
}
.notification-info {
color: black;
background: #d9edf7;
}
.notification-warning {
color: #8a6d3b;
background: #fcf8e3;
}
.notification-error {
background: red; background: red;
} }
@ -776,11 +842,20 @@ i.toggle-icon:hover {
margin-bottom: 16px; margin-bottom: 16px;
} }
.new-repo .section-title {
float: right;
color: #aaa;
}
.new-repo .repo-option { .new-repo .repo-option {
margin: 6px; margin: 6px;
margin-top: 16px; margin-top: 16px;
} }
.new-repo .repo-option label {
font-weight: normal;
}
.new-repo .repo-option i { .new-repo .repo-option i {
font-size: 18px; font-size: 18px;
padding-left: 10px; padding-left: 10px;
@ -2122,16 +2197,16 @@ p.editable:hover i {
padding-right: 6px; padding-right: 6px;
} }
.delete-ui { .delete-ui-element {
outline: none; outline: none;
} }
.delete-ui i { .delete-ui-element i {
cursor: pointer; cursor: pointer;
vertical-align: middle; vertical-align: middle;
} }
.delete-ui .delete-ui-button { .delete-ui-element .delete-ui-button {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
color: white; color: white;
@ -2147,15 +2222,15 @@ p.editable:hover i {
transition: width 500ms ease-in-out; transition: width 500ms ease-in-out;
} }
.delete-ui .delete-ui-button button { .delete-ui-element .delete-ui-button button {
padding: 4px; padding: 4px;
} }
.delete-ui:focus i { .delete-ui-element:focus i {
visibility: hidden; visibility: hidden;
} }
.delete-ui:focus .delete-ui-button { .delete-ui-element:focus .delete-ui-button {
width: 60px; width: 60px;
} }

View file

@ -0,0 +1,4 @@
<span class="delete-ui-element" ng-click="focus()">
<span class="delete-ui-button" ng-click="performDelete()"><button class="btn btn-danger">{{ buttonTitleInternal }}</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="left" title="{{ deleteTitle }}"></i>
</span>

View file

@ -39,11 +39,14 @@
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown"> <a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" /> <img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
{{ user.username }} {{ user.username }}
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan" <span class="badge user-notification notification-animated"
bs-tooltip="(user.askForPassword ? 'A password is needed for this account<br>' : '') + (overPlan ? 'You are using more private repositories than your plan allows' : '')" ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses"
bs-tooltip=""
title="User Notifications"
data-placement="left" data-placement="left"
data-container="body"> data-container="body">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} {{ notificationService.notifications.length }}
</span> </span>
<b class="caret"></b> <b class="caret"></b>
</a> </a>
@ -51,8 +54,16 @@
<li> <li>
<a href="/user/" target="{{ appLinkTarget() }}"> <a href="/user/" target="{{ appLinkTarget() }}">
Account Settings Account Settings
<span class="badge user-notification" ng-show="user.askForPassword || overPlan"> </a>
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }} </li>
<li ng-if="notificationService.notifications.length">
<a href="javascript:void(0)" data-template="/static/directives/notification-bar.html"
data-animation="am-slide-right" bs-aside="aside" data-container="body">
Notifications
<span class="badge user-notification"
ng-class="notificationService.notificationClasses"
ng-show="notificationService.notifications.length">
{{ notificationService.notifications.length }}
</span> </span>
</a> </a>
</li> </li>

View file

@ -0,0 +1,15 @@
<div class="aside" tabindex="-1" role="dialog">
<div class="aside-dialog">
<div class="aside-content">
<div class="aside-header">
<button type="button" class="close" ng-click="$hide()">&times;</button>
<h4 class="aside-title">Notifications</h4>
</div>
<div class="aside-body">
<div ng-repeat="notification in notificationService.notifications">
<div class="notification-view" notification="notification" parent="this"></div>
</div>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,11 @@
<div class="notification-view-element">
<div class="container" ng-click="showNotification();">
<div class="circle" ng-class="getClass(notification)"></div>
<div class="message" ng-bind-html="getMessage(notification)"></div>
<div class="orginfo" ng-if="notification.organization">
<img src="//www.gravatar.com/avatar/{{ getGravatar(notification.organization) }}?s=24&d=identicon" />
<span class="orgname">{{ notification.organization }}</span>
</div>
<div class="datetime">{{ parseDate(notification.created) | date:'medium'}}</div>
</div>
</div>

View file

@ -1,5 +1,6 @@
<button class="btn btn-success" data-trigger="click" bs-popover="'static/directives/popup-input-dialog.html'" <button class="btn btn-success" data-trigger="click"
data-placement="bottom" ng-click="popupShown()"> data-content-template="static/directives/popup-input-dialog.html"
data-placement="bottom" ng-click="popupShown()" bs-popover>
<span ng-transclude></span> <span ng-transclude></span>
</button> </button>

View file

@ -1,4 +1,4 @@
<form name="popupinput" ng-submit="inputSubmit(); hide()" novalidate> <form name="popupinput" ng-submit="inputSubmit(); $hide()" novalidate>
<input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="hide()" <input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="$hide()"
ng-pattern="getRegexp(pattern)" ng-model="inputValue" ng-trim="false" ng-minlength="2" required> ng-pattern="getRegexp(pattern)" ng-model="inputValue" ng-trim="false" ng-minlength="2" required>
</form> </form>

View file

@ -48,10 +48,7 @@
<span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span> <span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deletePrototype(prototype)"></span>
<span class="delete-ui-button" ng-click="deletePrototype(prototype)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -24,10 +24,7 @@
</a> </a>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span>
<span class="delete-ui-button" ng-click="deleteRobot(robotInfo)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Robot Account"></i>
</span>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -3,7 +3,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"> <div class="panel-heading">
<h4 class="panel-title accordion-title"> <h4 class="panel-title accordion-title">
<a class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseSignin"> <a id="signinToggle" class="accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseSignin">
Sign In Sign In
</a> </a>
</h4> </h4>

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="453.54px" height="453.54px" viewBox="0 0 453.54 453.54" enable-background="new 0 0 453.54 453.54" xml:space="preserve">
<rect fill="#00A9D3" width="453.54" height="453.54"/>
<g>
<path fill="#CAD5DA" d="M88.553,86.368L119.628,34l16.113,6.042l21.004,17.264c0,0,14.963,16.688,15.826,16.977
c0.863,0.288,18.415-12.373,18.415-12.373l9.783-1.726c0,0,10.934,46.038,11.221,48.339s32.515,54.958,32.803,56.972
c0.287,2.014-0.288,72.222-2.302,73.085c-2.015,0.863-108.189,9.494-108.189,9.494l-59.561-4.604V120.608L88.553,86.368z"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="111.6489" y1="52.2725" x2="107.9083" y2="201.6065">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<polygon fill="url(#SVGID_1_)" points="120.204,36.59 91.143,86.943 78.194,120.32 83.374,199.735 139.482,208.655 "/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="155.4292" y1="35.3975" x2="192.2593" y2="175.8124">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<polygon fill="url(#SVGID_2_)" points="122.218,36.59 146.1,190.24 240.478,169.235 151.147,111.113 "/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="180.3018" y1="100.7939" x2="155.2686" y2="32.0245">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<polygon fill="url(#SVGID_3_)" points="125.383,38.316 154.156,107.085 206.812,107.085 198.899,63.493 178.039,86.368
169.695,80.9 155.883,59.896 136.029,42.632 "/>
</g>
<path fill="#CAD5DA" d="M50.447,81.587L59.739,77l2.667,8.333L69.573,95.5l6.667,41.5c0,0-20,18.333-20.667,18.5s-19.167-5-19.167-5
l5.833-35.5l1.91-18.329L50.447,81.587z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="54.2388" y1="80.3335" x2="54.2388" y2="118.3335">
<stop offset="0" style="stop-color:#FFFFFF"/>
<stop offset="1" style="stop-color:#000000"/>
</linearGradient>
<polygon fill="url(#SVGID_4_)" points="51.406,82 45.906,96 44.072,115.585 64.406,118.333 56.322,80.333 "/>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

99
static/img/500/ship.svg Normal file
View file

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="90px" height="453.54px" viewBox="0 0 90 453.54" enable-background="new 0 0 453.54 453.54" xml:space="preserve">
<g>
<rect x="79.797" y="74.656" fill="#231F20" width="0.561" height="1.094"/>
<rect x="80.756" y="74.656" fill="#231F20" width="0.561" height="1.094"/>
<g>
<polygon fill="#4B5059" points="79.16,77.125 79.16,76.188 81.723,76.188 81.723,75.25 79.098,75.25 77.848,80.812 81.723,80.812
81.723,78.5 79.16,78.5 79.16,77.562 81.723,77.562 81.723,77.125 "/>
<rect x="79.16" y="76.188" fill="#A73D37" width="2.562" height="0.938"/>
<rect x="79.16" y="77.562" fill="#A73D37" width="2.562" height="0.938"/>
</g>
<rect x="72.41" y="79.625" fill="#9BA9B2" width="10.5" height="7.75"/>
<polygon fill="#E8E5D1" points="80.598,86.062 78.41,81.125 71.098,81.125 71.098,81.844 69.504,81.844 69.504,84.156
71.098,84.156 71.098,93.711 84.598,94.25 84.598,86.062 "/>
<polyline fill="#E8E5D1" points="2.41,90.819 2.41,85.562 2.973,85.562 3.66,90.819 "/>
<g>
<g>
<rect x="31.306" y="91.138" fill="#707E68" width="5.821" height="1.972"/>
<rect x="25.159" y="91.138" fill="#BFA176" width="5.821" height="1.972"/>
<rect x="37.451" y="91.138" fill="#B07959" width="5.821" height="1.972"/>
<rect x="43.597" y="91.138" fill="#9A6B50" width="5.821" height="1.972"/>
<rect x="49.742" y="91.138" fill="#7E3F20" width="5.821" height="1.972"/>
<rect x="55.887" y="91.138" fill="#D6543B" width="5.822" height="1.972"/>
<rect x="62.033" y="91.138" fill="#B07959" width="5.821" height="1.972"/>
</g>
<g>
<g>
<rect x="62.033" y="88.847" fill="#707E68" width="5.821" height="1.972"/>
<rect x="55.887" y="88.847" fill="#B07959" width="5.822" height="1.972"/>
<rect x="49.742" y="88.847" fill="#D2B48C" width="5.821" height="1.972"/>
<rect x="43.597" y="88.847" fill="#7E3F20" width="5.821" height="1.972"/>
<rect x="37.451" y="88.847" fill="#D6543B" width="5.821" height="1.972"/>
<rect x="31.306" y="88.847" fill="#DEA374" width="5.821" height="1.972"/>
</g>
<g>
<rect x="25.159" y="88.847" fill="#707E68" width="5.821" height="1.972"/>
<rect x="19.014" y="88.847" fill="#B07959" width="5.821" height="1.972"/>
<rect x="12.868" y="88.847" fill="#D2B48C" width="5.822" height="1.972"/>
<rect x="6.723" y="88.847" fill="#7E3F20" width="5.821" height="1.972"/>
</g>
</g>
<g>
<rect x="37.45" y="84.266" fill="#707E68" width="5.821" height="1.972"/>
<rect x="43.597" y="84.266" fill="#BFA176" width="5.821" height="1.972"/>
<rect x="31.305" y="84.266" fill="#B07959" width="5.821" height="1.972"/>
<rect x="25.159" y="84.266" fill="#9A6B50" width="5.821" height="1.972"/>
<rect x="19.014" y="84.266" fill="#7E3F20" width="5.821" height="1.972"/>
<rect x="12.868" y="84.266" fill="#D6543B" width="5.822" height="1.972"/>
<rect x="6.723" y="84.266" fill="#B07959" width="5.821" height="1.972"/>
</g>
<g>
<g>
<rect x="6.723" y="86.557" fill="#707E68" width="5.821" height="1.972"/>
<rect x="12.868" y="86.557" fill="#B07959" width="5.822" height="1.972"/>
<rect x="19.014" y="86.557" fill="#D2B48C" width="5.821" height="1.972"/>
<rect x="25.159" y="86.557" fill="#7E3F20" width="5.821" height="1.972"/>
<rect x="31.305" y="86.557" fill="#D6543B" width="5.821" height="1.972"/>
<rect x="37.45" y="86.557" fill="#DEA374" width="5.821" height="1.972"/>
</g>
<g>
<rect x="43.597" y="86.557" fill="#707E68" width="5.821" height="1.972"/>
<rect x="49.742" y="86.557" fill="#B07959" width="5.821" height="1.972"/>
<rect x="55.887" y="86.557" fill="#D2B48C" width="5.822" height="1.972"/>
<rect x="62.033" y="86.557" fill="#7E3F20" width="5.821" height="1.972"/>
</g>
</g>
</g>
<polygon fill="#A82526" points="87.473,92.819 24.348,92.819 24.348,90.069 6.848,90.069 0,90.069 1.721,94.688 87.473,94.688 "/>
<path fill="#59565F" d="M1.721,94.688l0.502,1.347c-5,2.965,0,5.61,0,5.61s83.5,0.799,83.677,0s1.573-3.076,1.573-3.076v-3.881
H1.721z"/>
<g>
<rect x="69.91" y="82.312" fill="#636B62" width="1.156" height="0.844"/>
<rect x="71.551" y="82.312" fill="#636B62" width="1.156" height="0.844"/>
<rect x="73.191" y="82.312" fill="#636B62" width="1.156" height="0.844"/>
<rect x="74.832" y="82.312" fill="#636B62" width="1.156" height="0.844"/>
<rect x="76.473" y="82.312" fill="#636B62" width="1.156" height="0.844"/>
</g>
<g>
<rect x="77.246" y="87.163" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="78.422" y="87.163" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="79.598" y="87.163" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="80.773" y="87.163" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="81.949" y="87.163" fill="#9BA9B2" width="0.828" height="0.844"/>
</g>
<g>
<rect x="77.309" y="89.225" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="78.484" y="89.225" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="79.66" y="89.225" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="80.836" y="89.225" fill="#9BA9B2" width="0.828" height="0.844"/>
<rect x="82.012" y="89.225" fill="#9BA9B2" width="0.828" height="0.844"/>
</g>
<circle fill="#5F3F2B" cx="4.191" cy="92.281" r="0.531"/>
<rect x="80.186" y="82.312" fill="#808080" width="0.828" height="0.844"/>
<rect x="81.361" y="82.312" fill="#808080" width="0.828" height="0.844"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

39
static/img/500/water.svg Normal file
View file

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 15.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="453.54px" height="453.54px" viewBox="0 0 453.54 453.54" enable-background="new 0 0 453.54 453.54" xml:space="preserve">
<g>
<path fill="#608DA2" d="M35.218,327.768c0,0,6.968-3.055,15.596-5.071c8.628-2.019,32.723-5.351,38.561-6.217
s9.791-1.617,9.791-1.617s-1.682-3.031,3.623-6.049c0,0-1.65,5.794,6.934,6.829c0,0-1.543,3.026-6.945,1.529
c-5.403-1.498-23.869,18.74-50.771,15.825C52.005,332.997,35.635,333.734,35.218,327.768z"/>
<path fill="#608DA2" d="M52.425,331.592c0,0,1.417-0.072,2.878,2.231c1.462,2.306,2.81,4.634,4.659,3.272
c0,0,0.811-0.655-0.449-3.124s-1.633-2.268-3.502-3.169"/>
<path fill="#CCCCCC" d="M35.778,328.087c0,0,20.057-1.34,23.473-1.82c3.416-0.48,13.04-1.496,15.736-2.264
s23.476-6.962,25.139-7.27c1.664-0.308,7.606-0.167,7.836-0.738c0.229-0.572-6.029,0.688-5.465-6.043c0,0-3.349,0.708-3.042,4.812
l0.079,0.359c0,0-21.463,3.516-25.714,3.978C69.568,319.562,46.746,322.869,35.778,328.087z"/>
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="225.002" y1="450.7471" x2="228.5022" y2="101.2466">
<stop offset="0" style="stop-color:#2E3192"/>
<stop offset="1" style="stop-color:#0071BC"/>
</linearGradient>
<path opacity="0.8" fill="url(#SVGID_1_)" d="M450.2,93.711c-3.546,0-3.546,2.115-7.091,2.115c-3.546,0-3.546-2.115-7.092-2.115
s-3.546,2.115-7.09,2.115c-3.545,0-3.545-2.115-7.089-2.115c-3.545,0-3.545,2.115-7.089,2.115c-3.546,0-3.546-2.115-7.092-2.115
s-3.546,2.115-7.092,2.115c-3.545,0-3.545-2.115-7.091-2.115s-3.546,2.115-7.091,2.115c-3.544,0-3.544-2.115-7.09-2.115
c-3.545,0-3.545,2.115-7.089,2.115c-3.547,0-3.547-2.115-7.093-2.115c-3.547,0-3.547,2.115-7.093,2.115s-3.546-2.115-7.09-2.115
c-3.546,0-3.546,2.115-7.091,2.115s-3.545-2.115-7.091-2.115c-3.547,0-3.547,2.115-7.093,2.115s-3.546-2.115-7.093-2.115
c-3.546,0-3.546,2.115-7.09,2.115c-3.546,0-3.546-2.115-7.092-2.115c-3.547,0-3.547,2.115-7.093,2.115
c-3.547,0-3.547-2.115-7.093-2.115s-3.546,2.115-7.092,2.115c-3.545,0-3.545-2.115-7.092-2.115c-3.546,0-3.546,2.115-7.093,2.115
c-3.546,0-3.546-2.115-7.092-2.115c-3.547,0-3.547,2.115-7.092,2.115c-3.547,0-3.547-2.115-7.094-2.115
c-3.546,0-3.546,2.115-7.092,2.115c-3.547,0-3.547-2.115-7.094-2.115s-3.547,2.115-7.093,2.115c-3.545,0-3.545-2.115-7.093-2.115
c-3.547,0-3.547,2.115-7.092,2.115c-3.546,0-3.546-2.115-7.093-2.115s-3.547,2.115-7.095,2.115c-3.544,0-3.544-2.115-7.089-2.115
s-3.544,2.115-7.088,2.115c-3.544,0-3.544-2.115-7.091-2.115c-3.546,0-3.546,2.115-7.093,2.115c-3.545,0-3.545-2.115-7.089-2.115
c-3.546,0-3.546,2.115-7.091,2.115c-3.547,0-3.547-2.115-7.094-2.115c-3.544,0-3.544,2.115-7.091,2.115s-3.546-2.115-7.093-2.115
c-3.546,0-3.546,2.115-7.093,2.115c-3.547,0-3.547-2.115-7.093-2.115c-3.546,0-3.546,2.115-7.093,2.115
c-3.548,0-3.548-2.115-7.095-2.115s-3.547,2.115-7.093,2.115c-3.548,0-3.548-2.115-7.096-2.115s-3.548,2.115-7.096,2.115
c-3.547,0-3.547-2.115-7.096-2.115c-3.548,0-3.548,2.115-7.095,2.115c-3.548,0-3.548-2.115-7.096-2.115
c-3.549,0-3.549,2.115-7.098,2.115c-3.546,0-3.546-2.115-7.093-2.115s-3.547,2.115-7.095,2.115s-3.548-2.115-7.096-2.115
c-3.547,0-3.547,2.115-7.096,2.115c-3.549,0-3.549-2.115-7.098-2.115c-3.546,0-3.546,2.115-7.094,2.115
c-3.55,0-3.55-2.115-7.1-2.115c-3.553,0-3.553,2.115-7.105,2.115c-1.646,0-2.527-0.454-3.354-0.942V453.54h453.54V94.647
C452.718,94.162,451.837,93.711,450.2,93.711z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 167 KiB

After

Width:  |  Height:  |  Size: 183 KiB

View file

@ -102,9 +102,88 @@ function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || ''); return Markdown.getSanitizingConverter().makeHtml(string || '');
} }
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) { quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false; cfpLoadingBarProvider.includeSpinner = false;
/**
* Specialized wrapper around array which provides a toggle() method for viewing the contents of the
* array in a manner that is asynchronously filled in over a short time period. This prevents long
* pauses in the UI for ngRepeat's when the array is significant in size.
*/
$provide.factory('AngularViewArray', ['$interval', function($interval) {
var ADDTIONAL_COUNT = 50;
function _ViewArray() {
this.isVisible = false;
this.visibleEntries = null;
this.hasEntries = false;
this.entries = [];
this.timerRef_ = null;
this.currentIndex_ = 0;
}
_ViewArray.prototype.push = function(elem) {
this.entries.push(elem);
this.hasEntries = true;
if (this.isVisible) {
this.setVisible(true);
}
};
_ViewArray.prototype.toggle = function() {
this.setVisible(!this.isVisible);
};
_ViewArray.prototype.setVisible = function(newState) {
this.isVisible = newState;
this.visibleEntries = [];
this.currentIndex_ = 0;
if (newState) {
this.showAdditionalEntries_();
this.startTimer_();
} else {
this.stopTimer_();
}
};
_ViewArray.prototype.showAdditionalEntries_ = function() {
var i = 0;
for (i = this.currentIndex_; i < (this.currentIndex_ + ADDTIONAL_COUNT) && i < this.entries.length; ++i) {
this.visibleEntries.push(this.entries[i]);
}
this.currentIndex_ = i;
if (this.currentIndex_ >= this.entries.length) {
this.stopTimer_();
}
};
_ViewArray.prototype.startTimer_ = function() {
var that = this;
this.timerRef_ = $interval(function() {
that.showAdditionalEntries_();
}, 10);
};
_ViewArray.prototype.stopTimer_ = function() {
if (this.timerRef_) {
$interval.cancel(this.timerRef_);
this.timerRef_ = null;
}
};
var service = {
'create': function() {
return new _ViewArray();
}
};
return service;
}]);
$provide.factory('UtilService', ['$sanitize', function($sanitize) { $provide.factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {}; var utilService = {};
@ -143,6 +222,49 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return builderService; return builderService;
}]); }]);
$provide.factory('StringBuilderService', ['$sce', function($sce) {
var stringBuilderService = {};
stringBuilderService.buildString = function(value_or_func, metadata) {
var fieldIcons = {
'username': 'user',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large'
};
var description = value_or_func;
if (typeof description != 'string') {
description = description(metadata);
}
for (var key in metadata) {
if (metadata.hasOwnProperty(key)) {
var value = metadata[key] != null ? metadata[key].toString() : '(Unknown)';
var markedDown = getMarkedDown(value);
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
var icon = fieldIcons[key];
if (icon) {
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
}
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
}
}
return $sce.trustAsHtml(description.replace('\n', '<br>'));
};
return stringBuilderService;
}]);
$provide.factory('ImageMetadataService', ['UtilService', function(UtilService) { $provide.factory('ImageMetadataService', ['UtilService', function(UtilService) {
var metadataService = {}; var metadataService = {};
@ -360,7 +482,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
anonymous: true, anonymous: true,
username: null, username: null,
email: null, email: null,
askForPassword: false,
organizations: [], organizations: [],
logins: [] logins: []
} }
@ -467,6 +588,101 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
return userService; return userService;
}]); }]);
$provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService',
function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) {
var notificationService = {
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': []
};
var pollTimerHandle = null;
var notificationKinds = {
'test_notification': {
'level': 'primary',
'message': 'This notification is a long message for testing',
'page': '/about/'
},
'password_required': {
'level': 'error',
'message': 'In order to begin pushing and pulling repositories to Quay.io, a password must be set for your account',
'page': '/user?tab=password'
},
'over_private_usage': {
'level': 'error',
'message': 'Namespace {namespace} is over its allowed private repository count. ' +
'<br><br>Please upgrade your plan to avoid disruptions in service.',
'page': function(metadata) {
var organization = UserService.getOrganization(metadata['namespace']);
if (organization) {
return '/organization/' + metadata['namespace'] + '/admin';
} else {
return '/user';
}
}
}
};
notificationService.getPage = function(notification) {
var page = notificationKinds[notification['kind']]['page'];
if (typeof page != 'string') {
page = page(notification['metadata']);
}
return page;
};
notificationService.getMessage = function(notification) {
var kindInfo = notificationKinds[notification['kind']];
return StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
};
notificationService.getClass = function(notification) {
return 'notification-' + notificationKinds[notification['kind']]['level'];
};
notificationService.getClasses = function(notifications) {
var classes = [];
for (var i = 0; i < notifications.length; ++i) {
var notification = notifications[i];
classes.push(notificationService.getClass(notification));
}
return classes.join(' ');
};
notificationService.update = function() {
var user = UserService.currentUser();
if (!user || user.anonymous) {
return;
}
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
});
};
notificationService.reset = function() {
$interval.cancel(pollTimerHandle);
pollTimerHandle = $interval(notificationService.update, 5 * 60 * 1000 /* five minutes */);
};
// Watch for plan changes and update.
PlanService.registerListener(this, function(plan) {
notificationService.reset();
notificationService.update();
});
// Watch for user changes and update.
$rootScope.$watch(function() { return UserService.currentUser(); }, function(currentUser) {
notificationService.reset();
notificationService.update();
});
return notificationService;
}]);
$provide.factory('KeyService', ['$location', function($location) { $provide.factory('KeyService', ['$location', function($location) {
var keyService = {} var keyService = {}
@ -1405,7 +1621,7 @@ quayApp.directive('logsView', function () {
'repository': '=repository', 'repository': '=repository',
'performer': '=performer' 'performer': '=performer'
}, },
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder) { controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService) {
$scope.loading = true; $scope.loading = true;
$scope.logs = null; $scope.logs = null;
$scope.kindsAllowed = null; $scope.kindsAllowed = null;
@ -1620,43 +1836,9 @@ quayApp.directive('logsView', function () {
return $scope.chart.getColor(kind); return $scope.chart.getColor(kind);
}; };
$scope.getDescription = function(log) { $scope.getDescription = function(log) {
var fieldIcons = {
'username': 'user',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large'
};
log.metadata['_ip'] = log.ip ? log.ip : null; log.metadata['_ip'] = log.ip ? log.ip : null;
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
var description = logDescriptions[log.kind] || log.kind;
if (typeof description != 'string') {
description = description(log.metadata);
}
for (var key in log.metadata) {
if (log.metadata.hasOwnProperty(key)) {
var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)';
var markedDown = getMarkedDown(value);
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
var icon = fieldIcons[key];
if (icon) {
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
}
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
}
}
return $sce.trustAsHtml(description.replace('\n', '<br>'));
}; };
$scope.$watch('organization', update); $scope.$watch('organization', update);
@ -1921,6 +2103,31 @@ quayApp.directive('prototypeManager', function () {
}); });
quayApp.directive('deleteUi', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/delete-ui.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'deleteTitle': '=deleteTitle',
'buttonTitle': '=buttonTitle',
'performDelete': '&performDelete'
},
controller: function($scope, $element) {
$scope.buttonTitleInternal = $scope.buttonTitle || 'Delete';
$element.children().attr('tabindex', 0);
$scope.focus = function() {
$element[0].firstChild.focus();
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('popupInputButton', function () { quayApp.directive('popupInputButton', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -1939,7 +2146,7 @@ quayApp.directive('popupInputButton', function () {
var box = $('#input-box'); var box = $('#input-box');
box[0].value = ''; box[0].value = '';
box.focus(); box.focus();
}, 10); }, 40);
}; };
$scope.getRegexp = function(pattern) { $scope.getRegexp = function(pattern) {
@ -2153,26 +2360,12 @@ quayApp.directive('headerBar', function () {
restrict: 'C', restrict: 'C',
scope: { scope: {
}, },
controller: function($scope, $element, $location, UserService, PlanService, ApiService) { controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
$scope.overPlan = false; $scope.notificationService = NotificationService;
var checkOverPlan = function() { // Monitor any user changes and place the current user into the scope.
if ($scope.user.anonymous) { UserService.updateUserIn($scope);
$scope.overPlan = false;
return;
}
ApiService.getUserPrivateAllowed().then(function(resp) {
$scope.overPlan = !resp['privateAllowed'];
});
};
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope, checkOverPlan);
// Monitor any plan changes.
PlanService.registerListener(this, checkOverPlan);
$scope.signout = function() { $scope.signout = function() {
ApiService.logout().then(function() { ApiService.logout().then(function() {
UserService.load(); UserService.load();
@ -3314,6 +3507,54 @@ quayApp.directive('buildProgress', function () {
}); });
quayApp.directive('notificationView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/notification-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'notification': '=notification',
'parent': '=parent'
},
controller: function($scope, $element, $location, UserService, NotificationService) {
$scope.getMessage = function(notification) {
return NotificationService.getMessage(notification);
};
$scope.getGravatar = function(orgname) {
var organization = UserService.getOrganization(orgname);
return organization['gravatar'] || '';
};
$scope.parseDate = function(dateString) {
return Date.parse(dateString);
};
$scope.showNotification = function() {
var url = NotificationService.getPage($scope.notification);
if (url) {
var parts = url.split('?')
$location.path(parts[0]);
if (parts.length > 1) {
$location.search(parts[1]);
}
$scope.parent.$hide();
}
};
$scope.getClass = function(notification) {
return NotificationService.getClass(notification);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('dockerfileBuildDialog', function () { quayApp.directive('dockerfileBuildDialog', function () {
var directiveDefinitionObject = { var directiveDefinitionObject = {
priority: 0, priority: 0,
@ -3594,6 +3835,11 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
} }
} }
if (response.status == 500) {
document.location = '/500';
return false;
}
return true; return true;
}); });

View file

@ -919,7 +919,8 @@ function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootSc
getBuildInfo(); getBuildInfo();
} }
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html) { function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
ansi2html, AngularViewArray) {
var namespace = $routeParams.namespace; var namespace = $routeParams.namespace;
var name = $routeParams.name; var name = $routeParams.name;
var pollTimerHandle = null; var pollTimerHandle = null;
@ -985,19 +986,9 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
}; };
$scope.hasLogs = function(container) { $scope.hasLogs = function(container) {
return ((container.logs && container.logs.length) || (container._logs && container._logs.length)); return container.logs.hasEntries;
}; };
$scope.toggleLogs = function(container) {
if (container._logs) {
container.logs = container._logs;
container._logs = null;
} else {
container._logs = container.logs;
container.logs = null;
}
};
$scope.setCurrentBuild = function(buildId, opt_updateURL) { $scope.setCurrentBuild = function(buildId, opt_updateURL) {
if (!$scope.builds) { return; } if (!$scope.builds) { return; }
@ -1085,17 +1076,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var entry = logs[i]; var entry = logs[i];
var type = entry['type'] || 'entry'; var type = entry['type'] || 'entry';
if (type == 'command' || type == 'phase' || type == 'error') { if (type == 'command' || type == 'phase' || type == 'error') {
entry['_logs'] = []; entry['logs'] = AngularViewArray.create();
entry['index'] = startIndex + i; entry['index'] = startIndex + i;
$scope.logEntries.push(entry); $scope.logEntries.push(entry);
$scope.currentParentEntry = entry; $scope.currentParentEntry = entry;
} else if ($scope.currentParentEntry) { } else if ($scope.currentParentEntry) {
if ($scope.currentParentEntry['logs']) { $scope.currentParentEntry['logs'].push(entry);
$scope.currentParentEntry['logs'].push(entry);
} else {
$scope.currentParentEntry['_logs'].push(entry);
}
} }
} }
}; };
@ -1885,13 +1872,16 @@ function V1Ctrl($scope, $location, UserService) {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
} }
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService) { function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) {
UserService.updateUserIn($scope); UserService.updateUserIn($scope);
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId;
$scope.repo = { $scope.repo = {
'is_public': 1, 'is_public': 1,
'description': '', 'description': '',
'initialize': false 'initialize': ''
}; };
// Watch the namespace on the repo. If it changes, we update the plan and the public/private // Watch the namespace on the repo. If it changes, we update the plan and the public/private
@ -1960,12 +1950,20 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService
$scope.creating = false; $scope.creating = false;
$scope.created = created; $scope.created = created;
// Repository created. Start the upload process if applicable. // Start the upload process if applicable.
if ($scope.repo.initialize) { if ($scope.repo.initialize == 'dockerfile' || $scope.repo.initialize == 'zipfile') {
$scope.createdForBuild = created; $scope.createdForBuild = created;
return; return;
} }
// Conduct the Github redirect if applicable.
if ($scope.repo.initialize == 'github') {
window.location = 'https://github.com/login/oauth/authorize?client_id=' + $scope.githubClientId +
'&scope=repo,user:email&redirect_uri=' + $scope.githubRedirectUri + '/trigger/' +
repo.namespace + '/' + repo.name;
return;
}
// Otherwise, redirect to the repo page. // Otherwise, redirect to the repo page.
$location.path('/repository/' + created.namespace + '/' + created.name); $location.path('/repository/' + created.namespace + '/' + created.name);
}, function(result) { }, function(result) {

8
static/lib/angular-motion.min.css vendored Normal file

File diff suppressed because one or more lines are too long

3543
static/lib/angular-strap.js vendored Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

9
static/lib/angular-strap.tpl.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -106,15 +106,20 @@
</div> </div>
<div class="tour-section row"> <div class="tour-section row">
<div class="col-md-7"><img src="/static/img/repo-changes.png" title="View Image - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/image/..." class="img-responsive"></div> <div class="col-md-7"><img src="/static/img/build-history.png" title="View Image - Quay.io"
data-screenshot-url="https://quay.io/repository/devtable/building/build"
class="img-responsive"></div>
<div class="col-md-5"> <div class="col-md-5">
<div class="tour-section-title">Docker diff in the cloud</div> <div class="tour-section-title">Dockerfile Build in the cloud</div>
<div class="tour-section-description"> <div class="tour-section-description">
We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were <b>added</b>, <b>changed</b>, or <b>removed</b> for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view. Like to use <b>Dockerfiles</b> to build your images? Simply upload your Dockerfile (and any additional files it needs) and we'll build your Dockerfile into an image and push it to your repository.
</div>
<div class="tour-section-description">
If you store your Dockerfile in <i class="fa fa-github fa-lg" style="margin: 6px;"></i><b>GitHub</b>, add a <b>Build Trigger</b> to your repository and we'll start a Dockerfile build for every change you make.
</div> </div>
</div> </div>
</div> </div>
<div class="tour-section row"> <div class="tour-section row">
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-admin.png" title="Repository Admin - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex/admin" class="img-responsive"></div> <div class="col-md-7 col-md-push-5"><img src="/static/img/repo-admin.png" title="Repository Admin - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex/admin" class="img-responsive"></div>
<div class="col-md-5 col-md-pull-7"> <div class="col-md-5 col-md-pull-7">
@ -128,4 +133,14 @@
<div class="tour-section-description">Want to share with the world? Make your repository <b>fully public</b>.</div> <div class="tour-section-description">Want to share with the world? Make your repository <b>fully public</b>.</div>
</div> </div>
</div> </div>
<div class="tour-section row">
<div class="col-md-7"><img src="/static/img/repo-changes.png" title="View Image - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/image/..." class="img-responsive"></div>
<div class="col-md-5">
<div class="tour-section-title">Docker diff whenever you need it</div>
<div class="tour-section-description">
We wanted to know what was changing in each image of our repositories just as much as you do. So we added diffs. Now you can see exactly which files were <b>added</b>, <b>changed</b>, or <b>removed</b> for each image. We've also provided two awesome ways to view your changes, either in a filterable list, or in a drill down tree view.
</div>
</div>
</div>
</div> </div>

View file

@ -25,13 +25,18 @@
<span class="namespace-selector" user="user" namespace="repo.namespace" require-create="true"></span> <span class="namespace-selector" user="user" namespace="repo.namespace" require-create="true"></span>
<span style="color: #ccc">/</span> <span style="color: #ccc">/</span>
<span class="name-container"> <span class="name-container">
<input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name" required autofocus data-trigger="manual" data-content="{{ createError }}" data-placement="right"> <input id="repoName" name="repoName" type="text" class="form-control" placeholder="Repository Name" ng-model="repo.name"
required autofocus data-trigger="manual" data-content="{{ createError }}" data-placement="right" ng-pattern="/^[.a-z0-9_-]+$/">
</span>
<span class="alert alert-warning" ng-show="!newRepoForm.repoName.$error.required && !newRepoForm.repoName.$valid" style="margin-left: 10px;">
Repository names must match [a-z0-9_-]+
</span> </span>
</div> </div>
</div> </div>
<div class="section"> <div class="section">
<strong>Description:</strong><br> <div class="section-title">Repository Description</div>
<br>
<div class="description markdown-input" content="repo.description" can-write="true" <div class="description markdown-input" content="repo.description" can-write="true"
field-title="'repository description'"></div> field-title="'repository description'"></div>
</div> </div>
@ -42,13 +47,14 @@
<div class="row"> <div class="row">
<div class="col-md-1"></div> <div class="col-md-1"></div>
<div class="col-md-8"> <div class="col-md-8">
<div class="section-title">Repository Visibility</div>
<div class="section"> <div class="section">
<div class="repo-option"> <div class="repo-option">
<input type="radio" id="publicrepo" name="publicorprivate" ng-model="repo.is_public" value="1"> <input type="radio" id="publicrepo" name="publicorprivate" ng-model="repo.is_public" value="1">
<i class="fa fa-unlock fa-large" title="Public Repository"></i> <i class="fa fa-unlock fa-large" title="Public Repository"></i>
<div class="option-description"> <div class="option-description">
<label for="publicrepo">Public</label> <label for="publicrepo"><strong>Public</strong></label>
<span class="description-text">Anyone can see and pull from this repository. You choose who can push.</span> <span class="description-text">Anyone can see and pull from this repository. You choose who can push.</span>
</div> </div>
</div> </div>
@ -57,7 +63,7 @@
<i class="fa fa-lock fa-large" title="Private Repository"></i> <i class="fa fa-lock fa-large" title="Private Repository"></i>
<div class="option-description"> <div class="option-description">
<label for="privaterepo">Private</label> <label for="privaterepo"><strong>Private</strong></label>
<span class="description-text">You choose who can see, pull and push from/to this repository.</span> <span class="description-text">You choose who can see, pull and push from/to this repository.</span>
</div> </div>
</div> </div>
@ -68,7 +74,8 @@
In order to make this repository private In order to make this repository private
<span ng-if="isUserNamespace">under your personal namespace</span> <span ng-if="isUserNamespace">under your personal namespace</span>
<span ng-if="!isUserNamespace">under the organization <b>{{ repo.namespace }}</b></span>, you will need to upgrade your plan to <span ng-if="!isUserNamespace">under the organization <b>{{ repo.namespace }}</b></span>, you will need to upgrade your plan to
<b style="border-bottom: 1px dotted black;" bs-tooltip="'<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories'"> <b style="border-bottom: 1px dotted black;" data-html="true"
title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
{{ planRequired.title }} {{ planRequired.title }}
</b>. </b>.
This will cost $<span>{{ planRequired.price / 100 }}</span>/month. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
@ -90,31 +97,75 @@
</div> </div>
</div> </div>
<!-- Initialize repository -->
<div class="row"> <div class="row">
<div class="col-md-1"></div> <div class="col-md-1"></div>
<div class="col-md-8"> <div class="col-md-8">
<div class="section"> <div class="section">
<input type="checkbox" class="cbox" id="initialize" name="initialize" ng-model="repo.initialize"> <div class="section-title">Initialize repository</div>
<div class="option-description">
<label for="initialize">Initialize Repository from <a href="http://www.docker.io/learn/dockerfile/" target="_new">Dockerfile</a></label>
<span class="description-text">Automatically populate your repository with a new image constructed from a Dockerfile</span>
</div>
<div class="initialize-repo" ng-show="repo.initialize"> <div style="padding-top: 10px;">
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)" <!-- Empty -->
build-started="handleBuildStarted()" build-failed="handleBuildFailed(message)" start-now="createdForBuild" <div class="repo-option">
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div> <input type="radio" id="initEmpty" name="initialize" ng-model="repo.initialize" value="">
<i class="fa fa-hdd-o fa-lg" style="padding: 6px; padding-left: 8px; padding-right: 6px;"></i>
<label for="initEmpty" style="color: #aaa;">(Empty repository)</label>
</div>
<!-- Dockerfile -->
<div class="repo-option">
<input type="radio" id="initDockerfile" name="initialize" ng-model="repo.initialize" value="dockerfile">
<i class="fa fa-file fa-lg" style="padding: 6px; padding-left: 10px; padding-right: 8px;"></i>
<label for="initDockerfile">Initialize from a <b>Dockerfile</b></label>
</div>
<!-- Zip file -->
<div class="repo-option">
<input type="radio" id="initZipfile" name="initialize" ng-model="repo.initialize" value="zipfile">
<i class="fa fa-archive fa-lg" style="padding: 6px; padding-left: 10px; padding-right: 8px;"></i>
<label for="initZipfile">Initialize from a ZIP file (containing a Dockerfile and other supporting files)</label>
</div>
<!-- Github -->
<div class="repo-option">
<input type="radio" id="initGithub" name="initialize" ng-model="repo.initialize" value="github">
<i class="fa fa-github fa-lg" style="padding: 6px; padding-left: 10px; padding-right: 12px;"></i>
<label for="initGithub">Link to a GitHub Repository</label>
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="row" ng-show="repo.initialize == 'dockerfile' || repo.initialize == 'zipfile'">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="section">
<div class="section-title">Upload <span ng-if="repo.initialize == 'dockerfile'">Dockerfile</span><span ng-if="repo.initialize == 'zipfile'">ZIP file</span></div>
<div style="padding-top: 20px;">
<div class="initialize-repo">
<div class="dockerfile-build-form" repository="createdForBuild" upload-failed="handleBuildFailed(message)"
build-started="handleBuildStarted()" build-failed="handleBuildFailed(message)" start-now="createdForBuild"
has-dockerfile="hasDockerfile" uploading="uploading" building="building"></div>
</div>
</div>
</div>
</div>
</div>
<div class="row" ng-show="repo.initialize == 'github'">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="alert alert-info">
You will be redirected to authorize via GitHub once the repository has been created
</div>
</div>
</div>
<div class="row"> <div class="row">
<div class="col-md-1"></div> <div class="col-md-1"></div>
<div class="col-md-8"> <div class="col-md-8">
<button class="btn btn-large btn-success" type="submit" <button class="btn btn-large btn-success" type="submit"
ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || (repo.initialize && !hasDockerfile)"> ng-disabled="uploading || building || newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan)) || ((repo.initialize == 'dockerfile' || repo.initialize == 'zipfile') && !hasDockerfile)">
Create Repository Create Repository
</button> </button>
</div> </div>

View file

@ -112,10 +112,7 @@
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span> <span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'team')"></span>
<span class="delete-ui-button" ng-click="deleteRole(name, 'team')"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td> </td>
</tr> </tr>
@ -132,10 +129,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0" title="Delete Permission"> <span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'user')"></span>
<span class="delete-ui-button" ng-click="deleteRole(name, 'user')"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Permission"></i>
</span>
</td> </td>
</tr> </tr>
@ -180,10 +174,7 @@
</div> </div>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Token'" perform-delete="deleteToken(token.code)"></span>
<span class="delete-ui-button" ng-click="deleteToken(token.code)"><button class="btn btn-danger" type="button">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Token"></i>
</span>
</td> </td>
</tr> </tr>
@ -222,10 +213,7 @@
<tr ng-repeat="webhook in webhooks"> <tr ng-repeat="webhook in webhooks">
<td>{{ webhook.parameters.url }}</td> <td>{{ webhook.parameters.url }}</td>
<td> <td>
<span class="delete-ui" tabindex="0"> <span class="delete-ui" delete-title="'Delete Webhook'" perform-delete="deleteWebhook(webhook)"></span>
<span class="delete-ui-button" ng-click="deleteWebhook(webhook)"><button class="btn btn-danger">Delete</button></span>
<i class="fa fa-times" bs-tooltip="tooltip.title" data-placement="right" title="Delete Webhook"></i>
</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View file

@ -70,9 +70,9 @@
<div class="log-container" ng-class="container.type" ng-repeat="container in logEntries"> <div class="log-container" ng-class="container.type" ng-repeat="container in logEntries">
<div class="container-header" ng-class="container.type == 'phase' ? container.message : ''" <div class="container-header" ng-class="container.type == 'phase' ? container.message : ''"
ng-switch on="container.type" ng-click="toggleLogs(container)"> ng-switch on="container.type" ng-click="container.logs.toggle()">
<i class="fa chevron" <i class="fa chevron"
ng-class="container.logs ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)"></i> ng-class="container.logs.isVisible ? 'fa-chevron-down' : 'fa-chevron-right'" ng-show="hasLogs(container)"></i>
<div ng-switch-when="phase"> <div ng-switch-when="phase">
<span class="container-content build-log-phase" phase="container"></span> <span class="container-content build-log-phase" phase="container"></span>
</div> </div>
@ -85,8 +85,8 @@
</div> </div>
<!-- Display the entries for the container --> <!-- Display the entries for the container -->
<div class="container-logs" ng-show="container.logs"> <div class="container-logs" ng-show="container.logs.isVisible">
<div class="log-entry" bindonce ng-repeat="entry in container.logs"> <div class="log-entry" bindonce ng-repeat="entry in container.logs.visibleEntries">
<span class="id" bo-text="$index + container.index + 1"></span> <span class="id" bo-text="$index + container.index + 1"></span>
<span class="message" bo-html="processANSI(entry.message, container)"></span> <span class="message" bo-html="processANSI(entry.message, container)"></span>
</div> </div>

View file

@ -17,10 +17,8 @@
<span class="entity-reference" entity="member" namespace="organization.name"></span> <span class="entity-reference" entity="member" namespace="organization.name"></span>
</td> </td>
<td> <td>
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers"> <span class="delete-ui" delete-title="'Remove User From Team'" button-title="'Remove'"
<span class="delete-ui-button" ng-click="removeMember(member.name)"><button class="btn btn-danger">Remove</button></span> perform-delete="removeMember(member.name)" ng-if="canEditMembers"></span>
<i class="fa fa-times"></i>
</span>
</td> </td>
</tr> </tr>

View file

@ -16,7 +16,8 @@
<div class="repo-controls"> <div class="repo-controls">
<!-- Builds --> <!-- Builds -->
<div class="dropdown" data-placement="top" style="display: inline-block" <div class="dropdown" data-placement="top" style="display: inline-block"
bs-tooltip="runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build'" bs-tooltip=""
title="{{ runningBuilds.length ? 'Dockerfile Builds Running: ' + (runningBuilds.length) : 'Dockerfile Build' }}"
ng-show="repo.can_write || buildHistory.length"> ng-show="repo.can_write || buildHistory.length">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown"> <button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i> <i class="fa fa-tasks fa-lg"></i>
@ -50,7 +51,7 @@
<!-- Admin --> <!-- Admin -->
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}" <a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}"
ng-show="repo.can_admin"> ng-show="repo.can_admin">
<button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="top"> <button class="btn btn-default" title="Repository Settings" bs-tooltip="tooltip" data-placement="top">
<i class="fa fa-cog fa-lg"></i></button></a> <i class="fa fa-cog fa-lg"></i></button></a>
<!-- Pull Command --> <!-- Pull Command -->
@ -170,7 +171,7 @@
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10"> <div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
<span class="size-limiter"> <span class="size-limiter">
<span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}" <span class="size-bar" style="{{ 'width:' + (image.size / getTotalSize(currentTag)) * 100 + '%' }}"
bs-tooltip="image.size | bytes"></span> bs-tooltip="" title="{{ image.size | bytes }}"></span>
</span> </span>
<span class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span> <span class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
</div> </div>
@ -204,7 +205,8 @@
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt> <dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer"> <dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
<pre class="formatted-command trimmed" <pre class="formatted-command trimmed"
bs-tooltip="getTooltipCommand(currentImage)" data-html="true"
bs-tooltip="" title="{{ getTooltipCommand(currentImage) }}"
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre> data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
</dd> </dd>
</dl> </dl>
@ -294,7 +296,8 @@
<!--<i class="fa fa-archive"></i>--> <!--<i class="fa fa-archive"></i>-->
<span class="image-listing-circle"></span> <span class="image-listing-circle"></span>
<span class="image-listing-line"></span> <span class="image-listing-line"></span>
<span class="context-tooltip image-listing-id" bs-tooltip="getFirstTextLine(image.comment)"> <span class="context-tooltip image-listing-id" bs-tooltip="" title="{{ getFirstTextLine(image.comment) }}"
data-html="true">
{{ image.id.substr(0, 12) }} {{ image.id.substr(0, 12) }}
</span> </span>
</div> </div>

91
templates/500.html Normal file
View file

@ -0,0 +1,91 @@
<html>
<title>Quay.io - Something went wrong!</title>
<head>
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
<style type="text/css">
.ship-header {
width: 100%;
height: 400px;
position: relative;
}
.ship-header div.layer {
position: absolute;
top: 0px;
bottom: 0px;
left: 0px;
right: 0px;
}
.ship-header .background {
background-size: cover;
background-image: url(/static/img/500/background.svg);
}
.ship-header .ship {
background-size: cover;
background-image: url(/static/img/500/ship.svg);
position: absolute;
top: 0px;
bottom: 0px;
right: 0px;
width: 19%;
}
.ship-header .water {
background-size: cover;
background-image: url(/static/img/500/water.svg);
}
@-webkit-keyframes steaming {
0% {
-webkit-transform: translateX(0px) scaleX(1);
}
50% {
-webkit-transform: translateX(-180%) scaleX(1);
}
51% {
-webkit-transform: translateX(-180%) scaleX(-1);
}
99% {
-webkit-transform: translateX(0px) scaleX(-1);
}
100% {
-webkit-transform: translateX(0px) scaleX(1);
}
}
.ship-header .ship {
-webkit-animation-name: steaming;
-webkit-animation-duration: 30s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
}
.information {
padding: 30px;
text-align: center;
}
.information h3 {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div id="header" class="ship-header">
<div class="background layer"></div>
<div id="ship" class="ship"></div>
<div class="water layer"></div>
</div>
<div class="information">
<img src="/static/img/quay-logo.png">
<h3>Something went wrong on our end!</h3>
<h4>
We're currently working to fix the problem, but if its persists please feel free to <a href="/contact">contact us</a>. In the meantime, try a refreshing drink (or just refreshing).
</h4>
</div>
</body>
</html>

View file

@ -18,6 +18,8 @@
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'> <link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="/static/css/quay.css"> <link rel="stylesheet" href="/static/css/quay.css">
<link rel="stylesheet" href="/static/lib/angular-motion.min.css">
<link rel="stylesheet" href="/static/lib/bootstrap-additions.min.css">
<!-- Icons --> <!-- Icons -->
<link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" /> <link rel="shortcut icon" href="/static/img/favicon.ico" type="image/x-icon" />
@ -39,16 +41,17 @@
<script src="//code.jquery.com/jquery.js"></script> <script src="//code.jquery.com/jquery.js"></script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-route.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-route.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-sanitize.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-sanitize.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-animate.min.js"></script> <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.9/angular-animate.min.js"></script>
<script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script> <script src="//cdn.jsdelivr.net/g/bootbox@4.1.0,underscorejs@1.5.2,restangular@1.2.0"></script>
<!-- ,typeahead.js@0.10.1 --> <!-- ,typeahead.js@0.10.1 -->
<script src="/static/lib/loading-bar.js"></script> <script src="/static/lib/loading-bar.js"></script>
<script src="/static/lib/angular-strap.min.js"></script> <script src="/static/lib/angular-strap.min.js"></script>
<script src="static/lib/angular-strap.tpl.min.js"></script>
<script src="/static/lib/angulartics.js"></script> <script src="/static/lib/angulartics.js"></script>
<script src="/static/lib/angulartics-mixpanel.js"></script> <script src="/static/lib/angulartics-mixpanel.js"></script>
<script src="/static/lib/angulartics-google-analytics.js"></script> <script src="/static/lib/angulartics-google-analytics.js"></script>

Binary file not shown.

View file

@ -71,7 +71,6 @@ UPDATE_REPO_DETAILS = {
'description': 'A new description', 'description': 'A new description',
} }
class IndexTestSpec(object): class IndexTestSpec(object):
def __init__(self, url, sess_repo=None, anon_code=403, no_access_code=403, def __init__(self, url, sess_repo=None, anon_code=403, no_access_code=403,
read_code=200, admin_code=200): read_code=200, admin_code=200):

View file

@ -788,6 +788,27 @@ class TestDeleteRepository(ApiTestCase):
class TestGetRepository(ApiTestCase): class TestGetRepository(ApiTestCase):
PUBLIC_REPO = PUBLIC_USER + '/publicrepo' PUBLIC_REPO = PUBLIC_USER + '/publicrepo'
def test_getrepo_badnames(self):
self.login(ADMIN_ACCESS_USER)
bad_names = ['logs', 'build', 'tokens', 'foo.bar', 'foo-bar', 'foo_bar']
# For each bad name, create the repo.
for bad_name in bad_names:
json = self.postJsonResponse(RepositoryList, expected_code=201,
data=dict(repository=bad_name, visibility='public',
description=''))
# Make sure we can retrieve its information.
json = self.getJsonResponse(Repository,
params=dict(repository=ADMIN_ACCESS_USER + '/' + bad_name))
self.assertEquals(ADMIN_ACCESS_USER, json['namespace'])
self.assertEquals(bad_name, json['name'])
self.assertEquals(True, json['is_public'])
def test_getrepo_public_asguest(self): def test_getrepo_public_asguest(self):
json = self.getJsonResponse(Repository, json = self.getJsonResponse(Repository,
params=dict(repository=self.PUBLIC_REPO)) params=dict(repository=self.PUBLIC_REPO))