Merge branch 'master' into contact
Conflicts: static/js/controllers.js templates/base.html
11
README.md
|
@ -1,27 +1,26 @@
|
||||||
to prepare a new host:
|
to prepare a new host:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo apt-get install software-properties-common
|
|
||||||
sudo apt-add-repository -y ppa:nginx/stable
|
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev
|
sudo apt-get install -y git python-virtualenv python-dev phantomjs libjpeg8 libjpeg62-dev libfreetype6 libfreetype6-dev libevent-dev gdebi-core
|
||||||
sudo apt-get install -y nginx-full
|
|
||||||
```
|
```
|
||||||
|
|
||||||
check out the code:
|
check out the code:
|
||||||
|
|
||||||
```
|
```
|
||||||
git clone https://bitbucket.org/yackob03/quay.git
|
git clone https://bitbucket.org/yackob03/quay.git
|
||||||
|
cd quay
|
||||||
virtualenv --distribute venv
|
virtualenv --distribute venv
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
|
sudo gdebi --n binary_dependencies/*.deb
|
||||||
```
|
```
|
||||||
|
|
||||||
running:
|
running:
|
||||||
|
|
||||||
```
|
```
|
||||||
sudo nginx -c `pwd`/nginx.conf
|
sudo mkdir -p /mnt/nginx/ && sudo /usr/local/nginx/sbin/nginx -c `pwd`/nginx.conf
|
||||||
STACK=prod gunicorn -D --workers 4 -b unix:/tmp/gunicorn.sock --worker-class gevent -t 2000 application:application
|
STACK=prod gunicorn -c gunicorn_config.py application:application
|
||||||
```
|
```
|
||||||
|
|
||||||
start the workers:
|
start the workers:
|
||||||
|
|
|
@ -34,7 +34,7 @@ def process_basic_auth(auth):
|
||||||
if len(credentials) != 2:
|
if len(credentials) != 2:
|
||||||
logger.debug('Invalid basic auth credential format.')
|
logger.debug('Invalid basic auth credential format.')
|
||||||
|
|
||||||
if credentials[0] == '$token':
|
elif credentials[0] == '$token':
|
||||||
# Use as token auth
|
# Use as token auth
|
||||||
try:
|
try:
|
||||||
token = model.load_token_data(credentials[1])
|
token = model.load_token_data(credentials[1])
|
||||||
|
@ -77,7 +77,6 @@ def process_basic_auth(auth):
|
||||||
|
|
||||||
# We weren't able to authenticate via basic auth.
|
# We weren't able to authenticate via basic auth.
|
||||||
logger.debug('Basic auth present but could not be validated.')
|
logger.debug('Basic auth present but could not be validated.')
|
||||||
abort(401)
|
|
||||||
|
|
||||||
|
|
||||||
def process_token(auth):
|
def process_token(auth):
|
||||||
|
|
BIN
binary_dependencies/nginx_1.4.2-nobuffer-1_amd64.deb
Normal file
|
@ -22,5 +22,5 @@ RUN venv/bin/pip install -r requirements.txt
|
||||||
|
|
||||||
VOLUME /var/lib/docker
|
VOLUME /var/lib/docker
|
||||||
|
|
||||||
EXPOSE 5002:5002
|
EXPOSE 5002
|
||||||
CMD startserver
|
CMD startserver
|
|
@ -6,10 +6,9 @@ import re
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from flask import Flask, jsonify, url_for, abort, make_response
|
from flask import Flask, jsonify, abort, make_response
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
from tempfile import TemporaryFile, mkdtemp
|
from tempfile import TemporaryFile, mkdtemp
|
||||||
from uuid import uuid4
|
|
||||||
from multiprocessing.pool import ThreadPool
|
from multiprocessing.pool import ThreadPool
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
|
|
||||||
|
@ -53,16 +52,24 @@ def prepare_dockerfile(request_file):
|
||||||
return build_dir
|
return build_dir
|
||||||
|
|
||||||
|
|
||||||
|
def total_completion(statuses, total_images):
|
||||||
|
percentage_with_sizes = float(len(statuses.values()))/total_images
|
||||||
|
sent_bytes = sum([status[u'current'] for status in statuses.values()])
|
||||||
|
total_bytes = sum([status[u'total'] for status in statuses.values()])
|
||||||
|
return float(sent_bytes)/total_bytes*percentage_with_sizes
|
||||||
|
|
||||||
|
|
||||||
def build_image(build_dir, tag_name, num_steps, result_object):
|
def build_image(build_dir, tag_name, num_steps, result_object):
|
||||||
try:
|
try:
|
||||||
logger.debug('Starting build.')
|
logger.debug('Starting build.')
|
||||||
docker_cl = docker.Client(version='1.5')
|
docker_cl = docker.Client(timeout=1200)
|
||||||
result_object['status'] = 'building'
|
result_object['status'] = 'building'
|
||||||
build_status = docker_cl.build(path=build_dir, tag=tag_name)
|
build_status = docker_cl.build(path=build_dir, tag=tag_name, stream=True)
|
||||||
|
|
||||||
current_step = 0
|
current_step = 0
|
||||||
built_image = None
|
built_image = None
|
||||||
for status in build_status:
|
for status in build_status:
|
||||||
|
# logger.debug('Status: %s', str(status))
|
||||||
step_increment = re.search(r'Step ([0-9]+) :', status)
|
step_increment = re.search(r'Step ([0-9]+) :', status)
|
||||||
if step_increment:
|
if step_increment:
|
||||||
current_step = int(step_increment.group(1))
|
current_step = int(step_increment.group(1))
|
||||||
|
@ -84,40 +91,36 @@ def build_image(build_dir, tag_name, num_steps, result_object):
|
||||||
result_object['message'] = 'Unable to build dockerfile.'
|
result_object['message'] = 'Unable to build dockerfile.'
|
||||||
return
|
return
|
||||||
|
|
||||||
history = docker_cl.history(built_image)
|
history = json.loads(docker_cl.history(built_image))
|
||||||
num_images = len(history)
|
num_images = len(history)
|
||||||
result_object['total_images'] = num_images
|
result_object['total_images'] = num_images
|
||||||
|
|
||||||
result_object['status'] = 'pushing'
|
result_object['status'] = 'pushing'
|
||||||
logger.debug('Pushing to tag name: %s' % tag_name)
|
logger.debug('Pushing to tag name: %s' % tag_name)
|
||||||
resp = docker_cl.push(tag_name)
|
resp = docker_cl.push(tag_name, stream=True)
|
||||||
|
|
||||||
current_image = 0
|
for status_str in resp:
|
||||||
image_progress = 0
|
status = json.loads(status_str)
|
||||||
for status in resp:
|
logger.debug('Status: %s', status_str)
|
||||||
if u'status' in status:
|
if u'status' in status:
|
||||||
status_msg = status[u'status']
|
status_msg = status[u'status']
|
||||||
next_image = r'(Pushing|Image) [a-z0-9]+( already pushed, skipping)?$'
|
|
||||||
match = re.match(next_image, status_msg)
|
|
||||||
if match:
|
|
||||||
current_image += 1
|
|
||||||
image_progress = 0
|
|
||||||
logger.debug('Now pushing image %s/%s' %
|
|
||||||
(current_image, num_images))
|
|
||||||
|
|
||||||
elif status_msg == u'Pushing' and u'progress' in status:
|
if status_msg == 'Pushing':
|
||||||
percent = r'\(([0-9]+)%\)'
|
if u'progressDetail' in status and u'id' in status:
|
||||||
match = re.search(percent, status[u'progress'])
|
image_id = status[u'id']
|
||||||
if match:
|
detail = status[u'progressDetail']
|
||||||
image_progress = int(match.group(1))
|
|
||||||
|
|
||||||
result_object['current_image'] = current_image
|
if u'current' in detail and 'total' in detail:
|
||||||
result_object['image_completion_percent'] = image_progress
|
images = result_object['image_completion']
|
||||||
|
|
||||||
|
images[image_id] = detail
|
||||||
|
result_object['push_completion'] = total_completion(images,
|
||||||
|
num_images)
|
||||||
|
|
||||||
elif u'errorDetail' in status:
|
elif u'errorDetail' in status:
|
||||||
result_object['status'] = 'error'
|
result_object['status'] = 'error'
|
||||||
if u'message' in status[u'errorDetail']:
|
if u'message' in status[u'errorDetail']:
|
||||||
result_object['message'] = status[u'errorDetail'][u'message']
|
result_object['message'] = str(status[u'errorDetail'][u'message'])
|
||||||
return
|
return
|
||||||
|
|
||||||
result_object['status'] = 'complete'
|
result_object['status'] = 'complete'
|
||||||
|
@ -133,15 +136,14 @@ MIME_PROCESSORS = {
|
||||||
'application/octet-stream': prepare_dockerfile,
|
'application/octet-stream': prepare_dockerfile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# If this format it should also be changed in the api method get_repo_builds
|
||||||
build = {
|
build = {
|
||||||
'total_commands': None,
|
'total_commands': None,
|
||||||
'total_images': None,
|
|
||||||
'current_command': None,
|
'current_command': None,
|
||||||
'current_image': None,
|
'push_completion': 0.0,
|
||||||
'image_completion_percent': None,
|
|
||||||
'status': 'waiting',
|
'status': 'waiting',
|
||||||
'message': None,
|
'message': None,
|
||||||
|
'image_completion': {},
|
||||||
}
|
}
|
||||||
pool = ThreadPool(1)
|
pool = ThreadPool(1)
|
||||||
|
|
||||||
|
|
|
@ -127,6 +127,7 @@ class DigitalOceanConfig(object):
|
||||||
DO_SSH_KEY_ID = '46986'
|
DO_SSH_KEY_ID = '46986'
|
||||||
DO_SSH_PRIVATE_KEY_FILENAME = 'certs/digital_ocean'
|
DO_SSH_PRIVATE_KEY_FILENAME = 'certs/digital_ocean'
|
||||||
DO_ALLOWED_REGIONS = {1, 4}
|
DO_ALLOWED_REGIONS = {1, 4}
|
||||||
|
DO_DOCKER_IMAGE = 1341147
|
||||||
|
|
||||||
|
|
||||||
class BuildNodeConfig(object):
|
class BuildNodeConfig(object):
|
||||||
|
|
|
@ -167,7 +167,9 @@ class Image(BaseModel):
|
||||||
checksum = CharField(null=True)
|
checksum = CharField(null=True)
|
||||||
created = DateTimeField(null=True)
|
created = DateTimeField(null=True)
|
||||||
comment = TextField(null=True)
|
comment = TextField(null=True)
|
||||||
|
command = TextField(null=True)
|
||||||
repository = ForeignKeyField(Repository)
|
repository = ForeignKeyField(Repository)
|
||||||
|
image_size = BigIntegerField(null=True)
|
||||||
|
|
||||||
# '/' separated list of ancestory ids, e.g. /1/2/6/7/10/
|
# '/' separated list of ancestory ids, e.g. /1/2/6/7/10/
|
||||||
ancestors = CharField(index=True, default='/', max_length=64535)
|
ancestors = CharField(index=True, default='/', max_length=64535)
|
||||||
|
|
141
data/model.py
|
@ -6,12 +6,14 @@ import operator
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from database import *
|
from database import *
|
||||||
from util.validation import *
|
from util.validation import *
|
||||||
from util.names import format_robot_username
|
from util.names import format_robot_username
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
store = app.config['STORAGE']
|
||||||
|
|
||||||
|
|
||||||
class DataModelException(Exception):
|
class DataModelException(Exception):
|
||||||
|
@ -306,12 +308,18 @@ def create_federated_user(username, email, service_name, service_id):
|
||||||
return new_user
|
return new_user
|
||||||
|
|
||||||
|
|
||||||
|
def attach_federated_login(user, service_name, service_id):
|
||||||
|
service = LoginService.get(LoginService.name == service_name)
|
||||||
|
FederatedLogin.create(user=user, service=service, service_ident=service_id)
|
||||||
|
return user
|
||||||
|
|
||||||
|
|
||||||
def verify_federated_login(service_name, service_id):
|
def verify_federated_login(service_name, service_id):
|
||||||
selected = FederatedLogin.select(FederatedLogin, User)
|
selected = FederatedLogin.select(FederatedLogin, User)
|
||||||
with_service = selected.join(LoginService)
|
with_service = selected.join(LoginService)
|
||||||
with_user = with_service.switch(FederatedLogin).join(User)
|
with_user = with_service.switch(FederatedLogin).join(User)
|
||||||
found = with_user.where(FederatedLogin.service_ident == service_id,
|
found = with_user.where(FederatedLogin.service_ident == service_id,
|
||||||
LoginService.name == service_name)
|
LoginService.name == service_name)
|
||||||
|
|
||||||
found_list = list(found)
|
found_list = list(found)
|
||||||
|
|
||||||
|
@ -321,14 +329,25 @@ def verify_federated_login(service_name, service_id):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def list_federated_logins(user):
|
||||||
|
selected = FederatedLogin.select(FederatedLogin.service_ident,
|
||||||
|
LoginService.name)
|
||||||
|
joined = selected.join(LoginService)
|
||||||
|
return joined.where(LoginService.name != 'quayrobot',
|
||||||
|
FederatedLogin.user == user)
|
||||||
|
|
||||||
|
|
||||||
def create_confirm_email_code(user):
|
def create_confirm_email_code(user):
|
||||||
code = EmailConfirmation.create(user=user, email_confirm=True)
|
code = EmailConfirmation.create(user=user, email_confirm=True)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
|
||||||
def confirm_user_email(code):
|
def confirm_user_email(code):
|
||||||
code = EmailConfirmation.get(EmailConfirmation.code == code,
|
try:
|
||||||
EmailConfirmation.email_confirm == True)
|
code = EmailConfirmation.get(EmailConfirmation.code == code,
|
||||||
|
EmailConfirmation.email_confirm == True)
|
||||||
|
except EmailConfirmation.DoesNotExist:
|
||||||
|
raise DataModelException('Invalid email confirmation code.')
|
||||||
|
|
||||||
user = code.user
|
user = code.user
|
||||||
user.verified = True
|
user.verified = True
|
||||||
|
@ -345,6 +364,9 @@ def create_reset_password_email_code(email):
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
raise InvalidEmailAddressException('Email address was not found.');
|
raise InvalidEmailAddressException('Email address was not found.');
|
||||||
|
|
||||||
|
if user.organization:
|
||||||
|
raise InvalidEmailAddressException('Organizations can not have passwords.')
|
||||||
|
|
||||||
code = EmailConfirmation.create(user=user, pw_reset=True)
|
code = EmailConfirmation.create(user=user, pw_reset=True)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
|
@ -383,7 +405,6 @@ def get_matching_teams(team_prefix, organization):
|
||||||
|
|
||||||
def get_matching_users(username_prefix, robot_namespace=None,
|
def get_matching_users(username_prefix, robot_namespace=None,
|
||||||
organization=None):
|
organization=None):
|
||||||
Org = User.alias()
|
|
||||||
direct_user_query = (User.username ** (username_prefix + '%') &
|
direct_user_query = (User.username ** (username_prefix + '%') &
|
||||||
(User.organization == False) & (User.robot == False))
|
(User.organization == False) & (User.robot == False))
|
||||||
|
|
||||||
|
@ -393,14 +414,16 @@ def get_matching_users(username_prefix, robot_namespace=None,
|
||||||
(User.username ** (robot_prefix + '%') &
|
(User.username ** (robot_prefix + '%') &
|
||||||
(User.robot == True)))
|
(User.robot == True)))
|
||||||
|
|
||||||
query = User.select(User.username, Org.username, User.robot).where(direct_user_query)
|
query = (User
|
||||||
|
.select(User.username, fn.Sum(Team.id), User.robot)
|
||||||
|
.group_by(User.username)
|
||||||
|
.where(direct_user_query))
|
||||||
|
|
||||||
if organization:
|
if organization:
|
||||||
with_team = query.join(TeamMember, JOIN_LEFT_OUTER).join(Team,
|
query = (query
|
||||||
JOIN_LEFT_OUTER)
|
.join(TeamMember, JOIN_LEFT_OUTER)
|
||||||
with_org = with_team.join(Org, JOIN_LEFT_OUTER,
|
.join(Team, JOIN_LEFT_OUTER, on=((Team.id == TeamMember.team) &
|
||||||
on=(Org.id == Team.organization))
|
(Team.organization == organization))))
|
||||||
query = with_org.where((Org.id == organization) | (Org.id >> None))
|
|
||||||
|
|
||||||
|
|
||||||
class MatchingUserResult(object):
|
class MatchingUserResult(object):
|
||||||
|
@ -408,7 +431,7 @@ def get_matching_users(username_prefix, robot_namespace=None,
|
||||||
self.username = args[0]
|
self.username = args[0]
|
||||||
self.is_robot = args[2]
|
self.is_robot = args[2]
|
||||||
if organization:
|
if organization:
|
||||||
self.is_org_member = (args[1] == organization.username)
|
self.is_org_member = (args[1] != None)
|
||||||
else:
|
else:
|
||||||
self.is_org_member = None
|
self.is_org_member = None
|
||||||
|
|
||||||
|
@ -489,13 +512,23 @@ def get_user_teams_within_org(username, organization):
|
||||||
User.username == username)
|
User.username == username)
|
||||||
|
|
||||||
|
|
||||||
def get_visible_repositories(username=None, include_public=True, limit=None,
|
def get_visible_repository_count(username=None, include_public=True, sort=False, namespace=None):
|
||||||
|
return get_visible_repository_internal(username=username, include_public=include_public,
|
||||||
|
sort=sort, namespace=namespace, get_count=True)
|
||||||
|
|
||||||
|
def get_visible_repositories(username=None, include_public=True, page=None, limit=None,
|
||||||
sort=False, namespace=None):
|
sort=False, namespace=None):
|
||||||
|
return get_visible_repository_internal(username=username, include_public=include_public, page=page,
|
||||||
|
limit=limit, sort=sort, namespace=namespace, get_count=False)
|
||||||
|
|
||||||
|
|
||||||
|
def get_visible_repository_internal(username=None, include_public=True, limit=None, page=None,
|
||||||
|
sort=False, namespace=None, get_count=False):
|
||||||
if not username and not include_public:
|
if not username and not include_public:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
query = (Repository
|
query = (Repository
|
||||||
.select(Repository, Visibility)
|
.select() # Note: We need to leave this blank for the get_count case. Otherwise, MySQL/RDS complains.
|
||||||
.distinct()
|
.distinct()
|
||||||
.join(Visibility)
|
.join(Visibility)
|
||||||
.switch(Repository)
|
.switch(Repository)
|
||||||
|
@ -543,10 +576,19 @@ def get_visible_repositories(username=None, include_public=True, limit=None,
|
||||||
else:
|
else:
|
||||||
where_clause = new_clause
|
where_clause = new_clause
|
||||||
|
|
||||||
if limit:
|
if sort:
|
||||||
query.limit(limit)
|
query = query.order_by(Repository.description.desc())
|
||||||
|
|
||||||
return query.where(where_clause)
|
if page:
|
||||||
|
query = query.paginate(page, limit)
|
||||||
|
elif limit:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
|
where = query.where(where_clause)
|
||||||
|
if get_count:
|
||||||
|
return where.count()
|
||||||
|
else:
|
||||||
|
return where
|
||||||
|
|
||||||
|
|
||||||
def get_matching_repositories(repo_term, username=None):
|
def get_matching_repositories(repo_term, username=None):
|
||||||
|
@ -702,8 +744,23 @@ def set_image_checksum(docker_image_id, repository, checksum):
|
||||||
return fetched
|
return fetched
|
||||||
|
|
||||||
|
|
||||||
|
def set_image_size(docker_image_id, namespace_name, repository_name, image_size):
|
||||||
|
joined = Image.select().join(Repository)
|
||||||
|
image_list = list(joined.where(Repository.name == repository_name,
|
||||||
|
Repository.namespace == namespace_name,
|
||||||
|
Image.docker_image_id == docker_image_id))
|
||||||
|
|
||||||
|
if not image_list:
|
||||||
|
raise DataModelException('No image with specified id and repository')
|
||||||
|
|
||||||
|
fetched = image_list[0]
|
||||||
|
fetched.image_size = image_size
|
||||||
|
fetched.save()
|
||||||
|
return fetched
|
||||||
|
|
||||||
|
|
||||||
def set_image_metadata(docker_image_id, namespace_name, repository_name,
|
def set_image_metadata(docker_image_id, namespace_name, repository_name,
|
||||||
created_date_str, comment, parent=None):
|
created_date_str, comment, command, parent=None):
|
||||||
joined = Image.select().join(Repository)
|
joined = Image.select().join(Repository)
|
||||||
image_list = list(joined.where(Repository.name == repository_name,
|
image_list = list(joined.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_name,
|
Repository.namespace == namespace_name,
|
||||||
|
@ -715,6 +772,7 @@ def set_image_metadata(docker_image_id, namespace_name, repository_name,
|
||||||
fetched = image_list[0]
|
fetched = image_list[0]
|
||||||
fetched.created = dateutil.parser.parse(created_date_str)
|
fetched.created = dateutil.parser.parse(created_date_str)
|
||||||
fetched.comment = comment
|
fetched.comment = comment
|
||||||
|
fetched.command = command
|
||||||
|
|
||||||
if parent:
|
if parent:
|
||||||
fetched.ancestors = '%s%s/' % (parent.ancestors, parent.id)
|
fetched.ancestors = '%s%s/' % (parent.ancestors, parent.id)
|
||||||
|
@ -736,6 +794,50 @@ def list_repository_tags(namespace_name, repository_name):
|
||||||
return with_image.where(Repository.name == repository_name,
|
return with_image.where(Repository.name == repository_name,
|
||||||
Repository.namespace == namespace_name)
|
Repository.namespace == namespace_name)
|
||||||
|
|
||||||
|
def delete_tag_and_images(namespace_name, repository_name, tag_name):
|
||||||
|
all_images = get_repository_images(namespace_name, repository_name)
|
||||||
|
all_tags = list_repository_tags(namespace_name, repository_name)
|
||||||
|
|
||||||
|
# Find the tag's information.
|
||||||
|
found_tag = None
|
||||||
|
for tag in all_tags:
|
||||||
|
if tag.name == tag_name:
|
||||||
|
found_tag = tag
|
||||||
|
break
|
||||||
|
|
||||||
|
if not found_tag:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Build the set of database IDs corresponding to the tag's ancestor images,
|
||||||
|
# as well as the tag's image itself.
|
||||||
|
tag_image_ids = set(found_tag.image.ancestors.split('/'))
|
||||||
|
tag_image_ids.add(str(found_tag.image.id))
|
||||||
|
|
||||||
|
# Filter out any images that belong to any other tags.
|
||||||
|
for tag in all_tags:
|
||||||
|
if tag.name != tag_name:
|
||||||
|
# Remove all ancestors of the tag.
|
||||||
|
tag_image_ids = tag_image_ids - set(tag.image.ancestors.split('/'))
|
||||||
|
|
||||||
|
# Remove the current image ID.
|
||||||
|
tag_image_ids.discard(str(tag.image.id))
|
||||||
|
|
||||||
|
# Find all the images that belong to the tag.
|
||||||
|
tag_images = [image for image in all_images
|
||||||
|
if str(image.id) in tag_image_ids]
|
||||||
|
|
||||||
|
# Delete the tag found.
|
||||||
|
found_tag.delete_instance()
|
||||||
|
|
||||||
|
# Delete the images found.
|
||||||
|
for image in tag_images:
|
||||||
|
image.delete_instance()
|
||||||
|
|
||||||
|
repository_path = store.image_path(namespace_name, repository_name,
|
||||||
|
image.docker_image_id)
|
||||||
|
logger.debug('Recursively deleting image path: %s' % repository_path)
|
||||||
|
store.remove(repository_path)
|
||||||
|
|
||||||
|
|
||||||
def get_tag_image(namespace_name, repository_name, tag_name):
|
def get_tag_image(namespace_name, repository_name, tag_name):
|
||||||
joined = Image.select().join(RepositoryTag).join(Repository)
|
joined = Image.select().join(RepositoryTag).join(Repository)
|
||||||
|
@ -933,6 +1035,11 @@ def purge_repository(namespace_name, repository_name):
|
||||||
Repository.namespace == namespace_name)
|
Repository.namespace == namespace_name)
|
||||||
fetched.delete_instance(recursive=True)
|
fetched.delete_instance(recursive=True)
|
||||||
|
|
||||||
|
repository_path = store.repository_namespace_path(namespace_name,
|
||||||
|
repository_name)
|
||||||
|
logger.debug('Recursively deleting path: %s' % repository_path)
|
||||||
|
store.remove(repository_path)
|
||||||
|
|
||||||
|
|
||||||
def get_private_repo_count(username):
|
def get_private_repo_count(username):
|
||||||
joined = Repository.select().join(Visibility)
|
joined = Repository.select().join(Visibility)
|
||||||
|
|
|
@ -1,20 +1,13 @@
|
||||||
import json
|
PLANS = [
|
||||||
import itertools
|
# Deprecated Plans
|
||||||
|
|
||||||
USER_PLANS = [
|
|
||||||
{
|
|
||||||
'title': 'Open Source',
|
|
||||||
'price': 0,
|
|
||||||
'privateRepos': 0,
|
|
||||||
'stripeId': 'free',
|
|
||||||
'audience': 'Share with the world',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'title': 'Micro',
|
'title': 'Micro',
|
||||||
'price': 700,
|
'price': 700,
|
||||||
'privateRepos': 5,
|
'privateRepos': 5,
|
||||||
'stripeId': 'micro',
|
'stripeId': 'micro',
|
||||||
'audience': 'For smaller teams',
|
'audience': 'For smaller teams',
|
||||||
|
'bus_features': False,
|
||||||
|
'deprecated': True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Basic',
|
'title': 'Basic',
|
||||||
|
@ -22,6 +15,8 @@ USER_PLANS = [
|
||||||
'privateRepos': 10,
|
'privateRepos': 10,
|
||||||
'stripeId': 'small',
|
'stripeId': 'small',
|
||||||
'audience': 'For your basic team',
|
'audience': 'For your basic team',
|
||||||
|
'bus_features': False,
|
||||||
|
'deprecated': True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Medium',
|
'title': 'Medium',
|
||||||
|
@ -29,6 +24,8 @@ USER_PLANS = [
|
||||||
'privateRepos': 20,
|
'privateRepos': 20,
|
||||||
'stripeId': 'medium',
|
'stripeId': 'medium',
|
||||||
'audience': 'For medium teams',
|
'audience': 'For medium teams',
|
||||||
|
'bus_features': False,
|
||||||
|
'deprecated': True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Large',
|
'title': 'Large',
|
||||||
|
@ -36,16 +33,28 @@ USER_PLANS = [
|
||||||
'privateRepos': 50,
|
'privateRepos': 50,
|
||||||
'stripeId': 'large',
|
'stripeId': 'large',
|
||||||
'audience': 'For larger teams',
|
'audience': 'For larger teams',
|
||||||
|
'bus_features': False,
|
||||||
|
'deprecated': True,
|
||||||
},
|
},
|
||||||
]
|
|
||||||
|
|
||||||
BUSINESS_PLANS = [
|
# Active plans
|
||||||
{
|
{
|
||||||
'title': 'Open Source',
|
'title': 'Open Source',
|
||||||
'price': 0,
|
'price': 0,
|
||||||
'privateRepos': 0,
|
'privateRepos': 0,
|
||||||
'stripeId': 'bus-free',
|
'stripeId': 'free',
|
||||||
'audience': 'Committment to FOSS',
|
'audience': 'Committment to FOSS',
|
||||||
|
'bus_features': False,
|
||||||
|
'deprecated': False,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Personal',
|
||||||
|
'price': 1200,
|
||||||
|
'privateRepos': 5,
|
||||||
|
'stripeId': 'personal',
|
||||||
|
'audience': 'Individuals',
|
||||||
|
'bus_features': False,
|
||||||
|
'deprecated': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Skiff',
|
'title': 'Skiff',
|
||||||
|
@ -53,6 +62,8 @@ BUSINESS_PLANS = [
|
||||||
'privateRepos': 10,
|
'privateRepos': 10,
|
||||||
'stripeId': 'bus-micro',
|
'stripeId': 'bus-micro',
|
||||||
'audience': 'For startups',
|
'audience': 'For startups',
|
||||||
|
'bus_features': True,
|
||||||
|
'deprecated': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Yacht',
|
'title': 'Yacht',
|
||||||
|
@ -60,6 +71,8 @@ BUSINESS_PLANS = [
|
||||||
'privateRepos': 20,
|
'privateRepos': 20,
|
||||||
'stripeId': 'bus-small',
|
'stripeId': 'bus-small',
|
||||||
'audience': 'For small businesses',
|
'audience': 'For small businesses',
|
||||||
|
'bus_features': True,
|
||||||
|
'deprecated': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Freighter',
|
'title': 'Freighter',
|
||||||
|
@ -67,6 +80,8 @@ BUSINESS_PLANS = [
|
||||||
'privateRepos': 50,
|
'privateRepos': 50,
|
||||||
'stripeId': 'bus-medium',
|
'stripeId': 'bus-medium',
|
||||||
'audience': 'For normal businesses',
|
'audience': 'For normal businesses',
|
||||||
|
'bus_features': True,
|
||||||
|
'deprecated': False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'title': 'Tanker',
|
'title': 'Tanker',
|
||||||
|
@ -74,14 +89,16 @@ BUSINESS_PLANS = [
|
||||||
'privateRepos': 125,
|
'privateRepos': 125,
|
||||||
'stripeId': 'bus-large',
|
'stripeId': 'bus-large',
|
||||||
'audience': 'For large businesses',
|
'audience': 'For large businesses',
|
||||||
|
'bus_features': True,
|
||||||
|
'deprecated': False,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def get_plan(id):
|
def get_plan(plan_id):
|
||||||
""" Returns the plan with the given ID or None if none. """
|
""" Returns the plan with the given ID or None if none. """
|
||||||
for plan in itertools.chain(USER_PLANS, BUSINESS_PLANS):
|
for plan in PLANS:
|
||||||
if plan['stripeId'] == id:
|
if plan['stripeId'] == plan_id:
|
||||||
return plan
|
return plan
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
408
endpoints/api.py
|
@ -12,7 +12,7 @@ from collections import defaultdict
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from data.queue import dockerfile_build_queue
|
from data.queue import dockerfile_build_queue
|
||||||
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
from data.plans import PLANS, get_plan
|
||||||
from app import app
|
from app import app
|
||||||
from util.email import send_confirmation_email, send_recovery_email
|
from util.email import send_confirmation_email, send_recovery_email
|
||||||
from util.names import parse_repository_name, format_robot_username
|
from util.names import parse_repository_name, format_robot_username
|
||||||
|
@ -25,20 +25,53 @@ from auth.permissions import (ReadRepositoryPermission,
|
||||||
AdministerOrganizationPermission,
|
AdministerOrganizationPermission,
|
||||||
OrganizationMemberPermission,
|
OrganizationMemberPermission,
|
||||||
ViewTeamPermission)
|
ViewTeamPermission)
|
||||||
from endpoints import registry
|
from endpoints.common import common_login
|
||||||
from endpoints.web import common_login
|
|
||||||
from util.cache import cache_control
|
from util.cache import cache_control
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
|
||||||
store = app.config['STORAGE']
|
store = app.config['STORAGE']
|
||||||
user_files = app.config['USERFILES']
|
user_files = app.config['USERFILES']
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
route_data = None
|
||||||
|
|
||||||
|
def get_route_data():
|
||||||
|
global route_data
|
||||||
|
if route_data:
|
||||||
|
return route_data
|
||||||
|
|
||||||
|
routes = []
|
||||||
|
for rule in app.url_map.iter_rules():
|
||||||
|
if rule.rule.startswith('/api/'):
|
||||||
|
endpoint_method = globals()[rule.endpoint]
|
||||||
|
is_internal = '__internal_call' in dir(endpoint_method)
|
||||||
|
is_org_api = '__user_call' in dir(endpoint_method)
|
||||||
|
methods = list(rule.methods.difference(['HEAD', 'OPTIONS']))
|
||||||
|
|
||||||
|
route = {
|
||||||
|
'name': rule.endpoint,
|
||||||
|
'methods': methods,
|
||||||
|
'path': rule.rule,
|
||||||
|
'parameters': list(rule.arguments)
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_org_api:
|
||||||
|
route['user_method'] = endpoint_method.__user_call
|
||||||
|
|
||||||
|
routes.append(route)
|
||||||
|
|
||||||
|
route_data = {
|
||||||
|
'endpoints': routes
|
||||||
|
}
|
||||||
|
return route_data
|
||||||
|
|
||||||
|
|
||||||
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
||||||
performer = current_user.db_user()
|
performer = current_user.db_user()
|
||||||
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr,
|
model.log_action(kind, user_or_orgname, performer=performer,
|
||||||
metadata=metadata, repository=repo)
|
ip=request.remote_addr, metadata=metadata, repository=repo)
|
||||||
|
|
||||||
|
|
||||||
def api_login_required(f):
|
def api_login_required(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
|
@ -58,26 +91,51 @@ def api_login_required(f):
|
||||||
return decorated_view
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
|
def internal_api_call(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
decorated_view.__internal_call = True
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
|
||||||
|
def org_api_call(user_call_name):
|
||||||
|
def internal_decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_view(*args, **kwargs):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
decorated_view.__user_call = user_call_name
|
||||||
|
return decorated_view
|
||||||
|
|
||||||
|
return internal_decorator
|
||||||
|
|
||||||
@app.errorhandler(model.DataModelException)
|
@app.errorhandler(model.DataModelException)
|
||||||
def handle_dme(ex):
|
def handle_dme(ex):
|
||||||
return make_response(ex.message, 400)
|
return make_response(ex.message, 400)
|
||||||
|
|
||||||
|
|
||||||
@app.errorhandler(KeyError)
|
@app.errorhandler(KeyError)
|
||||||
def handle_dme(ex):
|
def handle_dme_key_error(ex):
|
||||||
return make_response(ex.message, 400)
|
return make_response(ex.message, 400)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/discovery')
|
||||||
|
def discovery():
|
||||||
|
return jsonify(get_route_data())
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/')
|
@app.route('/api/')
|
||||||
|
@internal_api_call
|
||||||
def welcome():
|
def welcome():
|
||||||
return make_response('welcome', 200)
|
return make_response('welcome', 200)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/plans/')
|
@app.route('/api/plans/')
|
||||||
def plans_list():
|
def list_plans():
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'user': USER_PLANS,
|
'plans': PLANS,
|
||||||
'business': BUSINESS_PLANS,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -93,6 +151,14 @@ def user_view(user):
|
||||||
|
|
||||||
organizations = model.get_user_organizations(user.username)
|
organizations = model.get_user_organizations(user.username)
|
||||||
|
|
||||||
|
def login_view(login):
|
||||||
|
return {
|
||||||
|
'service': login.service.name,
|
||||||
|
'service_identifier': login.service_ident,
|
||||||
|
}
|
||||||
|
|
||||||
|
logins = model.list_federated_logins(user)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'verified': user.verified,
|
'verified': user.verified,
|
||||||
'anonymous': False,
|
'anonymous': False,
|
||||||
|
@ -101,12 +167,14 @@ def user_view(user):
|
||||||
'gravatar': compute_hash(user.email),
|
'gravatar': compute_hash(user.email),
|
||||||
'askForPassword': user.password_hash is None,
|
'askForPassword': user.password_hash is None,
|
||||||
'organizations': [org_view(o) for o in organizations],
|
'organizations': [org_view(o) for o in organizations],
|
||||||
|
'logins': [login_view(login) for login in logins],
|
||||||
'can_create_repo': True,
|
'can_create_repo': True,
|
||||||
'invoice_email': user.invoice_email
|
'invoice_email': user.invoice_email
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['GET'])
|
@app.route('/api/user/', methods=['GET'])
|
||||||
|
@internal_api_call
|
||||||
def get_logged_in_user():
|
def get_logged_in_user():
|
||||||
if current_user.is_anonymous():
|
if current_user.is_anonymous():
|
||||||
return jsonify({'anonymous': True})
|
return jsonify({'anonymous': True})
|
||||||
|
@ -118,8 +186,30 @@ def get_logged_in_user():
|
||||||
return jsonify(user_view(user))
|
return jsonify(user_view(user))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/user/private', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
|
def get_user_private_count():
|
||||||
|
user = current_user.db_user()
|
||||||
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
repos_allowed = 0
|
||||||
|
|
||||||
|
if user.stripe_id:
|
||||||
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||||
|
if cus.subscription:
|
||||||
|
plan = get_plan(cus.subscription.plan.id)
|
||||||
|
if plan:
|
||||||
|
repos_allowed = plan['privateRepos']
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'privateCount': private_repos,
|
||||||
|
'reposAllowed': repos_allowed
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/convert', methods=['POST'])
|
@app.route('/api/user/convert', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
def convert_user_to_organization():
|
def convert_user_to_organization():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
convert_data = request.get_json()
|
convert_data = request.get_json()
|
||||||
|
@ -144,7 +234,7 @@ def convert_user_to_organization():
|
||||||
|
|
||||||
# Subscribe the organization to the new plan.
|
# Subscribe the organization to the new plan.
|
||||||
plan = convert_data['plan']
|
plan = convert_data['plan']
|
||||||
subscribe(user, plan, None, BUSINESS_PLANS)
|
subscribe(user, plan, None, True) # Require business plans
|
||||||
|
|
||||||
# Convert the user to an organization.
|
# Convert the user to an organization.
|
||||||
model.convert_user_to_organization(user, model.get_user(admin_username))
|
model.convert_user_to_organization(user, model.get_user(admin_username))
|
||||||
|
@ -152,11 +242,11 @@ def convert_user_to_organization():
|
||||||
|
|
||||||
# And finally login with the admin credentials.
|
# And finally login with the admin credentials.
|
||||||
return conduct_signin(admin_username, admin_password)
|
return conduct_signin(admin_username, admin_password)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['PUT'])
|
@app.route('/api/user/', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
def change_user_details():
|
def change_user_details():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
|
|
||||||
|
@ -183,7 +273,8 @@ def change_user_details():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['POST'])
|
@app.route('/api/user/', methods=['POST'])
|
||||||
def create_user_api():
|
@internal_api_call
|
||||||
|
def create_new_user():
|
||||||
user_data = request.get_json()
|
user_data = request.get_json()
|
||||||
|
|
||||||
existing_user = model.get_user(user_data['username'])
|
existing_user = model.get_user(user_data['username'])
|
||||||
|
@ -209,7 +300,8 @@ def create_user_api():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/signin', methods=['POST'])
|
@app.route('/api/signin', methods=['POST'])
|
||||||
def signin_api():
|
@internal_api_call
|
||||||
|
def signin_user():
|
||||||
signin_data = request.get_json()
|
signin_data = request.get_json()
|
||||||
|
|
||||||
username = signin_data['username']
|
username = signin_data['username']
|
||||||
|
@ -243,6 +335,7 @@ def conduct_signin(username, password):
|
||||||
|
|
||||||
@app.route("/api/signout", methods=['POST'])
|
@app.route("/api/signout", methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
identity_changed.send(app, identity=AnonymousIdentity())
|
identity_changed.send(app, identity=AnonymousIdentity())
|
||||||
|
@ -250,7 +343,8 @@ def logout():
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/recovery", methods=['POST'])
|
@app.route("/api/recovery", methods=['POST'])
|
||||||
def send_recovery():
|
@internal_api_call
|
||||||
|
def request_recovery_email():
|
||||||
email = request.get_json()['email']
|
email = request.get_json()['email']
|
||||||
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)
|
||||||
|
@ -272,9 +366,10 @@ def get_matching_users(prefix):
|
||||||
def get_matching_entities(prefix):
|
def get_matching_entities(prefix):
|
||||||
teams = []
|
teams = []
|
||||||
|
|
||||||
namespace_name = request.args.get('namespace', None)
|
namespace_name = request.args.get('namespace', '')
|
||||||
robot_namespace = None
|
robot_namespace = None
|
||||||
organization = None
|
organization = None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
organization = model.get_organization(namespace_name)
|
organization = model.get_organization(namespace_name)
|
||||||
|
|
||||||
|
@ -308,7 +403,7 @@ def get_matching_entities(prefix):
|
||||||
'is_robot': user.is_robot,
|
'is_robot': user.is_robot,
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.is_org_member is not None:
|
if organization is not None:
|
||||||
user_json['is_org_member'] = user.is_robot or user.is_org_member
|
user_json['is_org_member'] = user.is_robot or user.is_org_member
|
||||||
|
|
||||||
return user_json
|
return user_json
|
||||||
|
@ -334,7 +429,8 @@ def team_view(orgname, team):
|
||||||
|
|
||||||
@app.route('/api/organization/', methods=['POST'])
|
@app.route('/api/organization/', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_organization_api():
|
@internal_api_call
|
||||||
|
def create_organization():
|
||||||
org_data = request.get_json()
|
org_data = request.get_json()
|
||||||
existing = None
|
existing = None
|
||||||
|
|
||||||
|
@ -398,6 +494,7 @@ def get_organization(orgname):
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>', methods=['PUT'])
|
@app.route('/api/organization/<orgname>', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@org_api_call('change_user_details')
|
||||||
def change_organization_details(orgname):
|
def change_organization_details(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
@ -406,7 +503,7 @@ def change_organization_details(orgname):
|
||||||
except model.InvalidOrganizationException:
|
except model.InvalidOrganizationException:
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
org_data = request.get_json();
|
org_data = request.get_json()
|
||||||
if 'invoice_email' in org_data:
|
if 'invoice_email' in org_data:
|
||||||
logger.debug('Changing invoice_email for organization: %s', org.username)
|
logger.debug('Changing invoice_email for organization: %s', org.username)
|
||||||
model.change_invoice_email(org, org_data['invoice_email'])
|
model.change_invoice_email(org, org_data['invoice_email'])
|
||||||
|
@ -456,7 +553,7 @@ def get_organization_member(orgname, membername):
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
member_dict = None
|
member_dict = None
|
||||||
member_teams = model.get_organization_members_with_teams(org, membername = membername)
|
member_teams = model.get_organization_members_with_teams(org, membername=membername)
|
||||||
for member in member_teams:
|
for member in member_teams:
|
||||||
if not member_dict:
|
if not member_dict:
|
||||||
member_dict = {'username': member.user.username,
|
member_dict = {'username': member.user.username,
|
||||||
|
@ -475,6 +572,7 @@ def get_organization_member(orgname, membername):
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
def get_organization_private_allowed(orgname):
|
def get_organization_private_allowed(orgname):
|
||||||
permission = CreateRepositoryPermission(orgname)
|
permission = CreateRepositoryPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
@ -484,7 +582,11 @@ def get_organization_private_allowed(orgname):
|
||||||
if organization.stripe_id:
|
if organization.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(organization.stripe_id)
|
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
repos_allowed = get_plan(cus.subscription.plan.id)
|
repos_allowed = 0
|
||||||
|
plan = get_plan(cus.subscription.plan.id)
|
||||||
|
if plan:
|
||||||
|
repos_allowed = plan['privateRepos']
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'privateAllowed': (private_repos < repos_allowed)
|
'privateAllowed': (private_repos < repos_allowed)
|
||||||
})
|
})
|
||||||
|
@ -526,17 +628,20 @@ def update_organization_team(orgname, teamname):
|
||||||
log_action('org_create_team', orgname, {'team': teamname})
|
log_action('org_create_team', orgname, {'team': teamname})
|
||||||
|
|
||||||
if is_existing:
|
if is_existing:
|
||||||
if 'description' in details and team.description != details['description']:
|
if ('description' in details and
|
||||||
|
team.description != details['description']):
|
||||||
team.description = details['description']
|
team.description = details['description']
|
||||||
team.save()
|
team.save()
|
||||||
log_action('org_set_team_description', orgname, {'team': teamname, 'description': team.description})
|
log_action('org_set_team_description', orgname,
|
||||||
|
{'team': teamname, 'description': team.description})
|
||||||
|
|
||||||
if 'role' in details:
|
if 'role' in details:
|
||||||
role = model.get_team_org_role(team).name
|
role = model.get_team_org_role(team).name
|
||||||
if role != details['role']:
|
if role != details['role']:
|
||||||
team = model.set_team_org_permission(team, details['role'],
|
team = model.set_team_org_permission(team, details['role'],
|
||||||
current_user.db_user().username)
|
current_user.db_user().username)
|
||||||
log_action('org_set_team_role', orgname, {'team': teamname, 'role': details['role']})
|
log_action('org_set_team_role', orgname,
|
||||||
|
{'team': teamname, 'role': details['role']})
|
||||||
|
|
||||||
resp = jsonify(team_view(orgname, team))
|
resp = jsonify(team_view(orgname, team))
|
||||||
if not is_existing:
|
if not is_existing:
|
||||||
|
@ -604,7 +709,8 @@ def update_organization_team_member(orgname, teamname, membername):
|
||||||
|
|
||||||
# Add the user to the team.
|
# Add the user to the team.
|
||||||
model.add_user_to_team(user, team)
|
model.add_user_to_team(user, team)
|
||||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
log_action('org_add_team_member', orgname,
|
||||||
|
{'member': membername, 'team': teamname})
|
||||||
return jsonify(member_view(user))
|
return jsonify(member_view(user))
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
@ -619,7 +725,8 @@ def delete_organization_team_member(orgname, teamname, membername):
|
||||||
# Remote the user from the team.
|
# Remote the user from the team.
|
||||||
invoking_user = current_user.db_user().username
|
invoking_user = current_user.db_user().username
|
||||||
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
||||||
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
log_action('org_remove_team_member', orgname,
|
||||||
|
{'member': membername, 'team': teamname})
|
||||||
return make_response('Deleted', 204)
|
return make_response('Deleted', 204)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
@ -627,7 +734,7 @@ def delete_organization_team_member(orgname, teamname, membername):
|
||||||
|
|
||||||
@app.route('/api/repository', methods=['POST'])
|
@app.route('/api/repository', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_repo_api():
|
def create_repo():
|
||||||
owner = current_user.db_user()
|
owner = current_user.db_user()
|
||||||
req = request.get_json()
|
req = request.get_json()
|
||||||
namespace_name = req['namespace'] if 'namespace' in req else owner.username
|
namespace_name = req['namespace'] if 'namespace' in req else owner.username
|
||||||
|
@ -648,7 +755,9 @@ def create_repo_api():
|
||||||
repo.description = req['description']
|
repo.description = req['description']
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
log_action('create_repo', namespace_name, {'repo': repository_name, 'namespace': namespace_name}, repo=repo)
|
log_action('create_repo', namespace_name,
|
||||||
|
{'repo': repository_name, 'namespace': namespace_name},
|
||||||
|
repo=repo)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'namespace': namespace_name,
|
'namespace': namespace_name,
|
||||||
'name': repository_name
|
'name': repository_name
|
||||||
|
@ -658,7 +767,7 @@ def create_repo_api():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/find/repository', methods=['GET'])
|
@app.route('/api/find/repository', methods=['GET'])
|
||||||
def match_repos_api():
|
def find_repos():
|
||||||
prefix = request.args.get('query', '')
|
prefix = request.args.get('query', '')
|
||||||
|
|
||||||
def repo_view(repo):
|
def repo_view(repo):
|
||||||
|
@ -681,7 +790,7 @@ def match_repos_api():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/', methods=['GET'])
|
@app.route('/api/repository/', methods=['GET'])
|
||||||
def list_repos_api():
|
def list_repos():
|
||||||
def repo_view(repo_obj):
|
def repo_view(repo_obj):
|
||||||
return {
|
return {
|
||||||
'namespace': repo_obj.namespace,
|
'namespace': repo_obj.namespace,
|
||||||
|
@ -690,11 +799,13 @@ def list_repos_api():
|
||||||
'is_public': repo_obj.visibility.name == 'public',
|
'is_public': repo_obj.visibility.name == 'public',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page = request.args.get('page', None)
|
||||||
limit = request.args.get('limit', None)
|
limit = request.args.get('limit', None)
|
||||||
namespace_filter = request.args.get('namespace', None)
|
namespace_filter = request.args.get('namespace', None)
|
||||||
include_public = request.args.get('public', 'true')
|
include_public = request.args.get('public', 'true')
|
||||||
include_private = request.args.get('private', 'true')
|
include_private = request.args.get('private', 'true')
|
||||||
sort = request.args.get('sort', 'false')
|
sort = request.args.get('sort', 'false')
|
||||||
|
include_count = request.args.get('count', 'false')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
limit = int(limit) if limit else None
|
limit = int(limit) if limit else None
|
||||||
|
@ -703,28 +814,45 @@ def list_repos_api():
|
||||||
|
|
||||||
include_public = include_public == 'true'
|
include_public = include_public == 'true'
|
||||||
include_private = include_private == 'true'
|
include_private = include_private == 'true'
|
||||||
|
include_count = include_count == 'true'
|
||||||
sort = sort == 'true'
|
sort = sort == 'true'
|
||||||
|
if page:
|
||||||
|
try:
|
||||||
|
page = int(page)
|
||||||
|
except:
|
||||||
|
page = None
|
||||||
|
|
||||||
username = None
|
username = None
|
||||||
if current_user.is_authenticated() and include_private:
|
if current_user.is_authenticated() and include_private:
|
||||||
username = current_user.db_user().username
|
username = current_user.db_user().username
|
||||||
|
|
||||||
repo_query = model.get_visible_repositories(username, limit=limit,
|
repo_count = None
|
||||||
|
if include_count:
|
||||||
|
repo_count = model.get_visible_repository_count(username,
|
||||||
|
include_public=include_public,
|
||||||
|
sort=sort,
|
||||||
|
namespace=namespace_filter)
|
||||||
|
|
||||||
|
repo_query = model.get_visible_repositories(username, limit=limit, page=page,
|
||||||
include_public=include_public,
|
include_public=include_public,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
namespace=namespace_filter)
|
namespace=namespace_filter)
|
||||||
|
|
||||||
repos = [repo_view(repo) for repo in repo_query]
|
repos = [repo_view(repo) for repo in repo_query]
|
||||||
response = {
|
response = {
|
||||||
'repositories': repos
|
'repositories': repos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if include_count:
|
||||||
|
response['count'] = repo_count
|
||||||
|
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>', methods=['PUT'])
|
@app.route('/api/repository/<path:repository>', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def update_repo_api(namespace, repository):
|
def update_repo(namespace, repository):
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
|
@ -733,7 +861,8 @@ def update_repo_api(namespace, repository):
|
||||||
repo.description = values['description']
|
repo.description = values['description']
|
||||||
repo.save()
|
repo.save()
|
||||||
|
|
||||||
log_action('set_repo_description', namespace, {'repo': repository, 'description': values['description']},
|
log_action('set_repo_description', namespace,
|
||||||
|
{'repo': repository, 'description': values['description']},
|
||||||
repo=repo)
|
repo=repo)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True
|
'success': True
|
||||||
|
@ -746,14 +875,15 @@ def update_repo_api(namespace, repository):
|
||||||
methods=['POST'])
|
methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def change_repo_visibility_api(namespace, repository):
|
def change_repo_visibility(namespace, repository):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
if repo:
|
if repo:
|
||||||
values = request.get_json()
|
values = request.get_json()
|
||||||
model.set_repository_visibility(repo, values['visibility'])
|
model.set_repository_visibility(repo, values['visibility'])
|
||||||
log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']},
|
log_action('change_repo_visibility', namespace,
|
||||||
|
{'repo': repository, 'visibility': values['visibility']},
|
||||||
repo=repo)
|
repo=repo)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True
|
'success': True
|
||||||
|
@ -769,8 +899,8 @@ def delete_repository(namespace, repository):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
model.purge_repository(namespace, repository)
|
model.purge_repository(namespace, repository)
|
||||||
registry.delete_repository_storage(namespace, repository)
|
log_action('delete_repo', namespace,
|
||||||
log_action('delete_repo', namespace, {'repo': repository, 'namespace': namespace})
|
{'repo': repository, 'namespace': namespace})
|
||||||
return make_response('Deleted', 204)
|
return make_response('Deleted', 204)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
@ -781,14 +911,16 @@ def image_view(image):
|
||||||
'id': image.docker_image_id,
|
'id': image.docker_image_id,
|
||||||
'created': image.created,
|
'created': image.created,
|
||||||
'comment': image.comment,
|
'comment': image.comment,
|
||||||
|
'command': json.loads(image.command) if image.command else None,
|
||||||
'ancestors': image.ancestors,
|
'ancestors': image.ancestors,
|
||||||
'dbid': image.id,
|
'dbid': image.id,
|
||||||
|
'size': image.image_size,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
@app.route('/api/repository/<path:repository>', methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def get_repo_api(namespace, repository):
|
def get_repo(namespace, repository):
|
||||||
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
logger.debug('Get repo: %s/%s' % (namespace, repository))
|
||||||
|
|
||||||
def tag_view(tag):
|
def tag_view(tag):
|
||||||
|
@ -849,15 +981,15 @@ def get_repo_builds(namespace, repository):
|
||||||
return node_status
|
return node_status
|
||||||
|
|
||||||
# If there was no status url, do the best we can
|
# If there was no status url, do the best we can
|
||||||
|
# The format of this block should mirror that of the buildserver.
|
||||||
return {
|
return {
|
||||||
'id': build_obj.id,
|
'id': build_obj.id,
|
||||||
'total_commands': None,
|
'total_commands': None,
|
||||||
'total_images': None,
|
|
||||||
'current_command': None,
|
'current_command': None,
|
||||||
'current_image': None,
|
'push_completion': 0.0,
|
||||||
'image_completion_percent': None,
|
|
||||||
'status': build_obj.phase,
|
'status': build_obj.phase,
|
||||||
'message': None,
|
'message': None,
|
||||||
|
'image_completion': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
builds = model.list_repository_builds(namespace, repository)
|
builds = model.list_repository_builds(namespace, repository)
|
||||||
|
@ -887,7 +1019,8 @@ def request_repo_build(namespace, repository):
|
||||||
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
|
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
|
||||||
|
|
||||||
log_action('build_dockerfile', namespace,
|
log_action('build_dockerfile', namespace,
|
||||||
{'repo': repository, 'namespace': namespace, 'fileid': dockerfile_id}, repo=repo)
|
{'repo': repository, 'namespace': namespace,
|
||||||
|
'fileid': dockerfile_id}, repo=repo)
|
||||||
|
|
||||||
resp = jsonify({
|
resp = jsonify({
|
||||||
'started': True
|
'started': True
|
||||||
|
@ -918,7 +1051,8 @@ def create_webhook(namespace, repository):
|
||||||
resp.headers['Location'] = url_for('get_webhook', repository=repo_string,
|
resp.headers['Location'] = url_for('get_webhook', repository=repo_string,
|
||||||
public_id=webhook.public_id)
|
public_id=webhook.public_id)
|
||||||
log_action('add_repo_webhook', namespace,
|
log_action('add_repo_webhook', namespace,
|
||||||
{'repo': repository, 'webhook_id': webhook.public_id}, repo=repo)
|
{'repo': repository, 'webhook_id': webhook.public_id},
|
||||||
|
repo=repo)
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
abort(403) # Permissions denied
|
abort(403) # Permissions denied
|
||||||
|
@ -969,6 +1103,7 @@ def delete_webhook(namespace, repository, public_id):
|
||||||
|
|
||||||
@app.route('/api/filedrop/', methods=['POST'])
|
@app.route('/api/filedrop/', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
def get_filedrop_url():
|
def get_filedrop_url():
|
||||||
mime_type = request.get_json()['mimeType']
|
mime_type = request.get_json()['mimeType']
|
||||||
(url, file_id) = user_files.prepare_for_drop(mime_type)
|
(url, file_id) = user_files.prepare_for_drop(mime_type)
|
||||||
|
@ -1051,6 +1186,24 @@ def get_image_changes(namespace, repository, image_id):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>/tag/<tag>',
|
||||||
|
methods=['DELETE'])
|
||||||
|
@parse_repository_name
|
||||||
|
def delete_full_tag(namespace, repository, tag):
|
||||||
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
|
if permission.can():
|
||||||
|
model.delete_tag_and_images(namespace, repository, tag)
|
||||||
|
|
||||||
|
username = current_user.db_user().username
|
||||||
|
log_action('delete_tag', namespace,
|
||||||
|
{'username': username, 'repo': repository, 'tag': tag},
|
||||||
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
|
return make_response('Deleted', 204)
|
||||||
|
|
||||||
|
abort(403) # Permission denied
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
@ -1118,7 +1271,8 @@ def list_repo_user_permissions(namespace, repository):
|
||||||
current_func = role_view_func
|
current_func = role_view_func
|
||||||
|
|
||||||
def wrapped_role_org_view(repo_perm):
|
def wrapped_role_org_view(repo_perm):
|
||||||
return wrap_role_view_org(current_func(repo_perm), repo_perm.user, org_members)
|
return wrap_role_view_org(current_func(repo_perm), repo_perm.user,
|
||||||
|
org_members)
|
||||||
|
|
||||||
role_view_func = wrapped_role_org_view
|
role_view_func = wrapped_role_org_view
|
||||||
|
|
||||||
|
@ -1203,7 +1357,8 @@ def change_user_permissions(namespace, repository, username):
|
||||||
return error_resp
|
return error_resp
|
||||||
|
|
||||||
log_action('change_repo_permission', namespace,
|
log_action('change_repo_permission', namespace,
|
||||||
{'username': username, 'repo': repository, 'role': new_permission['role']},
|
{'username': username, 'repo': repository,
|
||||||
|
'role': new_permission['role']},
|
||||||
repo=model.get_repository(namespace, repository))
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
resp = jsonify(perm_view)
|
resp = jsonify(perm_view)
|
||||||
|
@ -1230,7 +1385,8 @@ def change_team_permissions(namespace, repository, teamname):
|
||||||
new_permission['role'])
|
new_permission['role'])
|
||||||
|
|
||||||
log_action('change_repo_permission', namespace,
|
log_action('change_repo_permission', namespace,
|
||||||
{'team': teamname, 'repo': repository, 'role': new_permission['role']},
|
{'team': teamname, 'repo': repository,
|
||||||
|
'role': new_permission['role']},
|
||||||
repo=model.get_repository(namespace, repository))
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
resp = jsonify(role_view(perm))
|
resp = jsonify(role_view(perm))
|
||||||
|
@ -1257,7 +1413,8 @@ def delete_user_permissions(namespace, repository, username):
|
||||||
error_resp.status_code = 400
|
error_resp.status_code = 400
|
||||||
return error_resp
|
return error_resp
|
||||||
|
|
||||||
log_action('delete_repo_permission', namespace, {'username': username, 'repo': repository},
|
log_action('delete_repo_permission', namespace,
|
||||||
|
{'username': username, 'repo': repository},
|
||||||
repo=model.get_repository(namespace, repository))
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
return make_response('Deleted', 204)
|
return make_response('Deleted', 204)
|
||||||
|
@ -1274,7 +1431,8 @@ def delete_team_permissions(namespace, repository, teamname):
|
||||||
if permission.can():
|
if permission.can():
|
||||||
model.delete_team_permission(teamname, namespace, repository)
|
model.delete_team_permission(teamname, namespace, repository)
|
||||||
|
|
||||||
log_action('delete_repo_permission', namespace, {'team': teamname, 'repo': repository},
|
log_action('delete_repo_permission', namespace,
|
||||||
|
{'team': teamname, 'repo': repository},
|
||||||
repo=model.get_repository(namespace, repository))
|
repo=model.get_repository(namespace, repository))
|
||||||
|
|
||||||
return make_response('Deleted', 204)
|
return make_response('Deleted', 204)
|
||||||
|
@ -1328,7 +1486,8 @@ def create_token(namespace, repository):
|
||||||
token = model.create_delegate_token(namespace, repository,
|
token = model.create_delegate_token(namespace, repository,
|
||||||
token_params['friendlyName'])
|
token_params['friendlyName'])
|
||||||
|
|
||||||
log_action('add_repo_accesstoken', namespace, {'repo': repository, 'token': token_params['friendlyName']},
|
log_action('add_repo_accesstoken', namespace,
|
||||||
|
{'repo': repository, 'token': token_params['friendlyName']},
|
||||||
repo = model.get_repository(namespace, repository))
|
repo = model.get_repository(namespace, repository))
|
||||||
|
|
||||||
resp = jsonify(token_view(token))
|
resp = jsonify(token_view(token))
|
||||||
|
@ -1353,7 +1512,8 @@ def change_token(namespace, repository, code):
|
||||||
new_permission['role'])
|
new_permission['role'])
|
||||||
|
|
||||||
log_action('change_repo_permission', namespace,
|
log_action('change_repo_permission', namespace,
|
||||||
{'repo': repository, 'token': token.friendly_name, 'code': code, 'role': new_permission['role']},
|
{'repo': repository, 'token': token.friendly_name, 'code': code,
|
||||||
|
'role': new_permission['role']},
|
||||||
repo = model.get_repository(namespace, repository))
|
repo = model.get_repository(namespace, repository))
|
||||||
|
|
||||||
resp = jsonify(token_view(token))
|
resp = jsonify(token_view(token))
|
||||||
|
@ -1372,7 +1532,8 @@ def delete_token(namespace, repository, code):
|
||||||
token = model.delete_delegate_token(namespace, repository, code)
|
token = model.delete_delegate_token(namespace, repository, code)
|
||||||
|
|
||||||
log_action('delete_repo_accesstoken', namespace,
|
log_action('delete_repo_accesstoken', namespace,
|
||||||
{'repo': repository, 'token': token.friendly_name, 'code': code},
|
{'repo': repository, 'token': token.friendly_name,
|
||||||
|
'code': code},
|
||||||
repo = model.get_repository(namespace, repository))
|
repo = model.get_repository(namespace, repository))
|
||||||
|
|
||||||
return make_response('Deleted', 204)
|
return make_response('Deleted', 204)
|
||||||
|
@ -1391,14 +1552,17 @@ def subscription_view(stripe_subscription, used_repos):
|
||||||
|
|
||||||
@app.route('/api/user/card', methods=['GET'])
|
@app.route('/api/user/card', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_user_card_api():
|
@internal_api_call
|
||||||
|
def get_user_card():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
return get_card(user)
|
return get_card(user)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/card', methods=['GET'])
|
@app.route('/api/organization/<orgname>/card', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_org_card_api(orgname):
|
@internal_api_call
|
||||||
|
@org_api_call('get_user_card')
|
||||||
|
def get_org_card(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
|
@ -1409,7 +1573,8 @@ def get_org_card_api(orgname):
|
||||||
|
|
||||||
@app.route('/api/user/card', methods=['POST'])
|
@app.route('/api/user/card', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def set_user_card_api():
|
@internal_api_call
|
||||||
|
def set_user_card():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
token = request.get_json()['token']
|
token = request.get_json()['token']
|
||||||
response = set_card(user, token)
|
response = set_card(user, token)
|
||||||
|
@ -1419,7 +1584,8 @@ def set_user_card_api():
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def set_org_card_api(orgname):
|
@org_api_call('set_user_card')
|
||||||
|
def set_org_card(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
|
@ -1461,21 +1627,22 @@ def get_card(user):
|
||||||
|
|
||||||
if default_card:
|
if default_card:
|
||||||
card_info = {
|
card_info = {
|
||||||
'owner': card.name,
|
'owner': default_card.name,
|
||||||
'type': card.type,
|
'type': default_card.type,
|
||||||
'last4': card.last4
|
'last4': default_card.last4
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsonify({'card': card_info})
|
return jsonify({'card': card_info})
|
||||||
|
|
||||||
@app.route('/api/user/plan', methods=['PUT'])
|
@app.route('/api/user/plan', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def subscribe_api():
|
@internal_api_call
|
||||||
|
def update_user_subscription():
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
plan = request_data['plan']
|
plan = request_data['plan']
|
||||||
token = request_data['token'] if 'token' in request_data else None
|
token = request_data['token'] if 'token' in request_data else None
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
return subscribe(user, plan, token, USER_PLANS)
|
return subscribe(user, plan, token, False) # Business features not required
|
||||||
|
|
||||||
|
|
||||||
def carderror_response(e):
|
def carderror_response(e):
|
||||||
|
@ -1486,15 +1653,22 @@ def carderror_response(e):
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
def subscribe(user, plan, token, accepted_plans):
|
def subscribe(user, plan, token, require_business_plan):
|
||||||
plan_found = None
|
plan_found = None
|
||||||
for plan_obj in accepted_plans:
|
for plan_obj in PLANS:
|
||||||
if plan_obj['stripeId'] == plan:
|
if plan_obj['stripeId'] == plan:
|
||||||
plan_found = plan_obj
|
plan_found = plan_obj
|
||||||
|
|
||||||
if not plan_found:
|
if not plan_found or plan_found['deprecated']:
|
||||||
|
logger.warning('Plan not found or deprecated: %s', plan)
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
|
if (require_business_plan and not plan_found['bus_features'] and not
|
||||||
|
plan_found['price'] == 0):
|
||||||
|
logger.warning('Business attempting to subscribe to personal plan: %s',
|
||||||
|
user.username)
|
||||||
|
abort(400)
|
||||||
|
|
||||||
private_repos = model.get_private_repo_count(user.username)
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
|
||||||
# This is the default response
|
# This is the default response
|
||||||
|
@ -1553,9 +1727,32 @@ def subscribe(user, plan, token, accepted_plans):
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/user/invoices', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
def list_user_invoices():
|
||||||
|
user = current_user.db_user()
|
||||||
|
if not user.stripe_id:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return get_invoices(user.stripe_id)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/invoices', methods=['GET'])
|
@app.route('/api/organization/<orgname>/invoices', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def org_invoices_api(orgname):
|
@org_api_call('list_user_invoices')
|
||||||
|
def list_org_invoices(orgname):
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
organization = model.get_organization(orgname)
|
||||||
|
if not organization.stripe_id:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
return get_invoices(organization.stripe_id)
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
def get_invoices(customer_id):
|
||||||
def invoice_view(i):
|
def invoice_view(i):
|
||||||
return {
|
return {
|
||||||
'id': i.id,
|
'id': i.id,
|
||||||
|
@ -1571,37 +1768,32 @@ def org_invoices_api(orgname):
|
||||||
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
'plan': i.lines.data[0].plan.id if i.lines.data[0].plan else None
|
||||||
}
|
}
|
||||||
|
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
invoices = stripe.Invoice.all(customer=customer_id, count=12)
|
||||||
if permission.can():
|
return jsonify({
|
||||||
organization = model.get_organization(orgname)
|
'invoices': [invoice_view(i) for i in invoices.data]
|
||||||
if not organization.stripe_id:
|
})
|
||||||
abort(404)
|
|
||||||
|
|
||||||
invoices = stripe.Invoice.all(customer=organization.stripe_id, count=12)
|
|
||||||
return jsonify({
|
|
||||||
'invoices': [invoice_view(i) for i in invoices.data]
|
|
||||||
})
|
|
||||||
|
|
||||||
abort(403)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
|
@app.route('/api/organization/<orgname>/plan', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def subscribe_org_api(orgname):
|
@internal_api_call
|
||||||
|
@org_api_call('update_user_subscription')
|
||||||
|
def update_org_subscription(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
request_data = request.get_json()
|
request_data = request.get_json()
|
||||||
plan = request_data['plan']
|
plan = request_data['plan']
|
||||||
token = request_data['token'] if 'token' in request_data else None
|
token = request_data['token'] if 'token' in request_data else None
|
||||||
organization = model.get_organization(orgname)
|
organization = model.get_organization(orgname)
|
||||||
return subscribe(organization, plan, token, BUSINESS_PLANS)
|
return subscribe(organization, plan, token, True) # Business plan required
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/user/plan', methods=['GET'])
|
@app.route('/api/user/plan', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_subscription():
|
@internal_api_call
|
||||||
|
def get_user_subscription():
|
||||||
user = current_user.db_user()
|
user = current_user.db_user()
|
||||||
private_repos = model.get_private_repo_count(user.username)
|
private_repos = model.get_private_repo_count(user.username)
|
||||||
|
|
||||||
|
@ -1619,6 +1811,8 @@ def get_subscription():
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/plan', methods=['GET'])
|
@app.route('/api/organization/<orgname>/plan', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@internal_api_call
|
||||||
|
@org_api_call('get_user_subscription')
|
||||||
def get_org_subscription(orgname):
|
def get_org_subscription(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
@ -1631,7 +1825,7 @@ def get_org_subscription(orgname):
|
||||||
return jsonify(subscription_view(cus.subscription, private_repos))
|
return jsonify(subscription_view(cus.subscription, private_repos))
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'plan': 'bus-free',
|
'plan': 'free',
|
||||||
'usedPrivateRepos': private_repos,
|
'usedPrivateRepos': private_repos,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1657,6 +1851,7 @@ def get_user_robots():
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/robots', methods=['GET'])
|
@app.route('/api/organization/<orgname>/robots', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@org_api_call('get_user_robots')
|
||||||
def get_org_robots(orgname):
|
def get_org_robots(orgname):
|
||||||
permission = OrganizationMemberPermission(orgname)
|
permission = OrganizationMemberPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
@ -1670,7 +1865,7 @@ def get_org_robots(orgname):
|
||||||
|
|
||||||
@app.route('/api/user/robots/<robot_shortname>', methods=['PUT'])
|
@app.route('/api/user/robots/<robot_shortname>', methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_robot(robot_shortname):
|
def create_user_robot(robot_shortname):
|
||||||
parent = current_user.db_user()
|
parent = current_user.db_user()
|
||||||
robot, password = model.create_robot(robot_shortname, parent)
|
robot, password = model.create_robot(robot_shortname, parent)
|
||||||
resp = jsonify(robot_view(robot.username, password))
|
resp = jsonify(robot_view(robot.username, password))
|
||||||
|
@ -1682,6 +1877,7 @@ def create_robot(robot_shortname):
|
||||||
@app.route('/api/organization/<orgname>/robots/<robot_shortname>',
|
@app.route('/api/organization/<orgname>/robots/<robot_shortname>',
|
||||||
methods=['PUT'])
|
methods=['PUT'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@org_api_call('create_user_robot')
|
||||||
def create_org_robot(orgname, robot_shortname):
|
def create_org_robot(orgname, robot_shortname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
@ -1697,7 +1893,7 @@ def create_org_robot(orgname, robot_shortname):
|
||||||
|
|
||||||
@app.route('/api/user/robots/<robot_shortname>', methods=['DELETE'])
|
@app.route('/api/user/robots/<robot_shortname>', methods=['DELETE'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def delete_robot(robot_shortname):
|
def delete_user_robot(robot_shortname):
|
||||||
parent = current_user.db_user()
|
parent = current_user.db_user()
|
||||||
model.delete_robot(format_robot_username(parent.username, robot_shortname))
|
model.delete_robot(format_robot_username(parent.username, robot_shortname))
|
||||||
log_action('delete_robot', parent.username, {'robot': robot_shortname})
|
log_action('delete_robot', parent.username, {'robot': robot_shortname})
|
||||||
|
@ -1707,6 +1903,7 @@ def delete_robot(robot_shortname):
|
||||||
@app.route('/api/organization/<orgname>/robots/<robot_shortname>',
|
@app.route('/api/organization/<orgname>/robots/<robot_shortname>',
|
||||||
methods=['DELETE'])
|
methods=['DELETE'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
|
@org_api_call('delete_user_robot')
|
||||||
def delete_org_robot(orgname, robot_shortname):
|
def delete_org_robot(orgname, robot_shortname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
|
@ -1718,27 +1915,27 @@ def delete_org_robot(orgname, robot_shortname):
|
||||||
|
|
||||||
|
|
||||||
def log_view(log):
|
def log_view(log):
|
||||||
view = {
|
view = {
|
||||||
'kind': log.kind.name,
|
'kind': log.kind.name,
|
||||||
'metadata': json.loads(log.metadata_json),
|
'metadata': json.loads(log.metadata_json),
|
||||||
'ip': log.ip,
|
'ip': log.ip,
|
||||||
'datetime': log.datetime,
|
'datetime': log.datetime,
|
||||||
|
}
|
||||||
|
|
||||||
|
if log.performer:
|
||||||
|
view['performer'] = {
|
||||||
|
'username': log.performer.username,
|
||||||
|
'is_robot': log.performer.robot,
|
||||||
}
|
}
|
||||||
|
|
||||||
if log.performer:
|
return view
|
||||||
view['performer'] = {
|
|
||||||
'username': log.performer.username,
|
|
||||||
'is_robot': log.performer.robot,
|
|
||||||
}
|
|
||||||
|
|
||||||
return view
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>/logs', methods=['GET'])
|
@app.route('/api/repository/<path:repository>/logs', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
def repo_logs_api(namespace, repository):
|
def list_repo_logs(namespace, repository):
|
||||||
permission = AdministerRepositoryPermission(namespace, repository)
|
permission = AdministerRepositoryPermission(namespace, repository)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
|
@ -1754,19 +1951,33 @@ def repo_logs_api(namespace, repository):
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/logs', methods=['GET'])
|
@app.route('/api/organization/<orgname>/logs', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def org_logs_api(orgname):
|
@org_api_call('list_user_logs')
|
||||||
|
def list_org_logs(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
if permission.can():
|
||||||
performer_name = request.args.get('performer', None)
|
performer_name = request.args.get('performer', None)
|
||||||
start_time = request.args.get('starttime', None)
|
start_time = request.args.get('starttime', None)
|
||||||
end_time = request.args.get('endtime', None)
|
end_time = request.args.get('endtime', None)
|
||||||
|
|
||||||
return get_logs(orgname, start_time, end_time, performer_name=performer_name)
|
return get_logs(orgname, start_time, end_time,
|
||||||
|
performer_name=performer_name)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
def get_logs(namespace, start_time, end_time, performer_name=None, repository=None):
|
@app.route('/api/user/logs', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
def list_user_logs():
|
||||||
|
performer_name = request.args.get('performer', None)
|
||||||
|
start_time = request.args.get('starttime', None)
|
||||||
|
end_time = request.args.get('endtime', None)
|
||||||
|
|
||||||
|
return get_logs(current_user.db_user().username, start_time, end_time,
|
||||||
|
performer_name=performer_name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_logs(namespace, start_time, end_time, performer_name=None,
|
||||||
|
repository=None):
|
||||||
performer = None
|
performer = None
|
||||||
if performer_name:
|
if performer_name:
|
||||||
performer = model.get_user(performer_name)
|
performer = model.get_user(performer_name)
|
||||||
|
@ -1790,7 +2001,8 @@ def get_logs(namespace, start_time, end_time, performer_name=None, repository=No
|
||||||
if not end_time:
|
if not end_time:
|
||||||
end_time = datetime.today()
|
end_time = datetime.today()
|
||||||
|
|
||||||
logs = model.list_logs(namespace, start_time, end_time, performer = performer, repository=repository)
|
logs = model.list_logs(namespace, start_time, end_time, performer=performer,
|
||||||
|
repository=repository)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'start_time': start_time,
|
'start_time': start_time,
|
||||||
'end_time': end_time,
|
'end_time': end_time,
|
||||||
|
|
48
endpoints/common.py
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask.ext.login import login_user, UserMixin
|
||||||
|
from flask.ext.principal import identity_changed
|
||||||
|
|
||||||
|
from data import model
|
||||||
|
from app import app, login_manager
|
||||||
|
from auth.permissions import QuayDeferredPermissionUser
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def load_user(username):
|
||||||
|
logger.debug('Loading user: %s' % username)
|
||||||
|
return _LoginWrappedDBUser(username)
|
||||||
|
|
||||||
|
class _LoginWrappedDBUser(UserMixin):
|
||||||
|
def __init__(self, db_username, db_user=None):
|
||||||
|
|
||||||
|
self._db_username = db_username
|
||||||
|
self._db_user = db_user
|
||||||
|
|
||||||
|
def db_user(self):
|
||||||
|
if not self._db_user:
|
||||||
|
self._db_user = model.get_user(self._db_username)
|
||||||
|
return self._db_user
|
||||||
|
|
||||||
|
def is_authenticated(self):
|
||||||
|
return self.db_user() is not None
|
||||||
|
|
||||||
|
def is_active(self):
|
||||||
|
return self.db_user().verified
|
||||||
|
|
||||||
|
def get_id(self):
|
||||||
|
return unicode(self._db_username)
|
||||||
|
|
||||||
|
|
||||||
|
def common_login(db_user):
|
||||||
|
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
|
||||||
|
logger.debug('Successfully signed in as: %s' % db_user.username)
|
||||||
|
new_identity = QuayDeferredPermissionUser(db_user.username, 'username')
|
||||||
|
identity_changed.send(app, identity=new_identity)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.debug('User could not be logged in, inactive?.')
|
||||||
|
return False
|
|
@ -63,14 +63,14 @@ def create_user():
|
||||||
model.load_token_data(password)
|
model.load_token_data(password)
|
||||||
return make_response('Verified', 201)
|
return make_response('Verified', 201)
|
||||||
except model.InvalidTokenException:
|
except model.InvalidTokenException:
|
||||||
abort(401)
|
return make_response('Invalid access token.', 400)
|
||||||
|
|
||||||
elif '+' in username:
|
elif '+' in username:
|
||||||
try:
|
try:
|
||||||
model.verify_robot(username, password)
|
model.verify_robot(username, password)
|
||||||
return make_response('Verified', 201)
|
return make_response('Verified', 201)
|
||||||
except model.InvalidRobotException:
|
except model.InvalidRobotException:
|
||||||
abort(401)
|
return make_response('Invalid robot account or password.', 400)
|
||||||
|
|
||||||
existing_user = model.get_user(username)
|
existing_user = model.get_user(username)
|
||||||
if existing_user:
|
if existing_user:
|
||||||
|
@ -78,7 +78,7 @@ def create_user():
|
||||||
if verified:
|
if verified:
|
||||||
return make_response('Verified', 201)
|
return make_response('Verified', 201)
|
||||||
else:
|
else:
|
||||||
abort(401)
|
return make_response('Invalid password.', 400)
|
||||||
else:
|
else:
|
||||||
# New user case
|
# New user case
|
||||||
new_user = model.create_user(username, password, user_data['email'])
|
new_user = model.create_user(username, password, user_data['email'])
|
||||||
|
@ -134,8 +134,8 @@ def create_repository(namespace, repository):
|
||||||
repo = model.get_repository(namespace, repository)
|
repo = model.get_repository(namespace, repository)
|
||||||
|
|
||||||
if not repo and get_authenticated_user() is None:
|
if not repo and get_authenticated_user() is None:
|
||||||
logger.debug('Attempt to create new repository with token auth.')
|
logger.debug('Attempt to create new repository without user auth.')
|
||||||
abort(400)
|
abort(401)
|
||||||
|
|
||||||
elif repo:
|
elif repo:
|
||||||
permission = ModifyRepositoryPermission(namespace, repository)
|
permission = ModifyRepositoryPermission(namespace, repository)
|
||||||
|
@ -158,10 +158,6 @@ def create_repository(namespace, repository):
|
||||||
for existing in model.get_repository_images(namespace, repository):
|
for existing in model.get_repository_images(namespace, repository):
|
||||||
if existing.docker_image_id in new_repo_images:
|
if existing.docker_image_id in new_repo_images:
|
||||||
added_images.pop(existing.docker_image_id)
|
added_images.pop(existing.docker_image_id)
|
||||||
else:
|
|
||||||
logger.debug('Deleting existing image with id: %s' %
|
|
||||||
existing.docker_image_id)
|
|
||||||
existing.delete_instance(recursive=True)
|
|
||||||
|
|
||||||
for image_description in added_images.values():
|
for image_description in added_images.values():
|
||||||
model.create_image(image_description['id'], repo)
|
model.create_image(image_description['id'], repo)
|
||||||
|
|
|
@ -43,6 +43,7 @@ def require_completion(f):
|
||||||
def wrapper(namespace, repository, *args, **kwargs):
|
def wrapper(namespace, repository, *args, **kwargs):
|
||||||
if store.exists(store.image_mark_path(namespace, repository,
|
if store.exists(store.image_mark_path(namespace, repository,
|
||||||
kwargs['image_id'])):
|
kwargs['image_id'])):
|
||||||
|
logger.warning('Image is already being uploaded: %s', kwargs['image_id'])
|
||||||
abort(400) # 'Image is being uploaded, retry later')
|
abort(400) # 'Image is being uploaded, retry later')
|
||||||
return f(namespace, repository, *args, **kwargs)
|
return f(namespace, repository, *args, **kwargs)
|
||||||
return wrapper
|
return wrapper
|
||||||
|
@ -87,6 +88,7 @@ def get_image_layer(namespace, repository, image_id, headers):
|
||||||
try:
|
try:
|
||||||
return Response(store.stream_read(path), headers=headers)
|
return Response(store.stream_read(path), headers=headers)
|
||||||
except IOError:
|
except IOError:
|
||||||
|
logger.warning('Image not found: %s', image_id)
|
||||||
abort(404) # 'Image not found', 404)
|
abort(404) # 'Image not found', 404)
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
@ -124,6 +126,11 @@ def put_image_layer(namespace, repository, image_id):
|
||||||
store.stream_write(layer_path, sr)
|
store.stream_write(layer_path, sr)
|
||||||
csums.append('sha256:{0}'.format(h.hexdigest()))
|
csums.append('sha256:{0}'.format(h.hexdigest()))
|
||||||
try:
|
try:
|
||||||
|
image_size = tmp.tell()
|
||||||
|
|
||||||
|
# Save the size of the image.
|
||||||
|
model.set_image_size(image_id, namespace, repository, image_size)
|
||||||
|
|
||||||
tmp.seek(0)
|
tmp.seek(0)
|
||||||
csums.append(checksums.compute_tarsum(tmp, json_data))
|
csums.append(checksums.compute_tarsum(tmp, json_data))
|
||||||
tmp.close()
|
tmp.close()
|
||||||
|
@ -141,7 +148,7 @@ def put_image_layer(namespace, repository, image_id):
|
||||||
return make_response('true', 200)
|
return make_response('true', 200)
|
||||||
# We check if the checksums provided matches one the one we computed
|
# We check if the checksums provided matches one the one we computed
|
||||||
if checksum not in csums:
|
if checksum not in csums:
|
||||||
logger.debug('put_image_layer: Wrong checksum')
|
logger.warning('put_image_layer: Wrong checksum')
|
||||||
abort(400) # 'Checksum mismatch, ignoring the layer')
|
abort(400) # 'Checksum mismatch, ignoring the layer')
|
||||||
# Checksum is ok, we remove the marker
|
# Checksum is ok, we remove the marker
|
||||||
store.remove(mark_path)
|
store.remove(mark_path)
|
||||||
|
@ -168,8 +175,10 @@ def put_image_checksum(namespace, repository, image_id):
|
||||||
|
|
||||||
checksum = request.headers.get('X-Docker-Checksum')
|
checksum = request.headers.get('X-Docker-Checksum')
|
||||||
if not checksum:
|
if not checksum:
|
||||||
|
logger.warning('Missing Image\'s checksum: %s', image_id)
|
||||||
abort(400) # 'Missing Image\'s checksum')
|
abort(400) # 'Missing Image\'s checksum')
|
||||||
if not session.get('checksum'):
|
if not session.get('checksum'):
|
||||||
|
logger.warning('Checksum not found in Cookie for image: %s', image_id)
|
||||||
abort(400) # 'Checksum not found in Cookie')
|
abort(400) # 'Checksum not found in Cookie')
|
||||||
if not store.exists(store.image_json_path(namespace, repository, image_id)):
|
if not store.exists(store.image_json_path(namespace, repository, image_id)):
|
||||||
abort(404) # 'Image not found', 404)
|
abort(404) # 'Image not found', 404)
|
||||||
|
@ -287,8 +296,11 @@ def put_image_json(namespace, repository, image_id):
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
pass
|
pass
|
||||||
if not data or not isinstance(data, dict):
|
if not data or not isinstance(data, dict):
|
||||||
|
logger.warning('Invalid JSON for image: %s json: %s', image_id,
|
||||||
|
request.data)
|
||||||
abort(400) # 'Invalid JSON')
|
abort(400) # 'Invalid JSON')
|
||||||
if 'id' not in data:
|
if 'id' not in data:
|
||||||
|
logger.warning('Missing key `id\' in JSON for image: %s', image_id)
|
||||||
abort(400) # 'Missing key `id\' in JSON')
|
abort(400) # 'Missing key `id\' in JSON')
|
||||||
# Read the checksum
|
# Read the checksum
|
||||||
checksum = request.headers.get('X-Docker-Checksum')
|
checksum = request.headers.get('X-Docker-Checksum')
|
||||||
|
@ -301,11 +313,14 @@ def put_image_json(namespace, repository, image_id):
|
||||||
# We cleanup any old checksum in case it's a retry after a fail
|
# We cleanup any old checksum in case it's a retry after a fail
|
||||||
store.remove(store.image_checksum_path(namespace, repository, image_id))
|
store.remove(store.image_checksum_path(namespace, repository, image_id))
|
||||||
if image_id != data['id']:
|
if image_id != data['id']:
|
||||||
|
logger.warning('JSON data contains invalid id for image: %s', image_id)
|
||||||
abort(400) # 'JSON data contains invalid id')
|
abort(400) # 'JSON data contains invalid id')
|
||||||
parent_id = data.get('parent')
|
parent_id = data.get('parent')
|
||||||
if parent_id and not store.exists(store.image_json_path(namespace,
|
if parent_id and not store.exists(store.image_json_path(namespace,
|
||||||
repository,
|
repository,
|
||||||
data['parent'])):
|
data['parent'])):
|
||||||
|
logger.warning('Image depends on a non existing parent image: %s',
|
||||||
|
image_id)
|
||||||
abort(400) # 'Image depends on a non existing parent')
|
abort(400) # 'Image depends on a non existing parent')
|
||||||
json_path = store.image_json_path(namespace, repository, image_id)
|
json_path = store.image_json_path(namespace, repository, image_id)
|
||||||
mark_path = store.image_mark_path(namespace, repository, image_id)
|
mark_path = store.image_mark_path(namespace, repository, image_id)
|
||||||
|
@ -319,8 +334,10 @@ def put_image_json(namespace, repository, image_id):
|
||||||
else:
|
else:
|
||||||
parent_obj = None
|
parent_obj = None
|
||||||
|
|
||||||
|
command_list = data.get('container_config', {}).get('Cmd', None)
|
||||||
|
command = json.dumps(command_list) if command_list else None
|
||||||
model.set_image_metadata(image_id, namespace, repository,
|
model.set_image_metadata(image_id, namespace, repository,
|
||||||
data.get('created'), data.get('comment'),
|
data.get('created'), data.get('comment'), command,
|
||||||
parent_obj)
|
parent_obj)
|
||||||
store.put_content(mark_path, 'true')
|
store.put_content(mark_path, 'true')
|
||||||
store.put_content(json_path, request.data)
|
store.put_content(json_path, request.data)
|
||||||
|
@ -328,14 +345,6 @@ def put_image_json(namespace, repository, image_id):
|
||||||
return make_response('true', 200)
|
return make_response('true', 200)
|
||||||
|
|
||||||
|
|
||||||
def delete_repository_storage(namespace, repository):
|
|
||||||
""" Caller should have already verified proper permissions. """
|
|
||||||
repository_path = store.repository_namespace_path(namespace, repository)
|
|
||||||
|
|
||||||
logger.debug('Recursively deleting path: %s' % repository_path)
|
|
||||||
store.remove(repository_path)
|
|
||||||
|
|
||||||
|
|
||||||
def process_image_changes(namespace, repository, image_id):
|
def process_image_changes(namespace, repository, image_id):
|
||||||
logger.debug('Generating diffs for image: %s' % image_id)
|
logger.debug('Generating diffs for image: %s' % image_id)
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,33 @@
|
||||||
|
import math
|
||||||
|
|
||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from flask import jsonify, send_file
|
from flask import jsonify
|
||||||
from app import app
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
def generate_image_completion(rand_func):
|
||||||
|
images = {}
|
||||||
|
for image_id in range(rand_func.randint(1, 11)):
|
||||||
|
total = int(math.pow(abs(rand_func.gauss(0, 1000)), 2))
|
||||||
|
current = rand_func.randint(0, total)
|
||||||
|
image_id = 'image_id_%s' % image_id
|
||||||
|
images[image_id] = {
|
||||||
|
'total': total,
|
||||||
|
'current': current,
|
||||||
|
}
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
@app.route('/test/build/status', methods=['GET'])
|
@app.route('/test/build/status', methods=['GET'])
|
||||||
def generate_random_build_status():
|
def generate_random_build_status():
|
||||||
response = {
|
response = {
|
||||||
'id': 1,
|
'id': 1,
|
||||||
'total_commands': None,
|
'total_commands': None,
|
||||||
'total_images': None,
|
|
||||||
'current_command': None,
|
'current_command': None,
|
||||||
'current_image': None,
|
'push_completion': 0.0,
|
||||||
'image_completion_percent': None,
|
|
||||||
'status': None,
|
'status': None,
|
||||||
'message': None,
|
'message': None,
|
||||||
|
'image_completion': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
random = SystemRandom()
|
random = SystemRandom()
|
||||||
|
@ -35,9 +49,8 @@ def generate_random_build_status():
|
||||||
'pushing': {
|
'pushing': {
|
||||||
'total_commands': 7,
|
'total_commands': 7,
|
||||||
'current_command': 7,
|
'current_command': 7,
|
||||||
'total_images': 11,
|
'push_completion': random.random(),
|
||||||
'current_image': random.randint(1, 11),
|
'image_completion': generate_image_completion(random),
|
||||||
'image_completion_percent': random.randint(0, 100),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
151
endpoints/web.py
|
@ -4,53 +4,33 @@ import stripe
|
||||||
|
|
||||||
from flask import (abort, redirect, request, url_for, render_template,
|
from flask import (abort, redirect, request, url_for, render_template,
|
||||||
make_response, Response)
|
make_response, Response)
|
||||||
from flask.ext.login import login_user, UserMixin
|
from flask.ext.login import login_required, current_user
|
||||||
from flask.ext.principal import identity_changed
|
|
||||||
from urlparse import urlparse
|
from urlparse import urlparse
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, login_manager, mixpanel
|
from app import app, mixpanel
|
||||||
from auth.permissions import (QuayDeferredPermissionUser,
|
from auth.permissions import AdministerOrganizationPermission
|
||||||
AdministerOrganizationPermission)
|
|
||||||
from util.invoice import renderInvoiceToPdf
|
from util.invoice import renderInvoiceToPdf
|
||||||
from util.seo import render_snapshot
|
from util.seo import render_snapshot
|
||||||
|
from util.cache import no_cache
|
||||||
|
from endpoints.api import get_route_data
|
||||||
|
from endpoints.common import common_login
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class _LoginWrappedDBUser(UserMixin):
|
def render_page_template(name, **kwargs):
|
||||||
def __init__(self, db_username, db_user=None):
|
return make_response(render_template(name, route_data=get_route_data(),
|
||||||
|
**kwargs))
|
||||||
self._db_username = db_username
|
|
||||||
self._db_user = db_user
|
|
||||||
|
|
||||||
def db_user(self):
|
|
||||||
if not self._db_user:
|
|
||||||
self._db_user = model.get_user(self._db_username)
|
|
||||||
return self._db_user
|
|
||||||
|
|
||||||
def is_authenticated(self):
|
|
||||||
return self.db_user() is not None
|
|
||||||
|
|
||||||
def is_active(self):
|
|
||||||
return self.db_user().verified
|
|
||||||
|
|
||||||
def get_id(self):
|
|
||||||
return unicode(self._db_username)
|
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def load_user(username):
|
|
||||||
logger.debug('Loading user: %s' % username)
|
|
||||||
return _LoginWrappedDBUser(username)
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/', methods=['GET'], defaults={'path': ''})
|
@app.route('/', methods=['GET'], defaults={'path': ''})
|
||||||
@app.route('/repository/<path:path>', methods=['GET'])
|
@app.route('/repository/<path:path>', methods=['GET'])
|
||||||
@app.route('/organization/<path:path>', methods=['GET'])
|
@app.route('/organization/<path:path>', methods=['GET'])
|
||||||
|
@no_cache
|
||||||
def index(path):
|
def index(path):
|
||||||
return render_template('index.html')
|
return render_page_template('index.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/snapshot', methods=['GET'])
|
@app.route('/snapshot', methods=['GET'])
|
||||||
|
@ -67,26 +47,32 @@ def snapshot(path = ''):
|
||||||
|
|
||||||
|
|
||||||
@app.route('/plans/')
|
@app.route('/plans/')
|
||||||
|
@no_cache
|
||||||
def plans():
|
def plans():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/guide/')
|
@app.route('/guide/')
|
||||||
|
@no_cache
|
||||||
def guide():
|
def guide():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/organizations/')
|
@app.route('/organizations/')
|
||||||
@app.route('/organizations/new/')
|
@app.route('/organizations/new/')
|
||||||
|
@no_cache
|
||||||
def organizations():
|
def organizations():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/user/')
|
@app.route('/user/')
|
||||||
|
@no_cache
|
||||||
def user():
|
def user():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/signin/')
|
@app.route('/signin/')
|
||||||
|
@no_cache
|
||||||
def signin():
|
def signin():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
@ -97,76 +83,85 @@ def contact():
|
||||||
|
|
||||||
|
|
||||||
@app.route('/new/')
|
@app.route('/new/')
|
||||||
|
@no_cache
|
||||||
def new():
|
def new():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/repository/')
|
@app.route('/repository/')
|
||||||
|
@no_cache
|
||||||
def repository():
|
def repository():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/security/')
|
@app.route('/security/')
|
||||||
|
@no_cache
|
||||||
def security():
|
def security():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/v1')
|
@app.route('/v1')
|
||||||
@app.route('/v1/')
|
@app.route('/v1/')
|
||||||
|
@no_cache
|
||||||
def v1():
|
def v1():
|
||||||
return index('')
|
return index('')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/status', methods=['GET'])
|
@app.route('/status', methods=['GET'])
|
||||||
|
@no_cache
|
||||||
def status():
|
def status():
|
||||||
return make_response('Healthy')
|
return make_response('Healthy')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tos', methods=['GET'])
|
@app.route('/tos', methods=['GET'])
|
||||||
|
@no_cache
|
||||||
def tos():
|
def tos():
|
||||||
return render_template('tos.html')
|
return render_page_template('tos.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/disclaimer', methods=['GET'])
|
@app.route('/disclaimer', methods=['GET'])
|
||||||
|
@no_cache
|
||||||
def disclaimer():
|
def disclaimer():
|
||||||
return render_template('disclaimer.html')
|
return render_page_template('disclaimer.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/privacy', methods=['GET'])
|
@app.route('/privacy', methods=['GET'])
|
||||||
|
@no_cache
|
||||||
def privacy():
|
def privacy():
|
||||||
return render_template('privacy.html')
|
return render_page_template('privacy.html')
|
||||||
|
|
||||||
|
|
||||||
@app.route('/receipt', methods=['GET'])
|
@app.route('/receipt', methods=['GET'])
|
||||||
def receipt():
|
def receipt():
|
||||||
|
if not current_user.is_authenticated():
|
||||||
|
abort(401)
|
||||||
|
return
|
||||||
|
|
||||||
id = request.args.get('id')
|
id = request.args.get('id')
|
||||||
if id:
|
if id:
|
||||||
invoice = stripe.Invoice.retrieve(id)
|
invoice = stripe.Invoice.retrieve(id)
|
||||||
if invoice:
|
if invoice:
|
||||||
org = model.get_user_or_org_by_customer_id(invoice.customer)
|
user_or_org = model.get_user_or_org_by_customer_id(invoice.customer)
|
||||||
if org and org.organization:
|
|
||||||
admin_org = AdministerOrganizationPermission(org.username)
|
if user_or_org:
|
||||||
if admin_org.can():
|
if user_or_org.organization:
|
||||||
file_data = renderInvoiceToPdf(invoice, org)
|
admin_org = AdministerOrganizationPermission(user_or_org.username)
|
||||||
return Response(file_data,
|
if not admin_org.can():
|
||||||
mimetype="application/pdf",
|
abort(404)
|
||||||
headers={"Content-Disposition":
|
return
|
||||||
"attachment;filename=receipt.pdf"})
|
else:
|
||||||
|
if not user_or_org.username == current_user.db_user().username:
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
file_data = renderInvoiceToPdf(invoice, user_or_org)
|
||||||
|
return Response(file_data,
|
||||||
|
mimetype="application/pdf",
|
||||||
|
headers={"Content-Disposition": "attachment;filename=receipt.pdf"})
|
||||||
abort(404)
|
abort(404)
|
||||||
|
|
||||||
def common_login(db_user):
|
|
||||||
if login_user(_LoginWrappedDBUser(db_user.username, db_user)):
|
|
||||||
logger.debug('Successfully signed in as: %s' % db_user.username)
|
|
||||||
new_identity = QuayDeferredPermissionUser(db_user.username, 'username')
|
|
||||||
identity_changed.send(app, identity=new_identity)
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.debug('User could not be logged in, inactive?.')
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
def exchange_github_code_for_token(code):
|
||||||
@app.route('/oauth2/github/callback', methods=['GET'])
|
|
||||||
def github_oauth_callback():
|
|
||||||
code = request.args.get('code')
|
code = request.args.get('code')
|
||||||
payload = {
|
payload = {
|
||||||
'client_id': app.config['GITHUB_CLIENT_ID'],
|
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||||
|
@ -181,19 +176,37 @@ def github_oauth_callback():
|
||||||
params=payload, headers=headers)
|
params=payload, headers=headers)
|
||||||
|
|
||||||
token = get_access_token.json()['access_token']
|
token = get_access_token.json()['access_token']
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
def get_github_user(token):
|
||||||
token_param = {
|
token_param = {
|
||||||
'access_token': token,
|
'access_token': token,
|
||||||
}
|
}
|
||||||
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
||||||
|
|
||||||
user_data = get_user.json()
|
return get_user.json()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/oauth2/github/callback', methods=['GET'])
|
||||||
|
def github_oauth_callback():
|
||||||
|
error = request.args.get('error', None)
|
||||||
|
if error:
|
||||||
|
return render_page_template('githuberror.html', error_message=error)
|
||||||
|
|
||||||
|
token = exchange_github_code_for_token(request.args.get('code'))
|
||||||
|
user_data = get_github_user(token)
|
||||||
|
|
||||||
username = user_data['login']
|
username = user_data['login']
|
||||||
github_id = user_data['id']
|
github_id = user_data['id']
|
||||||
|
|
||||||
v3_media_type = {
|
v3_media_type = {
|
||||||
'Accept': 'application/vnd.github.v3'
|
'Accept': 'application/vnd.github.v3'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
token_param = {
|
||||||
|
'access_token': token,
|
||||||
|
}
|
||||||
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
||||||
params=token_param, headers=v3_media_type)
|
params=token_param, headers=v3_media_type)
|
||||||
|
|
||||||
|
@ -220,18 +233,33 @@ def github_oauth_callback():
|
||||||
mixpanel.alias(to_login.username, state)
|
mixpanel.alias(to_login.username, state)
|
||||||
|
|
||||||
except model.DataModelException, ex:
|
except model.DataModelException, ex:
|
||||||
return render_template('githuberror.html', error_message=ex.message)
|
return render_page_template('githuberror.html', error_message=ex.message)
|
||||||
|
|
||||||
if common_login(to_login):
|
if common_login(to_login):
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
return render_template('githuberror.html')
|
return render_page_template('githuberror.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/oauth2/github/callback/attach', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def github_oauth_attach():
|
||||||
|
token = exchange_github_code_for_token(request.args.get('code'))
|
||||||
|
user_data = get_github_user(token)
|
||||||
|
github_id = user_data['id']
|
||||||
|
user_obj = current_user.db_user()
|
||||||
|
model.attach_federated_login(user_obj, 'github', github_id)
|
||||||
|
return redirect(url_for('user'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/confirm', methods=['GET'])
|
@app.route('/confirm', methods=['GET'])
|
||||||
def confirm_email():
|
def confirm_email():
|
||||||
code = request.values['code']
|
code = request.values['code']
|
||||||
user = model.confirm_user_email(code)
|
|
||||||
|
try:
|
||||||
|
user = model.confirm_user_email(code)
|
||||||
|
except model.DataModelException as ex:
|
||||||
|
return redirect(url_for('signin'))
|
||||||
|
|
||||||
common_login(user)
|
common_login(user)
|
||||||
|
|
||||||
|
@ -248,8 +276,3 @@ def confirm_recovery():
|
||||||
return redirect(url_for('user'))
|
return redirect(url_for('user'))
|
||||||
else:
|
else:
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/reset', methods=['GET'])
|
|
||||||
def password_reset():
|
|
||||||
pass
|
|
||||||
|
|
|
@ -1,19 +1,14 @@
|
||||||
import logging
|
import logging
|
||||||
import requests
|
|
||||||
import stripe
|
import stripe
|
||||||
|
|
||||||
from flask import (abort, redirect, request, url_for, render_template,
|
from flask import request, make_response
|
||||||
make_response)
|
|
||||||
from flask.ext.login import login_user, UserMixin, login_required
|
|
||||||
from flask.ext.principal import identity_changed, Identity, AnonymousIdentity
|
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app, login_manager, mixpanel
|
from app import app
|
||||||
from auth.permissions import QuayDeferredPermissionUser
|
|
||||||
from data.plans import USER_PLANS, BUSINESS_PLANS, get_plan
|
|
||||||
from util.invoice import renderInvoiceToHtml
|
from util.invoice import renderInvoiceToHtml
|
||||||
from util.email import send_invoice_email
|
from util.email import send_invoice_email
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -33,10 +28,10 @@ def stripe_webhook():
|
||||||
# Find the user associated with the customer ID.
|
# Find the user associated with the customer ID.
|
||||||
user = model.get_user_or_org_by_customer_id(customer_id)
|
user = model.get_user_or_org_by_customer_id(customer_id)
|
||||||
if user and user.invoice_email:
|
if user and user.invoice_email:
|
||||||
# Lookup the invoice.
|
# Lookup the invoice.
|
||||||
invoice = stripe.Invoice.retrieve(invoice_id)
|
invoice = stripe.Invoice.retrieve(invoice_id)
|
||||||
if invoice:
|
if invoice:
|
||||||
invoice_html = renderInvoiceToHtml(invoice, user)
|
invoice_html = renderInvoiceToHtml(invoice, user)
|
||||||
send_invoice_email(user.email, invoice_html)
|
send_invoice_email(user.email, invoice_html)
|
||||||
|
|
||||||
return make_response('Okay')
|
return make_response('Okay')
|
||||||
|
|
5
gunicorn_config.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
bind = 'unix:/tmp/gunicorn.sock'
|
||||||
|
workers = 8
|
||||||
|
worker_class = 'gevent'
|
||||||
|
timeout = 2000
|
||||||
|
daemon = True
|
30
initdb.py
|
@ -1,11 +1,9 @@
|
||||||
import logging
|
import logging
|
||||||
import string
|
import json
|
||||||
import shutil
|
|
||||||
import os
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import random
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask import url_for
|
|
||||||
from peewee import SqliteDatabase, create_model_tables, drop_model_tables
|
from peewee import SqliteDatabase, create_model_tables, drop_model_tables
|
||||||
|
|
||||||
from data.database import *
|
from data.database import *
|
||||||
|
@ -18,6 +16,15 @@ store = app.config['STORAGE']
|
||||||
|
|
||||||
SAMPLE_DIFFS = ['test/data/sample/diffs/diffs%s.json' % i
|
SAMPLE_DIFFS = ['test/data/sample/diffs/diffs%s.json' % i
|
||||||
for i in range(1, 10)]
|
for i in range(1, 10)]
|
||||||
|
SAMPLE_CMDS = [["/bin/bash"],
|
||||||
|
["/bin/sh", "-c",
|
||||||
|
"echo \"PasswordAuthentication no\" >> /etc/ssh/sshd_config"],
|
||||||
|
["/bin/sh", "-c",
|
||||||
|
"sed -i 's/#\\(force_color_prompt\\)/\\1/' /etc/skel/.bashrc"],
|
||||||
|
["/bin/sh", "-c", "#(nop) EXPOSE [8080]"],
|
||||||
|
["/bin/sh", "-c",
|
||||||
|
"#(nop) MAINTAINER Jake Moshenko <jake@devtable.com>"],
|
||||||
|
None]
|
||||||
|
|
||||||
REFERENCE_DATE = datetime(2013, 6, 23)
|
REFERENCE_DATE = datetime(2013, 6, 23)
|
||||||
TEST_STRIPE_ID = 'cus_2tmnh3PkXQS8NG'
|
TEST_STRIPE_ID = 'cus_2tmnh3PkXQS8NG'
|
||||||
|
@ -51,9 +58,14 @@ def __create_subtree(repo, structure, parent):
|
||||||
model.set_image_checksum(docker_image_id, repo, checksum)
|
model.set_image_checksum(docker_image_id, repo, checksum)
|
||||||
|
|
||||||
creation_time = REFERENCE_DATE + timedelta(days=image_num)
|
creation_time = REFERENCE_DATE + timedelta(days=image_num)
|
||||||
|
command_list = SAMPLE_CMDS[image_num % len(SAMPLE_CMDS)]
|
||||||
|
command = json.dumps(command_list) if command_list else None
|
||||||
new_image = model.set_image_metadata(docker_image_id, repo.namespace,
|
new_image = model.set_image_metadata(docker_image_id, repo.namespace,
|
||||||
repo.name, str(creation_time),
|
repo.name, str(creation_time),
|
||||||
'no comment', parent)
|
'no comment', command, parent)
|
||||||
|
|
||||||
|
model.set_image_size(docker_image_id, repo.namespace, repo.name,
|
||||||
|
random.randrange(1, 1024 * 1024 * 1024))
|
||||||
|
|
||||||
# Populate the diff file
|
# Populate the diff file
|
||||||
diff_path = store.image_file_diffs_path(repo.namespace, repo.name,
|
diff_path = store.image_file_diffs_path(repo.namespace, repo.name,
|
||||||
|
@ -123,6 +135,7 @@ def initialize_database():
|
||||||
LogEntryKind.create(name='push_repo')
|
LogEntryKind.create(name='push_repo')
|
||||||
LogEntryKind.create(name='pull_repo')
|
LogEntryKind.create(name='pull_repo')
|
||||||
LogEntryKind.create(name='delete_repo')
|
LogEntryKind.create(name='delete_repo')
|
||||||
|
LogEntryKind.create(name='delete_tag')
|
||||||
LogEntryKind.create(name='add_repo_permission')
|
LogEntryKind.create(name='add_repo_permission')
|
||||||
LogEntryKind.create(name='change_repo_permission')
|
LogEntryKind.create(name='change_repo_permission')
|
||||||
LogEntryKind.create(name='delete_repo_permission')
|
LogEntryKind.create(name='delete_repo_permission')
|
||||||
|
@ -160,6 +173,7 @@ def populate_database():
|
||||||
new_user_1 = model.create_user('devtable', 'password',
|
new_user_1 = model.create_user('devtable', 'password',
|
||||||
'jschorr@devtable.com')
|
'jschorr@devtable.com')
|
||||||
new_user_1.verified = True
|
new_user_1.verified = True
|
||||||
|
new_user_1.stripe_id = TEST_STRIPE_ID
|
||||||
new_user_1.save()
|
new_user_1.save()
|
||||||
|
|
||||||
model.create_robot('dtrobot', new_user_1)
|
model.create_robot('dtrobot', new_user_1)
|
||||||
|
@ -279,6 +293,12 @@ def populate_database():
|
||||||
model.log_action('pull_repo', org.username, repository=org_repo, timestamp=today,
|
model.log_action('pull_repo', org.username, repository=org_repo, timestamp=today,
|
||||||
metadata={'token': 'sometoken', 'token_code': 'somecode', 'repo': 'orgrepo'})
|
metadata={'token': 'sometoken', 'token_code': 'somecode', 'repo': 'orgrepo'})
|
||||||
|
|
||||||
|
model.log_action('delete_tag', org.username, performer=new_user_2, repository=org_repo, timestamp=today,
|
||||||
|
metadata={'username': new_user_2.username, 'repo': 'orgrepo', 'tag': 'sometag'})
|
||||||
|
|
||||||
|
model.log_action('pull_repo', org.username, repository=org_repo, timestamp=today,
|
||||||
|
metadata={'token_code': 'somecode', 'repo': 'orgrepo'})
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
logging.basicConfig(**app.config['LOGGING_CONFIG'])
|
logging.basicConfig(**app.config['LOGGING_CONFIG'])
|
||||||
initialize_database()
|
initialize_database()
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
worker_processes 1;
|
worker_processes 2;
|
||||||
|
|
||||||
user root nogroup;
|
user root nogroup;
|
||||||
pid /tmp/nginx.pid;
|
pid /mnt/nginx/nginx.pid;
|
||||||
error_log /tmp/nginx.error.log;
|
error_log /mnt/nginx/nginx.error.log;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
|
@ -11,10 +11,10 @@ events {
|
||||||
|
|
||||||
http {
|
http {
|
||||||
types_hash_max_size 2048;
|
types_hash_max_size 2048;
|
||||||
include /etc/nginx/mime.types;
|
include /usr/local/nginx/conf/mime.types.default;
|
||||||
|
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
access_log /tmp/nginx.access.log combined;
|
access_log /mnt/nginx/nginx.access.log combined;
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
root /root/quay/;
|
root /root/quay/;
|
||||||
|
@ -43,6 +43,7 @@ http {
|
||||||
server {
|
server {
|
||||||
listen 443 default;
|
listen 443 default;
|
||||||
client_max_body_size 8G;
|
client_max_body_size 8G;
|
||||||
|
client_body_temp_path /mnt/nginx/client_body 1 2;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
keepalive_timeout 5;
|
keepalive_timeout 5;
|
||||||
|
@ -71,8 +72,12 @@ http {
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_set_header Transfer-Encoding $http_transfer_encoding;
|
||||||
|
|
||||||
proxy_pass http://app_server;
|
proxy_pass http://app_server;
|
||||||
proxy_read_timeout 2000;
|
proxy_read_timeout 2000;
|
||||||
|
proxy_temp_path /mnt/nginx/proxy_temp 1 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
15
nginx.conf
|
@ -1,8 +1,8 @@
|
||||||
worker_processes 1;
|
worker_processes 8;
|
||||||
|
|
||||||
user nobody nogroup;
|
user nobody nogroup;
|
||||||
pid /tmp/nginx.pid;
|
pid /mnt/nginx/nginx.pid;
|
||||||
error_log /tmp/nginx.error.log;
|
error_log /mnt/nginx/nginx.error.log;
|
||||||
|
|
||||||
events {
|
events {
|
||||||
worker_connections 1024;
|
worker_connections 1024;
|
||||||
|
@ -11,10 +11,10 @@ events {
|
||||||
|
|
||||||
http {
|
http {
|
||||||
types_hash_max_size 2048;
|
types_hash_max_size 2048;
|
||||||
include /etc/nginx/mime.types;
|
include /usr/local/nginx/conf/mime.types.default;
|
||||||
|
|
||||||
default_type application/octet-stream;
|
default_type application/octet-stream;
|
||||||
access_log /tmp/nginx.access.log combined;
|
access_log /mnt/nginx/nginx.access.log combined;
|
||||||
sendfile on;
|
sendfile on;
|
||||||
|
|
||||||
gzip on;
|
gzip on;
|
||||||
|
@ -41,6 +41,7 @@ http {
|
||||||
server {
|
server {
|
||||||
listen 443 default;
|
listen 443 default;
|
||||||
client_max_body_size 8G;
|
client_max_body_size 8G;
|
||||||
|
client_body_temp_path /mnt/nginx/client_body 1 2;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
keepalive_timeout 5;
|
keepalive_timeout 5;
|
||||||
|
@ -69,8 +70,12 @@ http {
|
||||||
proxy_redirect off;
|
proxy_redirect off;
|
||||||
proxy_buffering off;
|
proxy_buffering off;
|
||||||
|
|
||||||
|
proxy_request_buffering off;
|
||||||
|
proxy_set_header Transfer-Encoding $http_transfer_encoding;
|
||||||
|
|
||||||
proxy_pass http://app_server;
|
proxy_pass http://app_server;
|
||||||
proxy_read_timeout 2000;
|
proxy_read_timeout 2000;
|
||||||
|
proxy_temp_path /mnt/nginx/proxy_temp 1 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,20 +1,20 @@
|
||||||
APScheduler==2.1.1
|
APScheduler==2.1.1
|
||||||
Flask==0.10.1
|
Flask==0.10.1
|
||||||
Flask-Login==0.2.7
|
Flask-Login==0.2.9
|
||||||
Flask-Mail==0.9.0
|
Flask-Mail==0.9.0
|
||||||
Flask-Principal==0.4.0
|
Flask-Principal==0.4.0
|
||||||
Jinja2==2.7.1
|
Jinja2==2.7.1
|
||||||
MarkupSafe==0.18
|
MarkupSafe==0.18
|
||||||
Pillow==2.2.1
|
Pillow==2.3.0
|
||||||
PyMySQL==0.6.1
|
PyMySQL==0.6.1
|
||||||
Werkzeug==0.9.4
|
Werkzeug==0.9.4
|
||||||
argparse==1.2.1
|
argparse==1.2.1
|
||||||
beautifulsoup4==4.3.2
|
beautifulsoup4==4.3.2
|
||||||
blinker==1.3
|
blinker==1.3
|
||||||
boto==2.17.0
|
boto==2.21.2
|
||||||
distribute==0.6.34
|
distribute==0.6.34
|
||||||
ecdsa==0.10
|
ecdsa==0.10
|
||||||
gevent==0.13.8
|
gevent==1.0
|
||||||
greenlet==0.4.1
|
greenlet==0.4.1
|
||||||
gunicorn==18.0
|
gunicorn==18.0
|
||||||
html5lib==1.0b3
|
html5lib==1.0b3
|
||||||
|
@ -23,16 +23,16 @@ lockfile==0.9.1
|
||||||
marisa-trie==0.5.1
|
marisa-trie==0.5.1
|
||||||
mixpanel-py==3.0.0
|
mixpanel-py==3.0.0
|
||||||
paramiko==1.12.0
|
paramiko==1.12.0
|
||||||
peewee==2.1.5
|
peewee==2.1.7
|
||||||
py-bcrypt==0.4
|
py-bcrypt==0.4
|
||||||
pyPdf==1.13
|
pyPdf==1.13
|
||||||
pycrypto==2.6.1
|
pycrypto==2.6.1
|
||||||
python-daemon==1.6
|
python-daemon==1.6
|
||||||
python-dateutil==2.2
|
python-dateutil==2.2
|
||||||
python-digitalocean==0.5.1
|
python-digitalocean==0.6
|
||||||
reportlab==2.7
|
reportlab==2.7
|
||||||
requests==2.0.1
|
requests==2.1.0
|
||||||
six==1.4.1
|
six==1.4.1
|
||||||
stripe==1.9.8
|
stripe==1.11.0
|
||||||
wsgiref==0.1.2
|
wsgiref==0.1.2
|
||||||
xhtml2pdf==0.0.5
|
xhtml2pdf==0.0.5
|
||||||
|
|
|
@ -10,20 +10,6 @@ var casper = require('casper').create({
|
||||||
logLevel: "debug"
|
logLevel: "debug"
|
||||||
});
|
});
|
||||||
|
|
||||||
var disableOlark = function() {
|
|
||||||
casper.then(function() {
|
|
||||||
this.waitForText('Chat with us!', function() {
|
|
||||||
this.evaluate(function() {
|
|
||||||
console.log(olark);
|
|
||||||
window.olark.configure('box.start_hidden', true);
|
|
||||||
window.olark('api.box.hide');
|
|
||||||
});
|
|
||||||
}, function() {
|
|
||||||
// Do nothing, if olark never loaded we're ok with that
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var options = casper.cli.options;
|
var options = casper.cli.options;
|
||||||
var isDebug = !!options['d'];
|
var isDebug = !!options['d'];
|
||||||
|
|
||||||
|
@ -56,12 +42,18 @@ casper.thenClick('.form-signin button[type=submit]', function() {
|
||||||
this.waitForText('Top Repositories');
|
this.waitForText('Top Repositories');
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
casper.then(function() {
|
||||||
|
this.log('Generating user home screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'user-home.png');
|
this.capture(outputDir + 'user-home.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating repository view screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenOpen(rootUrl + 'repository/devtable/' + repo + '?tag=v2.0', function() {
|
casper.thenOpen(rootUrl + 'repository/devtable/' + repo + '?tag=v2.0', function() {
|
||||||
// Wait for the tree to initialize.
|
// Wait for the tree to initialize.
|
||||||
this.waitForSelector('.image-tree', function() {
|
this.waitForSelector('.image-tree', function() {
|
||||||
|
@ -70,12 +62,14 @@ casper.thenOpen(rootUrl + 'repository/devtable/' + repo + '?tag=v2.0', function(
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'repo-view.png');
|
this.capture(outputDir + 'repo-view.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating repository changes screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenClick('#current-image dd a', function() {
|
casper.thenClick('#current-image dd a', function() {
|
||||||
this.waitForSelector('.result-count', function() {
|
this.waitForSelector('.result-count', function() {
|
||||||
this.capture(outputDir + 'repo-changes.png', {
|
this.capture(outputDir + 'repo-changes.png', {
|
||||||
|
@ -87,58 +81,79 @@ casper.thenClick('#current-image dd a', function() {
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating repository admin screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenOpen(rootUrl + 'repository/devtable/' + repo + '/admin', function() {
|
casper.thenOpen(rootUrl + 'repository/devtable/' + repo + '/admin', function() {
|
||||||
this.waitForSelector('.repo-access-state');
|
this.waitForSelector('.repo-access-state');
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'repo-admin.png');
|
this.capture(outputDir + 'repo-admin.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating organization repo list screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenOpen(rootUrl + 'repository/?namespace=' + org, function() {
|
casper.thenOpen(rootUrl + 'repository/?namespace=' + org, function() {
|
||||||
this.waitForText('Repositories');
|
this.waitForText('Repositories');
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'org-repo-list.png');
|
this.capture(outputDir + 'org-repo-list.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating organization teams screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenOpen(rootUrl + 'organization/' + org, function() {
|
casper.thenOpen(rootUrl + 'organization/' + org, function() {
|
||||||
this.waitForSelector('.organization-name');
|
this.waitForSelector('.organization-name');
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'org-teams.png');
|
this.capture(outputDir + 'org-teams.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating organization admin screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenOpen(rootUrl + 'organization/' + org + '/admin', function() {
|
casper.thenOpen(rootUrl + 'organization/' + org + '/admin', function() {
|
||||||
this.waitForSelector('#repository-usage-chart');
|
this.waitForSelector('#repository-usage-chart');
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'org-admin.png');
|
this.capture(outputDir + 'org-admin.png');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating organization logs screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenClick('a[data-target="#logs"]', function() {
|
casper.thenClick('a[data-target="#logs"]', function() {
|
||||||
this.waitForSelector('svg > g', function() {
|
this.waitForSelector('svg > g', function() {
|
||||||
this.capture(outputDir + 'org-logs.png');
|
this.wait(1000, function() {
|
||||||
|
this.capture(outputDir + 'org-logs.png', {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
width: width,
|
||||||
|
height: height + 200
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
casper.then(function() {
|
||||||
|
this.log('Generating oganization repository admin screenshot.');
|
||||||
|
});
|
||||||
|
|
||||||
casper.thenOpen(rootUrl + 'repository/' + org + '/' + orgrepo + '/admin', function() {
|
casper.thenOpen(rootUrl + 'repository/' + org + '/' + orgrepo + '/admin', function() {
|
||||||
this.waitForText('outsideorg')
|
this.waitForText('outsideorg')
|
||||||
});
|
});
|
||||||
|
|
||||||
disableOlark();
|
|
||||||
|
|
||||||
casper.then(function() {
|
casper.then(function() {
|
||||||
this.capture(outputDir + 'org-repo-admin.png');
|
this.capture(outputDir + 'org-repo-admin.png');
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,6 +3,76 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 410px) {
|
||||||
|
.olrk-normal {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.codetooltipcontainer .tooltip-inner {
|
||||||
|
white-space:pre;
|
||||||
|
max-width:none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.codetooltip {
|
||||||
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-view-element {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-view-element .resource-spinner {
|
||||||
|
z-index: 1;
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0s ease-in-out;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-view-element .resource-content {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-view-element .resource-content.visible {
|
||||||
|
z-index: 2;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-view-element .resource-error {
|
||||||
|
margin: 10px;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #444;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resource-view-element .resource-spinner.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 1s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-spinner {
|
||||||
|
display: inline-block;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: #444;
|
||||||
|
border-left-color: #444;
|
||||||
|
border-radius: 10px;
|
||||||
|
-webkit-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
-moz-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
-ms-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
-o-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar-spinner {
|
||||||
|
top: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
.entity-search-element input {
|
.entity-search-element input {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
@ -48,10 +118,7 @@ html, body {
|
||||||
.tooltip {
|
.tooltip {
|
||||||
word-break: normal !important;
|
word-break: normal !important;
|
||||||
word-wrap: normal !important;
|
word-wrap: normal !important;
|
||||||
}
|
pointer-events: none;
|
||||||
|
|
||||||
.code-info {
|
|
||||||
border-bottom: 1px dashed #aaa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle-icon {
|
.toggle-icon {
|
||||||
|
@ -136,12 +203,11 @@ i.toggle-icon:hover {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: auto !important;
|
height: auto !important;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0 auto -136px;
|
margin: 0 auto -176px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-container, .push {
|
.footer-container, .push {
|
||||||
height: 110px;
|
height: 74px;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-container.fixed {
|
.footer-container.fixed {
|
||||||
|
@ -193,6 +259,10 @@ i.toggle-icon:hover {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs-view-element .logs-date-picker {
|
||||||
|
width: 122px;
|
||||||
|
}
|
||||||
|
|
||||||
.logs-view-element .header input {
|
.logs-view-element .header input {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -608,49 +678,165 @@ i.toggle-icon:hover {
|
||||||
|
|
||||||
.plans-list .plan {
|
.plans-list .plan {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
|
||||||
padding: 10px;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-top: 4px solid #94C9F7;
|
|
||||||
font-size: 1.4em;
|
font-size: 1.4em;
|
||||||
margin-top: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plans-list .plan.small {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-top: 4px solid #428bca;
|
|
||||||
margin-top: 0px;
|
|
||||||
font-size: 1.6em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plans-list .plan.business-plan {
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-top: 4px solid #94F794;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plans-list .plan.bus-small {
|
.plans-list .plan.bus-small {
|
||||||
border: 1px solid #ddd;
|
border-top: 6px solid #46ac39;
|
||||||
border-top: 4px solid #47A447;
|
margin-top: -10px;
|
||||||
margin-top: 0px;
|
}
|
||||||
font-size: 1.6em;
|
|
||||||
|
.plans-list .plan.bus-small .plan-box {
|
||||||
|
background: black !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plans-list .plan:last-child {
|
.plans-list .plan:last-child {
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .plan-box {
|
||||||
|
background: #444;
|
||||||
|
padding: 10px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
.plans-list .plan .plan-title {
|
.plans-list .plan .plan-title {
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding-top: 25px;
|
||||||
|
padding-bottom: 20px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible-sm-inline {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-sm-inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 991px) and (min-width: 768px) {
|
||||||
|
.visible-sm-inline {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-sm-inline {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan-box .description {
|
||||||
|
color: white;
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan button {
|
||||||
|
margin-top: 6px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan.bus-small button {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .features-bar {
|
||||||
|
padding-top: 248px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .features-bar .feature .count {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .features-bar .feature {
|
||||||
|
height: 43px;
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.context-tooltip {
|
||||||
|
border-bottom: 1px dotted black;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .features-bar .feature i {
|
||||||
|
margin-left: 16px;
|
||||||
|
float: right;
|
||||||
|
width: 16px;
|
||||||
|
font-size: 16px;
|
||||||
|
text-align: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .features {
|
||||||
|
padding: 6px;
|
||||||
|
background: #eee;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .feature {
|
||||||
|
text-align: center;
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .feature:after {
|
||||||
|
content: "";
|
||||||
|
border-radius: 50%;
|
||||||
|
display: inline-block;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .visible-xs .feature {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .visible-xs .feature:after {
|
||||||
|
float: left;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .feature.notpresent {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .feature.present:after {
|
||||||
|
background: #428bca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan.business-plan .feature.present:after {
|
||||||
|
background: #46ac39;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .count, .plans-list .features-bar .count {
|
||||||
|
background: white;
|
||||||
|
border-bottom: 0px;
|
||||||
|
text-align: center !important;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .count b, .plans-list .features-bar .count b {
|
||||||
|
font-size: 1.5em;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan .feature:last-child {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plans-list .plan-price {
|
||||||
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.plan-price {
|
.plan-price {
|
||||||
margin-bottom: 10px;
|
|
||||||
display: block;
|
display: block;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.8em;
|
font-size: 1.8em;
|
||||||
|
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -678,7 +864,8 @@ i.toggle-icon:hover {
|
||||||
.plans-list .plan .description {
|
.plans-list .plan .description {
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-bottom: 10px;
|
height: 34px;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.plans-list .plan .smaller {
|
.plans-list .plan .smaller {
|
||||||
|
@ -829,12 +1016,20 @@ form input.ng-valid.ng-dirty,
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-footer {
|
.page-footer {
|
||||||
|
padding: 10px;
|
||||||
|
padding-bottom: 0px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-footer-padder {
|
||||||
|
margin-top: 76px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
padding: 10px;
|
|
||||||
padding-bottom: 40px;
|
overflow: hidden;
|
||||||
margin-top: 52px;
|
width: 100%;
|
||||||
border-top: 1px solid #eee;
|
height: 80px;
|
||||||
|
padding-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-footer .row {
|
.page-footer .row {
|
||||||
|
@ -909,12 +1104,20 @@ form input.ng-valid.ng-dirty,
|
||||||
.entity-mini-listing {
|
.entity-mini-listing {
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
white-space: nowrap !important;
|
white-space: nowrap !important;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.entity-mini-listing i {
|
.entity-mini-listing i {
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.entity-mini-listing i.fa-exclamation-triangle {
|
||||||
|
position: absolute;
|
||||||
|
right: -16px;
|
||||||
|
top: 4px;
|
||||||
|
color: #c09853;
|
||||||
|
}
|
||||||
|
|
||||||
.entity-mini-listing .warning {
|
.entity-mini-listing .warning {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
@ -967,20 +1170,84 @@ p.editable:hover i {
|
||||||
font-size: 1.15em;
|
font-size: 1.15em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tagContextMenu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 100000;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#tagContextMenu ul {
|
||||||
|
display: block;
|
||||||
|
position: static;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-controls {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
margin-top: 2px;
|
||||||
|
opacity: 0;
|
||||||
|
float: right;
|
||||||
|
-webkit-transition: opacity 200ms ease-in-out;
|
||||||
|
-moz-transition: opacity 200ms ease-in-out;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
.right-title {
|
.right-title {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: right;
|
float: right;
|
||||||
padding: 4px;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-dropdown .tag-count {
|
.right-tag-controls {
|
||||||
|
display: inline-block;
|
||||||
|
float: right;
|
||||||
|
padding: 4px;
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 1px solid #ccc;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -2px;
|
||||||
|
margin-right: -10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-tag-controls .tag-count {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
padding-right: 10px;
|
}
|
||||||
border-right: 1px solid #ccc;
|
|
||||||
|
.tag-image-sizes .tag-image-size {
|
||||||
|
height: 22px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-image-sizes .tag-image-size .size-title {
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-image-sizes .tag-image-size .size-limiter {
|
||||||
|
display: inline-block;
|
||||||
|
padding-right: 90px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-image-sizes .tag-image-size .size-bar {
|
||||||
|
display: inline-block;
|
||||||
|
background: steelblue;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#current-tag .control-bar {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding-top: 10px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-dropdown a {
|
.tag-dropdown a {
|
||||||
|
@ -993,6 +1260,61 @@ p.editable:hover i {
|
||||||
border: 0px;
|
border: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing {
|
||||||
|
margin: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing .image-listing-id {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing .image-listing-line {
|
||||||
|
border-left: 2px solid steelblue;
|
||||||
|
display: inline-block;
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 6px;
|
||||||
|
width: 1px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-line {
|
||||||
|
top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing.child .image-listing-line {
|
||||||
|
bottom: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing .image-listing-circle {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid steelblue;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
background: white;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .image-listings .image-listing.tag-image .image-listing-circle {
|
||||||
|
background: steelblue;
|
||||||
|
}
|
||||||
|
|
||||||
|
#confirmdeleteTagModal .more-changes {
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.repo .header {
|
.repo .header {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -1009,7 +1331,7 @@ p.editable:hover i {
|
||||||
|
|
||||||
.repo .description {
|
.repo .description {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.repo .empty-message {
|
.repo .empty-message {
|
||||||
|
@ -1343,6 +1665,18 @@ p.editable:hover i {
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo .formatted-command {
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo .formatted-command.trimmed {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.repo .changes-count-container {
|
.repo .changes-count-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
@ -1567,6 +1901,10 @@ p.editable:hover i {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-admin .check-green {
|
||||||
|
color: #46ac39;
|
||||||
|
}
|
||||||
|
|
||||||
#image-history-container {
|
#image-history-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 400px;
|
min-height: 400px;
|
||||||
|
@ -1617,7 +1955,7 @@ p.editable:hover i {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
#image-history-container .tags .tag {
|
#image-history-container .tags .tag, #confirmdeleteTagModal .tag {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -1864,28 +2202,28 @@ p.editable:hover i {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-title {
|
.billing-invoices-element .invoice-title {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-status .success {
|
.billing-invoices-element .invoice-status .success {
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-status .pending {
|
.billing-invoices-element .invoice-status .pending {
|
||||||
color: steelblue;
|
color: steelblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-status .danger {
|
.billing-invoices-element .invoice-status .danger {
|
||||||
color: red;
|
color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-amount:before {
|
.billing-invoices-element .invoice-amount:before {
|
||||||
content: '$';
|
content: '$';
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-details {
|
.billing-invoices-element .invoice-details {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
@ -1894,21 +2232,21 @@ p.editable:hover i {
|
||||||
border-left: 2px solid #eee !important;
|
border-left: 2px solid #eee !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-details td {
|
.billing-invoices-element .invoice-details td {
|
||||||
border: 0px solid transparent !important;
|
border: 0px solid transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-details dl {
|
.billing-invoices-element .invoice-details dl {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-details dd {
|
.billing-invoices-element .invoice-details dd {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .invoice-title:hover {
|
.billing-invoices-element .invoice-title:hover {
|
||||||
color: steelblue;
|
color: steelblue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2034,6 +2372,14 @@ p.editable:hover i {
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.plan-manager-element .plans-list-table .deprecated-plan {
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-manager-element .plans-list-table .deprecated-plan-label {
|
||||||
|
font-size: 0.7em;
|
||||||
|
}
|
||||||
|
|
||||||
.plans-table-element table {
|
.plans-table-element table {
|
||||||
margin: 20px;
|
margin: 20px;
|
||||||
border: 1px solid #eee;
|
border: 1px solid #eee;
|
||||||
|
@ -2179,16 +2525,37 @@ p.editable:hover i {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d3-tip .created {
|
.d3-tip .command {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: white;
|
color: white;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 6px;
|
font-family: Consolas, "Lucida Console", Monaco, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip .info-line {
|
||||||
|
display: block;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip .info-line i {
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #888;
|
||||||
}
|
}
|
||||||
|
|
||||||
.d3-tip .comment {
|
.d3-tip .comment {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
padding-bottom: 4px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-bottom: 1px dotted #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d3-tip .created {
|
||||||
|
font-size: 12px;
|
||||||
|
color: white;
|
||||||
|
display: block;
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
53
static/directives/billing-invoices.html
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<div class="billing-invoices-element">
|
||||||
|
<div ng-show="loading">
|
||||||
|
<div class="quay-spinner"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="!loading && !invoices">
|
||||||
|
No invoices have been created
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ng-show="!loading && invoices">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<th>Billing Date/Time</th>
|
||||||
|
<th>Amount Due</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody class="invoice" ng-repeat="invoice in invoices">
|
||||||
|
<tr class="invoice-title">
|
||||||
|
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
||||||
|
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
||||||
|
<td>
|
||||||
|
<span class="invoice-status">
|
||||||
|
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
|
||||||
|
<span class="danger" ng-show="!invoice.paid && invoice.attempted && invoice.closed">Payment failed</span>
|
||||||
|
<span class="danger" ng-show="!invoice.paid && invoice.attempted && !invoice.closed">Payment failed - Will retry soon</span>
|
||||||
|
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
||||||
|
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
|
||||||
|
<td colspan="3">
|
||||||
|
<dl class="dl-normal">
|
||||||
|
<dt>Billing Period</dt>
|
||||||
|
<dd>
|
||||||
|
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
|
||||||
|
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
|
@ -5,9 +5,9 @@
|
||||||
Credit Card
|
Credit Card
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<i class="fa fa-spinner fa-spin fa-2x" ng-show="!currentCard || changingCard"></i>
|
<div class="quay-spinner" ng-show="!currentCard || changingCard"></div>
|
||||||
<div class="current-card" ng-show="currentCard && !changingCard">
|
<div class="current-card" ng-show="currentCard && !changingCard">
|
||||||
<img src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4">
|
<img ng-src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4">
|
||||||
<span class="no-card-outline" ng-show="!currentCard.last4"></span>
|
<span class="no-card-outline" ng-show="!currentCard.last4"></span>
|
||||||
|
|
||||||
<span class="last4" ng-show="currentCard.last4">****-****-****-<b>{{ currentCard.last4 }}</b></span>
|
<span class="last4" ng-show="currentCard.last4">****-****-****-<b>{{ currentCard.last4 }}</b></span>
|
||||||
|
@ -24,7 +24,7 @@
|
||||||
<div class="panel">
|
<div class="panel">
|
||||||
<div class="panel-title">
|
<div class="panel-title">
|
||||||
Billing Options
|
Billing Options
|
||||||
<i class="fa fa-spinner fa-spin" ng-show="working"></i>
|
<div class="quay-spinner" ng-show="working"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="settings-option">
|
<div class="settings-option">
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" role="menu" aria-labelledby="entityDropdownMenu">
|
<ul class="dropdown-menu" role="menu" aria-labelledby="entityDropdownMenu">
|
||||||
<li ng-show="lazyLoading"><i class="fa fa-spinner"></i></li>
|
<li ng-show="lazyLoading" style="padding: 10px"><div class="quay-spinner"></div></li>
|
||||||
|
|
||||||
<li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading"
|
<li role="presentation" ng-repeat="team in teams" ng-show="!lazyLoading"
|
||||||
ng-click="setEntity(team.name, 'team', false)">
|
ng-click="setEntity(team.name, 'team', false)">
|
||||||
|
|
|
@ -38,14 +38,21 @@
|
||||||
<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">1</span>
|
<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' : '')"
|
||||||
|
data-placement="left"
|
||||||
|
data-container="body">
|
||||||
|
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
|
||||||
|
</span>
|
||||||
<b class="caret"></b>
|
<b class="caret"></b>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<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">1</span>
|
<span class="badge user-notification" ng-show="user.askForPassword || overPlan">
|
||||||
|
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
<li><a ng-href="/organizations/" target="{{ appLinkTarget() }}">Organizations</a></li>
|
||||||
|
|
8
static/directives/loading-status.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<div class="container loading-status-element">
|
||||||
|
<div ng-show="hasError && !loading">
|
||||||
|
<span ng-transclude></span>
|
||||||
|
</div>
|
||||||
|
<div ng-show="loading">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -6,9 +6,9 @@
|
||||||
<span class="entity-reference" name="performer.username" isrobot="performer.is_robot" ng-show="performer"></span>
|
<span class="entity-reference" name="performer.username" isrobot="performer.is_robot" ng-show="performer"></span>
|
||||||
<span id="logs-range" class="mini">
|
<span id="logs-range" class="mini">
|
||||||
From
|
From
|
||||||
<input type="text" class="input-small" name="start" ng-model="logStartDate" data-date-format="mm/dd/yyyy" bs-datepicker/>
|
<input type="text" class="logs-date-picker input-sm" name="start" ng-model="logStartDate" data-date-format="mm/dd/yyyy" bs-datepicker/>
|
||||||
<span class="add-on">to</span>
|
<span class="add-on">to</span>
|
||||||
<input type="text" class="input-small" name="end" ng-model="logEndDate" data-date-format="mm/dd/yyyy" bs-datepicker/>
|
<input type="text" class="logs-date-picker input-sm" name="end" ng-model="logEndDate" data-date-format="mm/dd/yyyy" bs-datepicker/>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="loading">
|
<div ng-show="loading">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner 3x"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!loading">
|
<div ng-show="!loading">
|
||||||
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">
|
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="plan-manager-element">
|
<div class="plan-manager-element">
|
||||||
<!-- Loading/Changing -->
|
<!-- Loading/Changing -->
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planLoading"></i>
|
<div class="quay-spinner 3x" ng-show="planLoading"></div>
|
||||||
|
|
||||||
<!-- Alerts -->
|
<!-- Alerts -->
|
||||||
<div class="alert alert-danger" ng-show="limit == 'over' && !planLoading">
|
<div class="alert alert-danger" ng-show="limit == 'over' && !planLoading">
|
||||||
|
@ -32,26 +32,35 @@
|
||||||
<td></td>
|
<td></td>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tr ng-repeat="plan in plans" ng-class="(subscribedPlan.stripeId === plan.stripeId) ? getActiveSubClass() : ''">
|
<tr ng-repeat="plan in plans" ng-show="isPlanVisible(plan, subscribedPlan)"
|
||||||
<td>{{ plan.title }}</td>
|
ng-class="{'active':(subscribedPlan.stripeId === plan.stripeId), 'deprecated-plan':plan.deprecated}">
|
||||||
|
<td>
|
||||||
|
{{ plan.title }}
|
||||||
|
<div class="deprecated-plan-label" ng-show="plan.deprecated">
|
||||||
|
<span class="context-tooltip" title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
<td>{{ plan.privateRepos }}</td>
|
<td>{{ plan.privateRepos }}</td>
|
||||||
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
|
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
|
||||||
<td class="controls">
|
<td class="controls">
|
||||||
<div ng-switch='plan.stripeId'>
|
<div ng-switch='plan.deprecated'>
|
||||||
<div ng-switch-when='bus-free'>
|
<div ng-switch-when='true'>
|
||||||
<button class="btn button-hidden">Hidden!</button>
|
<button class="btn btn-danger" ng-click="cancelSubscription()">
|
||||||
|
<span class="quay-spinner" ng-show="planChanging"></span>
|
||||||
|
<span ng-show="!planChanging">Cancel</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div ng-switch-default>
|
<div ng-switch-default>
|
||||||
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"
|
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"
|
||||||
ng-class="subscribedPlan.price == 0 ? 'btn-primary' : 'btn-default'"
|
ng-class="subscribedPlan.price == 0 ? 'btn-primary' : 'btn-default'"
|
||||||
ng-click="changeSubscription(plan.stripeId)">
|
ng-click="changeSubscription(plan.stripeId)">
|
||||||
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
|
<span class="quay-spinner" ng-show="planChanging"></span>
|
||||||
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
|
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
|
||||||
<span ng-show="!planChanging && subscribedPlan.price == 0">Subscribe</span>
|
<span ng-show="!planChanging && subscribedPlan.price == 0">Subscribe</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
|
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
|
||||||
ng-click="cancelSubscription()">
|
ng-click="cancelSubscription()">
|
||||||
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
|
<span class="quay-spinner" ng-show="planChanging"></span>
|
||||||
<span ng-show="!planChanging">Cancel</span>
|
<span ng-show="!planChanging">Cancel</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: visible' }}" title="Private Repository"></i>
|
<i class="fa fa-lock fa-lg" style="{{ repo.is_public ? 'visibility: hidden' : 'visibility: inherit' }}" title="Private Repository"></i>
|
||||||
<i class="fa fa-hdd"></i>
|
<i class="fa fa-hdd"></i>
|
||||||
|
|
11
static/directives/resource-view.html
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<div class="resource-view-element">
|
||||||
|
<div class="resource-spinner" ng-class="resource.loading ? 'visible' : ''">
|
||||||
|
<div class="small-spinner"></div>
|
||||||
|
</div>
|
||||||
|
<div class="resource-error" ng-show="!resource.loading && resource.hasError">
|
||||||
|
{{ errorMessage }}
|
||||||
|
</div>
|
||||||
|
<div class="resource-content" ng-class="(!resource.loading && !resource.hasError) ? 'visible' : ''">
|
||||||
|
<span ng-transclude></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -1,5 +1,5 @@
|
||||||
<div class="robots-manager-element">
|
<div class="robots-manager-element">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="loading"></i>
|
<div class="quay-spinner" ng-show="loading"></div>
|
||||||
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
|
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
|
||||||
|
|
||||||
<div class="container" ng-show="!loading">
|
<div class="container" ng-show="!loading">
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
<div ng-show="registering" style="text-align: center">
|
<div ng-show="registering" style="text-align: center">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner 2x"></div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="awaitingConfirmation">
|
<div ng-show="awaitingConfirmation">
|
||||||
<div class="sub-message">
|
<div class="sub-message">
|
||||||
|
|
1
static/directives/spinner.html
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<div class="small-spinner"></div>
|
|
@ -43,7 +43,7 @@
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Send Recovery Email</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">Send Recovery Email</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<div class="alert alert-danger" ng-show="invalidEmail">Unable to locate account.</div>
|
<div class="alert alert-danger" ng-show="invalidRecovery">{{errorMessage}}</div>
|
||||||
|
|
||||||
<div class="alert alert-success" ng-show="sent">Account recovery email was sent.</div>
|
<div class="alert alert-success" ng-show="sent">Account recovery email was sent.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 70 KiB |
BIN
static/img/org-logs.png
Normal file
After Width: | Height: | Size: 78 KiB |
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 61 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 83 KiB |
Before Width: | Height: | Size: 208 KiB After Width: | Height: | Size: 196 KiB |
719
static/js/app.js
13
static/js/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
$.ajax({
|
||||||
|
type: 'GET',
|
||||||
|
async: false,
|
||||||
|
url: '/api/discovery',
|
||||||
|
success: function(data) {
|
||||||
|
window.__endpoints = data.endpoints;
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
$('#couldnotloadModal').modal({});
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,10 +1,37 @@
|
||||||
|
/**
|
||||||
|
* Bind polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Compatibility
|
||||||
|
*/
|
||||||
|
if (!Function.prototype.bind) {
|
||||||
|
Function.prototype.bind = function (oThis) {
|
||||||
|
if (typeof this !== "function") {
|
||||||
|
// closest thing possible to the ECMAScript 5 internal IsCallable function
|
||||||
|
throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
|
||||||
|
}
|
||||||
|
|
||||||
|
var aArgs = Array.prototype.slice.call(arguments, 1),
|
||||||
|
fToBind = this,
|
||||||
|
fNOP = function () {},
|
||||||
|
fBound = function () {
|
||||||
|
return fToBind.apply(this instanceof fNOP && oThis
|
||||||
|
? this
|
||||||
|
: oThis,
|
||||||
|
aArgs.concat(Array.prototype.slice.call(arguments)));
|
||||||
|
};
|
||||||
|
|
||||||
|
fNOP.prototype = this.prototype;
|
||||||
|
fBound.prototype = new fNOP();
|
||||||
|
|
||||||
|
return fBound;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var DEPTH_HEIGHT = 100;
|
var DEPTH_HEIGHT = 100;
|
||||||
var DEPTH_WIDTH = 132;
|
var DEPTH_WIDTH = 132;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
* Based off of http://mbostock.github.io/d3/talk/20111018/tree.html by Mike Bostock (@mbostock)
|
||||||
*/
|
*/
|
||||||
function ImageHistoryTree(namespace, name, images, formatComment, formatTime) {
|
function ImageHistoryTree(namespace, name, images, formatComment, formatTime, formatCommand) {
|
||||||
/**
|
/**
|
||||||
* The namespace of the repo.
|
* The namespace of the repo.
|
||||||
*/
|
*/
|
||||||
|
@ -30,6 +57,11 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime) {
|
||||||
*/
|
*/
|
||||||
this.formatTime_ = formatTime;
|
this.formatTime_ = formatTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to invoke to format the command for an image.
|
||||||
|
*/
|
||||||
|
this.formatCommand_ = formatCommand;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current tag (if any).
|
* The current tag (if any).
|
||||||
*/
|
*/
|
||||||
|
@ -40,6 +72,11 @@ function ImageHistoryTree(namespace, name, images, formatComment, formatTime) {
|
||||||
*/
|
*/
|
||||||
this.currentImage_ = null;
|
this.currentImage_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently highlighted node (if any).
|
||||||
|
*/
|
||||||
|
this.currentNode_ = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counter for creating unique IDs.
|
* Counter for creating unique IDs.
|
||||||
*/
|
*/
|
||||||
|
@ -54,7 +91,7 @@ ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
|
||||||
var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
|
var cw = Math.max(document.getElementById(container).clientWidth, this.maxWidth_ * DEPTH_WIDTH);
|
||||||
var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10);
|
var ch = this.maxHeight_ * (DEPTH_HEIGHT + 10);
|
||||||
|
|
||||||
var margin = { top: 40, right: 20, bottom: 20, left: 40 };
|
var margin = { top: 40, right: 20, bottom: 20, left: 80 };
|
||||||
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
var m = [margin.top, margin.right, margin.bottom, margin.left];
|
||||||
var w = cw - m[1] - m[3];
|
var w = cw - m[1] - m[3];
|
||||||
var h = ch - m[0] - m[2];
|
var h = ch - m[0] - m[2];
|
||||||
|
@ -69,6 +106,25 @@ ImageHistoryTree.prototype.calculateDimensions_ = function(container) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
ImageHistoryTree.prototype.setupOverscroll_ = function() {
|
||||||
|
var container = this.container_;
|
||||||
|
var that = this;
|
||||||
|
var overscroll = $('#' + container).overscroll();
|
||||||
|
|
||||||
|
overscroll.on('overscroll:dragstart', function() {
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'hideTagMenu'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
overscroll.on('scroll', function() {
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'hideTagMenu'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the dimensions of the tree.
|
* Updates the dimensions of the tree.
|
||||||
*/
|
*/
|
||||||
|
@ -86,17 +142,22 @@ ImageHistoryTree.prototype.updateDimensions_ = function() {
|
||||||
$('#' + container).removeOverscroll();
|
$('#' + container).removeOverscroll();
|
||||||
var viewportHeight = $(window).height();
|
var viewportHeight = $(window).height();
|
||||||
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
var boundingBox = document.getElementById(container).getBoundingClientRect();
|
||||||
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 110) + 'px';
|
document.getElementById(container).style.maxHeight = (viewportHeight - boundingBox.top - 150) + 'px';
|
||||||
$('#' + container).overscroll();
|
|
||||||
|
|
||||||
|
this.setupOverscroll_();
|
||||||
|
|
||||||
// Update the tree.
|
// Update the tree.
|
||||||
var rootSvg = this.rootSvg_;
|
var rootSvg = this.rootSvg_;
|
||||||
var tree = this.tree_;
|
var tree = this.tree_;
|
||||||
var vis = this.vis_;
|
var vis = this.vis_;
|
||||||
|
|
||||||
|
|
||||||
|
var ow = w + m[1] + m[3];
|
||||||
|
var oh = h + m[0] + m[2];
|
||||||
rootSvg
|
rootSvg
|
||||||
.attr("width", w + m[1] + m[3])
|
.attr("width", ow)
|
||||||
.attr("height", h + m[0] + m[2]);
|
.attr("height", oh)
|
||||||
|
.attr("style", "width: " + ow + "px; height: " + oh + "px");
|
||||||
|
|
||||||
tree.size([w, h]);
|
tree.size([w, h]);
|
||||||
vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
vis.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
|
||||||
|
@ -131,6 +192,8 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
|
|
||||||
var formatComment = this.formatComment_;
|
var formatComment = this.formatComment_;
|
||||||
var formatTime = this.formatTime_;
|
var formatTime = this.formatTime_;
|
||||||
|
var formatCommand = this.formatCommand_;
|
||||||
|
|
||||||
var tip = d3.tip()
|
var tip = d3.tip()
|
||||||
.attr('class', 'd3-tip')
|
.attr('class', 'd3-tip')
|
||||||
.offset([-1, 24])
|
.offset([-1, 24])
|
||||||
|
@ -156,8 +219,10 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
if (d.image.comment) {
|
if (d.image.comment) {
|
||||||
html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
|
html += '<span class="comment">' + formatComment(d.image.comment) + '</span>';
|
||||||
}
|
}
|
||||||
html += '<span class="created">' + formatTime(d.image.created) + '</span>';
|
if (d.image.command && d.image.command.length) {
|
||||||
html += '<span class="full-id">' + d.image.id + '</span>';
|
html += '<span class="command info-line"><i class="fa fa-terminal"></i>' + formatCommand(d.image) + '</span>';
|
||||||
|
}
|
||||||
|
html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';
|
||||||
return html;
|
return html;
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -178,8 +243,7 @@ ImageHistoryTree.prototype.draw = function(container) {
|
||||||
this.root_.y0 = 0;
|
this.root_.y0 = 0;
|
||||||
|
|
||||||
this.setTag_(this.currentTag_);
|
this.setTag_(this.currentTag_);
|
||||||
|
this.setupOverscroll_();
|
||||||
$('#' + container).overscroll();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -208,6 +272,23 @@ ImageHistoryTree.prototype.setImage = function(imageId) {
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the highlighted path in the tree.
|
||||||
|
*/
|
||||||
|
ImageHistoryTree.prototype.setHighlightedPath_ = function(image) {
|
||||||
|
if (this.currentNode_) {
|
||||||
|
this.markPath_(this.currentNode_, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
var imageByDBID = this.imageByDBID_;
|
||||||
|
var currentNode = imageByDBID[image.dbid];
|
||||||
|
if (currentNode) {
|
||||||
|
this.markPath_(currentNode, true);
|
||||||
|
this.currentNode_ = currentNode;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the ancestors of the given image.
|
* Returns the ancestors of the given image.
|
||||||
*/
|
*/
|
||||||
|
@ -445,26 +526,15 @@ ImageHistoryTree.prototype.setTag_ = function(tagName) {
|
||||||
// Save the current tag.
|
// Save the current tag.
|
||||||
var previousTagName = this.currentTag_;
|
var previousTagName = this.currentTag_;
|
||||||
this.currentTag_ = tagName;
|
this.currentTag_ = tagName;
|
||||||
|
this.currentImage_ = null;
|
||||||
|
|
||||||
// Update the state of each existing node to no longer be highlighted.
|
// Update the path.
|
||||||
var previousImage = this.findImage_(function(image) {
|
var tagImage = this.findImage_(function(image) {
|
||||||
return image.tags.indexOf(previousTagName || '(no tag specified)') >= 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (previousImage) {
|
|
||||||
var currentNode = imageByDBID[previousImage.dbid];
|
|
||||||
this.markPath_(currentNode, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the new current image (if any).
|
|
||||||
this.currentImage_ = this.findImage_(function(image) {
|
|
||||||
return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
|
return image.tags.indexOf(tagName || '(no tag specified)') >= 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update the state of the new node path.
|
if (tagImage) {
|
||||||
if (this.currentImage_) {
|
this.setHighlightedPath_(tagImage);
|
||||||
var currentNode = imageByDBID[this.currentImage_.dbid];
|
|
||||||
this.markPath_(currentNode, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that the children are in the correct order.
|
// Ensure that the children are in the correct order.
|
||||||
|
@ -508,7 +578,9 @@ ImageHistoryTree.prototype.setImage_ = function(imageId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setHighlightedPath_(newImage);
|
||||||
this.currentImage_ = newImage;
|
this.currentImage_ = newImage;
|
||||||
|
this.currentTag_ = null;
|
||||||
this.update_(this.root_);
|
this.update_(this.root_);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -637,7 +709,7 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
if (tag == currentTag) {
|
if (tag == currentTag) {
|
||||||
kind = 'success';
|
kind = 'success';
|
||||||
}
|
}
|
||||||
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>';
|
html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"">' + tag + '</span>';
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
});
|
});
|
||||||
|
@ -649,6 +721,19 @@ ImageHistoryTree.prototype.update_ = function(source) {
|
||||||
if (tag) {
|
if (tag) {
|
||||||
that.changeTag_(tag);
|
that.changeTag_(tag);
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.on("contextmenu", function(d, e) {
|
||||||
|
d3.event.preventDefault();
|
||||||
|
|
||||||
|
var tag = this.getAttribute('data-tag');
|
||||||
|
if (tag) {
|
||||||
|
$(that).trigger({
|
||||||
|
'type': 'showTagMenu',
|
||||||
|
'tag': tag,
|
||||||
|
'clientX': d3.event.clientX,
|
||||||
|
'clientY': d3.event.clientY
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ensure the tags are visible.
|
// Ensure the tags are visible.
|
||||||
|
|
32
static/lib/angulartics-google-analytics.js
vendored
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* @license Angulartics v0.8.5
|
||||||
|
* (c) 2013 Luis Farzati http://luisfarzati.github.io/angulartics
|
||||||
|
* Universal Analytics update contributed by http://github.com/willmcclellan
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
(function(angular) {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngdoc overview
|
||||||
|
* @name angulartics.google.analytics
|
||||||
|
* Enables analytics support for Google Analytics (http://google.com/analytics)
|
||||||
|
*/
|
||||||
|
angular.module('angulartics.google.analytics', ['angulartics'])
|
||||||
|
.config(['$analyticsProvider', function ($analyticsProvider) {
|
||||||
|
|
||||||
|
// GA already supports buffered invocations so we don't need
|
||||||
|
// to wrap these inside angulartics.waitForVendorApi
|
||||||
|
|
||||||
|
$analyticsProvider.registerPageTrack(function (path) {
|
||||||
|
if (window._gaq) _gaq.push(['_trackPageview', path]);
|
||||||
|
if (window.ga) ga('send', 'pageview', path);
|
||||||
|
});
|
||||||
|
|
||||||
|
$analyticsProvider.registerEventTrack(function (action, properties) {
|
||||||
|
if (window._gaq) _gaq.push(['_trackEvent', properties.category, action, properties.label, properties.value]);
|
||||||
|
if (window.ga) ga('send', 'event', properties.category, action, properties.label, properties.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
}]);
|
||||||
|
})(angular);
|
793
static/lib/jquery.overscroll.js
Normal file
|
@ -0,0 +1,793 @@
|
||||||
|
/**
|
||||||
|
* Overscroll v1.7.3
|
||||||
|
* A jQuery Plugin that emulates the iPhone scrolling experience in a browser.
|
||||||
|
* http://azoffdesign.com/overscroll
|
||||||
|
*
|
||||||
|
* Intended for use with the latest jQuery
|
||||||
|
* http://code.jquery.com/jquery-latest.js
|
||||||
|
*
|
||||||
|
* Copyright 2013, Jonathan Azoff
|
||||||
|
* Licensed under the MIT license.
|
||||||
|
* https://github.com/azoff/overscroll/blob/master/mit.license
|
||||||
|
*
|
||||||
|
* For API documentation, see the README file
|
||||||
|
* http://azof.fr/pYCzuM
|
||||||
|
*
|
||||||
|
* Date: Tuesday, March 18th 2013
|
||||||
|
*/
|
||||||
|
(function(global, dom, browser, math, wait, cancel, namespace, $, none){
|
||||||
|
|
||||||
|
// We want to run this plug-in in strict-mode
|
||||||
|
// so that we may benefit from its optimizations
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// The key used to bind-instance specific data to an object
|
||||||
|
var datakey = 'overscroll';
|
||||||
|
|
||||||
|
// create <body> node if there's not one present (e.g., for test runners)
|
||||||
|
if (dom.body === null) {
|
||||||
|
dom.documentElement.appendChild(
|
||||||
|
dom.createElement('body')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// quick fix for IE 8 and below since getComputedStyle() is not supported
|
||||||
|
// TODO: find a better solution
|
||||||
|
if (!global.getComputedStyle) {
|
||||||
|
global.getComputedStyle = function (el, pseudo) {
|
||||||
|
this.el = el;
|
||||||
|
this.getPropertyValue = function (prop) {
|
||||||
|
var re = /(\-([a-z]){1})/g;
|
||||||
|
if (prop == 'float') prop = 'styleFloat';
|
||||||
|
if (re.test(prop)) {
|
||||||
|
prop = prop.replace(re, function () {
|
||||||
|
return arguments[2].toUpperCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return el.currentStyle[prop] ? el.currentStyle[prop] : null;
|
||||||
|
};
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// runs feature detection for overscroll
|
||||||
|
var compat = {
|
||||||
|
animate: (function(){
|
||||||
|
var fn = global.requestAnimationFrame ||
|
||||||
|
global.webkitRequestAnimationFrame ||
|
||||||
|
global.mozRequestAnimationFrame ||
|
||||||
|
global.oRequestAnimationFrame ||
|
||||||
|
global.msRequestAnimationFrame ||
|
||||||
|
function(callback) { wait(callback, 1000/60); };
|
||||||
|
return function(callback) {
|
||||||
|
fn.call(global, callback);
|
||||||
|
};
|
||||||
|
})(),
|
||||||
|
overflowScrolling: (function(){
|
||||||
|
var style = '';
|
||||||
|
var div = dom.createElement('div');
|
||||||
|
var prefixes = ['webkit', 'moz', 'o', 'ms'];
|
||||||
|
dom.body.appendChild(div);
|
||||||
|
$.each(prefixes, function(i, prefix){
|
||||||
|
div.style[prefix + 'OverflowScrolling'] = 'touch';
|
||||||
|
});
|
||||||
|
div.style.overflowScrolling = 'touch';
|
||||||
|
var computedStyle = global.getComputedStyle(div);
|
||||||
|
if (!!computedStyle.overflowScrolling) {
|
||||||
|
style = 'overflow-scrolling';
|
||||||
|
} else {
|
||||||
|
$.each(prefixes, function(i, prefix){
|
||||||
|
if (!!computedStyle[prefix + 'OverflowScrolling']) {
|
||||||
|
style = '-' + prefix + '-overflow-scrolling';
|
||||||
|
}
|
||||||
|
return !style;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
div.parentNode.removeChild(div);
|
||||||
|
return style;
|
||||||
|
})(),
|
||||||
|
cursor: (function() {
|
||||||
|
var div = dom.createElement('div');
|
||||||
|
var prefixes = ['webkit', 'moz'];
|
||||||
|
var gmail = 'https://mail.google.com/mail/images/2/';
|
||||||
|
var style = {
|
||||||
|
grab: 'url('+gmail+'openhand.cur), move',
|
||||||
|
grabbing: 'url('+gmail+'closedhand.cur), move'
|
||||||
|
};
|
||||||
|
dom.body.appendChild(div);
|
||||||
|
$.each(prefixes, function(i, prefix){
|
||||||
|
var found, cursor = '-' + prefix + '-grab';
|
||||||
|
div.style.cursor = cursor;
|
||||||
|
var computedStyle = global.getComputedStyle(div);
|
||||||
|
found = computedStyle.cursor === cursor;
|
||||||
|
if (found) {
|
||||||
|
style = {
|
||||||
|
grab: '-' + prefix + '-grab',
|
||||||
|
grabbing: '-' + prefix + '-grabbing'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return !found;
|
||||||
|
});
|
||||||
|
div.parentNode.removeChild(div);
|
||||||
|
return style;
|
||||||
|
})()
|
||||||
|
};
|
||||||
|
|
||||||
|
// These are all the events that could possibly
|
||||||
|
// be used by the plug-in
|
||||||
|
var events = {
|
||||||
|
drag: 'mousemove touchmove',
|
||||||
|
end: 'mouseup mouseleave click touchend touchcancel',
|
||||||
|
hover: 'mouseenter mouseleave',
|
||||||
|
ignored: 'select dragstart drag',
|
||||||
|
scroll: 'scroll',
|
||||||
|
start: 'mousedown touchstart',
|
||||||
|
wheel: 'mousewheel DOMMouseScroll'
|
||||||
|
};
|
||||||
|
|
||||||
|
// These settings are used to tweak drift settings
|
||||||
|
// for the plug-in
|
||||||
|
var settings = {
|
||||||
|
captureThreshold: 3,
|
||||||
|
driftDecay: 1.1,
|
||||||
|
driftSequences: 22,
|
||||||
|
driftTimeout: 100,
|
||||||
|
scrollDelta: 15,
|
||||||
|
thumbOpacity: 0.7,
|
||||||
|
thumbThickness: 6,
|
||||||
|
thumbTimeout: 400,
|
||||||
|
wheelDelta: 20,
|
||||||
|
wheelTicks: 120
|
||||||
|
};
|
||||||
|
|
||||||
|
// These defaults are used to complement any options
|
||||||
|
// passed into the plug-in entry point
|
||||||
|
var defaults = {
|
||||||
|
cancelOn: 'select,input,textarea',
|
||||||
|
direction: 'multi',
|
||||||
|
dragHold: false,
|
||||||
|
hoverThumbs: false,
|
||||||
|
scrollDelta: settings.scrollDelta,
|
||||||
|
showThumbs: true,
|
||||||
|
persistThumbs: false,
|
||||||
|
captureWheel: true,
|
||||||
|
wheelDelta: settings.wheelDelta,
|
||||||
|
wheelDirection: 'multi',
|
||||||
|
zIndex: 999,
|
||||||
|
ignoreSizing: false
|
||||||
|
};
|
||||||
|
|
||||||
|
// Triggers a DOM event on the overscrolled element.
|
||||||
|
// All events are namespaced under the overscroll name
|
||||||
|
function triggerEvent(event, target) {
|
||||||
|
target.trigger('overscroll:' + event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function to return a timestamp
|
||||||
|
function time() {
|
||||||
|
return (new Date()).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Captures the position from an event, modifies the properties
|
||||||
|
// of the second argument to persist the position, and then
|
||||||
|
// returns the modified object
|
||||||
|
function capturePosition(event, position, index) {
|
||||||
|
position.x = event.pageX;
|
||||||
|
position.y = event.pageY;
|
||||||
|
position.time = time();
|
||||||
|
position.index = index;
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to move the thumbs around an overscrolled element
|
||||||
|
function moveThumbs(thumbs, sizing, left, top) {
|
||||||
|
|
||||||
|
var ml, mt;
|
||||||
|
|
||||||
|
if (thumbs && thumbs.added) {
|
||||||
|
if (thumbs.horizontal) {
|
||||||
|
ml = left * (1 + sizing.container.width / sizing.container.scrollWidth);
|
||||||
|
mt = top + sizing.thumbs.horizontal.top;
|
||||||
|
thumbs.horizontal.css('margin', mt + 'px 0 0 ' + ml + 'px');
|
||||||
|
}
|
||||||
|
if (thumbs.vertical) {
|
||||||
|
ml = left + sizing.thumbs.vertical.left;
|
||||||
|
mt = top * (1 + sizing.container.height / sizing.container.scrollHeight);
|
||||||
|
thumbs.vertical.css('margin', mt + 'px 0 0 ' + ml + 'px');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to toggle the thumbs on and off
|
||||||
|
// of an overscrolled element
|
||||||
|
function toggleThumbs(thumbs, options, dragging) {
|
||||||
|
if (thumbs && thumbs.added && !options.persistThumbs) {
|
||||||
|
if (dragging) {
|
||||||
|
if (thumbs.vertical) {
|
||||||
|
thumbs.vertical.stop(true, true).fadeTo('fast', settings.thumbOpacity);
|
||||||
|
}
|
||||||
|
if (thumbs.horizontal) {
|
||||||
|
thumbs.horizontal.stop(true, true).fadeTo('fast', settings.thumbOpacity);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (thumbs.vertical) {
|
||||||
|
thumbs.vertical.fadeTo('fast', 0);
|
||||||
|
}
|
||||||
|
if (thumbs.horizontal) {
|
||||||
|
thumbs.horizontal.fadeTo('fast', 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defers click event listeners to after a mouseup event.
|
||||||
|
// Used to avoid unintentional clicks
|
||||||
|
function deferClick(target) {
|
||||||
|
var clicks, key = 'events';
|
||||||
|
var events = $._data ? $._data(target[0], key) : target.data(key);
|
||||||
|
if (events && events.click) {
|
||||||
|
clicks = events.click.slice();
|
||||||
|
target.off('click').one('click', function(){
|
||||||
|
$.each(clicks, function(i, click){
|
||||||
|
target.click(click);
|
||||||
|
}); return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggles thumbs on hover. This event is only triggered
|
||||||
|
// if the hoverThumbs option is set
|
||||||
|
function hover(event) {
|
||||||
|
var data = event.data,
|
||||||
|
thumbs = data.thumbs,
|
||||||
|
options = data.options,
|
||||||
|
dragging = event.type === 'mouseenter';
|
||||||
|
toggleThumbs(thumbs, options, dragging);
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function is only ever used when the overscrolled element
|
||||||
|
// scrolled outside of the scope of this plugin.
|
||||||
|
function scroll(event) {
|
||||||
|
var data = event.data;
|
||||||
|
if (!data.flags.dragged) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
moveThumbs(data.thumbs, data.sizing, this.scrollLeft, this.scrollTop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// handles mouse wheel scroll events
|
||||||
|
function wheel(event) {
|
||||||
|
|
||||||
|
// prevent any default wheel behavior
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var data = event.data,
|
||||||
|
options = data.options,
|
||||||
|
sizing = data.sizing,
|
||||||
|
thumbs = data.thumbs,
|
||||||
|
dwheel = data.wheel,
|
||||||
|
flags = data.flags,
|
||||||
|
original = event.originalEvent,
|
||||||
|
delta = 0, deltaX = 0, deltaY = 0;
|
||||||
|
|
||||||
|
// stop any drifts
|
||||||
|
flags.drifting = false;
|
||||||
|
|
||||||
|
// normalize the wheel ticks
|
||||||
|
if (original.detail) {
|
||||||
|
delta = -original.detail;
|
||||||
|
if (original.detailX) {
|
||||||
|
deltaX = -original.detailX;
|
||||||
|
}
|
||||||
|
if (original.detailY) {
|
||||||
|
deltaY = -original.detailY;
|
||||||
|
}
|
||||||
|
} else if (original.wheelDelta) {
|
||||||
|
delta = original.wheelDelta / settings.wheelTicks;
|
||||||
|
if (original.wheelDeltaX) {
|
||||||
|
deltaX = original.wheelDeltaX / settings.wheelTicks;
|
||||||
|
}
|
||||||
|
if (original.wheelDeltaY) {
|
||||||
|
deltaY = original.wheelDeltaY / settings.wheelTicks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply a pixel delta to each tick
|
||||||
|
delta *= options.wheelDelta;
|
||||||
|
deltaX *= options.wheelDelta;
|
||||||
|
deltaY *= options.wheelDelta;
|
||||||
|
|
||||||
|
// initialize flags if this is the first tick
|
||||||
|
if (!dwheel) {
|
||||||
|
data.target.data(datakey).dragging = flags.dragging = true;
|
||||||
|
data.wheel = dwheel = { timeout: null };
|
||||||
|
toggleThumbs(thumbs, options, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// actually modify scroll offsets
|
||||||
|
if (options.wheelDirection === 'vertical'){
|
||||||
|
/*jshint validthis:true */
|
||||||
|
this.scrollTop -= delta;
|
||||||
|
} else if ( options.wheelDirection === 'horizontal') {
|
||||||
|
this.scrollLeft -= delta;
|
||||||
|
} else {
|
||||||
|
this.scrollLeft -= deltaX;
|
||||||
|
this.scrollTop -= deltaY || delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dwheel.timeout) { cancel(dwheel.timeout); }
|
||||||
|
|
||||||
|
moveThumbs(thumbs, sizing, this.scrollLeft, this.scrollTop);
|
||||||
|
|
||||||
|
dwheel.timeout = wait(function() {
|
||||||
|
data.target.data(datakey).dragging = flags.dragging = false;
|
||||||
|
toggleThumbs(thumbs, options, data.wheel = null);
|
||||||
|
}, settings.thumbTimeout);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// updates the current scroll offset during a mouse move
|
||||||
|
function drag(event) {
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var data = event.data,
|
||||||
|
touches = event.originalEvent.touches,
|
||||||
|
options = data.options,
|
||||||
|
sizing = data.sizing,
|
||||||
|
thumbs = data.thumbs,
|
||||||
|
position = data.position,
|
||||||
|
flags = data.flags,
|
||||||
|
target = data.target.get(0);
|
||||||
|
|
||||||
|
|
||||||
|
// correct page coordinates for touch devices
|
||||||
|
if (touches && touches.length) {
|
||||||
|
event = touches[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flags.dragged) {
|
||||||
|
toggleThumbs(thumbs, options, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
flags.dragged = true;
|
||||||
|
|
||||||
|
if (options.direction !== 'vertical') {
|
||||||
|
target.scrollLeft -= (event.pageX - position.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.options.direction !== 'horizontal') {
|
||||||
|
target.scrollTop -= (event.pageY - position.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
capturePosition(event, data.position);
|
||||||
|
|
||||||
|
if (--data.capture.index <= 0) {
|
||||||
|
data.target.data(datakey).dragging = flags.dragging = true;
|
||||||
|
capturePosition(event, data.capture, settings.captureThreshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// sends the overscrolled element into a drift
|
||||||
|
function drift(target, event, callback) {
|
||||||
|
|
||||||
|
var data = event.data, dx, dy, xMod, yMod,
|
||||||
|
capture = data.capture,
|
||||||
|
options = data.options,
|
||||||
|
sizing = data.sizing,
|
||||||
|
thumbs = data.thumbs,
|
||||||
|
elapsed = time() - capture.time,
|
||||||
|
scrollLeft = target.scrollLeft,
|
||||||
|
scrollTop = target.scrollTop,
|
||||||
|
decay = settings.driftDecay;
|
||||||
|
|
||||||
|
// only drift if enough time has passed since
|
||||||
|
// the last capture event
|
||||||
|
if (elapsed > settings.driftTimeout) {
|
||||||
|
callback(data); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// determine offset between last capture and current time
|
||||||
|
dx = options.scrollDelta * (event.pageX - capture.x);
|
||||||
|
dy = options.scrollDelta * (event.pageY - capture.y);
|
||||||
|
|
||||||
|
// update target scroll offsets
|
||||||
|
if (options.direction !== 'vertical') {
|
||||||
|
scrollLeft -= dx;
|
||||||
|
} if (options.direction !== 'horizontal') {
|
||||||
|
scrollTop -= dy;
|
||||||
|
}
|
||||||
|
|
||||||
|
// split the distance to travel into a set of sequences
|
||||||
|
xMod = dx / settings.driftSequences;
|
||||||
|
yMod = dy / settings.driftSequences;
|
||||||
|
|
||||||
|
triggerEvent('driftstart', data.target);
|
||||||
|
|
||||||
|
data.drifting = true;
|
||||||
|
|
||||||
|
// animate the drift sequence
|
||||||
|
compat.animate(function render() {
|
||||||
|
if (data.drifting) {
|
||||||
|
var min = 1, max = -1;
|
||||||
|
data.drifting = false;
|
||||||
|
if (yMod > min && target.scrollTop > scrollTop || yMod < max && target.scrollTop < scrollTop) {
|
||||||
|
data.drifting = true;
|
||||||
|
target.scrollTop -= yMod;
|
||||||
|
yMod /= decay;
|
||||||
|
}
|
||||||
|
if (xMod > min && target.scrollLeft > scrollLeft || xMod < max && target.scrollLeft < scrollLeft) {
|
||||||
|
data.drifting = true;
|
||||||
|
target.scrollLeft -= xMod;
|
||||||
|
xMod /= decay;
|
||||||
|
}
|
||||||
|
moveThumbs(thumbs, sizing, target.scrollLeft, target.scrollTop);
|
||||||
|
compat.animate(render);
|
||||||
|
} else {
|
||||||
|
triggerEvent('driftend', data.target);
|
||||||
|
callback(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// starts the drag operation and binds the mouse move handler
|
||||||
|
function start(event) {
|
||||||
|
|
||||||
|
var data = event.data,
|
||||||
|
touches = event.originalEvent.touches,
|
||||||
|
target = data.target,
|
||||||
|
dstart = data.start = $(event.target),
|
||||||
|
flags = data.flags;
|
||||||
|
|
||||||
|
// stop any drifts
|
||||||
|
flags.drifting = false;
|
||||||
|
|
||||||
|
// only start drag if the user has not explictly banned it.
|
||||||
|
if (dstart.size() && !dstart.is(data.options.cancelOn)) {
|
||||||
|
|
||||||
|
// without this the simple "click" event won't be recognized on touch clients
|
||||||
|
if (!touches) { event.preventDefault(); }
|
||||||
|
|
||||||
|
if (!compat.overflowScrolling) {
|
||||||
|
target.css('cursor', compat.cursor.grabbing);
|
||||||
|
target.data(datakey).dragging = flags.dragging = flags.dragged = false;
|
||||||
|
|
||||||
|
// apply the drag listeners to the doc or target
|
||||||
|
if(data.options.dragHold) {
|
||||||
|
$(document).on(events.drag, data, drag);
|
||||||
|
} else {
|
||||||
|
target.on(events.drag, data, drag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data.position = capturePosition(event, {});
|
||||||
|
data.capture = capturePosition(event, {}, settings.captureThreshold);
|
||||||
|
triggerEvent('dragstart', target);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ends the drag operation and unbinds the mouse move handler
|
||||||
|
function stop(event) {
|
||||||
|
|
||||||
|
var data = event.data,
|
||||||
|
target = data.target,
|
||||||
|
options = data.options,
|
||||||
|
flags = data.flags,
|
||||||
|
thumbs = data.thumbs,
|
||||||
|
|
||||||
|
// hides the thumbs after the animation is done
|
||||||
|
done = function () {
|
||||||
|
if (thumbs && !options.hoverThumbs) {
|
||||||
|
toggleThumbs(thumbs, options, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// remove drag listeners from doc or target
|
||||||
|
if(options.dragHold) {
|
||||||
|
$(document).unbind(events.drag, drag);
|
||||||
|
} else {
|
||||||
|
target.unbind(events.drag, drag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// only fire events and drift if we started with a
|
||||||
|
// valid position
|
||||||
|
if (data.position) {
|
||||||
|
|
||||||
|
triggerEvent('dragend', target);
|
||||||
|
|
||||||
|
// only drift if a drag passed our threshold
|
||||||
|
if (flags.dragging && !compat.overflowScrolling) {
|
||||||
|
drift(target.get(0), event, done);
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// only if we moved, and the mouse down is the same as
|
||||||
|
// the mouse up target do we defer the event
|
||||||
|
if (flags.dragging && !compat.overflowScrolling && data.start && data.start.is(event.target)) {
|
||||||
|
deferClick(data.start);
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear all internal flags and settings
|
||||||
|
target.data(datakey).dragging =
|
||||||
|
data.start =
|
||||||
|
data.capture =
|
||||||
|
data.position =
|
||||||
|
flags.dragged =
|
||||||
|
flags.dragging = false;
|
||||||
|
|
||||||
|
// set the cursor back to normal
|
||||||
|
target.css('cursor', compat.cursor.grab);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensures that a full set of options are provided
|
||||||
|
// for the plug-in. Also does some validation
|
||||||
|
function getOptions(options) {
|
||||||
|
|
||||||
|
// fill in missing values with defaults
|
||||||
|
options = $.extend({}, defaults, options);
|
||||||
|
|
||||||
|
// check for inconsistent directional restrictions
|
||||||
|
if (options.direction !== 'multi' && options.direction !== options.wheelDirection) {
|
||||||
|
options.wheelDirection = options.direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure positive values for deltas
|
||||||
|
options.scrollDelta = math.abs(parseFloat(options.scrollDelta));
|
||||||
|
options.wheelDelta = math.abs(parseFloat(options.wheelDelta));
|
||||||
|
|
||||||
|
// fix values for scroll offset
|
||||||
|
options.scrollLeft = options.scrollLeft === none ? null : math.abs(parseFloat(options.scrollLeft));
|
||||||
|
options.scrollTop = options.scrollTop === none ? null : math.abs(parseFloat(options.scrollTop));
|
||||||
|
|
||||||
|
return options;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the sizing information (bounding box) for the
|
||||||
|
// target DOM element
|
||||||
|
function getSizing(target) {
|
||||||
|
|
||||||
|
var $target = $(target),
|
||||||
|
width = $target.width(),
|
||||||
|
height = $target.height(),
|
||||||
|
scrollWidth = width >= target.scrollWidth ? width : target.scrollWidth,
|
||||||
|
scrollHeight = height >= target.scrollHeight ? height : target.scrollHeight,
|
||||||
|
hasScroll = scrollWidth > width || scrollHeight > height;
|
||||||
|
|
||||||
|
return {
|
||||||
|
valid: hasScroll,
|
||||||
|
container: {
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
scrollWidth: scrollWidth,
|
||||||
|
scrollHeight: scrollHeight
|
||||||
|
},
|
||||||
|
thumbs: {
|
||||||
|
horizontal: {
|
||||||
|
width: width * width / scrollWidth,
|
||||||
|
height: settings.thumbThickness,
|
||||||
|
corner: settings.thumbThickness / 2,
|
||||||
|
left: 0,
|
||||||
|
top: height - settings.thumbThickness
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
width: settings.thumbThickness,
|
||||||
|
height: height * height / scrollHeight,
|
||||||
|
corner: settings.thumbThickness / 2,
|
||||||
|
left: width - settings.thumbThickness,
|
||||||
|
top: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to get (or implicitly creates) the
|
||||||
|
// remover function for the target passed
|
||||||
|
// in as an argument
|
||||||
|
function getRemover(target, orCreate) {
|
||||||
|
|
||||||
|
var $target = $(target), thumbs,
|
||||||
|
data = $target.data(datakey) || {},
|
||||||
|
style = $target.attr('style'),
|
||||||
|
fallback = orCreate ? function () {
|
||||||
|
|
||||||
|
data = $target.data(datakey);
|
||||||
|
thumbs = data.thumbs;
|
||||||
|
|
||||||
|
// restore original styles (if any)
|
||||||
|
if (style) {
|
||||||
|
$target.attr('style', style);
|
||||||
|
} else {
|
||||||
|
$target.removeAttr('style');
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any created thumbs
|
||||||
|
if (thumbs) {
|
||||||
|
if (thumbs.horizontal) { thumbs.horizontal.remove(); }
|
||||||
|
if (thumbs.vertical) { thumbs.vertical.remove(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any bound overscroll events and data
|
||||||
|
$target
|
||||||
|
.removeData(datakey)
|
||||||
|
.off(events.wheel, wheel)
|
||||||
|
.off(events.start, start)
|
||||||
|
.off(events.end, stop)
|
||||||
|
.off(events.ignored, ignore);
|
||||||
|
|
||||||
|
} : $.noop;
|
||||||
|
|
||||||
|
return $.isFunction(data.remover) ? data.remover : fallback;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Genterates CSS specific to a particular thumb.
|
||||||
|
// It requires sizing data and options
|
||||||
|
function getThumbCss(size, options) {
|
||||||
|
return {
|
||||||
|
position: 'absolute',
|
||||||
|
opacity: options.persistThumbs ? settings.thumbOpacity : 0,
|
||||||
|
'background-color': 'black',
|
||||||
|
width: size.width + 'px',
|
||||||
|
height: size.height + 'px',
|
||||||
|
'border-radius': size.corner + 'px',
|
||||||
|
'margin': size.top + 'px 0 0 ' + size.left + 'px',
|
||||||
|
'z-index': options.zIndex
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the DOM elements used as "thumbs" within
|
||||||
|
// the target container.
|
||||||
|
function createThumbs(target, sizing, options) {
|
||||||
|
|
||||||
|
var div = '<div/>',
|
||||||
|
thumbs = {},
|
||||||
|
css = false;
|
||||||
|
|
||||||
|
if (sizing.container.scrollWidth > 0 && options.direction !== 'vertical') {
|
||||||
|
css = getThumbCss(sizing.thumbs.horizontal, options);
|
||||||
|
thumbs.horizontal = $(div).css(css).prependTo(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sizing.container.scrollHeight > 0 && options.direction !== 'horizontal') {
|
||||||
|
css = getThumbCss(sizing.thumbs.vertical, options);
|
||||||
|
thumbs.vertical = $(div).css(css).prependTo(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
thumbs.added = !!css;
|
||||||
|
|
||||||
|
return thumbs;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignores events on the overscroll element
|
||||||
|
function ignore(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function takes a jQuery element, some
|
||||||
|
// (optional) options, and sets up event metadata
|
||||||
|
// for each instance the plug-in affects
|
||||||
|
function setup(target, options) {
|
||||||
|
|
||||||
|
// create initial data properties for this instance
|
||||||
|
options = getOptions(options);
|
||||||
|
var sizing = getSizing(target),
|
||||||
|
thumbs, data = {
|
||||||
|
options: options, sizing: sizing,
|
||||||
|
flags: { dragging: false },
|
||||||
|
remover: getRemover(target, true)
|
||||||
|
};
|
||||||
|
|
||||||
|
// only apply handlers if the overscrolled element
|
||||||
|
// actually has an area to scroll
|
||||||
|
if (sizing.valid || options.ignoreSizing) {
|
||||||
|
// provide a circular-reference, enable events, and
|
||||||
|
// apply any required CSS
|
||||||
|
data.target = target = $(target).css({
|
||||||
|
position: 'relative',
|
||||||
|
cursor: compat.cursor.grab
|
||||||
|
}).on(events.start, data, start)
|
||||||
|
.on(events.end, data, stop)
|
||||||
|
.on(events.ignored, data, ignore);
|
||||||
|
|
||||||
|
// apply the stop listeners for drag end
|
||||||
|
if(options.dragHold) {
|
||||||
|
$(document).on(events.end, data, stop);
|
||||||
|
} else {
|
||||||
|
data.target.on(events.end, data, stop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply any user-provided scroll offsets
|
||||||
|
if (options.scrollLeft !== null) {
|
||||||
|
target.scrollLeft(options.scrollLeft);
|
||||||
|
} if (options.scrollTop !== null) {
|
||||||
|
target.scrollTop(options.scrollTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
// use native oversroll, if it exists
|
||||||
|
if (compat.overflowScrolling) {
|
||||||
|
target.css(compat.overflowScrolling, 'touch');
|
||||||
|
} else {
|
||||||
|
target.on(events.scroll, data, scroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check to see if the user would like mousewheel support
|
||||||
|
if (options.captureWheel) {
|
||||||
|
target.on(events.wheel, data, wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add thumbs and listeners (if we're showing them)
|
||||||
|
if (options.showThumbs) {
|
||||||
|
if (compat.overflowScrolling) {
|
||||||
|
target.css('overflow', 'scroll');
|
||||||
|
} else {
|
||||||
|
target.css('overflow', 'hidden');
|
||||||
|
data.thumbs = thumbs = createThumbs(target, sizing, options);
|
||||||
|
if (thumbs.added) {
|
||||||
|
moveThumbs(thumbs, sizing, target.scrollLeft(), target.scrollTop());
|
||||||
|
if (options.hoverThumbs) {
|
||||||
|
target.on(events.hover, data, hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.css('overflow', 'hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
target.data(datakey, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes any event listeners and other instance-specific
|
||||||
|
// data from the target. It attempts to leave the target
|
||||||
|
// at the state it found it.
|
||||||
|
function teardown(target) {
|
||||||
|
getRemover(target)();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the entry-point for enabling the plug-in;
|
||||||
|
// You can find it's exposure point at the end
|
||||||
|
// of this closure
|
||||||
|
function overscroll(options) {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
return this.removeOverscroll().each(function() {
|
||||||
|
setup(this, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is the entry-point for disabling the plug-in;
|
||||||
|
// You can find it's exposure point at the end
|
||||||
|
// of this closure
|
||||||
|
function removeOverscroll() {
|
||||||
|
/*jshint validthis:true */
|
||||||
|
return this.each(function () {
|
||||||
|
teardown(this);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extend overscroll to expose settings to the user
|
||||||
|
overscroll.settings = settings;
|
||||||
|
|
||||||
|
// Extend jQuery's prototype to expose the plug-in.
|
||||||
|
// If the supports native overflowScrolling, overscroll will not
|
||||||
|
// attempt to override the browser's built in support
|
||||||
|
$.extend(namespace, {
|
||||||
|
overscroll: overscroll,
|
||||||
|
removeOverscroll: removeOverscroll
|
||||||
|
});
|
||||||
|
|
||||||
|
})(window, document, navigator, Math, setTimeout, clearTimeout, jQuery.fn, jQuery);
|
102
static/lib/loading-bar.css
Executable file
|
@ -0,0 +1,102 @@
|
||||||
|
|
||||||
|
/* Make clicks pass-through */
|
||||||
|
#loading-bar,
|
||||||
|
#loading-bar-spinner {
|
||||||
|
pointer-events: none;
|
||||||
|
-webkit-pointer-events: none;
|
||||||
|
-webkit-transition: 0.5s linear all;
|
||||||
|
-moz-transition: 0.5s linear all;
|
||||||
|
-o-transition: 0.5s linear all;
|
||||||
|
transition: 0.5s linear all;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar.ng-enter,
|
||||||
|
#loading-bar.ng-leave.ng-leave-active,
|
||||||
|
#loading-bar-spinner.ng-enter,
|
||||||
|
#loading-bar-spinner.ng-leave.ng-leave-active {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar.ng-enter.ng-enter-active,
|
||||||
|
#loading-bar.ng-leave,
|
||||||
|
#loading-bar-spinner.ng-enter.ng-enter-active,
|
||||||
|
#loading-bar-spinner.ng-leave {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar .bar {
|
||||||
|
-webkit-transition: width 350ms;
|
||||||
|
-moz-transition: width 350ms;
|
||||||
|
-o-transition: width 350ms;
|
||||||
|
transition: width 350ms;
|
||||||
|
|
||||||
|
background: #29d;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fancy blur effect */
|
||||||
|
#loading-bar .peg {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
right: 0px;
|
||||||
|
width: 100px;
|
||||||
|
height: 100%;
|
||||||
|
box-shadow: 0 0 10px #29d, 0 0 5px #29d;
|
||||||
|
opacity: 1.0;
|
||||||
|
|
||||||
|
-webkit-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
-moz-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
-ms-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
-o-transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
transform: rotate(3deg) translate(0px, -4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar-spinner {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 100;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-bar-spinner .spinner-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
|
||||||
|
border: solid 2px transparent;
|
||||||
|
border-top-color: #29d;
|
||||||
|
border-left-color: #29d;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
-webkit-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
-moz-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
-ms-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
-o-animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
animation: loading-bar-spinner 400ms linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@-webkit-keyframes loading-bar-spinner {
|
||||||
|
0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); }
|
||||||
|
100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@-moz-keyframes loading-bar-spinner {
|
||||||
|
0% { -moz-transform: rotate(0deg); transform: rotate(0deg); }
|
||||||
|
100% { -moz-transform: rotate(360deg); transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@-o-keyframes loading-bar-spinner {
|
||||||
|
0% { -o-transform: rotate(0deg); transform: rotate(0deg); }
|
||||||
|
100% { -o-transform: rotate(360deg); transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@-ms-keyframes loading-bar-spinner {
|
||||||
|
0% { -ms-transform: rotate(0deg); transform: rotate(0deg); }
|
||||||
|
100% { -ms-transform: rotate(360deg); transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
@keyframes loading-bar-spinner {
|
||||||
|
0% { transform: rotate(0deg); transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); transform: rotate(360deg); }
|
||||||
|
}
|
271
static/lib/loading-bar.js
Executable file
|
@ -0,0 +1,271 @@
|
||||||
|
/*
|
||||||
|
* angular-loading-bar
|
||||||
|
*
|
||||||
|
* intercepts XHR requests and creates a loading bar.
|
||||||
|
* Based on the excellent nprogress work by rstacruz (more info in readme)
|
||||||
|
*
|
||||||
|
* (c) 2013 Wes Cruver
|
||||||
|
* License: MIT
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Alias the loading bar so it can be included using a simpler
|
||||||
|
// (and maybe more professional) module name:
|
||||||
|
angular.module('angular-loading-bar', ['chieffancypants.loadingBar']);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadingBarInterceptor service
|
||||||
|
*
|
||||||
|
* Registers itself as an Angular interceptor and listens for XHR requests.
|
||||||
|
*/
|
||||||
|
angular.module('chieffancypants.loadingBar', [])
|
||||||
|
.config(['$httpProvider', function ($httpProvider) {
|
||||||
|
|
||||||
|
var interceptor = ['$q', '$cacheFactory', 'cfpLoadingBar', function ($q, $cacheFactory, cfpLoadingBar) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The total number of requests made
|
||||||
|
*/
|
||||||
|
var reqsTotal = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The number of requests completed (either successfully or not)
|
||||||
|
*/
|
||||||
|
var reqsCompleted = 0;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* calls cfpLoadingBar.complete() which removes the
|
||||||
|
* loading bar from the DOM.
|
||||||
|
*/
|
||||||
|
function setComplete() {
|
||||||
|
cfpLoadingBar.complete();
|
||||||
|
reqsCompleted = 0;
|
||||||
|
reqsTotal = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the response has already been cached
|
||||||
|
* @param {Object} config the config option from the request
|
||||||
|
* @return {Boolean} retrns true if cached, otherwise false
|
||||||
|
*/
|
||||||
|
function isCached(config) {
|
||||||
|
var cache;
|
||||||
|
var defaults = $httpProvider.defaults;
|
||||||
|
|
||||||
|
if (config.method !== 'GET' || config.cache === false) {
|
||||||
|
config.cached = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.cache === true && defaults.cache === undefined) {
|
||||||
|
cache = $cacheFactory.get('$http');
|
||||||
|
} else if (defaults.cache !== undefined) {
|
||||||
|
cache = defaults.cache;
|
||||||
|
} else {
|
||||||
|
cache = config.cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cached = cache !== undefined ?
|
||||||
|
cache.get(config.url) !== undefined : false;
|
||||||
|
|
||||||
|
if (config.cached !== undefined && cached !== config.cached) {
|
||||||
|
return config.cached;
|
||||||
|
}
|
||||||
|
config.cached = cached;
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
'request': function(config) {
|
||||||
|
// Check to make sure this request hasn't already been cached and that
|
||||||
|
// the requester didn't explicitly ask us to ignore this request:
|
||||||
|
if (!config.ignoreLoadingBar && !isCached(config)) {
|
||||||
|
if (reqsTotal === 0) {
|
||||||
|
cfpLoadingBar.start();
|
||||||
|
}
|
||||||
|
reqsTotal++;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
|
||||||
|
'response': function(response) {
|
||||||
|
if (!isCached(response.config)) {
|
||||||
|
reqsCompleted++;
|
||||||
|
if (reqsCompleted >= reqsTotal) {
|
||||||
|
setComplete();
|
||||||
|
} else {
|
||||||
|
cfpLoadingBar.set(reqsCompleted / reqsTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
|
||||||
|
'responseError': function(rejection) {
|
||||||
|
if (!isCached(rejection.config)) {
|
||||||
|
reqsCompleted++;
|
||||||
|
if (reqsCompleted >= reqsTotal) {
|
||||||
|
setComplete();
|
||||||
|
} else {
|
||||||
|
cfpLoadingBar.set(reqsCompleted / reqsTotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}];
|
||||||
|
|
||||||
|
$httpProvider.interceptors.push(interceptor);
|
||||||
|
}])
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading Bar
|
||||||
|
*
|
||||||
|
* This service handles adding and removing the actual element in the DOM.
|
||||||
|
* Generally, best practices for DOM manipulation is to take place in a
|
||||||
|
* directive, but because the element itself is injected in the DOM only upon
|
||||||
|
* XHR requests, and it's likely needed on every view, the best option is to
|
||||||
|
* use a service.
|
||||||
|
*/
|
||||||
|
.provider('cfpLoadingBar', function() {
|
||||||
|
|
||||||
|
this.includeSpinner = true;
|
||||||
|
this.includeBar = true;
|
||||||
|
this.parentSelector = 'body';
|
||||||
|
|
||||||
|
this.$get = ['$document', '$timeout', '$animate', '$rootScope', function ($document, $timeout, $animate, $rootScope) {
|
||||||
|
|
||||||
|
var $parentSelector = this.parentSelector,
|
||||||
|
$parent = $document.find($parentSelector),
|
||||||
|
loadingBarContainer = angular.element('<div id="loading-bar"><div class="bar"><div class="peg"></div></div></div>'),
|
||||||
|
loadingBar = loadingBarContainer.find('div').eq(0),
|
||||||
|
spinner = angular.element('<div id="loading-bar-spinner"><div class="spinner-icon"></div></div>');
|
||||||
|
|
||||||
|
var incTimeout,
|
||||||
|
completeTimeout,
|
||||||
|
started = false,
|
||||||
|
status = 0;
|
||||||
|
|
||||||
|
var includeSpinner = this.includeSpinner;
|
||||||
|
var includeBar = this.includeBar;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts the loading bar element into the dom, and sets it to 2%
|
||||||
|
*/
|
||||||
|
function _start() {
|
||||||
|
$timeout.cancel(completeTimeout);
|
||||||
|
|
||||||
|
// do not continually broadcast the started event:
|
||||||
|
if (started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootScope.$broadcast('cfpLoadingBar:started');
|
||||||
|
started = true;
|
||||||
|
|
||||||
|
if (includeBar) {
|
||||||
|
$animate.enter(loadingBarContainer, $parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (includeSpinner) {
|
||||||
|
$animate.enter(spinner, $parent);
|
||||||
|
}
|
||||||
|
_set(0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the loading bar's width to a certain percent.
|
||||||
|
*
|
||||||
|
* @param n any value between 0 and 1
|
||||||
|
*/
|
||||||
|
function _set(n) {
|
||||||
|
if (!started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pct = (n * 100) + '%';
|
||||||
|
loadingBar.css('width', pct);
|
||||||
|
status = n;
|
||||||
|
|
||||||
|
// increment loadingbar to give the illusion that there is always
|
||||||
|
// progress but make sure to cancel the previous timeouts so we don't
|
||||||
|
// have multiple incs running at the same time.
|
||||||
|
$timeout.cancel(incTimeout);
|
||||||
|
incTimeout = $timeout(function() {
|
||||||
|
_inc();
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the loading bar by a random amount
|
||||||
|
* but slows down as it progresses
|
||||||
|
*/
|
||||||
|
function _inc() {
|
||||||
|
if (_status() >= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rnd = 0;
|
||||||
|
|
||||||
|
// TODO: do this mathmatically instead of through conditions
|
||||||
|
|
||||||
|
var stat = _status();
|
||||||
|
if (stat >= 0 && stat < 0.25) {
|
||||||
|
// Start out between 3 - 6% increments
|
||||||
|
rnd = (Math.random() * (5 - 3 + 1) + 3) / 100;
|
||||||
|
} else if (stat >= 0.25 && stat < 0.65) {
|
||||||
|
// increment between 0 - 3%
|
||||||
|
rnd = (Math.random() * 3) / 100;
|
||||||
|
} else if (stat >= 0.65 && stat < 0.9) {
|
||||||
|
// increment between 0 - 2%
|
||||||
|
rnd = (Math.random() * 2) / 100;
|
||||||
|
} else if (stat >= 0.9 && stat < 0.99) {
|
||||||
|
// finally, increment it .5 %
|
||||||
|
rnd = 0.005;
|
||||||
|
} else {
|
||||||
|
// after 99%, don't increment:
|
||||||
|
rnd = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pct = _status() + rnd;
|
||||||
|
_set(pct);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _status() {
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _complete() {
|
||||||
|
$rootScope.$broadcast('cfpLoadingBar:completed');
|
||||||
|
_set(1);
|
||||||
|
|
||||||
|
// Attempt to aggregate any start/complete calls within 500ms:
|
||||||
|
completeTimeout = $timeout(function() {
|
||||||
|
$animate.leave(loadingBarContainer, function() {
|
||||||
|
status = 0;
|
||||||
|
started = false;
|
||||||
|
});
|
||||||
|
$animate.leave(spinner);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
start : _start,
|
||||||
|
set : _set,
|
||||||
|
status : _status,
|
||||||
|
inc : _inc,
|
||||||
|
complete : _complete,
|
||||||
|
includeSpinner : this.includeSpinner,
|
||||||
|
parentSelector : this.parentSelector
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}]; //
|
||||||
|
}); // wtf javascript. srsly
|
||||||
|
})(); //
|
|
@ -93,6 +93,15 @@ Email: my@email.com</pre>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<h3>Deleting a tag <span class="label label-info">Requires Admin Access</span></h3>
|
||||||
|
<div class="container">
|
||||||
|
<div class="description-overview">
|
||||||
|
A specific tag and all its images can be deleted by right clicking on the tag in the repository history tree and choosing "Delete Tag". This will delete the tag and any images <b>unique to it</b>. Images will not be deleted until all tags sharing them are deleted.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<a name="#post-hook"></a>
|
<a name="#post-hook"></a>
|
||||||
<h3>Using push webhooks <span class="label label-info">Requires Admin Access</span></h3>
|
<h3>Using push webhooks <span class="label label-info">Requires Admin Access</span></h3>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
@ -101,16 +110,16 @@ Email: my@email.com</pre>
|
||||||
as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
|
as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
|
||||||
<pre>
|
<pre>
|
||||||
{
|
{
|
||||||
<span class="code-info" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
|
<span class="context-tooltip" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
|
||||||
<span class="code-info" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
|
<span class="context-tooltip" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
|
||||||
<span class="code-info" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
|
<span class="context-tooltip" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
|
||||||
<span class="code-info" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
|
<span class="context-tooltip" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
|
||||||
<span class="code-info" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
|
<span class="context-tooltip" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
|
||||||
"latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
|
"latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
|
||||||
},
|
},
|
||||||
<span class="code-info" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
|
<span class="context-tooltip" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
|
||||||
<span class="code-info" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
|
<span class="context-tooltip" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
|
||||||
<span class="code-info" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
|
<span class="context-tooltip" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
|
||||||
}
|
}
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,97 +1,99 @@
|
||||||
<div class="container" ng-show="!loading && !image">
|
<div class="resource-view" resource="image" error-message="'No image found'">
|
||||||
No image found
|
<div class="container repo repo-image-view">
|
||||||
</div>
|
<div class="header">
|
||||||
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||||
|
<h3>
|
||||||
|
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
|
||||||
|
<span style="color: #aaa;"> {{repo.namespace}}</span>
|
||||||
|
<span style="color: #ccc">/</span>
|
||||||
|
<span style="color: #666;">{{repo.name}}</span>
|
||||||
|
<span style="color: #ccc">/</span>
|
||||||
|
<span>{{image.value.id.substr(0, 12)}}</span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="loading" ng-show="loading">
|
<!-- Comment -->
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<blockquote ng-show="image.value.comment">
|
||||||
</div>
|
<span class="markdown-view" content="image.value.comment"></span>
|
||||||
|
</blockquote>
|
||||||
|
|
||||||
<div class="container repo repo-image-view" ng-show="!loading && image">
|
<!-- Information -->
|
||||||
<div class="header">
|
<dl class="dl-normal">
|
||||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
<dt>Full Image ID</dt>
|
||||||
<h3>
|
<dd>
|
||||||
<i class="fa fa-archive fa-lg" style="color: #aaa; margin-right: 10px;"></i>
|
<div>
|
||||||
<span style="color: #aaa;"> {{repo.namespace}}</span>
|
<div class="id-container">
|
||||||
<span style="color: #ccc">/</span>
|
<div class="input-group">
|
||||||
<span style="color: #666;">{{repo.name}}</span>
|
<input id="full-id" type="text" class="form-control" value="{{ image.value.id }}" readonly>
|
||||||
<span style="color: #ccc">/</span>
|
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
|
||||||
<span>{{image.id.substr(0, 12)}}</span>
|
<i class="fa fa-copy"></i>
|
||||||
</h3>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Comment -->
|
|
||||||
<blockquote ng-show="image.comment">
|
<div id="clipboardCopied" style="display: none">
|
||||||
<span class="markdown-view" content="image.comment"></span>
|
Copied to clipboard
|
||||||
</blockquote>
|
|
||||||
|
|
||||||
<!-- Information -->
|
|
||||||
<dl class="dl-normal">
|
|
||||||
<dt>Full Image ID</dt>
|
|
||||||
<dd>
|
|
||||||
<div>
|
|
||||||
<div class="id-container">
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="full-id" type="text" class="form-control" value="{{ image.id }}" readonly>
|
|
||||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="full-id">
|
|
||||||
<i class="fa fa-copy"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</dd>
|
||||||
<div id="clipboardCopied" style="display: none">
|
<dt>Created</dt>
|
||||||
Copied to clipboard
|
<dd am-time-ago="parseDate(image.value.created)"></dd>
|
||||||
</div>
|
<dt>Compressed Image Size</dt>
|
||||||
</div>
|
<dd><span class="context-tooltip"
|
||||||
</dd>
|
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||||
<dt>Created</dt>
|
bs-tooltip="tooltip.title" data-container="body">{{ image.value.size | bytes }}</span>
|
||||||
<dd am-time-ago="parseDate(image.created)"></dd>
|
</dd>
|
||||||
</dl>
|
|
||||||
|
|
||||||
<!-- Changes tabs -->
|
<dt ng-show="image.value.command && image.value.command.length">Command</dt>
|
||||||
<div ng-show="combinedChanges.length > 0">
|
<dd ng-show="image.value.command && image.value.command.length">
|
||||||
<b>File Changes:</b>
|
<pre class="formatted-command">{{ getFormattedCommand(image.value) }}</pre>
|
||||||
<br>
|
</dd>
|
||||||
<br>
|
</dl>
|
||||||
<ul class="nav nav-tabs">
|
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
|
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Changes tab content -->
|
<!-- Changes tabs -->
|
||||||
<div class="tab-content" ng-show="combinedChanges.length > 0">
|
<div ng-show="combinedChanges.length > 0">
|
||||||
<!-- Filterable view -->
|
<b>File Changes:</b>
|
||||||
<div class="tab-pane active" id="filterable">
|
<br>
|
||||||
<div class="changes-container full-changes-container">
|
<br>
|
||||||
<div class="change-side-controls">
|
<ul class="nav nav-tabs">
|
||||||
<div class="result-count">
|
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#filterable">Filterable View</a></li>
|
||||||
Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#tree" ng-click="initializeTree()">Tree View</a></li>
|
||||||
</div>
|
</ul>
|
||||||
<div class="filter-input">
|
</div>
|
||||||
<input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
|
|
||||||
</div>
|
<!-- Changes tab content -->
|
||||||
</div>
|
<div class="tab-content" ng-show="combinedChanges.length > 0">
|
||||||
<div style="height: 28px;"></div>
|
<!-- Filterable view -->
|
||||||
<div class="changes-list well well-sm">
|
<div class="tab-pane active" id="filterable">
|
||||||
<div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
|
<div class="changes-container full-changes-container">
|
||||||
No matching changes
|
<div class="change-side-controls">
|
||||||
</div>
|
<div class="result-count">
|
||||||
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
Showing {{(combinedChanges | filter:search | limitTo:50).length}} of {{(combinedChanges | filter:search).length}} results
|
||||||
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
</div>
|
||||||
<span title="{{change.file}}">
|
<div class="filter-input">
|
||||||
<span style="color: #888;">
|
<input id="change-filter" class="form-control" placeholder="Filter Changes" type="text" ng-model="search.$">
|
||||||
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
|
<div style="height: 28px;"></div>
|
||||||
|
<div class="changes-list well well-sm">
|
||||||
|
<div ng-show="(combinedChanges | filter:search | limitTo:1).length == 0">
|
||||||
|
No matching changes
|
||||||
|
</div>
|
||||||
|
<div class="change" ng-repeat="change in combinedChanges | filter:search | limitTo:50">
|
||||||
|
<i ng-class="{'added': 'fa fa-plus-square', 'removed': 'fa fa-minus-square', 'changed': 'fa fa-pencil-square'}[change.kind]"></i>
|
||||||
|
<span title="{{change.file}}">
|
||||||
|
<span style="color: #888;">
|
||||||
|
<span ng-repeat="folder in getFolders(change.file)"><a href="javascript:void(0)" ng-click="setFolderFilter(getFolder(change.file), $index)">{{folder}}</a>/</span></span><span>{{getFilename(change.file)}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Tree view -->
|
<!-- Tree view -->
|
||||||
<div class="tab-pane" id="tree">
|
<div class="tab-pane" id="tree">
|
||||||
<div id="changes-tree-container" class="changes-container" onresize="tree && tree.notifyResized()"></div>
|
<div id="changes-tree-container" class="changes-container" onresize="tree && tree.notifyResized()"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,29 +5,32 @@
|
||||||
<div ng-show="user.anonymous">
|
<div ng-show="user.anonymous">
|
||||||
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
|
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
|
||||||
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
||||||
<div class="sellcall"><a href="/plans/">Private repository plans starting at $7/mo</a></div>
|
<div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="!user.anonymous">
|
<div ng-show="!user.anonymous">
|
||||||
<div ng-show="loadingmyrepos">
|
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
|
||||||
</div>
|
<div class="resource-view" resource="my_repositories">
|
||||||
<span class="namespace-selector" user="user" namespace="namespace" ng-show="!loadingmyrepos && user.organizations"></span>
|
<!-- Repos -->
|
||||||
<div ng-show="!loadingmyrepos && myrepos.length > 0">
|
<div ng-show="my_repositories.value.length > 0">
|
||||||
<h2>Top Repositories</h2>
|
<h2>Top Repositories</h2>
|
||||||
<div class="repo-listing" ng-repeat="repository in myrepos">
|
<div class="repo-listing" ng-repeat="repository in my_repositories.value">
|
||||||
<span class="repo-circle no-background" repo="repository"></span>
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
|
<div class="markdown-view description" content="repository.description" first-line-only="true"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div ng-show="!loadingmyrepos && myrepos.length == 0">
|
<!-- No Repos -->
|
||||||
<div class="sub-message" style="margin-top: 20px">
|
<div ng-show="my_repositories.value.length == 0">
|
||||||
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
|
<div class="sub-message" style="margin-top: 20px">
|
||||||
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
|
<span ng-show="namespace != user.username">You don't have access to any repositories in this organization yet.</span>
|
||||||
<div class="options">
|
<span ng-show="namespace == user.username">You don't have any repositories yet!</span>
|
||||||
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
<div class="options">
|
||||||
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
|
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
|
<a class="btn btn-success" href="/new/" ng-show="canCreateRepo(namespace)">Create a new repository</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -41,8 +44,8 @@
|
||||||
<div ng-show="!user.anonymous" class="user-welcome">
|
<div ng-show="!user.anonymous" class="user-welcome">
|
||||||
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
<img class="gravatar" src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=128&d=identicon" />
|
||||||
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
<div class="sub-message">Welcome <b>{{ user.username }}</b>!</div>
|
||||||
<a ng-show="myrepos" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
<a ng-show="my_repositories.value" class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
<a ng-show="myrepos" class="btn btn-success" href="/new/">Create a new repository</a>
|
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- col -->
|
</div> <!-- col -->
|
||||||
</div> <!-- row -->
|
</div> <!-- row -->
|
||||||
|
@ -82,7 +85,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tour-section row">
|
<div class="tour-section row">
|
||||||
<div class="col-md-7"><img src="/static/img/user-home.png" title="User Home - Quay" data-screenshot-url="https://quay.io/" class="img-responsive"></div>
|
<div class="col-md-7"><img src="/static/img/user-home.png" title="User Home - Quay.io" data-screenshot-url="https://quay.io/" class="img-responsive"></div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="tour-section-title">Customized for you</div>
|
<div class="tour-section-title">Customized for you</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
|
@ -93,7 +96,7 @@
|
||||||
</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-view.png" title="Repository View - Quay" data-screenshot-url="https://quay.io/repository/devtable/complex" class="img-responsive"></div>
|
<div class="col-md-7 col-md-push-5"><img src="/static/img/repo-view.png" title="Repository View - Quay.io" data-screenshot-url="https://quay.io/repository/devtable/complex" class="img-responsive"></div>
|
||||||
<div class="col-md-5 col-md-pull-7">
|
<div class="col-md-5 col-md-pull-7">
|
||||||
<div class="tour-section-title">Useful views of respositories</div>
|
<div class="tour-section-title">Useful views of respositories</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
|
@ -103,7 +106,7 @@
|
||||||
</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" data-screenshot-url="https://quay.io/repository/devtable/image/..." class="img-responsive"></div>
|
<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="col-md-5">
|
||||||
<div class="tour-section-title">Docker diff in the cloud</div>
|
<div class="tour-section-title">Docker diff in the cloud</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
|
@ -113,7 +116,7 @@
|
||||||
</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" 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">
|
||||||
<div class="tour-section-title">Share at your control</div>
|
<div class="tour-section-title">Share at your control</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
<div class="loading" ng-show="loading || creating">
|
<div class="loading" ng-show="creating">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container create-org" ng-show="!loading && !creating">
|
<div class="container create-org" ng-show="!creating">
|
||||||
|
|
||||||
<div class="row header-row">
|
<div class="row header-row">
|
||||||
<div class="col-md-8 col-md-offset-1">
|
<div class="col-md-8 col-md-offset-1">
|
||||||
|
@ -72,7 +72,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan">
|
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
|
||||||
|
analytics-on analytics-event="create_organization">
|
||||||
Create Organization
|
Create Organization
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
<div class="container" ng-show="user.anonymous">
|
<div class="container" ng-show="user.anonymous">
|
||||||
<h3>Please <a href="/signin/">sign in</a></h3>
|
<div class="col-sm-6 col-sm-offset-3">
|
||||||
|
<div class="user-setup" redirect-url="'/new/'"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" ng-show="!user.anonymous && building">
|
<div class="container" ng-show="!user.anonymous && building">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" ng-show="!user.anonymous && creating">
|
<div class="container" ng-show="!user.anonymous && creating">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container" ng-show="!user.anonymous && uploading">
|
<div class="container" ng-show="!user.anonymous && uploading">
|
||||||
|
@ -72,12 +74,18 @@
|
||||||
<!-- Payment -->
|
<!-- Payment -->
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
In order to make this repository private, you’ll need to upgrade your plan from <b>{{ subscribedPlan.title }}</b> to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
In order to make this repository private, you’ll need to upgrade your plan to
|
||||||
|
<b style="border-bottom: 1px dotted black;" bs-tooltip="'<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories'">
|
||||||
|
{{ planRequired.title }}
|
||||||
|
</b>.
|
||||||
|
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
|
<div class="quay-spinner" ng-show="planChanging"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="quay-spinner" ng-show="repo.is_public == '0' && checkingPlan"></div>
|
||||||
|
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace">
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
This organization has reached its private repository limit. Please contact your administrator.
|
This organization has reached its private repository limit. Please contact your administrator.
|
||||||
|
@ -112,7 +120,10 @@
|
||||||
<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" ng-disabled="newRepoForm.$invalid || (repo.is_public == '0' && planRequired)">Create Repository</button>
|
<button class="btn btn-large btn-success" type="submit"
|
||||||
|
ng-disabled="newRepoForm.$invalid || (repo.is_public == '0' && (planRequired || checkingPlan))">
|
||||||
|
Create Repository
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,5 @@
|
||||||
<div class="loading" ng-show="loading">
|
<div class="resource-view" resource="orgResource" error-message="'No organization found'"></div>
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="org-admin container" ng-show="organization">
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="loading" ng-show="!loading && !organization">
|
|
||||||
No matching organization found
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="org-admin container" ng-show="!loading && organization">
|
|
||||||
<div class="organization-header" organization="organization" clickable="true"></div>
|
<div class="organization-header" organization="organization" clickable="true"></div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -47,66 +40,12 @@
|
||||||
|
|
||||||
<!-- Billing History tab -->
|
<!-- Billing History tab -->
|
||||||
<div id="billing" class="tab-pane">
|
<div id="billing" class="tab-pane">
|
||||||
<div ng-show="invoiceLoading">
|
<div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-show="!invoiceLoading && !invoices">
|
|
||||||
No invoices have been created
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-show="!invoiceLoading && invoices">
|
|
||||||
<table class="table">
|
|
||||||
<thead>
|
|
||||||
<th>Billing Date/Time</th>
|
|
||||||
<th>Amount Due</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th></th>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody class="invoice" ng-repeat="invoice in invoices">
|
|
||||||
<tr class="invoice-title">
|
|
||||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
|
||||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
|
||||||
<td>
|
|
||||||
<span class="invoice-status">
|
|
||||||
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
|
|
||||||
<span class="danger" ng-show="!invoice.paid && invoice.attempted && invoice.closed">Payment failed</span>
|
|
||||||
<span class="danger" ng-show="!invoice.paid && invoice.attempted && !invoice.closed">Payment failed - Will retry soon</span>
|
|
||||||
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
|
||||||
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
|
|
||||||
<td colspan="3">
|
|
||||||
<dl class="dl-normal">
|
|
||||||
<dt>Billing Period</dt>
|
|
||||||
<dd>
|
|
||||||
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
|
|
||||||
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
|
|
||||||
</dd>
|
|
||||||
<dt>Plan</dt>
|
|
||||||
<dd>
|
|
||||||
<span>{{ invoice.plan ? plan_map[invoice.plan].title : '(N/A)' }}</span>
|
|
||||||
</dd>
|
|
||||||
</dl>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members tab -->
|
<!-- Members tab -->
|
||||||
<div id="members" class="tab-pane">
|
<div id="members" class="tab-pane">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i>
|
<div class="quay-spinner" ng-show="membersLoading"></div>
|
||||||
|
|
||||||
<div ng-show="!membersLoading">
|
<div ng-show="!membersLoading">
|
||||||
<div class="side-controls">
|
<div class="side-controls">
|
||||||
<div class="result-count">
|
<div class="result-count">
|
||||||
|
|
|
@ -1,16 +1,6 @@
|
||||||
<div class="org-member-logs container" ng-show="loading">
|
<div class="resource-view" resource="memberResource" error-message="'Member not found'">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="org-member-logs container">
|
||||||
</div>
|
<div class="organization-header" organization="organization" clickable="true"></div>
|
||||||
|
<div class="logs-view" organization="organization" performer="memberInfo" visible="organization && memberInfo && ready"></div>
|
||||||
<div class="container" ng-show="!loading && !organization">
|
</div>
|
||||||
Organization not found
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" ng-show="!loading && !memberInfo">
|
|
||||||
Member not found
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="org-member-logs container" ng-show="!loading && organization && memberInfo">
|
|
||||||
<div class="organization-header" organization="organization" clickable="true"></div>
|
|
||||||
<div class="logs-view" organization="organization" performer="memberInfo" visible="organization && memberInfo && ready"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,51 +1,45 @@
|
||||||
<div class="loading" ng-show="loading">
|
<div class="resource-view" resource="orgResource" error-message="'No matching organization found'">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="org-view container">
|
||||||
</div>
|
<div class="organization-header" organization="organization">
|
||||||
|
<div class="header-buttons" ng-show="organization.is_admin">
|
||||||
|
|
||||||
<div class="loading" ng-show="!loading && !organization">
|
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
|
||||||
No matching organization found
|
submitted="createTeam(value)">
|
||||||
</div>
|
<i class="fa fa-group"></i> Create Team
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="org-view container" ng-show="!loading && organization">
|
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
|
||||||
<div class="organization-header" organization="organization">
|
</div>
|
||||||
<div class="header-buttons" ng-show="organization.is_admin">
|
|
||||||
|
|
||||||
<span class="popup-input-button" pattern="TEAM_PATTERN" placeholder="'Team Name'"
|
|
||||||
submitted="createTeam(value)">
|
|
||||||
<i class="fa fa-group"></i> Create Team
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row hidden-xs">
|
<div class="row hidden-xs">
|
||||||
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
|
<div class="col-md-4 col-md-offset-8 col-sm-5 col-sm-offset-7 header-col" ng-show="organization.is_admin">
|
||||||
Team Permissions
|
Team Permissions
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
|
<i class="info-icon fa fa-info-circle" data-placement="bottom" data-original-title="" title=""
|
||||||
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
|
data-content="Global permissions for the team and its members<br><br><dl><dt>Member</dt><dd>Permissions are assigned on a per repository basis</dd><dt>Creator</dt><dd>A team can create its own repositories</dd><dt>Admin</dt><dd>A team has full control of the organization</dd></dl>"></i>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
|
<div class="team-listing" ng-repeat="(name, team) in organization.teams">
|
||||||
<div id="team-{{name}}" class="row">
|
<div id="team-{{name}}" class="row">
|
||||||
<div class="col-sm-7 col-md-8">
|
<div class="col-sm-7 col-md-8">
|
||||||
<div class="team-title">
|
<div class="team-title">
|
||||||
<i class="fa fa-group"></i>
|
<i class="fa fa-group"></i>
|
||||||
<span ng-show="team.can_view">
|
<span ng-show="team.can_view">
|
||||||
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
|
<a href="/organization/{{ organization.name }}/teams/{{ team.name }}">{{ team.name }}</a>
|
||||||
</span>
|
</span>
|
||||||
<span ng-show="!team.can_view">
|
<span ng-show="!team.can_view">
|
||||||
{{ team.name }}
|
{{ team.name }}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="team-description markdown-view" content="team.description" first-line-only="true"></div>
|
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
|
||||||
</div>
|
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
|
||||||
|
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
|
||||||
<div class="col-sm-5 col-md-4 control-col" ng-show="organization.is_admin">
|
</div>
|
||||||
<span class="role-group" current-role="team.role" role-changed="setRole(role, team.name)" roles="teamRoles"></span>
|
|
||||||
<button class="btn btn-sm btn-danger" ng-click="askDeleteTeam(team.name)">Delete</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<div class="container org-list conntent-container">
|
<div class="container org-list conntent-container">
|
||||||
<div class="loading" ng-show="loading">
|
<div class="loading" ng-show="!user">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-bar-right">
|
<div class="button-bar-right">
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="tour-section row">
|
<div class="tour-section row">
|
||||||
<div class="col-md-7"><img src="/static/img/org-repo-list.png" title="Repositories - Quay" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
|
<div class="col-md-7"><img src="/static/img/org-repo-list.png" title="Repositories - Quay.io" data-screenshot-url="https://quay.io/repository/" class="img-responsive"></div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
<div class="tour-section-title">A central collection of repositories</div>
|
<div class="tour-section-title">A central collection of repositories</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
|
@ -57,7 +57,7 @@
|
||||||
</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/org-admin.png" title="buynlarge Admin - Quay" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
|
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-admin.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge/admin" class="img-responsive"></div>
|
||||||
<div class="col-md-5 col-md-pull-7">
|
<div class="col-md-5 col-md-pull-7">
|
||||||
<div class="tour-section-title">Organization settings at a glance</div>
|
<div class="tour-section-title">Organization settings at a glance</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
|
@ -73,8 +73,29 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tour-section row">
|
<div class="tour-section row">
|
||||||
<div class="col-md-7"><img src="/static/img/org-teams.png" title="buynlarge - Quay" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
<div class="col-md-7"><img src="/static/img/org-logs.png" title="buynlarge Admin - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||||
<div class="col-md-5">
|
<div class="col-md-5">
|
||||||
|
<div class="tour-section-title">Logging for comprehensive analysis</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
Every time a user in your organization performs an action it is logged
|
||||||
|
and categorized in a way that allows for a complete understanding of
|
||||||
|
how your repositories have been accessed and modified. Each log entry
|
||||||
|
includes the action performed, the authorization which allowed the action
|
||||||
|
to occur, and additional relevant data such as the name of the item
|
||||||
|
which was modified or accessed.
|
||||||
|
</div>
|
||||||
|
<div class="tour-section-description">
|
||||||
|
For those times when you need full control when generating reports from
|
||||||
|
your logs, we also allow you to export your logs in JSON format. These
|
||||||
|
can be ingested by custom tooling solutions allowing you to visualize
|
||||||
|
reports in whatever format you require.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tour-section row">
|
||||||
|
<div class="col-md-7 col-md-push-5"><img src="/static/img/org-teams.png" title="buynlarge - Quay.io" data-screenshot-url="https://quay.io/organization/buynlarge" class="img-responsive"></div>
|
||||||
|
<div class="col-md-5 col-md-pull-7">
|
||||||
<div class="tour-section-title">Teams simplify access controls</div>
|
<div class="tour-section-title">Teams simplify access controls</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
Teams allow your organization to delegate access to your namespace and
|
Teams allow your organization to delegate access to your namespace and
|
||||||
|
@ -94,8 +115,8 @@
|
||||||
</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/org-repo-admin.png" title="buynlarge/orgrepo - Quay" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
|
<div class="col-md-7"><img src="/static/img/org-repo-admin.png" title="buynlarge/orgrepo - Quay.io" data-screenshot-url="https://quay.io/repository/buynlarge/orgrepo" class="img-responsive"></div>
|
||||||
<div class="col-md-5 col-md-pull-7">
|
<div class="col-md-5">
|
||||||
<div class="tour-section-title">Fine-grained control of sharing</div>
|
<div class="tour-section-title">Fine-grained control of sharing</div>
|
||||||
<div class="tour-section-description">
|
<div class="tour-section-description">
|
||||||
Repositories that you create within your organization can be assigned
|
Repositories that you create within your organization can be assigned
|
||||||
|
|
|
@ -1,44 +1,107 @@
|
||||||
<div class="container plans content-container">
|
<div class="container plans content-container">
|
||||||
<div class="callout">
|
|
||||||
Plans & Pricing
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="all-plans">
|
|
||||||
All plans include <span class="feature">unlimited public repositories</span> and <span class="feature">unlimited sharing</span>. All paid plans have a <span class="feature">14-day free trial</span>.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row plans-list">
|
<div class="row plans-list">
|
||||||
<div class="col-xs-0 col-lg-1"></div>
|
<div class="col-sm-2">
|
||||||
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.user">
|
<div class="features-bar hidden-xs">
|
||||||
<div class="plan" ng-class="plan.stripeId">
|
<div class="visible-lg" style="height: 50px"></div>
|
||||||
<div class="plan-title">{{ plan.title }}</div>
|
<div class="visible-md visible-sm" style="height: 70px"></div>
|
||||||
<div class="plan-price">${{ plan.price/100 }}</div>
|
|
||||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
<div class="feature">
|
||||||
<div class="description">{{ plan.audience }}</div>
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
<div class="smaller">SSL secured connections</div>
|
title="All plans have unlimited public repositories">
|
||||||
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
|
<span class="hidden-sm-inline">Public Repositories</span>
|
||||||
</div>
|
<span class="visible-sm-inline">Public Repos</span>
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-hdd visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="SSL encryption is enabled end-to-end for all operations">
|
||||||
|
SSL Encryption
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-lock visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
|
||||||
|
Robot accounts
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-wrench visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="Repository images can be built directly from Dockerfiles">
|
||||||
|
Dockerfile Build
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-upload visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||||
|
Teams
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-group visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
|
||||||
|
Logging
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-bar-chart-o visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="Administrators can view and download the full invoice history for their organization">
|
||||||
|
Invoice History
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-calendar visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="feature">
|
||||||
|
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||||
|
title="All plans have a 14-day free trial">
|
||||||
|
<span class="hidden-sm-inline">14-Day Free Trial</span>
|
||||||
|
<span class="visible-sm-inline">14-Day Trial</span>
|
||||||
|
</span>
|
||||||
|
<i class="fa fa-clock-o visible-lg"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-sm-2 plan-container" ng-repeat="plan in plans" ng-show="plan.price > 0 && !plan.deprecated">
|
||||||
|
<div class="plan" ng-class="plan.stripeId + ' ' + (plan.bus_features ? 'business-plan' : '')">
|
||||||
|
<div class="plan-box">
|
||||||
|
<div class="plan-title">{{ plan.title }}</div>
|
||||||
|
<div class="plan-price">${{ plan.price/100 }}</div>
|
||||||
|
|
||||||
<div class="callout">
|
<div class="description">{{ plan.audience }}</div>
|
||||||
Business Plan Pricing
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="all-plans">
|
<div class="features hidden-xs">
|
||||||
All business plans include all of the personal plan features, plus: <span class="business-feature">organizations</span> and <span class="business-feature">teams</span> with <span class="business-feature">delegated access</span> to the organization. All business plans have a <span class="business-feature">14-day free trial</span>.
|
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||||
</div>
|
<div class="feature present"></div>
|
||||||
|
<div class="feature present"></div>
|
||||||
|
<div class="feature present"></div>
|
||||||
|
<div class="feature present"></div>
|
||||||
|
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||||
|
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||||
|
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||||
|
<div class="feature present"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="features visible-xs">
|
||||||
|
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||||
|
<div class="feature present">Unlimited Public Repositories</div>
|
||||||
|
<div class="feature present">SSL Encryption</div>
|
||||||
|
<div class="feature present">Robot accounts</div>
|
||||||
|
<div class="feature present">Dockerfile Build</div>
|
||||||
|
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
|
||||||
|
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
|
||||||
|
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
|
||||||
|
<div class="feature present">14-Day Free Trial</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="btn btn-block" ng-class="plan.bus_features ? 'btn-success' : 'btn-primary'"
|
||||||
|
ng-click="buyNow(plan.stripeId)">Start <span class="hidden-sm-inline">Free</span> Trial</button>
|
||||||
|
|
||||||
<div class="row plans-list">
|
|
||||||
<div class="col-xs-0 col-lg-1"></div>
|
|
||||||
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.business">
|
|
||||||
<div class="plan business-plan" ng-class="plan.stripeId">
|
|
||||||
<div class="plan-title">{{ plan.title }}</div>
|
|
||||||
<div class="plan-price">${{ plan.price/100 }}</div>
|
|
||||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
|
||||||
<div class="description">{{ plan.audience }}</div>
|
|
||||||
<div class="smaller">SSL secured connections</div>
|
|
||||||
<button class="btn btn-success btn-block" ng-click="createOrg(plan.stripeId)">Sign Up Now</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -82,7 +145,7 @@
|
||||||
<div class="user-setup" signed-in="signedIn()" redirect-url="'/plans/'"></div>
|
<div class="user-setup" signed-in="signedIn()" redirect-url="'/plans/'"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal" ng-click="cancelNotedPlan()">Close</button>
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
</div>
|
</div>
|
||||||
</div><!-- /.modal-content -->
|
</div><!-- /.modal-content -->
|
||||||
</div><!-- /.modal-dialog -->
|
</div><!-- /.modal-dialog -->
|
||||||
|
|
|
@ -1,13 +1,5 @@
|
||||||
|
<div class="resource-view" resource="repository" error-message="'No repository found'"></div>
|
||||||
<div class="loading" ng-show="loading">
|
<div class="container repo repo-admin" ng-show="repo">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" ng-show="!loading && (!repo || !permissions)">
|
|
||||||
No repository found
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container repo repo-admin" ng-show="!loading && repo && permissions">
|
|
||||||
<div class="header row">
|
<div class="header row">
|
||||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name }}" class="back"><i class="fa fa-chevron-left"></i></a>
|
||||||
<h3>
|
<h3>
|
||||||
|
@ -35,7 +27,7 @@
|
||||||
<div id="logs" class="tab-pane">
|
<div id="logs" class="tab-pane">
|
||||||
<div class="logs-view" repository="repo" visible="logsShown"></div>
|
<div class="logs-view" repository="repo" visible="logsShown"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Permissions tab -->
|
<!-- Permissions tab -->
|
||||||
<div id="permissions" class="tab-pane active">
|
<div id="permissions" class="tab-pane active">
|
||||||
<!-- User Access Permissions -->
|
<!-- User Access Permissions -->
|
||||||
|
@ -137,7 +129,7 @@
|
||||||
|
|
||||||
<tr>
|
<tr>
|
||||||
<td class="admin-search">
|
<td class="admin-search">
|
||||||
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName"required>
|
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName" required>
|
||||||
</td>
|
</td>
|
||||||
<td class="admin-search">
|
<td class="admin-search">
|
||||||
<button type="submit" ng-disabled="createTokenForm.$invalid" class="btn btn-sm btn-default">Create</button>
|
<button type="submit" ng-disabled="createTokenForm.$invalid" class="btn btn-sm btn-default">Create</button>
|
||||||
|
@ -156,39 +148,44 @@
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="URLs which will be invoked with an HTTP POST and JSON payload when a successful push to the repository occurs."></i>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="URLs which will be invoked with an HTTP POST and JSON payload when a successful push to the repository occurs."></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body" ng-show="webhooksLoading">
|
<div class="panel-body">
|
||||||
Loading webhooks: <i class="fa fa-spinner fa-spin fa-2x" style="vertical-align: middle; margin-left: 4px"></i>
|
<div class="resource-view" resource="webhooksResource" error-message="'Could not load webhooks'">
|
||||||
</div>
|
<table class="permissions">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td style="width: 500px;">Webhook URL</td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<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>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="panel-body" ng-show="!webhooksLoading">
|
<form name="createWebhookForm" ng-submit="createWebhook()">
|
||||||
<table class="permissions" ng-form="newWebhookForm">
|
<table class="permissions">
|
||||||
<thead>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td style="width: 500px;">Webhook URL</td>
|
<td style="width: 500px;">
|
||||||
<td></td>
|
<input type="url" class="form-control" placeholder="New webhook url..." ng-model="newWebhook.url" required>
|
||||||
</tr>
|
</td>
|
||||||
</thead>
|
<td>
|
||||||
|
<button class="btn btn-primary" type="submit" ng-disabled="createWebhookForm.$invalid">Create</button>
|
||||||
<tbody>
|
</td>
|
||||||
<tr ng-repeat="webhook in webhooks">
|
</tr>
|
||||||
<td>{{ webhook.parameters.url }}</td>
|
</tbody>
|
||||||
<td>
|
</table>
|
||||||
<span class="delete-ui" tabindex="0">
|
</form>
|
||||||
<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>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input type="url" class="form-control" placeholder="New webhook url..." ng-model="newWebhook.url" required>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<button class="btn btn-primary" type="submit" ng-click="createWebhook()">Create</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<div class="right-info">
|
<div class="right-info">
|
||||||
Quay will <b>POST</b> to these webhooks whenever a push occurs. See the <a href="/guide">User Guide</a> for more information.
|
Quay will <b>POST</b> to these webhooks whenever a push occurs. See the <a href="/guide">User Guide</a> for more information.
|
||||||
|
@ -240,132 +237,136 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
</div>
|
||||||
shown="!!shownToken" counter="shownTokenCounter">
|
|
||||||
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="cannotchangeModal">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">Cannot change</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
The selected action could not be performed because you do not have that authority.
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="makepublicModal">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">Make Repository Public</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
Warning: This will allow <b>anyone</b> to pull from this repository
|
|
||||||
</div>
|
|
||||||
Are you sure you want to make this repository public?
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-danger" ng-click="changeAccess('public')">Make Public</button>
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="makeprivateModal">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">Make Repository Private</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
Warning: Only users on the permissions list will be able to access this repository.
|
|
||||||
</div>
|
|
||||||
Are you sure you want to make this repository private?
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-success" ng-click="changeAccess('private')">Make Private</button>
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="channgechangepermModal">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">Cannot change permissions</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
|
|
||||||
<span ng-show="changePermError">{{ changePermError }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
|
||||||
<div class="modal fade" id="confirmdeleteModal">
|
|
||||||
<div class="modal-dialog">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">Delete Repository?</h4>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
Are you <b>absolutely, positively</b> sure you would like to delete this repository? This <b>cannot be undone</b>.
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-danger" ng-click="deleteRepo()">Delete Repository</button>
|
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
|
||||||
</div>
|
|
||||||
</div><!-- /.modal-content -->
|
|
||||||
</div><!-- /.modal-dialog -->
|
|
||||||
</div><!-- /.modal -->
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Auth dialog -->
|
||||||
<div class="modal fade" id="confirmaddoutsideModal">
|
<div class="docker-auth-dialog" username="'$token'" token="shownToken.code"
|
||||||
<div class="modal-dialog">
|
shown="!!shownToken" counter="shownTokenCounter">
|
||||||
<div class="modal-content">
|
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
|
||||||
<div class="modal-header">
|
</div>
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
|
||||||
<h4 class="modal-title">Add User?</h4>
|
<!-- Modal message dialog -->
|
||||||
</div>
|
<div class="modal fade" id="cannotchangeModal">
|
||||||
<div class="modal-body">
|
<div class="modal-dialog">
|
||||||
The selected user is outside of your organization. Are you sure you want to grant the user access to this repository?
|
<div class="modal-content">
|
||||||
</div>
|
<div class="modal-header">
|
||||||
<div class="modal-footer">
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
<button type="button" class="btn btn-primary" ng-click="grantRole()">Yes, I'm sure</button>
|
<h4 class="modal-title">Cannot change</h4>
|
||||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
</div>
|
||||||
</div>
|
<div class="modal-body">
|
||||||
</div><!-- /.modal-content -->
|
The selected action could not be performed because you do not have that authority.
|
||||||
</div><!-- /.modal-dialog -->
|
</div>
|
||||||
</div><!-- /.modal -->
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="makepublicModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Make Repository Public</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
Warning: This will allow <b>anyone</b> to pull from this repository
|
||||||
|
</div>
|
||||||
|
Are you sure you want to make this repository public?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" ng-click="changeAccess('public')">Make Public</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="makeprivateModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Make Repository Private</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
Warning: Only users on the permissions list will be able to access this repository.
|
||||||
|
</div>
|
||||||
|
Are you sure you want to make this repository private?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-success" ng-click="changeAccess('private')">Make Private</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="channgechangepermModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Cannot change permissions</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<span ng-show="!changePermError">You do not have permission to change the permissions on the repository.</span>
|
||||||
|
<span ng-show="changePermError">{{ changePermError }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="confirmdeleteModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Delete Repository?</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Are you <b>absolutely, positively</b> sure you would like to delete this repository? This <b>cannot be undone</b>.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-danger" ng-click="deleteRepo()">Delete Repository</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="confirmaddoutsideModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Add User?</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
The selected user is outside of your organization. Are you sure you want to grant the user access to this repository?
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="grantRole()">Yes, I'm sure</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
<div class="loading" ng-show="loading">
|
<div class="container">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container" ng-show="!loading">
|
|
||||||
<div class="repo-list" ng-show="!user.anonymous">
|
<div class="repo-list" ng-show="!user.anonymous">
|
||||||
<div ng-class="user.organizations.length ? 'section-header' : ''">
|
<div ng-class="user.organizations.length ? 'section-header' : ''">
|
||||||
<div class="button-bar-right">
|
<div class="button-bar-right">
|
||||||
|
@ -26,31 +22,47 @@
|
||||||
|
|
||||||
<h3 ng-show="namespace == user.username">Your Repositories</h3>
|
<h3 ng-show="namespace == user.username">Your Repositories</h3>
|
||||||
<h3 ng-show="namespace != user.username">Repositories</h3>
|
<h3 ng-show="namespace != user.username">Repositories</h3>
|
||||||
|
|
||||||
<div ng-show="user_repositories.length > 0">
|
<div class="resource-view" resource="user_repositories">
|
||||||
<div class="repo-listing" ng-repeat="repository in user_repositories">
|
<!-- User/Org has repositories -->
|
||||||
<span class="repo-circle no-background" repo="repository"></span>
|
<div ng-show="user_repositories.value.length > 0">
|
||||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<div class="repo-listing" ng-repeat="repository in user_repositories.value">
|
||||||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
|
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
|
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- User/Org has no repositories -->
|
||||||
|
<div ng-show="user_repositories.value.length == 0" style="padding:20px;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
|
||||||
|
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
|
||||||
|
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="user_repositories.length == 0" style="padding:20px;">
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
|
|
||||||
<h4 ng-show="namespace != user.username">This organization doesn't have any repositories, or you have not been provided access.</h4>
|
|
||||||
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="repo-list">
|
<div class="repo-list">
|
||||||
<h3>Top Public Repositories</h3>
|
<h3>Top Public Repositories</h3>
|
||||||
<div class="repo-listing" ng-repeat="repository in public_repositories">
|
<div class="resource-view" resource="public_repositories">
|
||||||
<span class="repo-circle no-background" repo="repository"></span>
|
<div class="repo-listing" ng-repeat="repository in public_repositories.value">
|
||||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
|
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||||
|
</div>
|
||||||
|
<div class="page-controls">
|
||||||
|
<button class="btn btn-default" title="Previous Page" bs-tooltip="title" ng-show="page > 1"
|
||||||
|
ng-click="movePublicPage(-1)">
|
||||||
|
<i class="fa fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-default" title="Next Page" bs-tooltip="title" ng-show="page < publicPageCount"
|
||||||
|
ng-click="movePublicPage(1)">
|
||||||
|
<i class="fa fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,47 +1,42 @@
|
||||||
<div class="loading" ng-show="loading">
|
<div class="resource-view" resource="orgResource" error-message="'No matching organization'">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="team-view container">
|
||||||
</div>
|
<div class="organization-header" organization="organization" team-name="teamname"></div>
|
||||||
|
|
||||||
|
<div class="resource-view" resource="membersResource" error-message="'No matching team found'">
|
||||||
|
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
||||||
|
content-changed="updateForDescription" field-title="'team description'"></div>
|
||||||
|
|
||||||
<div class="loading" ng-show="!loading && !organization">
|
<div class="panel panel-default">
|
||||||
No matching team found
|
<div class="panel-heading">Team Members
|
||||||
</div>
|
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
|
||||||
|
</div>
|
||||||
<div class="team-view container" ng-show="!loading && organization">
|
<div class="panel-body">
|
||||||
<div class="organization-header" organization="organization" team-name="teamname"></div>
|
<table class="permissions">
|
||||||
|
<tr ng-repeat="(name, member) in members">
|
||||||
<div class="description markdown-input" content="team.description" can-write="organization.is_admin"
|
<td class="user entity">
|
||||||
content-changed="updateForDescription" field-title="'team description'"></div>
|
<span class="entity-reference" name="member.username" isrobot="member.is_robot"></span>
|
||||||
|
</td>
|
||||||
<div class="panel panel-default">
|
<td>
|
||||||
<div class="panel-heading">Team Members
|
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers">
|
||||||
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Users that inherit all permissions delegated to this team"></i>
|
<span class="delete-ui-button" ng-click="removeMember(member.username)"><button class="btn btn-danger">Remove</button></span>
|
||||||
</div>
|
<i class="fa fa-times"></i>
|
||||||
<div class="panel-body">
|
</span>
|
||||||
<table class="permissions">
|
</td>
|
||||||
<tr ng-repeat="(name, member) in members">
|
</tr>
|
||||||
<td class="user entity">
|
|
||||||
<span class="entity-reference" name="member.username" isrobot="member.is_robot"></span>
|
<tr ng-show="canEditMembers">
|
||||||
</td>
|
<td colspan="2">
|
||||||
<td>
|
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
|
||||||
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers">
|
entity-selected="addNewMember" is-organization="true"></span>
|
||||||
<span class="delete-ui-button" ng-click="removeMember(member.username)"><button class="btn btn-danger">Remove</button></span>
|
</td>
|
||||||
<i class="fa fa-times"></i>
|
</tr>
|
||||||
</span>
|
</table>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
</div>
|
||||||
|
|
||||||
<tr ng-show="canEditMembers">
|
|
||||||
<td colspan="2">
|
|
||||||
<span class="entity-search" namespace="orgname" include-teams="false" input-title="'Add a user...'"
|
|
||||||
entity-selected="addNewMember" is-organization="true"></span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Modal message dialog -->
|
<!-- Modal message dialog -->
|
||||||
<div class="modal fade" id="cannotChangeTeamModal">
|
<div class="modal fade" id="cannotChangeTeamModal">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
<div class="loading" ng-show="loading">
|
<div class="loading" ng-show="!user">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loading" ng-show="!loading && !user">
|
<div class="loading" ng-show="user.anonymous">
|
||||||
No matching user found
|
No matching user found
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="user-admin container" ng-show="!loading && user">
|
<div class="user-admin container" ng-show="!user.anonymous">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="organization-header-element">
|
<div class="organization-header-element">
|
||||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
||||||
|
@ -27,16 +27,24 @@
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing">Billing Options</a></li>
|
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li>
|
||||||
|
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Set Password</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
|
||||||
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#github">GitHub Login</a></li>
|
||||||
|
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
|
<!-- Logs tab -->
|
||||||
|
<div id="logs" class="tab-pane">
|
||||||
|
<div class="logs-view" user="user" visible="logsShown"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Plans tab -->
|
<!-- Plans tab -->
|
||||||
<div id="plan" class="tab-pane active">
|
<div id="plan" class="tab-pane active">
|
||||||
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
|
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
|
||||||
|
@ -45,18 +53,48 @@
|
||||||
<!-- Change password tab -->
|
<!-- Change password tab -->
|
||||||
<div id="password" class="tab-pane">
|
<div id="password" class="tab-pane">
|
||||||
<div class="loading" ng-show="updatingUser">
|
<div class="loading" ng-show="updatingUser">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="quay-spinner 3x"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
|
<div class="panel">
|
||||||
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
<div class="panel-title">Change Password</div>
|
||||||
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required>
|
|
||||||
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword"
|
|
||||||
match="user.password" required>
|
|
||||||
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
|
|
||||||
analytics-on analytics-event="register">Change Password</button>
|
|
||||||
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
||||||
</form>
|
|
||||||
|
<div ng-show="!updatingUser" class="panel-body">
|
||||||
|
<form class="form-change-pw col-md-6" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
|
||||||
|
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
||||||
|
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required>
|
||||||
|
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword"
|
||||||
|
match="cuser.password" required>
|
||||||
|
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
|
||||||
|
analytics-on analytics-event="change_pass">Change Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="github" class="tab-pane">
|
||||||
|
<div class="loading" ng-show="!cuser">
|
||||||
|
<div class="quay-spinner 3x"></div>
|
||||||
|
</div>
|
||||||
|
<div class="row" ng-show="cuser">
|
||||||
|
<div class="panel">
|
||||||
|
<div class="panel-title">GitHub Login</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div ng-show="githubLogin" class="lead col-md-8">
|
||||||
|
<span class="fa-stack">
|
||||||
|
<i class="fa fa-circle fa-stack-2x check-green"></i>
|
||||||
|
<i class="fa fa-check fa-stack-1x fa-inverse"></i>
|
||||||
|
</span>
|
||||||
|
This account is connected with GitHub account: <b>{{githubLogin}}</b>
|
||||||
|
</div>
|
||||||
|
<div ng-show="!githubLogin" class="col-md-8">
|
||||||
|
<a href="https://github.com/login/oauth/authorize?client_id={{ githubClientId }}&scope=user:email{{ github_state_clause }}&redirect_uri={{ githubRedirectUri }}/attach" class="btn btn-primary"><i class="fa fa-github fa-lg"></i> Connect with GitHub</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -66,10 +104,15 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Billing options tab -->
|
<!-- Billing options tab -->
|
||||||
<div id="billing" class="tab-pane">
|
<div id="billingoptions" class="tab-pane">
|
||||||
<div class="billing-options" user="user"></div>
|
<div class="billing-options" user="user"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Billing History tab -->
|
||||||
|
<div id="billing" class="tab-pane">
|
||||||
|
<div class="billing-invoices" user="user" visible="invoicesShown"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Convert to organization tab -->
|
<!-- Convert to organization tab -->
|
||||||
<div id="migrate" class="tab-pane">
|
<div id="migrate" class="tab-pane">
|
||||||
<!-- Step 0 -->
|
<!-- Step 0 -->
|
||||||
|
@ -83,11 +126,11 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body" ng-show="user.organizations.length == 0">
|
<div class="panel-body" ng-show="user.organizations.length == 0">
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-warning">
|
||||||
Converting a user account into an organization <b>cannot be undone</b>.<br> Here be many fire-breathing dragons!
|
Note: Converting a user account into an organization <b>cannot be undone</b>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button class="btn btn-danger" ng-click="showConvertForm()">Start conversion process</button>
|
<button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -110,7 +153,7 @@
|
||||||
ng-model="org.adminUser" required autofocus>
|
ng-model="org.adminUser" required autofocus>
|
||||||
<input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password"
|
<input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password"
|
||||||
ng-model="org.adminPassword" required>
|
ng-model="org.adminPassword" required>
|
||||||
<span class="description">The username and password for an <b>existing account</b> that will become administrator of the organization</span>
|
<span class="description">The username and password for the account that will become administrator of the organization</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Plans Table -->
|
<!-- Plans Table -->
|
||||||
|
@ -120,7 +163,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan">
|
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"
|
||||||
|
analytics-on analytics-event="convert_to_organization">
|
||||||
Convert To Organization
|
Convert To Organization
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,193 +1,276 @@
|
||||||
<div class="container" ng-show="!loading && !repo">
|
<div id="tagContextMenu" class="dropdown clearfix" tabindex="-1">
|
||||||
No repository found
|
<ul class="dropdown-menu" role="menu" aria-labelledby="dropdownMenu">
|
||||||
|
<li><a tabindex="-1" href="javascript:void(0)" ng-click="askDeleteTag(currentMenuTag)">Delete Tag</a></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="loading" ng-show="loading">
|
<div class="resource-view" resource="repository" error-message="'No Repository Found'">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="container repo">
|
||||||
</div>
|
<!-- Repo Header -->
|
||||||
|
<div class="header">
|
||||||
|
<h3>
|
||||||
|
<span class="repo-circle" repo="repo"></span>
|
||||||
|
<span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
||||||
|
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
|
||||||
|
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
||||||
|
<i class="fa fa-cog fa-lg"></i>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Pull command -->
|
||||||
|
<div class="pull-command visible-md visible-lg" style="display: none;">
|
||||||
|
<span class="pull-command-title">Pull repository:</span>
|
||||||
|
<div class="pull-container">
|
||||||
|
<div class="input-group">
|
||||||
|
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
||||||
|
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
||||||
|
<i class="fa fa-copy"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container repo" ng-show="!loading && repo">
|
<div id="clipboardCopied" class="hovering" style="display: none">
|
||||||
<!-- Repo Header -->
|
Copied to clipboard
|
||||||
<div class="header">
|
|
||||||
<h3>
|
|
||||||
<span class="repo-circle" repo="repo"></span>
|
|
||||||
|
|
||||||
<span style="color: #aaa;"> {{repo.namespace}}</span> <span style="color: #ccc">/</span> {{repo.name}}
|
|
||||||
|
|
||||||
<span class="settings-cog" ng-show="repo.can_admin" title="Repository Settings" bs-tooltip="tooltip.title" data-placement="bottom">
|
|
||||||
<a href="{{ '/repository/' + repo.namespace + '/' + repo.name + '/admin' }}">
|
|
||||||
<i class="fa fa-cog fa-lg"></i>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Pull command -->
|
|
||||||
<div class="pull-command visible-md visible-lg" style="display: none;">
|
|
||||||
<span class="pull-command-title">Pull repository:</span>
|
|
||||||
<div class="pull-container">
|
|
||||||
<div class="input-group">
|
|
||||||
<input id="pull-text" type="text" class="form-control" value="{{ 'docker pull quay.io/' + repo.namespace + '/' + repo.name }}" readonly>
|
|
||||||
<span id="copyClipboard" class="input-group-addon" title="Copy to Clipboard" data-clipboard-target="pull-text">
|
|
||||||
<i class="fa fa-copy"></i>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="clipboardCopied" class="hovering" style="display: none">
|
<!-- Status boxes -->
|
||||||
Copied to clipboard
|
<div class="status-boxes">
|
||||||
|
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building"
|
||||||
|
bs-popover="'static/partials/build-status-item.html'" data-placement="bottom">
|
||||||
|
<span class="title">
|
||||||
|
<span class="quay-spinner"></span>
|
||||||
|
<b>Building Images</b>
|
||||||
|
</span>
|
||||||
|
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Status boxes -->
|
<!-- Description -->
|
||||||
<div class="status-boxes">
|
<div class="description markdown-input" content="repo.description" can-write="repo.can_write"
|
||||||
<div id="buildInfoBox" class="status-box" ng-show="repo.is_building"
|
content-changed="updateForDescription" field-title="'repository description'"></div>
|
||||||
bs-popover="'static/partials/build-status-item.html'" data-placement="bottom">
|
|
||||||
<span class="title">
|
|
||||||
<i class="fa fa-spinner fa-spin"></i>
|
|
||||||
<b>Building Images</b>
|
|
||||||
</span>
|
|
||||||
<span class="count" ng-class="buildsInfo ? 'visible' : ''"><span>{{ buildsInfo ? buildsInfo.length : '-' }}</span></span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Empty message -->
|
||||||
<div class="description markdown-input" content="repo.description" can-write="repo.can_write"
|
<div class="repo-content" ng-show="!currentTag.image && !currentImage && !repo.is_building">
|
||||||
content-changed="updateForDescription" field-title="'repository description'"></div>
|
<div class="empty-message">
|
||||||
|
This repository is empty
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Empty message -->
|
<div class="empty-description" ng-show="repo.can_write">
|
||||||
<div class="repo-content" ng-show="!currentTag.image && !repo.is_building">
|
To push images to this repository:<br><br>
|
||||||
<div class="empty-message">
|
<pre>sudo docker tag <i>0u123imageidgoeshere</i> quay.io/{{repo.namespace}}/{{repo.name}}
|
||||||
This repository is empty
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="empty-description" ng-show="repo.can_write">
|
|
||||||
To push images to this repository:<br><br>
|
|
||||||
<pre>sudo docker tag <i>0u123imageidgoeshere</i> quay.io/{{repo.namespace}}/{{repo.name}}
|
|
||||||
sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
sudo docker push quay.io/{{repo.namespace}}/{{repo.name}}</pre>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
|
<div class="repo-content" ng-show="!currentTag.image && repo.is_building">
|
||||||
<div class="empty-message">Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a></div>
|
<div class="empty-message">
|
||||||
</div>
|
Your build is currently processing, if this takes longer than an hour, please contact <a href="mailto:support@quay.io">Quay.io support</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Content view -->
|
<!-- Content view -->
|
||||||
<div class="repo-content" ng-show="currentTag.image">
|
<div class="repo-content" ng-show="currentTag.image || currentImage">
|
||||||
<!-- Image History -->
|
<!-- Image History -->
|
||||||
<div id="image-history" style="max-height: 10px;">
|
<div id="image-history" style="max-height: 10px;">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Tree View container -->
|
<!-- Tree View container -->
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
|
<div class="panel panel-default">
|
||||||
<div class="panel panel-default">
|
<!-- Image history tree -->
|
||||||
<div class="panel-heading">
|
<div class="resource-view" resource="imageHistory">
|
||||||
<!-- Tag dropdown -->
|
<div id="image-history-container" onresize="tree.notifyResized()"></div>
|
||||||
<div class="tag-dropdown dropdown" title="Tags" bs-tooltip="tooltip.title" data-placement="top">
|
</div>
|
||||||
<i class="fa fa-tag"><span class="tag-count">{{getTagCount(repo)}}</span></i>
|
</div>
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag.name}} <b class="caret"></b></a>
|
</div>
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li ng-repeat="tag in repo.tags">
|
<!-- Side Panel -->
|
||||||
<a href="javascript:void(0)" ng-click="setTag(tag.name)">{{tag.name}}</a>
|
<div class="col-md-4">
|
||||||
</li>
|
<div class="panel panel-default">
|
||||||
</ul>
|
<div class="panel-heading">
|
||||||
|
<!-- Dropdown -->
|
||||||
|
<div class="tag-dropdown dropdown" data-placement="top">
|
||||||
|
<i class="fa fa-tag" ng-show="currentTag"></i>
|
||||||
|
<i class="fa fa-archive" ng-show="!currentTag"></i>
|
||||||
|
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentTag ? currentTag.name : currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li ng-repeat="tag in repo.tags">
|
||||||
|
<a href="javascript:void(0)" ng-click="setTag(tag.name, true)">
|
||||||
|
<i class="fa fa-tag"></i>{{tag.name}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="divider"></li>
|
||||||
|
<li ng-repeat="image in imageHistory.value">
|
||||||
|
<a href="javascript:void(0)" ng-click="setImage(image.id, true)">
|
||||||
|
{{image.id.substr(0, 12)}}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<span class="right-tag-controls">
|
||||||
|
<i class="fa fa-tag" title="Tags" bs-tooltip="title">
|
||||||
|
<span class="tag-count">{{getTagCount(repo)}}</span>
|
||||||
|
</i>
|
||||||
|
<i class="fa fa-archive" title="Images" bs-tooltip="title">
|
||||||
|
<span class="tag-count">{{imageHistory.value.length}}</span>
|
||||||
|
</i>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="right-title">Tags</span>
|
|
||||||
|
|
||||||
</div>
|
<div class="panel-body">
|
||||||
|
<!-- Current Tag -->
|
||||||
|
<div id="current-tag" ng-show="currentTag">
|
||||||
|
<dl class="dl-normal">
|
||||||
|
<dt>Last Modified</dt>
|
||||||
|
<dd am-time-ago="parseDate(currentTag.image.created)"></dd>
|
||||||
|
<dt>Total Compressed Size</dt>
|
||||||
|
<dd><span class="context-tooltip"
|
||||||
|
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||||
|
bs-tooltip="tooltip.title" data-container="body">{{ getTotalSize(currentTag) | bytes }}</span>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<div class="tag-image-sizes">
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
|
||||||
<!-- Image history loading -->
|
<div class="control-bar" ng-show="repo.can_admin">
|
||||||
<div ng-hide="imageHistory" style="padding: 10px; text-align: center;">
|
<button class="btn btn-default" ng-click="askDeleteTag(currentTag.name)">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
Delete Tag
|
||||||
</div>
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Tree View itself -->
|
<!-- Current Image -->
|
||||||
<div id="image-history-container" onresize="tree.notifyResized()"></div>
|
<div id="current-image" ng-show="currentImage && !currentTag">
|
||||||
</div>
|
<div ng-show="currentImage.comment">
|
||||||
</div>
|
<blockquote style="margin-top: 10px;">
|
||||||
|
<span class="markdown-view" content="currentImage.comment"></span>
|
||||||
|
</blockquote>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Side Panel -->
|
<dl class="dl-normal">
|
||||||
<div class="col-md-4">
|
<dt>Created</dt>
|
||||||
<div class="panel panel-default">
|
<dd am-time-ago="parseDate(currentImage.created)"></dd>
|
||||||
<div class="panel-heading">
|
<dt>Image ID</dt>
|
||||||
<!-- Image dropdown -->
|
<dd><a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a></dd>
|
||||||
<div class="tag-dropdown dropdown" title="Images" bs-tooltip="tooltip.title" data-placement="top">
|
<dt>Compressed Image Size</dt>
|
||||||
<i class="fa fa-archive"><span class="tag-count">{{imageHistory.length}}</span></i>
|
<dd><span class="context-tooltip"
|
||||||
<a href="javascript:void(0)" class="dropdown-toggle" data-toggle="dropdown">{{currentImage.id.substr(0, 12)}} <b class="caret"></b></a>
|
title="The amount of data sent between Docker and Quay.io when pushing/pulling"
|
||||||
<ul class="dropdown-menu">
|
bs-tooltip="tooltip.title" data-container="body">{{ currentImage.size | bytes }}</span>
|
||||||
<li ng-repeat="image in imageHistory">
|
</dd>
|
||||||
<a href="javascript:void(0)" ng-click="setImage(image)">{{image.id.substr(0, 12)}}</a>
|
<dt ng-show="currentImage.command && currentImage.command.length">Command</dt>
|
||||||
</li>
|
<dd ng-show="currentImage.command && currentImage.command.length" class="codetooltipcontainer">
|
||||||
</ul>
|
<pre class="formatted-command trimmed"
|
||||||
|
bs-tooltip="getTooltipCommand(currentImage)"
|
||||||
|
data-placement="top">{{ getFormattedCommand(currentImage) }}</pre>
|
||||||
|
</dd>
|
||||||
|
</dl>
|
||||||
|
|
||||||
|
<!-- Image changes loading -->
|
||||||
|
<div class="resource-view" resource="currentImageChangeResource">
|
||||||
|
<div class="changes-container small-changes-container"
|
||||||
|
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
||||||
|
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
||||||
|
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added"
|
||||||
|
bs-tooltip="tooltip.title" data-placement="top">
|
||||||
|
<i class="fa fa-plus-square"></i>
|
||||||
|
<b>{{currentImageChanges.added.length}}</b>
|
||||||
|
</span>
|
||||||
|
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed"
|
||||||
|
bs-tooltip="tooltip.title" data-placement="top">
|
||||||
|
<i class="fa fa-minus-square"></i>
|
||||||
|
<b>{{currentImageChanges.removed.length}}</b>
|
||||||
|
</span>
|
||||||
|
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed"
|
||||||
|
bs-tooltip="tooltip.title" data-placement="top">
|
||||||
|
<i class="fa fa-pencil-square"></i>
|
||||||
|
<b>{{currentImageChanges.changed.length}}</b>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="collapseChanges" class="panel-collapse collapse in">
|
||||||
|
<div class="well well-sm">
|
||||||
|
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
|
||||||
|
<i class="fa fa-plus-square"></i>
|
||||||
|
<span title="{{file}}">{{file}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
|
||||||
|
<i class="fa fa-minus-square"></i>
|
||||||
|
<span title="{{file}}">{{file}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
|
||||||
|
<i class="fa fa-pencil-square"></i>
|
||||||
|
<span title="{{file}}">{{file}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
|
||||||
|
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">
|
||||||
|
And {{getMoreCount(currentImageChanges)}} more...
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="right-title">Image</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="panel-body">
|
|
||||||
<div id="current-image">
|
|
||||||
<div ng-show="currentImage.comment">
|
|
||||||
<blockquote style="margin-top: 10px;">
|
|
||||||
<span class="markdown-view" content="currentImage.comment"></span>
|
|
||||||
</blockquote>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dl class="dl-normal">
|
|
||||||
<dt>Created</dt>
|
|
||||||
<dd am-time-ago="parseDate(currentImage.created)"></dd>
|
|
||||||
<dt>Image ID</dt>
|
|
||||||
<dd><a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">{{ currentImage.id }}</a></dd>
|
|
||||||
</dl>
|
|
||||||
|
|
||||||
<!-- Image changes loading -->
|
|
||||||
<div ng-hide="currentImageChanges">
|
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="changes-container small-changes-container"
|
|
||||||
ng-show="currentImageChanges.changed.length || currentImageChanges.added.length || currentImageChanges.removed.length">
|
|
||||||
<div class="changes-count-container accordion-toggle" data-toggle="collapse" data-parent="#accordion" data-target="#collapseChanges">
|
|
||||||
<span class="change-count added" ng-show="currentImageChanges.added.length > 0" title="Files Added"
|
|
||||||
bs-tooltip="tooltip.title" data-placement="top">
|
|
||||||
<i class="fa fa-plus-square"></i>
|
|
||||||
<b>{{currentImageChanges.added.length}}</b>
|
|
||||||
</span>
|
|
||||||
<span class="change-count removed" ng-show="currentImageChanges.removed.length > 0" title="Files Removed"
|
|
||||||
bs-tooltip="tooltip.title" data-placement="top">
|
|
||||||
<i class="fa fa-minus-square"></i>
|
|
||||||
<b>{{currentImageChanges.removed.length}}</b>
|
|
||||||
</span>
|
|
||||||
<span class="change-count changed" ng-show="currentImageChanges.changed.length > 0" title="Files Changed"
|
|
||||||
bs-tooltip="tooltip.title" data-placement="top">
|
|
||||||
<i class="fa fa-pencil-square"></i>
|
|
||||||
<b>{{currentImageChanges.changed.length}}</b>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="collapseChanges" class="panel-collapse collapse in">
|
|
||||||
<div class="well well-sm">
|
|
||||||
<div class="change added" ng-repeat="file in currentImageChanges.added | limitTo:5">
|
|
||||||
<i class="fa fa-plus-square"></i>
|
|
||||||
<span title="{{file}}">{{file}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="change removed" ng-repeat="file in currentImageChanges.removed | limitTo:5">
|
|
||||||
<i class="fa fa-minus-square"></i>
|
|
||||||
<span title="{{file}}">{{file}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="change changed" ng-repeat="file in currentImageChanges.changed | limitTo:5">
|
|
||||||
<i class="fa fa-pencil-square"></i>
|
|
||||||
<span title="{{file}}">{{file}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="more-changes" ng-show="getMoreCount(currentImageChanges) > 0">
|
|
||||||
<a href="{{'/repository/' + repo.namespace + '/' + repo.name + '/image/' + currentImage.id}}">And {{getMoreCount(currentImageChanges)}} more...</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="confirmdeleteTagModal">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||||
|
<h4 class="modal-title">Delete tag
|
||||||
|
<span class="label tag" ng-class="tagToDelete == currentTag.name ? 'label-success' : 'label-default'">
|
||||||
|
{{ tagToDelete }}
|
||||||
|
</span>?
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Are you sure you want to delete tag
|
||||||
|
<span class="label tag" ng-class="tagToDelete == currentTag.name ? 'label-success' : 'label-default'">
|
||||||
|
{{ tagToDelete }}
|
||||||
|
</span>?
|
||||||
|
|
||||||
|
<div ng-show="tagSpecificImages(tagToDelete).length" style="margin-top: 20px">
|
||||||
|
The following images will also be deleted:
|
||||||
|
<div class="image-listings">
|
||||||
|
<div class="image-listing" ng-repeat="image in tagSpecificImages(tagToDelete) | limitTo:5"
|
||||||
|
ng-class="getImageListingClasses(image, tagToDelete)">
|
||||||
|
<!--<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)">
|
||||||
|
{{ image.id.substr(0, 12) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="more-changes" ng-show="tagSpecificImages(tagToDelete).length > 5">
|
||||||
|
And {{ tagSpecificImages(tagToDelete).length - 5 }} more...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" ng-click="deleteTag(tagToDelete)">Delete Tag</button>
|
||||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
|
|
|
@ -34,6 +34,10 @@ class Storage(object):
|
||||||
namespace,
|
namespace,
|
||||||
repository)
|
repository)
|
||||||
|
|
||||||
|
def image_path(self, namespace, repository, image_id):
|
||||||
|
return '{0}/{1}/{2}/{3}/'.format(self.images, namespace, repository,
|
||||||
|
image_id)
|
||||||
|
|
||||||
def image_json_path(self, namespace, repository, image_id):
|
def image_json_path(self, namespace, repository, image_id):
|
||||||
return '{0}/{1}/{2}/{3}/json'.format(self.images, namespace,
|
return '{0}/{1}/{2}/{3}/json'.format(self.images, namespace,
|
||||||
repository, image_id)
|
repository, image_id)
|
||||||
|
|
|
@ -11,6 +11,8 @@
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="/static/lib/loading-bar.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/4.0.0/css/font-awesome.css">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.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'>
|
<link href='//fonts.googleapis.com/css?family=Droid+Sans:400,700' rel='stylesheet' type='text/css'>
|
||||||
|
@ -40,13 +42,16 @@
|
||||||
|
|
||||||
<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.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-route.min.js"></script>
|
||||||
|
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.1/angular-sanitize.min.js"></script>
|
||||||
|
|
||||||
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
<script src="//cdn.jsdelivr.net/underscorejs/1.5.2/underscore-min.js"></script>
|
||||||
<script src="//cdn.jsdelivr.net/restangular/1.1.3/restangular.js"></script>
|
<script src="//cdn.jsdelivr.net/restangular/1.2.0/restangular.min.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/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/angular-moment.min.js"></script>
|
<script src="static/lib/angular-moment.min.js"></script>
|
||||||
<script src="static/lib/angular-cookies.min.js"></script>
|
<script src="static/lib/angular-cookies.min.js"></script>
|
||||||
|
@ -61,6 +66,10 @@
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
window.__endpoints = {{ route_data|safe }}.endpoints;
|
||||||
|
</script>
|
||||||
|
|
||||||
<script src="static/js/app.js"></script>
|
<script src="static/js/app.js"></script>
|
||||||
<script src="static/js/controllers.js"></script>
|
<script src="static/js/controllers.js"></script>
|
||||||
<script src="static/js/graphing.js"></script>
|
<script src="static/js/graphing.js"></script>
|
||||||
|
@ -72,6 +81,19 @@ var isProd = document.location.hostname === 'quay.io';
|
||||||
typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);
|
typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");for(g=0;g<i.length;g++)f(c,i[g]);
|
||||||
b._i.push([a,e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
|
b._i.push([a,e,d])};b.__SV=1.2}})(document,window.mixpanel||[]);
|
||||||
mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false, debug: !isProd });</script><!-- end Mixpanel -->
|
mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8cc7cc29c869f9", { track_pageview : false, debug: !isProd });</script><!-- end Mixpanel -->
|
||||||
|
|
||||||
|
<!-- start analytics --><script>
|
||||||
|
/*
|
||||||
|
var isProd = document.location.hostname === 'quay.io';
|
||||||
|
|
||||||
|
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||||
|
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||||
|
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||||
|
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
|
||||||
|
|
||||||
|
ga('create', isProd ? 'UA-34988886-5' : 'UA-34988886-4', 'quay.io');
|
||||||
|
*/
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div ng-class="!fixFooter ? 'wrapper' : ''">
|
<div ng-class="!fixFooter ? 'wrapper' : ''">
|
||||||
|
@ -85,27 +107,46 @@ mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-container" ng-class="fixFooter ? 'fixed' : ''">
|
<div class="footer-container" ng-class="fixFooter ? 'fixed' : ''">
|
||||||
<nav class="page-footer visible-lg visible-md">
|
<div class="page-footer-padder">
|
||||||
<div class="row">
|
<nav class="page-footer visible-lg visible-md">
|
||||||
<div class="col-md-8">
|
<div class="row">
|
||||||
<ul>
|
<div class="col-md-8">
|
||||||
<li><span class="copyright">©2013 DevTable, LLC</span></li>
|
<ul>
|
||||||
<li><a href="http://blog.devtable.com/">Blog</a></li>
|
<li><span class="copyright">©2013 DevTable, LLC</span></li>
|
||||||
<li><a href="/tos" target="_self">Terms of Service</a></li>
|
<li><a href="http://blog.devtable.com/">Blog</a></li>
|
||||||
<li><a href="/privacy" target="_self">Privacy Policy</a></li>
|
<li><a href="/tos" target="_self">Terms of Service</a></li>
|
||||||
<li><a href="/security/">Security</a></li>
|
<li><a href="/privacy" target="_self">Privacy Policy</a></li>
|
||||||
<li><b><a href="/contact/">Contact Us</a></b></li>
|
<li><a href="/security/">Security</a></li>
|
||||||
</ul>
|
<li><b><a href="/contact/">Contact Us</a></b></li>
|
||||||
</div>
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4 logo-container">
|
<div class="col-md-5 logo-container">
|
||||||
<a href="https://devtable.com"><img class="dt-logo" src="/static/img/dt-logo.png"></a>
|
<a href="https://devtable.com"><img class="dt-logo" src="/static/img/dt-logo.png"></a>
|
||||||
</div>
|
</div>
|
||||||
</div> <!-- row -->
|
</div> <!-- row -->
|
||||||
</nav>
|
</nav>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal message dialog -->
|
||||||
|
<div class="modal fade" id="couldnotloadModal" data-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h4 class="modal-title">Uh Oh...</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
Something went wrong when trying to load Quay.io! Please report this to <a href="mailto:support@quay.io">support@quay.io</a>.
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
</div>
|
||||||
|
</div><!-- /.modal-content -->
|
||||||
|
</div><!-- /.modal-dialog -->
|
||||||
|
</div><!-- /.modal -->
|
||||||
|
|
||||||
<!-- begin olark code -->
|
<!-- begin olark code -->
|
||||||
|
{% if request.host == 'quay.io' %}
|
||||||
<script data-cfasync="false" type='text/javascript'>/*<![CDATA[*/window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){
|
<script data-cfasync="false" type='text/javascript'>/*<![CDATA[*/window.olark||(function(c){var f=window,d=document,l=f.location.protocol=="https:"?"https:":"http:",z=c.name,r="load";var nt=function(){
|
||||||
f[z]=function(){
|
f[z]=function(){
|
||||||
(a.s=a.s||[]).push(arguments)};var a=f[z]._={
|
(a.s=a.s||[]).push(arguments)};var a=f[z]._={
|
||||||
|
@ -124,6 +165,7 @@ mixpanel.init(isProd ? "50ff2b2569faa3a51c8f5724922ffb7e" : "38014a0f27e7bdc3ff8
|
||||||
loader: "static.olark.com/jsclient/loader0.js",name:"olark",methods:["configure","extend","declare","identify"]});
|
loader: "static.olark.com/jsclient/loader0.js",name:"olark",methods:["configure","extend","declare","identify"]});
|
||||||
/* custom configuration goes here (www.olark.com/documentation) */
|
/* custom configuration goes here (www.olark.com/documentation) */
|
||||||
olark.identify('1189-336-10-9918');/*]]>*/</script><noscript><a href="https://www.olark.com/site/1189-336-10-9918/contact" title="Contact us" target="_blank">Questions? Feedback?</a> powered by <a href="http://www.olark.com?welcome" title="Olark live chat software">Olark live chat software</a></noscript>
|
olark.identify('1189-336-10-9918');/*]]>*/</script><noscript><a href="https://www.olark.com/site/1189-336-10-9918/contact" title="Contact us" target="_blank">Questions? Feedback?</a> powered by <a href="http://www.olark.com?welcome" title="Olark live chat software">Olark live chat software</a></noscript>
|
||||||
|
{% endif %}
|
||||||
<!-- end olark code -->
|
<!-- end olark code -->
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
|
|
|
@ -16,6 +16,8 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
Please register using the <a href="/">registration form</a> to continue.
|
Please register using the <a href="/">registration form</a> to continue.
|
||||||
|
You will be able to connect your github account to your Quay.io account
|
||||||
|
in the user settings.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -105,20 +105,20 @@ def build_specs():
|
||||||
return [
|
return [
|
||||||
TestSpec(url_for('welcome'), 200, 200, 200, 200),
|
TestSpec(url_for('welcome'), 200, 200, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('plans_list'), 200, 200, 200, 200),
|
TestSpec(url_for('list_plans'), 200, 200, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('get_logged_in_user'), 200, 200, 200, 200),
|
TestSpec(url_for('get_logged_in_user'), 200, 200, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('change_user_details'),
|
TestSpec(url_for('change_user_details'),
|
||||||
401, 200, 200, 200).set_method('PUT'),
|
401, 200, 200, 200).set_method('PUT'),
|
||||||
|
|
||||||
TestSpec(url_for('create_user_api'), 201, 201, 201,
|
TestSpec(url_for('create_new_user'), 201, 201, 201,
|
||||||
201).set_method('POST').set_data_from_obj(NEW_USER_DETAILS),
|
201).set_method('POST').set_data_from_obj(NEW_USER_DETAILS),
|
||||||
|
|
||||||
TestSpec(url_for('signin_api'), 200, 200, 200,
|
TestSpec(url_for('signin_user'), 200, 200, 200,
|
||||||
200).set_method('POST').set_data_from_obj(SIGNIN_DETAILS),
|
200).set_method('POST').set_data_from_obj(SIGNIN_DETAILS),
|
||||||
|
|
||||||
TestSpec(url_for('send_recovery'), 201, 201, 201,
|
TestSpec(url_for('request_recovery_email'), 201, 201, 201,
|
||||||
201).set_method('POST').set_data_from_obj(SEND_RECOVERY_DETAILS),
|
201).set_method('POST').set_data_from_obj(SEND_RECOVERY_DETAILS),
|
||||||
|
|
||||||
TestSpec(url_for('get_matching_users', prefix='dev'), 401, 200, 200, 200),
|
TestSpec(url_for('get_matching_users', prefix='dev'), 401, 200, 200, 200),
|
||||||
|
@ -161,29 +161,29 @@ def build_specs():
|
||||||
teamname=ORG_READERS, membername=ORG_OWNER),
|
teamname=ORG_READERS, membername=ORG_OWNER),
|
||||||
admin_code=400).set_method('DELETE'),
|
admin_code=400).set_method('DELETE'),
|
||||||
|
|
||||||
(TestSpec(url_for('create_repo_api'))
|
(TestSpec(url_for('create_repo'))
|
||||||
.set_method('POST')
|
.set_method('POST')
|
||||||
.set_data_from_obj(NEW_ORG_REPO_DETAILS)),
|
.set_data_from_obj(NEW_ORG_REPO_DETAILS)),
|
||||||
|
|
||||||
TestSpec(url_for('match_repos_api'), 200, 200, 200, 200),
|
TestSpec(url_for('find_repos'), 200, 200, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('list_repos_api'), 200, 200, 200, 200),
|
TestSpec(url_for('list_repos'), 200, 200, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('update_repo_api', repository=PUBLIC_REPO),
|
TestSpec(url_for('update_repo', repository=PUBLIC_REPO),
|
||||||
admin_code=403).set_method('PUT'),
|
admin_code=403).set_method('PUT'),
|
||||||
(TestSpec(url_for('update_repo_api', repository=ORG_REPO))
|
(TestSpec(url_for('update_repo', repository=ORG_REPO))
|
||||||
.set_method('PUT')
|
.set_method('PUT')
|
||||||
.set_data_from_obj(UPDATE_REPO_DETAILS)),
|
.set_data_from_obj(UPDATE_REPO_DETAILS)),
|
||||||
(TestSpec(url_for('update_repo_api', repository=PRIVATE_REPO))
|
(TestSpec(url_for('update_repo', repository=PRIVATE_REPO))
|
||||||
.set_method('PUT')
|
.set_method('PUT')
|
||||||
.set_data_from_obj(UPDATE_REPO_DETAILS)),
|
.set_data_from_obj(UPDATE_REPO_DETAILS)),
|
||||||
|
|
||||||
(TestSpec(url_for('change_repo_visibility_api', repository=PUBLIC_REPO),
|
(TestSpec(url_for('change_repo_visibility', repository=PUBLIC_REPO),
|
||||||
admin_code=403).set_method('POST')
|
admin_code=403).set_method('POST')
|
||||||
.set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
|
.set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
|
||||||
(TestSpec(url_for('change_repo_visibility_api', repository=ORG_REPO))
|
(TestSpec(url_for('change_repo_visibility', repository=ORG_REPO))
|
||||||
.set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
|
.set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
|
||||||
(TestSpec(url_for('change_repo_visibility_api', repository=PRIVATE_REPO))
|
(TestSpec(url_for('change_repo_visibility', repository=PRIVATE_REPO))
|
||||||
.set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
|
.set_method('POST').set_data_from_obj(CHANGE_VISIBILITY_DETAILS)),
|
||||||
|
|
||||||
TestSpec(url_for('delete_repository', repository=PUBLIC_REPO),
|
TestSpec(url_for('delete_repository', repository=PUBLIC_REPO),
|
||||||
|
@ -193,11 +193,11 @@ def build_specs():
|
||||||
TestSpec(url_for('delete_repository', repository=PRIVATE_REPO),
|
TestSpec(url_for('delete_repository', repository=PRIVATE_REPO),
|
||||||
admin_code=204).set_method('DELETE'),
|
admin_code=204).set_method('DELETE'),
|
||||||
|
|
||||||
TestSpec(url_for('get_repo_api', repository=PUBLIC_REPO),
|
TestSpec(url_for('get_repo', repository=PUBLIC_REPO),
|
||||||
200, 200, 200,200),
|
200, 200, 200,200),
|
||||||
TestSpec(url_for('get_repo_api', repository=ORG_REPO),
|
TestSpec(url_for('get_repo', repository=ORG_REPO),
|
||||||
403, 403, 200, 200),
|
403, 403, 200, 200),
|
||||||
TestSpec(url_for('get_repo_api', repository=PRIVATE_REPO),
|
TestSpec(url_for('get_repo', repository=PRIVATE_REPO),
|
||||||
403, 403, 200, 200),
|
403, 403, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('get_repo_builds', repository=PUBLIC_REPO),
|
TestSpec(url_for('get_repo_builds', repository=PUBLIC_REPO),
|
||||||
|
@ -403,20 +403,20 @@ def build_specs():
|
||||||
TestSpec(url_for('delete_token', repository=PRIVATE_REPO,
|
TestSpec(url_for('delete_token', repository=PRIVATE_REPO,
|
||||||
code=FAKE_TOKEN), admin_code=400).set_method('DELETE'),
|
code=FAKE_TOKEN), admin_code=400).set_method('DELETE'),
|
||||||
|
|
||||||
TestSpec(url_for('subscribe_api'), 401, 400, 400, 400).set_method('PUT'),
|
TestSpec(url_for('update_user_subscription'), 401, 400, 400, 400).set_method('PUT'),
|
||||||
|
|
||||||
TestSpec(url_for('subscribe_org_api', orgname=ORG),
|
TestSpec(url_for('update_org_subscription', orgname=ORG),
|
||||||
401, 403, 403, 400).set_method('PUT'),
|
401, 403, 403, 400).set_method('PUT'),
|
||||||
|
|
||||||
TestSpec(url_for('get_subscription'), 401, 200, 200, 200),
|
TestSpec(url_for('get_user_subscription'), 401, 200, 200, 200),
|
||||||
|
|
||||||
TestSpec(url_for('get_org_subscription', orgname=ORG)),
|
TestSpec(url_for('get_org_subscription', orgname=ORG)),
|
||||||
|
|
||||||
TestSpec(url_for('repo_logs_api', repository=PUBLIC_REPO), admin_code=403),
|
TestSpec(url_for('list_repo_logs', repository=PUBLIC_REPO), admin_code=403),
|
||||||
TestSpec(url_for('repo_logs_api', repository=ORG_REPO)),
|
TestSpec(url_for('list_repo_logs', repository=ORG_REPO)),
|
||||||
TestSpec(url_for('repo_logs_api', repository=PRIVATE_REPO)),
|
TestSpec(url_for('list_repo_logs', repository=PRIVATE_REPO)),
|
||||||
|
|
||||||
TestSpec(url_for('org_logs_api', orgname=ORG)),
|
TestSpec(url_for('list_org_logs', orgname=ORG)),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
20
tools/backfill_commands.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from data.database import Image
|
||||||
|
from app import app
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
store = app.config['STORAGE']
|
||||||
|
|
||||||
|
|
||||||
|
for image in Image.select():
|
||||||
|
if image.command == None:
|
||||||
|
image_json_path = store.image_json_path(image.repository.namespace,
|
||||||
|
image.repository.name,
|
||||||
|
image.docker_image_id)
|
||||||
|
if store.exists(image_json_path):
|
||||||
|
data = json.loads(store.get_content(image_json_path))
|
||||||
|
command_list = data.get('container_config', {}).get('Cmd', None)
|
||||||
|
command = json.dumps(command_list) if command_list else None
|
||||||
|
print 'Setting command to: %s' % command
|
||||||
|
image.command = command
|
||||||
|
image.save()
|
17
tools/backfillsizes.py
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
from data.database import Image
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
store = app.config['STORAGE']
|
||||||
|
|
||||||
|
|
||||||
|
for image in Image.select():
|
||||||
|
if image.image_size == None:
|
||||||
|
image_path = store.image_layer_path(image.repository.namespace,
|
||||||
|
image.repository.name,
|
||||||
|
image.docker_image_id)
|
||||||
|
if store.exists(image_path):
|
||||||
|
size = store.get_size(image_path)
|
||||||
|
print 'Setting image %s size to: %s' % (image.docker_image_id, size)
|
||||||
|
image.image_size = size
|
||||||
|
image.save()
|
|
@ -1,19 +1,36 @@
|
||||||
from app import stripe
|
from app import stripe
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
EXCLUDE_CID = {'cus_2iVlmwz8CpHgOj'}
|
EXCLUDE_CID = {'cus_2iVlmwz8CpHgOj'}
|
||||||
|
|
||||||
offset = 0
|
offset = 0
|
||||||
total_monthly_revenue = 0
|
total_monthly_revenue = 0
|
||||||
|
|
||||||
|
def empty_tuple():
|
||||||
|
return (0, 0)
|
||||||
|
|
||||||
|
plan_revenue = defaultdict(empty_tuple)
|
||||||
|
|
||||||
batch = stripe.Customer.all(count=100, offset=offset)
|
batch = stripe.Customer.all(count=100, offset=offset)
|
||||||
while batch.data:
|
while batch.data:
|
||||||
for cust in batch.data:
|
for cust in batch.data:
|
||||||
if cust.id not in EXCLUDE_CID and cust.subscription:
|
if cust.id not in EXCLUDE_CID and cust.subscription:
|
||||||
sub = cust.subscription
|
sub = cust.subscription
|
||||||
total_monthly_revenue += sub.plan.amount * sub.quantity
|
total_monthly_revenue += sub.plan.amount * sub.quantity
|
||||||
|
subscribers, revenue = plan_revenue[sub.plan.id]
|
||||||
|
plan_revenue[sub.plan.id] = (subscribers + 1,
|
||||||
|
revenue + sub.plan.amount * sub.quantity)
|
||||||
offset += len(batch.data)
|
offset += len(batch.data)
|
||||||
batch = stripe.Customer.all(count=100, offset=offset)
|
batch = stripe.Customer.all(count=100, offset=offset)
|
||||||
|
|
||||||
dollars = total_monthly_revenue / 100
|
def format_money(total_cents):
|
||||||
cents = total_monthly_revenue % 100
|
dollars = total_cents / 100
|
||||||
print 'Monthly revenue: $%d.%02d' % (dollars, cents)
|
cents = total_cents % 100
|
||||||
|
return dollars, cents
|
||||||
|
|
||||||
|
for plan_id, (subs, rev) in plan_revenue.items():
|
||||||
|
d, c = format_money(rev)
|
||||||
|
print '%s: $%d.%02d(%s)' % (plan_id, d, c, subs)
|
||||||
|
|
||||||
|
d, c = format_money(total_monthly_revenue)
|
||||||
|
print 'Monthly revenue: $%d.%02d' % (d, c)
|
||||||
|
|
|
@ -10,3 +10,12 @@ def cache_control(max_age=55):
|
||||||
return response
|
return response
|
||||||
return add_max_age
|
return add_max_age
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def no_cache(f):
|
||||||
|
@wraps(f)
|
||||||
|
def add_no_cache(*args, **kwargs):
|
||||||
|
response = f(*args, **kwargs)
|
||||||
|
response.headers['Cache-Control'] = 'no-cache'
|
||||||
|
return response
|
||||||
|
return add_no_cache
|
||||||
|
|
|
@ -26,7 +26,8 @@ formatter = logging.Formatter(FORMAT)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
BUILD_SERVER_CMD = ('docker run -d -lxc-conf="lxc.aa_profile=unconfined" ' +
|
BUILD_SERVER_CMD = ('docker run -d -p 5002:5002 ' +
|
||||||
|
'-lxc-conf="lxc.aa_profile=unconfined" ' +
|
||||||
'-privileged -e \'RESOURCE_URL=%s\' -e \'TAG=%s\' ' +
|
'-privileged -e \'RESOURCE_URL=%s\' -e \'TAG=%s\' ' +
|
||||||
'-e \'TOKEN=%s\' quay.io/quay/buildserver')
|
'-e \'TOKEN=%s\' quay.io/quay/buildserver')
|
||||||
|
|
||||||
|
@ -85,7 +86,7 @@ def babysit_builder(request):
|
||||||
api_key=do_api_key,
|
api_key=do_api_key,
|
||||||
name=name,
|
name=name,
|
||||||
region_id=regions.pop(),
|
region_id=regions.pop(),
|
||||||
image_id=1004145, # Docker on 13.04
|
image_id=app.config['DO_DOCKER_IMAGE'],
|
||||||
size_id=66, # 512MB,
|
size_id=66, # 512MB,
|
||||||
backup_active=False)
|
backup_active=False)
|
||||||
retry_command(droplet.create, [],
|
retry_command(droplet.create, [],
|
||||||
|
@ -189,7 +190,7 @@ def babysit_builder(request):
|
||||||
retry_command(droplet.destroy)
|
retry_command(droplet.destroy)
|
||||||
|
|
||||||
repository_build.status_url = None
|
repository_build.status_url = None
|
||||||
repository_build.build_node_id = None;
|
repository_build.build_node_id = None
|
||||||
repository_build.save()
|
repository_build.save()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|