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?
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,
RepositoryTag, EmailConfirmation, FederatedLogin, LoginService, QueueItem,
RepositoryBuild, Team, TeamMember, TeamRole, Webhook, LogEntryKind, LogEntry,
PermissionPrototype, ImageStorage, BuildTriggerService, RepositoryBuildTrigger,
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken]
OAuthApplication, OAuthAuthorizationCode, OAuthAccessToken, NotificationKind,
Notification]

View file

@ -58,7 +58,7 @@ class InvalidBuildTriggerException(DataModelException):
pass
def create_user(username, password, email):
def create_user(username, password, email, is_organization=False):
if not validate_email(email):
raise InvalidEmailAddressException('Invalid email address: %s' % email)
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,
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
except Exception as ex:
raise DataModelException(ex.message)
@ -100,7 +106,7 @@ def create_user(username, password, email):
def create_organization(name, email, creating_user):
try:
# 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.save()
@ -661,6 +667,9 @@ def change_password(user, new_password):
user.password_hash = pw_hash
user.save()
# Remove any password required notifications for the user.
delete_notifications_by_kind(user, 'password_required')
def change_invoice_email(user, invoice_email):
user.invoice_email = invoice_email
@ -1537,3 +1546,46 @@ def list_trigger_builds(namespace_name, repository_name, trigger_uuid,
limit):
return (list_repository_builds(namespace_name, repository_name, limit)
.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']
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):
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)
return {
'service': trigger.service.name,
@ -42,7 +56,7 @@ def build_status_view(build_obj, can_write=False):
'started': format_date(build_obj.started),
'display_name': build_obj.display_name,
'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,
'trigger': trigger_view(build_obj.trigger),
'resource_key': build_obj.resource_key,
@ -54,7 +68,7 @@ def build_status_view(build_obj, can_write=False):
return resp
@resource('/v1/repository/<path:repository>/build/')
@resource('/v1/repository/<repopath:repository>/build/')
class RepositoryBuildList(RepositoryParamResource):
""" Resource related to creating and listing repository builds. """
schemas = {
@ -127,7 +141,7 @@ class RepositoryBuildList(RepositoryParamResource):
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):
""" Resource for dealing with repository build status. """
@require_repo_read
@ -142,7 +156,7 @@ class RepositoryBuildStatus(RepositoryParamResource):
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):
""" Resource for loading repository build logs. """
@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):
""" Resource for listing repository images. """
@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):
""" Resource for handling repository images. """
@require_repo_read
@ -68,7 +68,7 @@ class RepositoryImage(RepositoryParamResource):
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):
""" 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
class RepositoryLogs(RepositoryParamResource):
""" 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
@resource('/v1/repository/<path:repository>/permissions/team/')
@resource('/v1/repository/<repopath:repository>/permissions/team/')
class RepositoryTeamPermissionList(RepositoryParamResource):
""" Resource for repository team permissions. """
@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):
""" Resource for repository user permissions. """
@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):
""" Resource for managing individual user permissions. """
schemas = {
@ -174,7 +174,7 @@ class RepositoryUserPermission(RepositoryParamResource):
return 'Deleted', 204
@resource('/v1/repository/<path:repository>/permissions/team/<teamname>')
@resource('/v1/repository/<repopath:repository>/permissions/team/<teamname>')
class RepositoryTeamPermission(RepositoryParamResource):
""" Resource for managing individual team permissions. """
schemas = {

View file

@ -152,7 +152,7 @@ def image_view(image):
'size': extended_props.image_size,
}
@resource('/v1/repository/<path:repository>')
@resource('/v1/repository/<repopath:repository>')
class Repository(RepositoryParamResource):
"""Operations for managing a specific repository."""
schemas = {
@ -248,7 +248,7 @@ class Repository(RepositoryParamResource):
return 'Deleted', 204
@resource('/v1/repository/<path:repository>/changevisibility')
@resource('/v1/repository/<repopath:repository>/changevisibility')
class RepositoryVisibility(RepositoryParamResource):
""" Custom verb for changing the visibility of the repository. """
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):
""" Resource for creating and listing repository tokens. """
schemas = {
@ -65,7 +65,7 @@ class RepositoryTokenList(RepositoryParamResource):
return token_view(token), 201
@resource('/v1/repository/<path:repository>/tokens/<code>')
@resource('/v1/repository/<repopath:repository>/tokens/<code>')
class RepositoryToken(RepositoryParamResource):
""" Resource for managing individual tokens. """
schemas = {

View file

@ -2,6 +2,7 @@ import logging
import stripe
from endpoints.api import request_error, log_action, NotFound
from endpoints.common import check_repository_usage
from data import model
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)
user.stripe_id = cus.id
user.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
except stripe.CardError as 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
cus.cancel_subscription()
cus.save()
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
else:
@ -89,6 +92,7 @@ def subscribe(user, plan, token, require_business_plan):
return carderror_response(e)
response_json = subscription_view(cus.subscription, private_repos)
check_repository_usage(user, plan_found)
log_action('account_change_plan', user.username, {'plan': plan})
return response_json, status_code

View file

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

View file

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

View file

@ -1,5 +1,6 @@
import logging
import stripe
import json
from flask import request
from flask.ext.login import logout_user
@ -8,7 +9,7 @@ from flask.ext.principal import identity_changed, AnonymousIdentity
from app import app
from endpoints.api import (ApiResource, nickname, resource, validate_json_request, request_error,
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.common import common_login
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/')
class User(ApiResource):
""" Operations related to users. """
@ -364,3 +374,15 @@ class Recovery(ApiResource):
code = model.create_reset_password_email_code(email)
send_recovery_email(email, code.code)
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):
""" Resource for dealing with listing and creating webhooks. """
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):
""" Resource for dealing with specific webhooks. """
@require_repo_admin

View file

@ -5,7 +5,7 @@ import urlparse
import json
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 data import model
@ -13,13 +13,20 @@ from data.queue import dockerfile_build_queue
from app import app, login_manager
from auth.permissions import QuayDeferredPermissionUser
from endpoints.api.discovery import swagger_route_data
from werkzeug.routing import BaseConverter
logger = logging.getLogger(__name__)
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():
global route_data
if route_data:
@ -87,13 +94,21 @@ app.jinja_env.globals['csrf_token'] = generate_csrf_token
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'
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,
trigger=None):
host = urlparse.urlparse(request.url).netloc

View file

@ -203,7 +203,7 @@ class GithubBuildTrigger(BuildTrigger):
try:
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)
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)
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']
@web.route('/', methods=['GET'], defaults={'path': ''})
@web.route('/organization/<path:path>', methods=['GET'])
@no_cache
@ -32,6 +33,11 @@ def index(path):
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/<path:path>', methods=['GET'])

View file

@ -225,6 +225,11 @@ def initialize_database():
LogEntryKind.create(name='setup_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():
logger.debug('Wiping all data from the DB.')
@ -262,6 +267,9 @@ def populate_database():
new_user_4.verified = True
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.verified = True
reader.save()
@ -270,6 +278,8 @@ def populate_database():
outside_org.verified = True
outside_org.save()
model.create_notification('test_notification', new_user_1, metadata={'some': 'value'})
__generate_repository(new_user_4, 'randomrepo', 'Random repo repository.', False,
[], (4, [], ['latest', 'prod']))

View file

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

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 {
margin-top: 10px;
padding: 20px;
@ -507,7 +558,22 @@ i.toggle-icon:hover {
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;
}
@ -776,11 +842,20 @@ i.toggle-icon:hover {
margin-bottom: 16px;
}
.new-repo .section-title {
float: right;
color: #aaa;
}
.new-repo .repo-option {
margin: 6px;
margin-top: 16px;
}
.new-repo .repo-option label {
font-weight: normal;
}
.new-repo .repo-option i {
font-size: 18px;
padding-left: 10px;
@ -2122,16 +2197,16 @@ p.editable:hover i {
padding-right: 6px;
}
.delete-ui {
.delete-ui-element {
outline: none;
}
.delete-ui i {
.delete-ui-element i {
cursor: pointer;
vertical-align: middle;
}
.delete-ui .delete-ui-button {
.delete-ui-element .delete-ui-button {
display: inline-block;
vertical-align: middle;
color: white;
@ -2147,15 +2222,15 @@ p.editable:hover i {
transition: width 500ms ease-in-out;
}
.delete-ui .delete-ui-button button {
.delete-ui-element .delete-ui-button button {
padding: 4px;
}
.delete-ui:focus i {
.delete-ui-element:focus i {
visibility: hidden;
}
.delete-ui:focus .delete-ui-button {
.delete-ui-element:focus .delete-ui-button {
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">
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
{{ user.username }}
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan"
bs-tooltip="(user.askForPassword ? 'A password is needed for this account<br>' : '') + (overPlan ? 'You are using more private repositories than your plan allows' : '')"
<span class="badge user-notification notification-animated"
ng-show="notificationService.notifications.length"
ng-class="notificationService.notificationClasses"
bs-tooltip=""
title="User Notifications"
data-placement="left"
data-container="body">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
{{ notificationService.notifications.length }}
</span>
<b class="caret"></b>
</a>
@ -51,8 +54,16 @@
<li>
<a href="/user/" target="{{ appLinkTarget() }}">
Account Settings
<span class="badge user-notification" ng-show="user.askForPassword || overPlan">
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
</a>
</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>
</a>
</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'"
data-placement="bottom" ng-click="popupShown()">
<button class="btn btn-success" data-trigger="click"
data-content-template="static/directives/popup-input-dialog.html"
data-placement="bottom" ng-click="popupShown()" bs-popover>
<span ng-transclude></span>
</button>

View file

@ -1,4 +1,4 @@
<form name="popupinput" ng-submit="inputSubmit(); hide()" novalidate>
<input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="hide()"
<form name="popupinput" ng-submit="inputSubmit(); $hide()" novalidate>
<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>
</form>

View file

@ -48,10 +48,7 @@
<span class="role-group" current-role="prototype.role" role-changed="setRole(role, prototype)" roles="roles"></span>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deletePrototype(prototype)"></span>
</td>
</tr>
</table>

View file

@ -24,10 +24,7 @@
</a>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Robot Account'" perform-delete="deleteRobot(robotInfo)"></span>
</td>
</tr>
</table>

View file

@ -3,7 +3,7 @@
<div class="panel panel-default">
<div class="panel-heading">
<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
</a>
</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 || '');
}
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;
/**
* 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) {
var utilService = {};
@ -143,6 +222,49 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
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) {
var metadataService = {};
@ -360,7 +482,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
anonymous: true,
username: null,
email: null,
askForPassword: false,
organizations: [],
logins: []
}
@ -467,6 +588,101 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
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) {
var keyService = {}
@ -1405,7 +1621,7 @@ quayApp.directive('logsView', function () {
'repository': '=repository',
'performer': '=performer'
},
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder) {
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder, StringBuilderService) {
$scope.loading = true;
$scope.logs = null;
$scope.kindsAllowed = null;
@ -1620,43 +1836,9 @@ quayApp.directive('logsView', function () {
return $scope.chart.getColor(kind);
};
$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'
};
$scope.getDescription = function(log) {
log.metadata['_ip'] = log.ip ? log.ip : null;
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>'));
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
};
$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 () {
var directiveDefinitionObject = {
priority: 0,
@ -1939,7 +2146,7 @@ quayApp.directive('popupInputButton', function () {
var box = $('#input-box');
box[0].value = '';
box.focus();
}, 10);
}, 40);
};
$scope.getRegexp = function(pattern) {
@ -2153,26 +2360,12 @@ quayApp.directive('headerBar', function () {
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, PlanService, ApiService) {
$scope.overPlan = false;
var checkOverPlan = function() {
if ($scope.user.anonymous) {
$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);
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService) {
$scope.notificationService = NotificationService;
// Monitor any user changes and place the current user into the scope.
UserService.updateUserIn($scope);
// Monitor any plan changes.
PlanService.registerListener(this, checkOverPlan);
$scope.signout = function() {
ApiService.logout().then(function() {
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 () {
var directiveDefinitionObject = {
priority: 0,
@ -3594,6 +3835,11 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
}
}
if (response.status == 500) {
document.location = '/500';
return false;
}
return true;
});

View file

@ -919,7 +919,8 @@ function BuildPackageCtrl($scope, Restangular, ApiService, $routeParams, $rootSc
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 name = $routeParams.name;
var pollTimerHandle = null;
@ -985,19 +986,9 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
};
$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) {
if (!$scope.builds) { return; }
@ -1085,17 +1076,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
var entry = logs[i];
var type = entry['type'] || 'entry';
if (type == 'command' || type == 'phase' || type == 'error') {
entry['_logs'] = [];
entry['logs'] = AngularViewArray.create();
entry['index'] = startIndex + i;
$scope.logEntries.push(entry);
$scope.currentParentEntry = entry;
$scope.currentParentEntry = entry;
} else if ($scope.currentParentEntry) {
if ($scope.currentParentEntry['logs']) {
$scope.currentParentEntry['logs'].push(entry);
} else {
$scope.currentParentEntry['_logs'].push(entry);
}
$scope.currentParentEntry['logs'].push(entry);
}
}
};
@ -1885,13 +1872,16 @@ function V1Ctrl($scope, $location, UserService) {
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);
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubClientId;
$scope.repo = {
'is_public': 1,
'description': '',
'initialize': false
'initialize': ''
};
// 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.created = created;
// Repository created. Start the upload process if applicable.
if ($scope.repo.initialize) {
// Start the upload process if applicable.
if ($scope.repo.initialize == 'dockerfile' || $scope.repo.initialize == 'zipfile') {
$scope.createdForBuild = created;
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.
$location.path('/repository/' + created.namespace + '/' + created.name);
}, 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 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="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">
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 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-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>
</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>

View file

@ -25,13 +25,18 @@
<span class="namespace-selector" user="user" namespace="repo.namespace" require-create="true"></span>
<span style="color: #ccc">/</span>
<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>
</div>
</div>
<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"
field-title="'repository description'"></div>
</div>
@ -42,13 +47,14 @@
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="section-title">Repository Visibility</div>
<div class="section">
<div class="repo-option">
<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>
<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>
</div>
</div>
@ -57,7 +63,7 @@
<i class="fa fa-lock fa-large" title="Private Repository"></i>
<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>
</div>
</div>
@ -68,7 +74,8 @@
In order to make this repository private
<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
<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 }}
</b>.
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
@ -90,31 +97,75 @@
</div>
</div>
<!-- Initialize repository -->
<div class="row">
<div class="col-md-1"></div>
<div class="col-md-8">
<div class="section">
<input type="checkbox" class="cbox" id="initialize" name="initialize" ng-model="repo.initialize">
<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="section-title">Initialize repository</div>
<div class="initialize-repo" ng-show="repo.initialize">
<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 style="padding-top: 10px;">
<!-- Empty -->
<div class="repo-option">
<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 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="col-md-1"></div>
<div class="col-md-8">
<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
</button>
</div>

View file

@ -112,10 +112,7 @@
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'team')"></span>
</td>
</tr>
@ -132,10 +129,7 @@
</div>
</td>
<td>
<span class="delete-ui" tabindex="0" title="Delete Permission">
<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>
<span class="delete-ui" delete-title="'Delete Permission'" perform-delete="deleteRole(name, 'user')"></span>
</td>
</tr>
@ -180,10 +174,7 @@
</div>
</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Token'" perform-delete="deleteToken(token.code)"></span>
</td>
</tr>
@ -222,10 +213,7 @@
<tr ng-repeat="webhook in webhooks">
<td>{{ webhook.parameters.url }}</td>
<td>
<span class="delete-ui" tabindex="0">
<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>
<span class="delete-ui" delete-title="'Delete Webhook'" perform-delete="deleteWebhook(webhook)"></span>
</td>
</tr>
</tbody>

View file

@ -70,9 +70,9 @@
<div class="log-container" ng-class="container.type" ng-repeat="container in logEntries">
<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"
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">
<span class="container-content build-log-phase" phase="container"></span>
</div>
@ -85,8 +85,8 @@
</div>
<!-- Display the entries for the container -->
<div class="container-logs" ng-show="container.logs">
<div class="log-entry" bindonce ng-repeat="entry in container.logs">
<div class="container-logs" ng-show="container.logs.isVisible">
<div class="log-entry" bindonce ng-repeat="entry in container.logs.visibleEntries">
<span class="id" bo-text="$index + container.index + 1"></span>
<span class="message" bo-html="processANSI(entry.message, container)"></span>
</div>

View file

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

View file

@ -16,7 +16,8 @@
<div class="repo-controls">
<!-- Builds -->
<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">
<button class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<i class="fa fa-tasks fa-lg"></i>
@ -50,7 +51,7 @@
<!-- Admin -->
<a id="admin-cog" href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/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>
<!-- Pull Command -->
@ -170,7 +171,7 @@
<div class="tag-image-size" ng-repeat="image in getImagesForTagBySize(currentTag) | limitTo: 10">
<span class="size-limiter">
<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 class="size-title"><a href="javascript:void(0)" ng-click="setImage(image.id, true)">{{ image.id.substr(0, 12) }}</a></span>
</div>
@ -204,7 +205,8 @@
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
<pre class="formatted-command trimmed"
bs-tooltip="getTooltipCommand(currentImage)"
data-html="true"
bs-tooltip="" title="{{ getTooltipCommand(currentImage) }}"
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
</dd>
</dl>
@ -294,7 +296,8 @@
<!--<i class="fa fa-archive"></i>-->
<span class="image-listing-circle"></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) }}
</span>
</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 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 -->
<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="//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.1/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.1/angular-animate.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.9/angular-route.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.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>
<!-- ,typeahead.js@0.10.1 -->
<script src="/static/lib/loading-bar.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-mixpanel.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',
}
class IndexTestSpec(object):
def __init__(self, url, sess_repo=None, anon_code=403, no_access_code=403,
read_code=200, admin_code=200):

View file

@ -788,6 +788,27 @@ class TestDeleteRepository(ApiTestCase):
class TestGetRepository(ApiTestCase):
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):
json = self.getJsonResponse(Repository,
params=dict(repository=self.PUBLIC_REPO))