Merge branch 'master' of https://bitbucket.org/yackob03/quay
Conflicts: nginx.conf
|
@ -134,9 +134,9 @@ class BuildNodeConfig(object):
|
|||
|
||||
|
||||
class TestConfig(FlaskConfig, FakeStorage, EphemeralDB, FakeUserfiles,
|
||||
FakeAnalytics):
|
||||
FakeAnalytics, StripeTestConfig):
|
||||
LOGGING_CONFIG = {
|
||||
'level': logging.DEBUG,
|
||||
'level': logging.WARN,
|
||||
'format': LOG_FORMAT
|
||||
}
|
||||
POPULATE_DB_TEST_DATA = True
|
||||
|
|
|
@ -211,7 +211,24 @@ class QueueItem(BaseModel):
|
|||
retries_remaining = IntegerField(default=5)
|
||||
|
||||
|
||||
class LogEntryKind(BaseModel):
|
||||
name = CharField(index=True)
|
||||
|
||||
|
||||
class LogEntry(BaseModel):
|
||||
kind = ForeignKeyField(LogEntryKind, index=True)
|
||||
account = ForeignKeyField(User, index=True, related_name='account')
|
||||
performer = ForeignKeyField(User, index=True, null=True,
|
||||
related_name='performer')
|
||||
repository = ForeignKeyField(Repository, index=True, null=True)
|
||||
access_token = ForeignKeyField(AccessToken, null=True)
|
||||
datetime = DateTimeField(default=datetime.now, index=True)
|
||||
ip = CharField(null=True)
|
||||
metadata_json = TextField(default='{}')
|
||||
|
||||
|
||||
all_models = [User, Repository, Image, AccessToken, Role,
|
||||
RepositoryPermission, Visibility, RepositoryTag,
|
||||
EmailConfirmation, FederatedLogin, LoginService, QueueItem,
|
||||
RepositoryBuild, Team, TeamMember, TeamRole, Webhook]
|
||||
RepositoryBuild, Team, TeamMember, TeamRole, Webhook,
|
||||
LogEntryKind, LogEntry]
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import bcrypt
|
||||
import logging
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import operator
|
||||
import json
|
||||
|
||||
from datetime import timedelta
|
||||
from database import *
|
||||
from util.validation import *
|
||||
from util.names import format_robot_username
|
||||
|
@ -201,7 +203,7 @@ def create_team(name, org, team_role_name, description=''):
|
|||
description=description)
|
||||
|
||||
|
||||
def __get_user_admin_teams(org_name, username):
|
||||
def __get_user_admin_teams(org_name, teamname, username):
|
||||
Org = User.alias()
|
||||
user_teams = Team.select().join(TeamMember).join(User)
|
||||
with_org = user_teams.switch(Team).join(Org,
|
||||
|
@ -928,7 +930,7 @@ def set_team_repo_permission(team_name, namespace_name, repository_name,
|
|||
def purge_repository(namespace_name, repository_name):
|
||||
fetched = Repository.get(Repository.name == repository_name,
|
||||
Repository.namespace == namespace_name)
|
||||
fetched.delete_instance(recursive=True, delete_nullable=True)
|
||||
fetched.delete_instance(recursive=True)
|
||||
|
||||
|
||||
def get_private_repo_count(username):
|
||||
|
@ -988,6 +990,7 @@ def set_repo_delegate_token_role(namespace_name, repository_name, code, role):
|
|||
def delete_delegate_token(namespace_name, repository_name, code):
|
||||
token = get_repo_delegate_token(namespace_name, repository_name, code)
|
||||
token.delete_instance()
|
||||
return token
|
||||
|
||||
|
||||
def load_token_data(code):
|
||||
|
@ -1053,3 +1056,20 @@ def list_webhooks(namespace_name, repository_name):
|
|||
def delete_webhook(namespace_name, repository_name, public_id):
|
||||
webhook = get_webhook(namespace_name, repository_name, public_id)
|
||||
webhook.delete_instance()
|
||||
return webhook
|
||||
|
||||
def list_logs(user_or_organization_name, repository = None):
|
||||
week_ago = datetime.today() - timedelta(7) # One week
|
||||
joined = LogEntry.select().join(User)
|
||||
if repository:
|
||||
joined = joined.where(LogEntry.repository == repository)
|
||||
|
||||
return joined.where(User.username == user_or_organization_name, LogEntry.datetime >= week_ago).order_by(LogEntry.datetime.desc())
|
||||
|
||||
def log_action(kind_name, user_or_organization_name, performer=None, repository=None,
|
||||
access_token=None, ip=None, metadata={}, datetime=datetime.today()):
|
||||
kind = LogEntryKind.get(LogEntryKind.name == kind_name)
|
||||
account = User.get(User.username == user_or_organization_name)
|
||||
entry = LogEntry.create(kind=kind, account=account, performer=performer,
|
||||
repository=repository, access_token=access_token, ip=ip,
|
||||
metadata_json=json.dumps(metadata), datetime=datetime)
|
||||
|
|
127
endpoints/api.py
|
@ -17,6 +17,7 @@ from app import app
|
|||
from util.email import send_confirmation_email, send_recovery_email
|
||||
from util.names import parse_repository_name, format_robot_username
|
||||
from util.gravatar import compute_hash
|
||||
|
||||
from auth.permissions import (ReadRepositoryPermission,
|
||||
ModifyRepositoryPermission,
|
||||
AdministerRepositoryPermission,
|
||||
|
@ -34,6 +35,11 @@ user_files = app.config['USERFILES']
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def log_action(kind, user_or_orgname, metadata={}, repo=None):
|
||||
performer = current_user.db_user()
|
||||
model.log_action(kind, user_or_orgname, performer=performer, ip=request.remote_addr,
|
||||
metadata=metadata, repository=repo)
|
||||
|
||||
def api_login_required(f):
|
||||
@wraps(f)
|
||||
def decorated_view(*args, **kwargs):
|
||||
|
@ -142,6 +148,7 @@ def convert_user_to_organization():
|
|||
|
||||
# Convert the user to an organization.
|
||||
model.convert_user_to_organization(user, model.get_user(admin_username))
|
||||
log_action('account_convert', user.username)
|
||||
|
||||
# And finally login with the admin credentials.
|
||||
return conduct_signin(admin_username, admin_password)
|
||||
|
@ -158,6 +165,7 @@ def change_user_details():
|
|||
try:
|
||||
if 'password' in user_data:
|
||||
logger.debug('Changing password for user: %s', user.username)
|
||||
log_action('account_change_password', user.username)
|
||||
model.change_password(user, user_data['password'])
|
||||
|
||||
if 'invoice_email' in user_data:
|
||||
|
@ -427,6 +435,7 @@ def get_organization_members(orgname):
|
|||
for member in members:
|
||||
if not member.user.username in members_dict:
|
||||
members_dict[member.user.username] = {'username': member.user.username,
|
||||
'is_robot': member.user.robot,
|
||||
'teams': []}
|
||||
|
||||
members_dict[member.user.username]['teams'].append(member.team.name)
|
||||
|
@ -485,14 +494,20 @@ def update_organization_team(orgname, teamname):
|
|||
|
||||
org = model.get_organization(orgname)
|
||||
team = model.create_team(teamname, org, role, description)
|
||||
log_action('org_create_team', orgname, {'team': teamname})
|
||||
|
||||
if is_existing:
|
||||
if 'description' in details:
|
||||
if 'description' in details and team.description != details['description']:
|
||||
team.description = details['description']
|
||||
team.save()
|
||||
log_action('org_set_team_description', orgname, {'team': teamname, 'description': team.description})
|
||||
|
||||
if 'role' in details:
|
||||
role = model.get_team_org_role(team).name
|
||||
if role != details['role']:
|
||||
team = model.set_team_org_permission(team, details['role'],
|
||||
current_user.db_user().username)
|
||||
log_action('org_set_team_role', orgname, {'team': teamname, 'role': details['role']})
|
||||
|
||||
resp = jsonify(team_view(orgname, team))
|
||||
if not is_existing:
|
||||
|
@ -509,6 +524,7 @@ def delete_organization_team(orgname, teamname):
|
|||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
model.remove_team(orgname, teamname, current_user.db_user().username)
|
||||
log_action('org_delete_team', orgname, {'team': teamname})
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403)
|
||||
|
@ -559,7 +575,7 @@ def update_organization_team_member(orgname, teamname, membername):
|
|||
|
||||
# Add the user to the team.
|
||||
model.add_user_to_team(user, team)
|
||||
|
||||
log_action('org_add_team_member', orgname, {'member': membername, 'team': teamname})
|
||||
return jsonify(member_view(user))
|
||||
|
||||
abort(403)
|
||||
|
@ -574,6 +590,7 @@ def delete_organization_team_member(orgname, teamname, membername):
|
|||
# Remote the user from the team.
|
||||
invoking_user = current_user.db_user().username
|
||||
model.remove_user_from_team(orgname, teamname, membername, invoking_user)
|
||||
log_action('org_remove_team_member', orgname, {'member': membername, 'team': teamname})
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403)
|
||||
|
@ -602,6 +619,7 @@ def create_repo_api():
|
|||
repo.description = req['description']
|
||||
repo.save()
|
||||
|
||||
log_action('create_repo', namespace_name, {'repo': repository_name, 'namespace': namespace_name}, repo=repo)
|
||||
return jsonify({
|
||||
'namespace': namespace_name,
|
||||
'name': repository_name
|
||||
|
@ -685,6 +703,9 @@ def update_repo_api(namespace, repository):
|
|||
values = request.get_json()
|
||||
repo.description = values['description']
|
||||
repo.save()
|
||||
|
||||
log_action('set_repo_description', namespace, {'repo': repository, 'description': values['description']},
|
||||
repo=repo)
|
||||
return jsonify({
|
||||
'success': True
|
||||
})
|
||||
|
@ -692,6 +713,24 @@ def update_repo_api(namespace, repository):
|
|||
abort(403)
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
@parse_repository_name
|
||||
def repo_logs_api(namespace, repository):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
abort(404)
|
||||
|
||||
logs = model.list_logs(namespace, repository = repo)
|
||||
return jsonify({
|
||||
'logs': [log_view(log) for log in logs]
|
||||
})
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
@app.route('/api/repository/<path:repository>/changevisibility',
|
||||
methods=['POST'])
|
||||
@api_login_required
|
||||
|
@ -703,6 +742,8 @@ def change_repo_visibility_api(namespace, repository):
|
|||
if repo:
|
||||
values = request.get_json()
|
||||
model.set_repository_visibility(repo, values['visibility'])
|
||||
log_action('change_repo_visibility', namespace, {'repo': repository, 'visibility': values['visibility']},
|
||||
repo=repo)
|
||||
return jsonify({
|
||||
'success': True
|
||||
})
|
||||
|
@ -718,6 +759,7 @@ def delete_repository(namespace, repository):
|
|||
if permission.can():
|
||||
model.purge_repository(namespace, repository)
|
||||
registry.delete_repository_storage(namespace, repository)
|
||||
log_action('delete_repo', namespace, {'repo': repository, 'namespace': namespace})
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403)
|
||||
|
@ -833,6 +875,9 @@ def request_repo_build(namespace, repository):
|
|||
tag)
|
||||
dockerfile_build_queue.put(json.dumps({'build_id': build_request.id}))
|
||||
|
||||
log_action('build_dockerfile', namespace,
|
||||
{'repo': repository, 'namespace': namespace, 'fileid': dockerfile_id}, repo=repo)
|
||||
|
||||
resp = jsonify({
|
||||
'started': True
|
||||
})
|
||||
|
@ -861,6 +906,8 @@ def create_webhook(namespace, repository):
|
|||
repo_string = '%s/%s' % (namespace, repository)
|
||||
resp.headers['Location'] = url_for('get_webhook', repository=repo_string,
|
||||
public_id=webhook.public_id)
|
||||
log_action('add_repo_webhook', namespace,
|
||||
{'repo': repository, 'webhook_id': webhook.public_id}, repo=repo)
|
||||
return resp
|
||||
|
||||
abort(403) # Permissions denied
|
||||
|
@ -901,6 +948,9 @@ def delete_webhook(namespace, repository, public_id):
|
|||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
model.delete_webhook(namespace, repository, public_id)
|
||||
log_action('delete_repo_webhook', namespace,
|
||||
{'repo': repository, 'webhook_id': public_id},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
return make_response('No Content', 204)
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1141,6 +1191,9 @@ def change_user_permissions(namespace, repository, username):
|
|||
error_resp.status_code = 400
|
||||
return error_resp
|
||||
|
||||
log_action('change_repo_permission', namespace,
|
||||
{'username': username, 'repo': repository, 'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(perm_view)
|
||||
if request.method == 'POST':
|
||||
|
@ -1165,6 +1218,10 @@ def change_team_permissions(namespace, repository, teamname):
|
|||
perm = model.set_team_repo_permission(teamname, namespace, repository,
|
||||
new_permission['role'])
|
||||
|
||||
log_action('change_repo_permission', namespace,
|
||||
{'team': teamname, 'repo': repository, 'role': new_permission['role']},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(role_view(perm))
|
||||
if request.method == 'POST':
|
||||
resp.status_code = 201
|
||||
|
@ -1189,6 +1246,9 @@ def delete_user_permissions(namespace, repository, username):
|
|||
error_resp.status_code = 400
|
||||
return error_resp
|
||||
|
||||
log_action('delete_repo_permission', namespace, {'username': username, 'repo': repository},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1202,6 +1262,10 @@ def delete_team_permissions(namespace, repository, teamname):
|
|||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
model.delete_team_permission(teamname, namespace, repository)
|
||||
|
||||
log_action('delete_repo_permission', namespace, {'team': teamname, 'repo': repository},
|
||||
repo=model.get_repository(namespace, repository))
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1253,6 +1317,9 @@ def create_token(namespace, repository):
|
|||
token = model.create_delegate_token(namespace, repository,
|
||||
token_params['friendlyName'])
|
||||
|
||||
log_action('add_repo_accesstoken', namespace, {'repo': repository, 'token': token_params['friendlyName']},
|
||||
repo = model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(token_view(token))
|
||||
resp.status_code = 201
|
||||
return resp
|
||||
|
@ -1274,6 +1341,10 @@ def change_token(namespace, repository, code):
|
|||
token = model.set_repo_delegate_token_role(namespace, repository, code,
|
||||
new_permission['role'])
|
||||
|
||||
log_action('change_repo_permission', namespace,
|
||||
{'repo': repository, 'token': token.friendly_name, 'code': code, 'role': new_permission['role']},
|
||||
repo = model.get_repository(namespace, repository))
|
||||
|
||||
resp = jsonify(token_view(token))
|
||||
return resp
|
||||
|
||||
|
@ -1287,7 +1358,12 @@ def change_token(namespace, repository, code):
|
|||
def delete_token(namespace, repository, code):
|
||||
permission = AdministerRepositoryPermission(namespace, repository)
|
||||
if permission.can():
|
||||
model.delete_delegate_token(namespace, repository, code)
|
||||
token = model.delete_delegate_token(namespace, repository, code)
|
||||
|
||||
log_action('delete_repo_accesstoken', namespace,
|
||||
{'repo': repository, 'token': token.friendly_name, 'code': code},
|
||||
repo = model.get_repository(namespace, repository))
|
||||
|
||||
return make_response('Deleted', 204)
|
||||
|
||||
abort(403) # Permission denied
|
||||
|
@ -1325,7 +1401,9 @@ def get_org_card_api(orgname):
|
|||
def set_user_card_api():
|
||||
user = current_user.db_user()
|
||||
token = request.get_json()['token']
|
||||
return set_card(user, token)
|
||||
response = set_card(user, token)
|
||||
log_action('account_change_cc', user.username)
|
||||
return response
|
||||
|
||||
|
||||
@app.route('/api/organization/<orgname>/card', methods=['POST'])
|
||||
|
@ -1335,7 +1413,9 @@ def set_org_card_api(orgname):
|
|||
if permission.can():
|
||||
organization = model.get_organization(orgname)
|
||||
token = request.get_json()['token']
|
||||
return jsonify(set_card(organization, token))
|
||||
response = set_card(organization, token)
|
||||
log_action('account_change_cc', orgname)
|
||||
return response
|
||||
|
||||
abort(403)
|
||||
|
||||
|
@ -1424,6 +1504,7 @@ def subscribe(user, plan, token, accepted_plans):
|
|||
cus = stripe.Customer.create(email=user.email, plan=plan, card=card)
|
||||
user.stripe_id = cus.id
|
||||
user.save()
|
||||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
except stripe.CardError as e:
|
||||
return carderror_response(e)
|
||||
|
||||
|
@ -1439,6 +1520,7 @@ def subscribe(user, plan, token, accepted_plans):
|
|||
# We only have to cancel the subscription if they actually have one
|
||||
cus.cancel_subscription()
|
||||
cus.save()
|
||||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
|
||||
else:
|
||||
# User may have been a previous customer who is resubscribing
|
||||
|
@ -1453,6 +1535,7 @@ def subscribe(user, plan, token, accepted_plans):
|
|||
return carderror_response(e)
|
||||
|
||||
response_json = subscription_view(cus.subscription, private_repos)
|
||||
log_action('account_change_plan', user.username, {'plan': plan})
|
||||
|
||||
resp = jsonify(response_json)
|
||||
resp.status_code = status_code
|
||||
|
@ -1580,6 +1663,7 @@ def create_robot(robot_shortname):
|
|||
parent = current_user.db_user()
|
||||
robot, password = model.create_robot(robot_shortname, parent)
|
||||
resp = jsonify(robot_view(robot.username, password))
|
||||
log_action('create_robot', parent.username, {'robot': robot_shortname})
|
||||
resp.status_code = 201
|
||||
return resp
|
||||
|
||||
|
@ -1593,6 +1677,7 @@ def create_org_robot(orgname, robot_shortname):
|
|||
parent = model.get_organization(orgname)
|
||||
robot, password = model.create_robot(robot_shortname, parent)
|
||||
resp = jsonify(robot_view(robot.username, password))
|
||||
log_action('create_robot', orgname, {'robot': robot_shortname})
|
||||
resp.status_code = 201
|
||||
return resp
|
||||
|
||||
|
@ -1604,6 +1689,7 @@ def create_org_robot(orgname, robot_shortname):
|
|||
def delete_robot(robot_shortname):
|
||||
parent = current_user.db_user()
|
||||
model.delete_robot(format_robot_username(parent.username, robot_shortname))
|
||||
log_action('delete_robot', parent.username, {'robot': robot_shortname})
|
||||
return make_response('No Content', 204)
|
||||
|
||||
|
||||
|
@ -1614,6 +1700,37 @@ def delete_org_robot(orgname, robot_shortname):
|
|||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
model.delete_robot(format_robot_username(orgname, robot_shortname))
|
||||
log_action('delete_robot', orgname, {'robot': robot_shortname})
|
||||
return make_response('No Content', 204)
|
||||
|
||||
abort(403)
|
||||
|
||||
|
||||
def log_view(log):
|
||||
view = {
|
||||
'kind': log.kind.name,
|
||||
'metadata': json.loads(log.metadata_json),
|
||||
'ip': log.ip,
|
||||
'datetime': log.datetime,
|
||||
}
|
||||
|
||||
if log.performer:
|
||||
view['performer'] = {
|
||||
'username': log.performer.username,
|
||||
'is_robot': log.performer.robot,
|
||||
}
|
||||
|
||||
return view
|
||||
|
||||
@app.route('/api/organization/<orgname>/logs', methods=['GET'])
|
||||
@api_login_required
|
||||
def org_logs_api(orgname):
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
logs = model.list_logs(orgname)
|
||||
return jsonify({
|
||||
'logs': [log_view(log) for log in logs]
|
||||
})
|
||||
|
||||
abort(403)
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ from auth.permissions import (ModifyRepositoryPermission, UserPermission,
|
|||
ReadRepositoryPermission,
|
||||
CreateRepositoryPermission)
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -169,11 +168,22 @@ def create_repository(namespace, repository):
|
|||
'repository': '%s/%s' % (namespace, repository),
|
||||
}
|
||||
|
||||
metadata = {
|
||||
'repo': repository,
|
||||
'namespace': namespace
|
||||
}
|
||||
|
||||
if get_authenticated_user():
|
||||
mixpanel.track(get_authenticated_user().username, 'push_repo',
|
||||
extra_params)
|
||||
metadata['username'] = get_authenticated_user().username
|
||||
else:
|
||||
mixpanel.track(get_validated_token().code, 'push_repo', extra_params)
|
||||
metadata['token'] = get_validated_token().friendly_name
|
||||
metadata['token_code'] = get_validated_token().code
|
||||
|
||||
model.log_action('push_repo', namespace, performer=get_authenticated_user(),
|
||||
ip=request.remote_addr, metadata=metadata, repository=repo)
|
||||
|
||||
return response
|
||||
|
||||
|
@ -232,8 +242,8 @@ def get_repository_images(namespace, repository):
|
|||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
|
||||
# TODO invalidate token?
|
||||
|
||||
if permission.can() or model.repository_is_public(namespace, repository):
|
||||
is_public = model.repository_is_public(namespace, repository)
|
||||
if permission.can() or is_public:
|
||||
# We can't rely on permissions to tell us if a repo exists anymore
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
|
@ -250,6 +260,18 @@ def get_repository_images(namespace, repository):
|
|||
resp = make_response(json.dumps(all_images), 200)
|
||||
resp.mimetype = 'application/json'
|
||||
|
||||
metadata = {
|
||||
'repo': repository,
|
||||
'namespace': namespace,
|
||||
}
|
||||
if get_authenticated_user():
|
||||
metadata['username'] = get_authenticated_user().username
|
||||
elif get_validated_token():
|
||||
metadata['token'] = get_validated_token().friendly_name
|
||||
metadata['token_code'] = get_validated_token().code
|
||||
else:
|
||||
metadata['public'] = True
|
||||
|
||||
pull_username = 'anonymous'
|
||||
if get_authenticated_user():
|
||||
pull_username = get_authenticated_user().username
|
||||
|
@ -257,8 +279,12 @@ def get_repository_images(namespace, repository):
|
|||
extra_params = {
|
||||
'repository': '%s/%s' % (namespace, repository),
|
||||
}
|
||||
mixpanel.track(pull_username, 'pull_repo', extra_params)
|
||||
|
||||
mixpanel.track(pull_username, 'pull_repo', extra_params)
|
||||
model.log_action('pull_repo', namespace,
|
||||
performer=get_authenticated_user(),
|
||||
ip=request.remote_addr, metadata=metadata,
|
||||
repository=repo)
|
||||
return resp
|
||||
|
||||
abort(403)
|
||||
|
|
61
initdb.py
|
@ -111,6 +111,36 @@ def initialize_database():
|
|||
LoginService.create(name='github')
|
||||
LoginService.create(name='quayrobot')
|
||||
|
||||
LogEntryKind.create(name='account_change_plan')
|
||||
LogEntryKind.create(name='account_change_cc')
|
||||
LogEntryKind.create(name='account_change_password')
|
||||
LogEntryKind.create(name='account_convert')
|
||||
|
||||
LogEntryKind.create(name='create_robot')
|
||||
LogEntryKind.create(name='delete_robot')
|
||||
|
||||
LogEntryKind.create(name='create_repo')
|
||||
LogEntryKind.create(name='push_repo')
|
||||
LogEntryKind.create(name='pull_repo')
|
||||
LogEntryKind.create(name='delete_repo')
|
||||
LogEntryKind.create(name='add_repo_permission')
|
||||
LogEntryKind.create(name='change_repo_permission')
|
||||
LogEntryKind.create(name='delete_repo_permission')
|
||||
LogEntryKind.create(name='change_repo_visibility')
|
||||
LogEntryKind.create(name='add_repo_accesstoken')
|
||||
LogEntryKind.create(name='delete_repo_accesstoken')
|
||||
LogEntryKind.create(name='add_repo_webhook')
|
||||
LogEntryKind.create(name='delete_repo_webhook')
|
||||
LogEntryKind.create(name='set_repo_description')
|
||||
LogEntryKind.create(name='build_dockerfile')
|
||||
|
||||
LogEntryKind.create(name='org_create_team')
|
||||
LogEntryKind.create(name='org_delete_team')
|
||||
LogEntryKind.create(name='org_add_team_member')
|
||||
LogEntryKind.create(name='org_remove_team_member')
|
||||
LogEntryKind.create(name='org_set_team_description')
|
||||
LogEntryKind.create(name='org_set_team_role')
|
||||
|
||||
|
||||
def wipe_database():
|
||||
logger.debug('Wiping all data from the DB.')
|
||||
|
@ -217,6 +247,37 @@ def populate_database():
|
|||
build.status_url = 'http://localhost:5000/test/build/status'
|
||||
build.save()
|
||||
|
||||
today = datetime.today()
|
||||
week_ago = today - timedelta(6)
|
||||
six_ago = today - timedelta(5)
|
||||
four_ago = today - timedelta(4)
|
||||
|
||||
model.log_action('org_create_team', org.username, performer=new_user_1, datetime=week_ago,
|
||||
metadata={'team': 'readers'})
|
||||
|
||||
model.log_action('org_set_team_role', org.username, performer=new_user_1, datetime=week_ago,
|
||||
metadata={'team': 'readers', 'role': 'read'})
|
||||
|
||||
model.log_action('create_repo', org.username, performer=new_user_1, repository=org_repo, datetime=week_ago,
|
||||
metadata={'namespace': org.username, 'repo': 'orgrepo'})
|
||||
|
||||
model.log_action('change_repo_permission', org.username, performer=new_user_2, repository=org_repo, datetime=six_ago,
|
||||
metadata={'username': new_user_1.username, 'repo': 'orgrepo', 'role': 'admin'})
|
||||
|
||||
model.log_action('change_repo_permission', org.username, performer=new_user_1, repository=org_repo, datetime=six_ago,
|
||||
metadata={'username': new_user_2.username, 'repo': 'orgrepo', 'role': 'read'})
|
||||
|
||||
model.log_action('add_repo_accesstoken', org.username, performer=new_user_1, repository=org_repo, datetime=four_ago,
|
||||
metadata={'repo': 'orgrepo', 'token': 'deploytoken'})
|
||||
|
||||
model.log_action('push_repo', org.username, performer=new_user_2, repository=org_repo, datetime=today,
|
||||
metadata={'username': new_user_2.username, 'repo': 'orgrepo'})
|
||||
|
||||
model.log_action('pull_repo', org.username, performer=new_user_2, repository=org_repo, datetime=today,
|
||||
metadata={'username': new_user_2.username, 'repo': 'orgrepo'})
|
||||
|
||||
model.log_action('pull_repo', org.username, repository=org_repo, datetime=today,
|
||||
metadata={'token': 'sometoken', 'token_code': 'somecode', 'repo': 'orgrepo'})
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(**app.config['LOGGING_CONFIG'])
|
||||
|
|
|
@ -19,6 +19,15 @@ http {
|
|||
|
||||
root /root/quay/;
|
||||
|
||||
gzip on;
|
||||
gzip_http_version 1.0;
|
||||
gzip_proxied any;
|
||||
gzip_min_length 500;
|
||||
gzip_disable "MSIE [1-6]\.";
|
||||
gzip_types text/plain text/xml text/css
|
||||
text/javascript application/x-javascript
|
||||
application/octet-stream;
|
||||
|
||||
upstream app_server {
|
||||
server unix:/tmp/gunicorn.sock fail_timeout=0;
|
||||
# For a TCP configuration:
|
||||
|
|
|
@ -40,6 +40,9 @@ casper.on("remote.message", function(msg, trace) {
|
|||
|
||||
casper.on("page.error", function(msg, trace) {
|
||||
this.echo("Page error: " + msg, "ERROR");
|
||||
for (var i = 0; i < trace.length; i++) {
|
||||
this.echo(JSON.stringify(trace[i]), "ERROR");
|
||||
}
|
||||
});
|
||||
|
||||
casper.start(rootUrl + 'signin', function () {
|
||||
|
@ -124,6 +127,12 @@ casper.then(function() {
|
|||
this.capture(outputDir + 'org-admin.png');
|
||||
});
|
||||
|
||||
casper.thenClick('a[data-target="#logs"]', function() {
|
||||
this.waitForSelector('svg > g', function() {
|
||||
this.capture(outputDir + 'org-logs.png');
|
||||
});
|
||||
});
|
||||
|
||||
casper.thenOpen(rootUrl + 'repository/' + org + '/' + orgrepo + '/admin', function() {
|
||||
this.waitForText('outsideorg')
|
||||
});
|
||||
|
|
|
@ -21,6 +21,40 @@ html, body {
|
|||
border-bottom: 1px dashed #aaa;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 22px;
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
i.toggle-icon:hover {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
.toggle-icon.active {
|
||||
border-radius: 4px;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.entity-reference .prefix {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.entity-reference-element i.fa-user {
|
||||
margin-left: 2px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.entity-reference-element i.fa-wrench {
|
||||
margin-left: 1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.entity-reference-element i.fa-group {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
|
||||
.docker-auth-dialog .token-dialog-body .well {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
@ -108,6 +142,41 @@ html, body {
|
|||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.logs-view-element .header {
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.logs-view-element .header .header-text {
|
||||
font-size: 24px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.logs-view-element .header .header-text .mini {
|
||||
font-size: 18px;
|
||||
color: #999;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
|
||||
.logs-view-element .header .right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.logs-view-element .log i.fa {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.logs-view-element .log .circle {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.billing-options-element .current-card {
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
|
@ -1175,20 +1244,6 @@ p.editable:hover i {
|
|||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.repo-admin .user i.fa-user {
|
||||
margin-left: 2px;
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
.repo-admin .user i.fa-wrench {
|
||||
margin-left: 1px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.repo-admin .team i.fa-group {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.repo-admin .entity {
|
||||
font-size: 1.2em;
|
||||
min-width: 300px;
|
||||
|
@ -2038,13 +2093,13 @@ p.editable:hover i {
|
|||
|
||||
/** D3 tooltip styling */
|
||||
|
||||
.d3-tip {
|
||||
line-height: 1;
|
||||
padding: 12px;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
color: #fff;
|
||||
border-radius: 2px;
|
||||
max-width: 500px;
|
||||
.d3-tip, .nvtooltip {
|
||||
line-height: 1 !important;
|
||||
padding: 12px !important;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
color: #fff !important;
|
||||
border-radius: 2px !important;
|
||||
max-width: 500px !important;
|
||||
z-index: 9999999;
|
||||
}
|
||||
|
||||
|
|
7
static/directives/entity-reference.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
<span class="entity-reference-element">
|
||||
<i class="fa fa-user" ng-show="!team && !isrobot" title="User" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-wrench" ng-show="!team && isrobot" title="Robot Account" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<i class="fa fa-group" ng-show="team" title="Team" bs-tooltip="tooltip.title" data-container="body"></i>
|
||||
<span ng-show="team"><a href="/organization/{{ orgname }}/teams/{{ team }}">{{team}}</a></span>
|
||||
<span ng-show="isrobot" class="prefix">{{getPrefix(name)}}</span><span>{{getShortenedName(name)}}</span>
|
||||
</span>
|
58
static/directives/logs-view.html
Normal file
|
@ -0,0 +1,58 @@
|
|||
<div class="logs-view-element">
|
||||
<div ng-show="loading">
|
||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||
</div>
|
||||
<div ng-show="!loading">
|
||||
<div class="container header">
|
||||
<span class="header-text">Usage Logs <span class="mini">For the last seven days</span></span>
|
||||
<span class="right">
|
||||
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
|
||||
ng-click="toggleChart()" title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">
|
||||
<svg style="width: 800px; height: 500px;"></svg>
|
||||
</div>
|
||||
|
||||
<div class="side-controls">
|
||||
<div class="result-count">
|
||||
Showing {{(logs | visibleLogFilter:kindsAllowed | filter:search | limitTo:150).length}} of
|
||||
{{(logs | visibleLogFilter:kindsAllowed | filter:search).length}} matching logs
|
||||
</div>
|
||||
<div class="filter-input">
|
||||
<input id="log-filter" class="form-control" placeholder="Filter Logs" type="text" ng-model="search.$">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Description</th>
|
||||
<th>Date/Time</th>
|
||||
<th>User/Token</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
<tr class="log" ng-repeat="log in (logs | visibleLogFilter:kindsAllowed | filter:search | limitTo:150)">
|
||||
<td>
|
||||
<span class="circle" style="{{ 'background: ' + getColor(log.kind) }}"></span>
|
||||
<span ng-bind-html="getDescription(log)"></span>
|
||||
</td>
|
||||
<td>{{ log.datetime }}</td>
|
||||
<td>
|
||||
<span ng-show="log.performer">
|
||||
<span class="entity-reference" name="log.performer.username" isrobot="log.performer.is_robot"></span>
|
||||
</span>
|
||||
<span ng-show="!log.performer && log.metadata.token">
|
||||
<i class="fa fa-key"></i>
|
||||
<span>{{ log.metadata.token }}</span>
|
||||
</span>
|
||||
<span ng-show="!log.performer && !log.metadata.token">
|
||||
(anonymous)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
|
@ -1,9 +1,9 @@
|
|||
<div class="organization-header-element">
|
||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon">
|
||||
<span class="organization-name" ng-show="teamName">
|
||||
<span class="organization-name" ng-show="teamName || clickable">
|
||||
<a href="/organization/{{ organization.name }}">{{ organization.name }}</a>
|
||||
</span>
|
||||
<span class="organization-name" ng-show="!teamName">
|
||||
<span class="organization-name" ng-show="!teamName && !clickable">
|
||||
{{ organization.name }}
|
||||
</span>
|
||||
|
||||
|
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 74 KiB |
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 67 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 55 KiB |
Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 64 KiB |
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 99 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 127 KiB |
Before Width: | Height: | Size: 194 KiB After Width: | Height: | Size: 208 KiB |
238
static/js/app.js
|
@ -460,6 +460,37 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('entityReference', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/entity-reference.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'name': '=name',
|
||||
'orgname': '=orgname',
|
||||
'team': '=team',
|
||||
'isrobot': '=isrobot'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getPrefix = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus + 1);
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('markdownView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -619,6 +650,210 @@ quayApp.directive('dockerAuthDialog', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.filter('visibleLogFilter', function () {
|
||||
return function (logs, allowed) {
|
||||
if (!allowed) {
|
||||
return logs;
|
||||
}
|
||||
|
||||
var filtered = [];
|
||||
angular.forEach(logs, function (log) {
|
||||
if (allowed[log.kind]) {
|
||||
filtered.push(log);
|
||||
}
|
||||
});
|
||||
|
||||
return filtered;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('logsView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/logs-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user',
|
||||
'visible': '=visible',
|
||||
'repository': '=repository'
|
||||
},
|
||||
controller: function($scope, $element, $sce, Restangular) {
|
||||
$scope.loading = true;
|
||||
$scope.logs = null;
|
||||
$scope.kindsAllowed = null;
|
||||
$scope.chartVisible = true;
|
||||
|
||||
var logDescriptions = {
|
||||
'account_change_plan': 'Change plan',
|
||||
'account_change_cc': 'Update credit card',
|
||||
'account_change_password': 'Change password',
|
||||
'account_convert': 'Convert account to organization',
|
||||
'create_robot': 'Create Robot Account: {robot}',
|
||||
'delete_robot': 'Delete Robot Account: {robot}',
|
||||
'create_repo': 'Create Repository: {repo}',
|
||||
'push_repo': 'Push to repository: {repo}',
|
||||
'pull_repo': function(metadata) {
|
||||
if (metadata.token) {
|
||||
return 'Pull repository {repo} via token {token}';
|
||||
} else if (metadata.username) {
|
||||
return 'Pull repository {repo} by {username}';
|
||||
} else {
|
||||
return 'Public pull of repository {repo} by {_ip}';
|
||||
}
|
||||
},
|
||||
'delete_repo': 'Delete repository: {repo}',
|
||||
'change_repo_permission': function(metadata) {
|
||||
if (metadata.username) {
|
||||
return 'Change permission for user {username} in repository {repo} to {role}';
|
||||
} else if (metadata.team) {
|
||||
return 'Change permission for team {team} in repository {repo} to {role}';
|
||||
} else if (metadata.token) {
|
||||
return 'Change permission for token {token} in repository {repo} to {role}';
|
||||
}
|
||||
},
|
||||
'delete_repo_permission': function(metadata) {
|
||||
if (metadata.username) {
|
||||
return 'Remove permission for user {username} from repository {repo}';
|
||||
} else if (metadata.team) {
|
||||
return 'Remove permission for team {team} from repository {repo}';
|
||||
} else if (metadata.token) {
|
||||
return 'Remove permission for token {token} from repository {repo}';
|
||||
}
|
||||
},
|
||||
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
|
||||
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
||||
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||
'delete_repo_webhook': 'Delete webhook in repository {repo}',
|
||||
'set_repo_description': 'Change description for repository {repo}: {description}',
|
||||
'build_dockerfile': 'Build image from Dockerfile for repository {repo}',
|
||||
'org_create_team': 'Create team: {team}',
|
||||
'org_delete_team': 'Delete team: {team}',
|
||||
'org_add_team_member': 'Add member {member} to team {team}',
|
||||
'org_remove_team_member': 'Remove member {member} from team {team}',
|
||||
'org_set_team_description': 'Change description of team {team}: {description}',
|
||||
'org_set_team_role': 'Change permission of team {team} to {role}'
|
||||
};
|
||||
|
||||
var logKinds = {
|
||||
'account_change_plan': 'Change plan',
|
||||
'account_change_cc': 'Update credit card',
|
||||
'account_change_password': 'Change password',
|
||||
'account_convert': 'Convert account to organization',
|
||||
'create_robot': 'Create Robot Account',
|
||||
'delete_robot': 'Delete Robot Account',
|
||||
'create_repo': 'Create Repository',
|
||||
'push_repo': 'Push to repository',
|
||||
'pull_repo': 'Pull repository',
|
||||
'delete_repo': 'Delete repository',
|
||||
'change_repo_permission': 'Change repository permission',
|
||||
'delete_repo_permission': 'Remove user permission from repository',
|
||||
'change_repo_visibility': 'Change repository visibility',
|
||||
'add_repo_accesstoken': 'Create access token',
|
||||
'delete_repo_accesstoken': 'Delete access token',
|
||||
'add_repo_webhook': 'Add webhook',
|
||||
'delete_repo_webhook': 'Delete webhook',
|
||||
'set_repo_description': 'Change repository description',
|
||||
'build_dockerfile': 'Build image from Dockerfile',
|
||||
'org_create_team': 'Create team',
|
||||
'org_delete_team': 'Delete team',
|
||||
'org_add_team_member': 'Add team member',
|
||||
'org_remove_team_member': 'Remove team member',
|
||||
'org_set_team_description': 'Change team description',
|
||||
'org_set_team_role': 'Change team permission'
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.visible || (!$scope.organization && !$scope.user && !$scope.repository)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
var url = getRestUrl('user/logs');
|
||||
if ($scope.organization) {
|
||||
url = getRestUrl('organization', $scope.organization.name, 'logs');
|
||||
}
|
||||
if ($scope.repository) {
|
||||
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
||||
}
|
||||
|
||||
var loadLogs = Restangular.one(url);
|
||||
loadLogs.customGET().then(function(resp) {
|
||||
if (!$scope.chart) {
|
||||
$scope.chart = new LogUsageChart(logKinds);
|
||||
$($scope.chart).bind('filteringChanged', function(e) {
|
||||
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
|
||||
});
|
||||
}
|
||||
|
||||
$scope.chart.draw('bar-chart', resp.logs);
|
||||
$scope.kindsAllowed = null;
|
||||
$scope.logs = resp.logs;
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleChart = function() {
|
||||
$scope.chartVisible = !$scope.chartVisible;
|
||||
};
|
||||
|
||||
$scope.isVisible = function(allowed, kind) {
|
||||
return allowed == null || allowed.hasOwnProperty(kind);
|
||||
};
|
||||
|
||||
$scope.getColor = function(kind) {
|
||||
return $scope.chart.getColor(kind);
|
||||
};
|
||||
|
||||
$scope.getDescription = function(log) {
|
||||
var fieldIcons = {
|
||||
'username': 'user',
|
||||
'team': 'group',
|
||||
'token': 'key',
|
||||
'repo': 'hdd',
|
||||
'robot': 'wrench'
|
||||
};
|
||||
|
||||
if (log.ip) {
|
||||
log.metadata['_ip'] = log.ip;
|
||||
}
|
||||
|
||||
var description = logDescriptions[log.kind] || logTitles[log.kind] || log.kind;
|
||||
if (typeof description != 'string') {
|
||||
description = description(log.metadata);
|
||||
}
|
||||
|
||||
for (var key in log.metadata) {
|
||||
if (log.metadata.hasOwnProperty(key)) {
|
||||
var markedDown = getMarkedDown(log.metadata[key].toString());
|
||||
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
||||
|
||||
var icon = fieldIcons[key];
|
||||
if (icon) {
|
||||
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
|
||||
}
|
||||
|
||||
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
|
||||
}
|
||||
}
|
||||
return $sce.trustAsHtml(description);
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
$scope.$watch('repository', update);
|
||||
$scope.$watch('visible', update);
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
quayApp.directive('robotsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -777,7 +1012,8 @@ quayApp.directive('organizationHeader', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'teamName': '=teamName'
|
||||
'teamName': '=teamName',
|
||||
'clickable': '=clickable'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
|
|
|
@ -426,15 +426,10 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
var name = $routeParams.name;
|
||||
|
||||
$scope.permissions = {'team': [], 'user': []};
|
||||
$scope.logsShown = 0;
|
||||
|
||||
$scope.getPrefix = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus + 1);
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsShown++;
|
||||
};
|
||||
|
||||
$scope.grantRole = function() {
|
||||
|
@ -598,7 +593,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
|||
permissionsFetch.get().then(function(resp) {
|
||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||
$rootScope.description = 'Administrator settings for ' + namespace + '/' + name +
|
||||
': Permissions, web hooks and other settings';
|
||||
': Permissions, webhooks and other settings';
|
||||
$scope.permissions[kind] = resp.permissions;
|
||||
checkLoading();
|
||||
}, function() {
|
||||
|
@ -1164,6 +1159,11 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
|||
$scope.membersLoading = true;
|
||||
$scope.membersFound = null;
|
||||
$scope.invoiceLoading = true;
|
||||
$scope.logsShown = 0;
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsShown++;
|
||||
};
|
||||
|
||||
$scope.planChanged = function(plan) {
|
||||
$scope.hasPaidPlan = plan && plan.price > 0;
|
||||
|
|
|
@ -1262,3 +1262,259 @@ RepositoryUsageChart.prototype.draw = function(container) {
|
|||
this.width_ = cw;
|
||||
this.drawInternal_();
|
||||
};
|
||||
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* A chart which displays the last seven days of actions in the account.
|
||||
*/
|
||||
function LogUsageChart(titleMap) {
|
||||
this.titleMap_ = titleMap;
|
||||
this.colorScale_ = d3.scale.category20();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds the D3-representation of the data.
|
||||
*/
|
||||
LogUsageChart.prototype.buildData_ = function(logs) {
|
||||
var parseDate = d3.time.format("%a, %d %b %Y %H:%M:%S GMT").parse
|
||||
|
||||
// Build entries for each kind of event that occurred, on each day. We have one
|
||||
// entry per {kind, day} pair.
|
||||
var map = {};
|
||||
var entries = [];
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var log = logs[i];
|
||||
var title = this.titleMap_[log.kind] || log.kind;
|
||||
var datetime = parseDate(log.datetime);
|
||||
var dateDay = datetime.getDate();
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay;
|
||||
}
|
||||
|
||||
var formatted = (datetime.getMonth() + 1) + '/' + dateDay;
|
||||
var adjusted = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate());
|
||||
var key = title + '_' + formatted;
|
||||
var found = map[key];
|
||||
if (!found) {
|
||||
found = {
|
||||
'kind': log.kind,
|
||||
'title': title,
|
||||
'adjusted': adjusted,
|
||||
'formatted': datetime.getDate(),
|
||||
'count': 0
|
||||
};
|
||||
|
||||
map[key] = found;
|
||||
entries.push(found);
|
||||
}
|
||||
found['count']++;
|
||||
}
|
||||
|
||||
this.entries_ = map;
|
||||
|
||||
// Build the data itself. We create a single entry for each possible kind of data, and then add (x, y) pairs
|
||||
// for the number of times that kind of event occurred on a particular day.
|
||||
var dataArray = [];
|
||||
var dataMap = {};
|
||||
var dateMap = {};
|
||||
|
||||
for (var i = 0; i < entries.length; ++i) {
|
||||
var entry = entries[i];
|
||||
var key = entry.title;
|
||||
var found = dataMap[key];
|
||||
if (!found) {
|
||||
found = {'key': key, 'values': [], 'kind': entry.kind};
|
||||
dataMap[key] = found;
|
||||
dataArray.push(found);
|
||||
}
|
||||
|
||||
found.values.push({
|
||||
'x': entry.adjusted,
|
||||
'y': entry.count
|
||||
});
|
||||
|
||||
dateMap[entry.adjusted.toString()] = entry.adjusted;
|
||||
}
|
||||
|
||||
// Note: nvd3 has a bug that causes d3 to fail if there is not an entry for every single
|
||||
// kind on each day that has data. Therefore, we pad those days with 0-length entries for each
|
||||
// kind.
|
||||
for (var i = 0; i < dataArray.length; ++i) {
|
||||
var datum = dataArray[i];
|
||||
for (var sDate in dateMap) {
|
||||
if (!dateMap.hasOwnProperty(sDate)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
var cDate = dateMap[sDate];
|
||||
var found = false;
|
||||
for (var j = 0; j < datum.values.length; ++j) {
|
||||
if (datum.values[j]['x'].getDate() == cDate.getDate()) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
datum.values.push({
|
||||
'x': cDate,
|
||||
'y': 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
datum.values.sort(function(a, b) {
|
||||
return a['x'].getDate() - b['x'].getDate();
|
||||
});
|
||||
}
|
||||
|
||||
return this.data_ = dataArray;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Renders the tooltip when hovering over an element in the chart.
|
||||
*/
|
||||
LogUsageChart.prototype.renderTooltip_ = function(d, e) {
|
||||
var key = d + '_' + e;
|
||||
var entry = this.entries_[key];
|
||||
if (!entry) {
|
||||
entry = {'count': 0};
|
||||
}
|
||||
|
||||
var s = entry.count == 1 ? '' : 's';
|
||||
return d + ' - ' + entry.count + ' time' + s + ' on ' + e;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns the color used in the chart for log entries of the given
|
||||
* kind.
|
||||
*/
|
||||
LogUsageChart.prototype.getColor = function(kind) {
|
||||
var colors = this.colorScale_.range();
|
||||
var index = 0;
|
||||
for (var i = 0; i < this.data_.length; ++i) {
|
||||
var datum = this.data_[i];
|
||||
var key = this.titleMap_[kind] || kind;
|
||||
if (datum.key == key) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return colors[index];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for when an element in the chart has been clicked.
|
||||
*/
|
||||
LogUsageChart.prototype.handleElementClicked_ = function(e) {
|
||||
var key = e.series.key;
|
||||
var kind = e.series.kind;
|
||||
var disabled = [];
|
||||
|
||||
var enabledCount = 0;
|
||||
var d = this.chart_.multibar.disabled();
|
||||
for (var i = 0; i < this.data_.length; ++i) {
|
||||
enabledCount += (d[i] ? 0 : 1);
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.data_.length; ++i) {
|
||||
disabled.push(enabledCount == 1 ? false : this.data_[i].key != key);
|
||||
}
|
||||
|
||||
var allowed = {};
|
||||
allowed[kind] = true;
|
||||
|
||||
this.chart_.dispatch.changeState({ 'disabled': disabled });
|
||||
$(this).trigger({
|
||||
'type': 'filteringChanged',
|
||||
'allowed': enabledCount == 1 ? null : allowed
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for when the state of the chart has changed.
|
||||
*/
|
||||
LogUsageChart.prototype.handleStateChange_ = function(e) {
|
||||
var allowed = {};
|
||||
var disabled = e.disabled;
|
||||
for (var i = 0; i < this.data_.length; ++i) {
|
||||
if (!disabled[i]) {
|
||||
allowed[this.data_[i].kind] = true;
|
||||
}
|
||||
}
|
||||
|
||||
$(this).trigger({
|
||||
'type': 'filteringChanged',
|
||||
'allowed': allowed
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Draws the chart in the given container element.
|
||||
*/
|
||||
LogUsageChart.prototype.draw = function(container, logData) {
|
||||
// Reset the container's contents.
|
||||
document.getElementById(container).innerHTML = '<svg></svg>';
|
||||
|
||||
// Returns a date offset from the given date by "days" Days.
|
||||
var offsetDate = function(d, days) {
|
||||
var copy = new Date(d.getTime());
|
||||
copy.setDate(copy.getDate() + days);
|
||||
return copy;
|
||||
};
|
||||
|
||||
var that = this;
|
||||
var data = this.buildData_(logData);
|
||||
nv.addGraph(function() {
|
||||
// Build the chart itself.
|
||||
var chart = nv.models.multiBarChart()
|
||||
.margin({top: 30, right: 30, bottom: 50, left: 30})
|
||||
.stacked(false)
|
||||
.staggerLabels(false)
|
||||
.tooltip(function(d, e) {
|
||||
return that.renderTooltip_(d, e);
|
||||
})
|
||||
.color(that.colorScale_.range())
|
||||
.groupSpacing(0.1);
|
||||
|
||||
chart.multibar.delay(0);
|
||||
|
||||
// Create the x-axis domain to encompass a week from today.
|
||||
var domain = [];
|
||||
var datetime = new Date();
|
||||
datetime = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate());
|
||||
for (var i = 7; i >= 0; --i) {
|
||||
domain.push(offsetDate(datetime, -1 * i));
|
||||
}
|
||||
|
||||
chart.xDomain(domain);
|
||||
|
||||
// Finish setting up the chart.
|
||||
chart.xAxis
|
||||
.tickFormat(d3.time.format("%m/%d"));
|
||||
|
||||
chart.yAxis
|
||||
.tickFormat(d3.format(',f'));
|
||||
|
||||
d3.select('#bar-chart svg')
|
||||
.datum(data)
|
||||
.transition()
|
||||
.duration(500)
|
||||
.call(chart);
|
||||
|
||||
nv.utils.windowResize(chart.update);
|
||||
|
||||
chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); });
|
||||
chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); });
|
||||
return that.chart_ = chart;
|
||||
});
|
||||
};
|
9275
static/lib/d3.js
vendored
Normal file
769
static/lib/nv.d3.css
Normal file
|
@ -0,0 +1,769 @@
|
|||
|
||||
/********************
|
||||
* HTML CSS
|
||||
*/
|
||||
|
||||
|
||||
.chartWrap {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/********************
|
||||
Box shadow and border radius styling
|
||||
*/
|
||||
.nvtooltip.with-3d-shadow, .with-3d-shadow .nvtooltip {
|
||||
-moz-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
box-shadow: 0 5px 10px rgba(0,0,0,.2);
|
||||
|
||||
-webkit-border-radius: 6px;
|
||||
-moz-border-radius: 6px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/********************
|
||||
* TOOLTIP CSS
|
||||
*/
|
||||
|
||||
.nvtooltip {
|
||||
position: absolute;
|
||||
background-color: rgba(255,255,255,1.0);
|
||||
padding: 1px;
|
||||
border: 1px solid rgba(0,0,0,.2);
|
||||
z-index: 10000;
|
||||
|
||||
font-family: Arial;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
pointer-events: none;
|
||||
|
||||
white-space: nowrap;
|
||||
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/*Give tooltips that old fade in transition by
|
||||
putting a "with-transitions" class on the container div.
|
||||
*/
|
||||
.nvtooltip.with-transitions, .with-transitions .nvtooltip {
|
||||
transition: opacity 250ms linear;
|
||||
-moz-transition: opacity 250ms linear;
|
||||
-webkit-transition: opacity 250ms linear;
|
||||
|
||||
transition-delay: 250ms;
|
||||
-moz-transition-delay: 250ms;
|
||||
-webkit-transition-delay: 250ms;
|
||||
}
|
||||
|
||||
.nvtooltip.x-nvtooltip,
|
||||
.nvtooltip.y-nvtooltip {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.nvtooltip h3 {
|
||||
margin: 0;
|
||||
padding: 4px 14px;
|
||||
line-height: 18px;
|
||||
font-weight: normal;
|
||||
background-color: rgba(247,247,247,0.75);
|
||||
text-align: center;
|
||||
|
||||
border-bottom: 1px solid #ebebeb;
|
||||
|
||||
-webkit-border-radius: 5px 5px 0 0;
|
||||
-moz-border-radius: 5px 5px 0 0;
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
.nvtooltip p {
|
||||
margin: 0;
|
||||
padding: 5px 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nvtooltip span {
|
||||
display: inline-block;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.nvtooltip table {
|
||||
margin: 6px;
|
||||
border-spacing:0;
|
||||
}
|
||||
|
||||
|
||||
.nvtooltip table td {
|
||||
padding: 2px 9px 2px 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nvtooltip table td.key {
|
||||
font-weight:normal;
|
||||
}
|
||||
.nvtooltip table td.value {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvtooltip table tr.highlight td {
|
||||
padding: 1px 9px 1px 0;
|
||||
border-bottom-style: solid;
|
||||
border-bottom-width: 1px;
|
||||
border-top-style: solid;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.nvtooltip table td.legend-color-guide div {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nvtooltip .footer {
|
||||
padding: 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
.nvtooltip-pending-removal {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
/********************
|
||||
* SVG CSS
|
||||
*/
|
||||
|
||||
|
||||
svg {
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
/* Trying to get SVG to act like a greedy block in all browsers */
|
||||
display: block;
|
||||
width:100%;
|
||||
height:100%;
|
||||
}
|
||||
|
||||
|
||||
svg text {
|
||||
font: normal 12px Arial;
|
||||
}
|
||||
|
||||
svg .title {
|
||||
font: bold 14px Arial;
|
||||
}
|
||||
|
||||
.nvd3 .nv-background {
|
||||
fill: white;
|
||||
fill-opacity: 0;
|
||||
/*
|
||||
pointer-events: none;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3.nv-noData {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Brush
|
||||
*/
|
||||
|
||||
.nv-brush .extent {
|
||||
fill-opacity: .125;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Legend
|
||||
*/
|
||||
|
||||
.nvd3 .nv-legend .nv-series {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nvd3 .nv-legend .disabled circle {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Axes
|
||||
*/
|
||||
.nvd3 .nv-axis {
|
||||
pointer-events:none;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-opacity: .75;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis path.domain {
|
||||
stroke-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis.nv-x path.domain {
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis line {
|
||||
fill: none;
|
||||
stroke: #e5e5e5;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis .zero line,
|
||||
/*this selector may not be necessary*/ .nvd3 .nv-axis line.zero {
|
||||
stroke-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3 .nv-axis .nv-axisMaxMin text {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvd3 .x .nv-axis .nv-axisMaxMin text,
|
||||
.nvd3 .x2 .nv-axis .nv-axisMaxMin text,
|
||||
.nvd3 .x3 .nv-axis .nv-axisMaxMin text {
|
||||
text-anchor: middle
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Brush
|
||||
*/
|
||||
|
||||
.nv-brush .resize path {
|
||||
fill: #eee;
|
||||
stroke: #666;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Bars
|
||||
*/
|
||||
|
||||
.nvd3 .nv-bars .negative rect {
|
||||
zfill: brown;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars rect {
|
||||
zfill: steelblue;
|
||||
fill-opacity: .75;
|
||||
|
||||
transition: fill-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars rect.hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars .hover rect {
|
||||
fill: lightblue;
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars text {
|
||||
fill: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
.nvd3 .nv-bars .hover text {
|
||||
fill: rgba(0,0,0,1);
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Bars
|
||||
*/
|
||||
|
||||
.nvd3 .nv-multibar .nv-groups rect,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups rect,
|
||||
.nvd3 .nv-discretebar .nv-groups rect {
|
||||
stroke-opacity: 0;
|
||||
|
||||
transition: fill-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear;
|
||||
}
|
||||
|
||||
.nvd3 .nv-multibar .nv-groups rect:hover,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups rect:hover,
|
||||
.nvd3 .nv-discretebar .nv-groups rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3 .nv-discretebar .nv-groups text,
|
||||
.nvd3 .nv-multibarHorizontal .nv-groups text {
|
||||
font-weight: bold;
|
||||
fill: rgba(0,0,0,1);
|
||||
stroke: rgba(0,0,0,0);
|
||||
}
|
||||
|
||||
/***********
|
||||
* Pie Chart
|
||||
*/
|
||||
|
||||
.nvd3.nv-pie path {
|
||||
stroke-opacity: 0;
|
||||
transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear, stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .nv-slice text {
|
||||
stroke: #000;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-pie path {
|
||||
stroke: #fff;
|
||||
stroke-width: 1px;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
.nvd3.nv-pie .hover path {
|
||||
fill-opacity: .7;
|
||||
}
|
||||
.nvd3.nv-pie .nv-label {
|
||||
pointer-events: none;
|
||||
}
|
||||
.nvd3.nv-pie .nv-label rect {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
/**********
|
||||
* Lines
|
||||
*/
|
||||
|
||||
.nvd3 .nv-groups path.nv-line {
|
||||
fill: none;
|
||||
stroke-width: 1.5px;
|
||||
/*
|
||||
stroke-linecap: round;
|
||||
shape-rendering: geometricPrecision;
|
||||
|
||||
transition: stroke-width 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear;
|
||||
|
||||
transition-delay: 250ms
|
||||
-moz-transition-delay: 250ms;
|
||||
-webkit-transition-delay: 250ms;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3 .nv-groups path.nv-line.nv-thin-line {
|
||||
stroke-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .nv-groups path.nv-area {
|
||||
stroke: none;
|
||||
/*
|
||||
stroke-linecap: round;
|
||||
shape-rendering: geometricPrecision;
|
||||
|
||||
stroke-width: 2.5px;
|
||||
transition: stroke-width 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear;
|
||||
|
||||
transition-delay: 250ms
|
||||
-moz-transition-delay: 250ms;
|
||||
-webkit-transition-delay: 250ms;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3 .nv-line.hover path {
|
||||
stroke-width: 6px;
|
||||
}
|
||||
|
||||
/*
|
||||
.nvd3.scatter .groups .point {
|
||||
fill-opacity: 0.1;
|
||||
stroke-opacity: 0.1;
|
||||
}
|
||||
*/
|
||||
|
||||
.nvd3.nv-line .nvd3.nv-scatter .nv-groups .nv-point {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-scatter.nv-single-point .nv-groups .nv-point {
|
||||
fill-opacity: .5 !important;
|
||||
stroke-opacity: .5 !important;
|
||||
}
|
||||
|
||||
|
||||
.with-transitions .nvd3 .nv-groups .nv-point {
|
||||
transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: stroke-width 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-scatter .nv-groups .nv-point.hover,
|
||||
.nvd3 .nv-groups .nv-point.hover {
|
||||
stroke-width: 7px;
|
||||
fill-opacity: .95 !important;
|
||||
stroke-opacity: .95 !important;
|
||||
}
|
||||
|
||||
|
||||
.nvd3 .nv-point-paths path {
|
||||
stroke: #aaa;
|
||||
stroke-opacity: 0;
|
||||
fill: #eee;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.nvd3 .nv-indexLine {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Distribution
|
||||
*/
|
||||
|
||||
.nvd3 .nv-distribution {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Scatter
|
||||
*/
|
||||
|
||||
/* **Attempting to remove this for useVoronoi(false), need to see if it's required anywhere
|
||||
.nvd3 .nv-groups .nv-point {
|
||||
pointer-events: none;
|
||||
}
|
||||
*/
|
||||
|
||||
.nvd3 .nv-groups .nv-point.hover {
|
||||
stroke-width: 20px;
|
||||
stroke-opacity: .5;
|
||||
}
|
||||
|
||||
.nvd3 .nv-scatter .nv-point.hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
.nv-group.hover .nv-point {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
/**********
|
||||
* Stacked Area
|
||||
*/
|
||||
|
||||
.nvd3.nv-stackedarea path.nv-area {
|
||||
fill-opacity: .7;
|
||||
/*
|
||||
stroke-opacity: .65;
|
||||
fill-opacity: 1;
|
||||
*/
|
||||
stroke-opacity: 0;
|
||||
|
||||
transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
-moz-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
-webkit-transition: fill-opacity 250ms linear, stroke-opacity 250ms linear;
|
||||
|
||||
/*
|
||||
transition-delay: 500ms;
|
||||
-moz-transition-delay: 500ms;
|
||||
-webkit-transition-delay: 500ms;
|
||||
*/
|
||||
|
||||
}
|
||||
|
||||
.nvd3.nv-stackedarea path.nv-area.hover {
|
||||
fill-opacity: .9;
|
||||
/*
|
||||
stroke-opacity: .85;
|
||||
*/
|
||||
}
|
||||
/*
|
||||
.d3stackedarea .groups path {
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
*/
|
||||
|
||||
|
||||
|
||||
.nvd3.nv-stackedarea .nv-groups .nv-point {
|
||||
stroke-opacity: 0;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
.nvd3.nv-stackedarea .nv-groups .nv-point.hover {
|
||||
stroke-width: 20px;
|
||||
stroke-opacity: .75;
|
||||
fill-opacity: 1;
|
||||
}*/
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Line Plus Bar
|
||||
*/
|
||||
|
||||
.nvd3.nv-linePlusBar .nv-bar rect {
|
||||
fill-opacity: .75;
|
||||
}
|
||||
|
||||
.nvd3.nv-linePlusBar .nv-bar rect:hover {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
|
||||
|
||||
/**********
|
||||
* Bullet
|
||||
*/
|
||||
|
||||
.nvd3.nv-bullet { font: 10px sans-serif; }
|
||||
.nvd3.nv-bullet .nv-measure { fill-opacity: .8; }
|
||||
.nvd3.nv-bullet .nv-measure:hover { fill-opacity: 1; }
|
||||
.nvd3.nv-bullet .nv-marker { stroke: #000; stroke-width: 2px; }
|
||||
.nvd3.nv-bullet .nv-markerTriangle { stroke: #000; fill: #fff; stroke-width: 1.5px; }
|
||||
.nvd3.nv-bullet .nv-tick line { stroke: #666; stroke-width: .5px; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s0 { fill: #eee; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s1 { fill: #ddd; }
|
||||
.nvd3.nv-bullet .nv-range.nv-s2 { fill: #ccc; }
|
||||
.nvd3.nv-bullet .nv-title { font-size: 14px; font-weight: bold; }
|
||||
.nvd3.nv-bullet .nv-subtitle { fill: #999; }
|
||||
|
||||
|
||||
.nvd3.nv-bullet .nv-range {
|
||||
fill: #bababa;
|
||||
fill-opacity: .4;
|
||||
}
|
||||
.nvd3.nv-bullet .nv-range:hover {
|
||||
fill-opacity: .7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Sparkline
|
||||
*/
|
||||
|
||||
.nvd3.nv-sparkline path {
|
||||
fill: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus g.nv-hoverValue {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-hoverValue line {
|
||||
stroke: #333;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus,
|
||||
.nvd3.nv-sparklineplus g {
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.nvd3 .nv-hoverArea {
|
||||
fill-opacity: 0;
|
||||
stroke-opacity: 0;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-xValue,
|
||||
.nvd3.nv-sparklineplus .nv-yValue {
|
||||
/*
|
||||
stroke: #666;
|
||||
*/
|
||||
stroke-width: 0;
|
||||
font-size: .9em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-yValue {
|
||||
stroke: #f66;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-maxValue {
|
||||
stroke: #2ca02c;
|
||||
fill: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-minValue {
|
||||
stroke: #d62728;
|
||||
fill: #d62728;
|
||||
}
|
||||
|
||||
.nvd3.nv-sparklineplus .nv-currentValue {
|
||||
/*
|
||||
stroke: #444;
|
||||
fill: #000;
|
||||
*/
|
||||
font-weight: bold;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
/**********
|
||||
* historical stock
|
||||
*/
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.hover {
|
||||
stroke-width: 4px;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.positive {
|
||||
stroke: #2ca02c;
|
||||
}
|
||||
|
||||
.nvd3.nv-ohlcBar .nv-ticks .nv-tick.negative {
|
||||
stroke: #d62728;
|
||||
}
|
||||
|
||||
.nvd3.nv-historicalStockChart .nv-axis .nv-axislabel {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.nvd3.nv-historicalStockChart .nv-dragTarget {
|
||||
fill-opacity: 0;
|
||||
stroke: none;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.nvd3 .nv-brush .extent {
|
||||
/*
|
||||
cursor: ew-resize !important;
|
||||
*/
|
||||
fill-opacity: 0 !important;
|
||||
}
|
||||
|
||||
.nvd3 .nv-brushBackground rect {
|
||||
stroke: #000;
|
||||
stroke-width: .4;
|
||||
fill: #fff;
|
||||
fill-opacity: .7;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**********
|
||||
* Indented Tree
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* TODO: the following 3 selectors are based on classes used in the example. I should either make them standard and leave them here, or move to a CSS file not included in the library
|
||||
*/
|
||||
.nvd3.nv-indentedtree .name {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree .clickable {
|
||||
color: #08C;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree span.clickable:hover {
|
||||
color: #005580;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
|
||||
.nvd3.nv-indentedtree .nv-childrenCount {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree .nv-treeicon {
|
||||
cursor: pointer;
|
||||
/*
|
||||
cursor: n-resize;
|
||||
*/
|
||||
}
|
||||
|
||||
.nvd3.nv-indentedtree .nv-treeicon.nv-folded {
|
||||
cursor: pointer;
|
||||
/*
|
||||
cursor: s-resize;
|
||||
*/
|
||||
}
|
||||
|
||||
/**********
|
||||
* Parallel Coordinates
|
||||
*/
|
||||
|
||||
.nvd3 .background path {
|
||||
fill: none;
|
||||
stroke: #ccc;
|
||||
stroke-opacity: .4;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .foreground path {
|
||||
fill: none;
|
||||
stroke: steelblue;
|
||||
stroke-opacity: .7;
|
||||
}
|
||||
|
||||
.nvd3 .brush .extent {
|
||||
fill-opacity: .3;
|
||||
stroke: #fff;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .axis line, .axis path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
.nvd3 .axis text {
|
||||
text-shadow: 0 1px 0 #fff;
|
||||
}
|
||||
|
||||
/****
|
||||
Interactive Layer
|
||||
*/
|
||||
.nvd3 .nv-interactiveGuideLine {
|
||||
pointer-events:none;
|
||||
}
|
||||
.nvd3 line.nv-guideline {
|
||||
stroke: #ccc;
|
||||
}
|
14346
static/lib/nv.d3.js
Normal file
6
static/lib/nv.d3.min.js
vendored
Normal file
|
@ -94,10 +94,10 @@ Email: my@email.com</pre>
|
|||
</div>
|
||||
|
||||
<a name="#post-hook"></a>
|
||||
<h3>Using push web hooks <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">
|
||||
A repository can have one or more <b>push web hooks</b> setup, which will be invoked whenever <u>a successful push occurs</u>. Web hooks can be managed from the repository's admin interface.
|
||||
<br><br> A web hook will be invoked
|
||||
A repository can have one or more <b>push webhooks</b> setup, which will be invoked whenever <u>a successful push occurs</u>. Webhooks can be managed from the repository's admin interface.
|
||||
<br><br> A webhook will be invoked
|
||||
as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
|
||||
<pre>
|
||||
{
|
||||
|
|
|
@ -7,13 +7,14 @@
|
|||
</div>
|
||||
|
||||
<div class="org-admin container" ng-show="!loading && organization">
|
||||
<div class="organization-header" organization="organization"></div>
|
||||
<div class="organization-header" organization="organization" clickable="true"></div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Side tabs -->
|
||||
<div class="col-md-2">
|
||||
<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><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="#members" ng-click="loadMembers()">Members</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
|
||||
|
@ -34,6 +35,11 @@
|
|||
<div class="robots-manager" organization="organization"></div>
|
||||
</div>
|
||||
|
||||
<!-- Logs tab -->
|
||||
<div id="logs" class="tab-pane">
|
||||
<div class="logs-view" organization="organization" visible="logsShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Options tab -->
|
||||
<div id="billingoptions" class="tab-pane">
|
||||
<div class="billing-options" organization="organization"></div>
|
||||
|
@ -113,14 +119,13 @@
|
|||
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<th>User</th>
|
||||
<th>User/Robot Account</th>
|
||||
<th>Teams</th>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="memberInfo in (membersFound | filter:search | limitTo:50)">
|
||||
<td>
|
||||
<i class="fa fa-user"></i>
|
||||
{{ memberInfo.username }}
|
||||
<span class="entity-reference" name="memberInfo.username" isrobot="memberInfo.is_robot"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="team-link" ng-repeat="team in memberInfo.teams">
|
||||
|
|
|
@ -20,9 +20,10 @@
|
|||
<div class="col-md-2">
|
||||
<ul class="nav nav-pills nav-stacked">
|
||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#permissions">Permissions</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#webhook" ng-click="loadWebhooks()">Web Hooks</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#webhook" ng-click="loadWebhooks()">Webhooks</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#publicprivate">Public/Private</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -30,6 +31,11 @@
|
|||
<div class="col-md-10">
|
||||
|
||||
<div class="tab-content">
|
||||
<!-- Logs tab -->
|
||||
<div id="logs" class="tab-pane">
|
||||
<div class="logs-view" repository="repo" visible="logsShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Permissions tab -->
|
||||
<div id="permissions" class="tab-pane active">
|
||||
<!-- User Access Permissions -->
|
||||
|
@ -52,8 +58,7 @@
|
|||
<!-- Team Permissions -->
|
||||
<tr ng-repeat="(name, permission) in permissions['team']">
|
||||
<td class="team entity">
|
||||
<i class="fa fa-group" title="Team" bs-tooltip="tooltip.title"></i>
|
||||
<span><a href="/organization/{{ repo.namespace }}/teams/{{ name }}">{{name}}</a></span>
|
||||
<span class="entity-reference" orgname="repo.namespace" team="name"></span>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
<span class="role-group" current-role="permission.role" role-changed="setRole(role, name, 'team')" roles="roles"></span>
|
||||
|
@ -69,9 +74,7 @@
|
|||
<!-- User Permissions -->
|
||||
<tr ng-repeat="(name, permission) in permissions['user']">
|
||||
<td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}">
|
||||
<i class="fa fa-user" ng-show="!permission.is_robot" title="User" bs-tooltip="tooltip.title"></i>
|
||||
<i class="fa fa-wrench" ng-show="permission.is_robot" title="Robot Account" bs-tooltip="tooltip.title"></i>
|
||||
<span class="prefix">{{getPrefix(name)}}</span><span>{{getShortenedName(name)}}</span>
|
||||
<span class="entity-reference" name="name" isrobot="permission.is_robot"></span>
|
||||
<i class="fa fa-exclamation-triangle" ng-show="permission.is_org_member === false" data-trigger="hover" bs-popover="{'content': 'This user is not a member of the organization'}"></i>
|
||||
</td>
|
||||
<td class="user-permissions">
|
||||
|
@ -149,19 +152,19 @@
|
|||
<!-- Webhook tab -->
|
||||
<div id="webhook" class="tab-pane">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Push Web Hooks
|
||||
<div class="panel-heading">Push Webhooks
|
||||
<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 class="panel-body" ng-show="webhooksLoading">
|
||||
Loading web hooks: <i class="fa fa-spinner fa-spin fa-2x" style="vertical-align: middle; margin-left: 4px"></i>
|
||||
Loading webhooks: <i class="fa fa-spinner fa-spin fa-2x" style="vertical-align: middle; margin-left: 4px"></i>
|
||||
</div>
|
||||
|
||||
<div class="panel-body" ng-show="!webhooksLoading">
|
||||
<table class="permissions" ng-form="newWebhookForm">
|
||||
<thead>
|
||||
<tr>
|
||||
<td style="width: 500px;">Web Hook URL</td>
|
||||
<td style="width: 500px;">Webhook URL</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -172,13 +175,13 @@
|
|||
<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 Web Hook"></i>
|
||||
<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 web hook url..." ng-model="newWebhook.url" required>
|
||||
<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>
|
||||
|
@ -188,7 +191,7 @@
|
|||
</table>
|
||||
|
||||
<div class="right-info">
|
||||
Quay will <b>POST</b> to these web hooks 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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -20,9 +20,7 @@
|
|||
<table class="permissions">
|
||||
<tr ng-repeat="(name, member) in members">
|
||||
<td class="user entity">
|
||||
<i class="fa fa-user" ng-show="!member.is_robot"></i>
|
||||
<i class="fa fa-wrench" ng-show="member.is_robot"></i>
|
||||
<span>{{ member.username }}</span>
|
||||
<span class="entity-reference" name="member.username" isrobot="member.is_robot"></span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="delete-ui" tabindex="0" title="Remove User" ng-show="canEditMembers">
|
||||
|
|
|
@ -14,13 +14,17 @@
|
|||
|
||||
{% block added_stylesheets %}
|
||||
<link rel="stylesheet" href="/static/lib/browser-chrome.css">
|
||||
<link rel="stylesheet" href="/static/lib/nv.d3.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block added_dependencies %}
|
||||
<script src="https://checkout.stripe.com/checkout.js"></script>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/moment.js/2.2.1/moment.min.js"></script>
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.3/d3.min.js"></script>
|
||||
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.3.10/d3.min.js"></script>
|
||||
<script src="static/lib/nv.d3.min.js"></script>
|
||||
|
||||
<script src="static/lib/ZeroClipboard.min.js"></script>
|
||||
|
||||
<script src="static/lib/d3-tip.js" charset="utf-8"></script>
|
||||
|
|
|
@ -20,6 +20,7 @@ FAKE_IMAGE_ID = str(uuid4())
|
|||
FAKE_TAG_NAME = str(uuid4())
|
||||
FAKE_USERNAME = str(uuid4())
|
||||
FAKE_TOKEN = str(uuid4())
|
||||
FAKE_WEBHOOK = str(uuid4())
|
||||
|
||||
NEW_ORG_REPO_DETAILS = {
|
||||
'repository': str(uuid4()),
|
||||
|
@ -29,9 +30,9 @@ NEW_ORG_REPO_DETAILS = {
|
|||
}
|
||||
|
||||
NEW_USER_DETAILS = {
|
||||
'username': 'bob',
|
||||
'username': 'bobby',
|
||||
'password': 'password',
|
||||
'email': 'jake@devtable.com',
|
||||
'email': 'bobby@tables.com',
|
||||
}
|
||||
|
||||
SEND_RECOVERY_DETAILS = {
|
||||
|
@ -217,6 +218,34 @@ def build_specs():
|
|||
admin_code=201).set_method('POST')
|
||||
.set_data_from_obj(CREATE_BUILD_DETAILS)),
|
||||
|
||||
TestSpec(url_for('create_webhook', repository=PUBLIC_REPO),
|
||||
admin_code=403).set_method('POST'),
|
||||
TestSpec(url_for('create_webhook',
|
||||
repository=ORG_REPO)).set_method('POST'),
|
||||
TestSpec(url_for('create_webhook',
|
||||
repository=PRIVATE_REPO)).set_method('POST'),
|
||||
|
||||
TestSpec(url_for('get_webhook', repository=PUBLIC_REPO,
|
||||
public_id=FAKE_WEBHOOK), admin_code=403),
|
||||
TestSpec(url_for('get_webhook', repository=ORG_REPO,
|
||||
public_id=FAKE_WEBHOOK), admin_code=400),
|
||||
TestSpec(url_for('get_webhook', repository=PRIVATE_REPO,
|
||||
public_id=FAKE_WEBHOOK), admin_code=400),
|
||||
|
||||
TestSpec(url_for('list_webhooks', repository=PUBLIC_REPO), admin_code=403),
|
||||
TestSpec(url_for('list_webhooks', repository=ORG_REPO)),
|
||||
TestSpec(url_for('list_webhooks', repository=PRIVATE_REPO)),
|
||||
|
||||
TestSpec(url_for('delete_webhook', repository=PUBLIC_REPO,
|
||||
public_id=FAKE_WEBHOOK),
|
||||
admin_code=403).set_method('DELETE'),
|
||||
TestSpec(url_for('delete_webhook', repository=ORG_REPO,
|
||||
public_id=FAKE_WEBHOOK),
|
||||
admin_code=400).set_method('DELETE'),
|
||||
TestSpec(url_for('delete_webhook', repository=PRIVATE_REPO,
|
||||
public_id=FAKE_WEBHOOK),
|
||||
admin_code=400).set_method('DELETE'),
|
||||
|
||||
TestSpec(url_for('list_repository_images', repository=PUBLIC_REPO),
|
||||
200, 200, 200, 200),
|
||||
TestSpec(url_for('list_repository_images', repository=ORG_REPO),
|
||||
|
@ -382,6 +411,12 @@ def build_specs():
|
|||
TestSpec(url_for('get_subscription'), 401, 200, 200, 200),
|
||||
|
||||
TestSpec(url_for('get_org_subscription', orgname=ORG)),
|
||||
|
||||
TestSpec(url_for('repo_logs_api', repository=PUBLIC_REPO), admin_code=403),
|
||||
TestSpec(url_for('repo_logs_api', repository=ORG_REPO)),
|
||||
TestSpec(url_for('repo_logs_api', repository=PRIVATE_REPO)),
|
||||
|
||||
TestSpec(url_for('org_logs_api', orgname=ORG)),
|
||||
]
|
||||
|
||||
|
||||
|
|
|
@ -42,8 +42,6 @@ class _SpecTestBuilder(type):
|
|||
rv = c.open(url, **open_kwargs)
|
||||
msg = '%s %s: %s expected: %s' % (open_kwargs['method'], url,
|
||||
rv.status_code, expected_status)
|
||||
if rv.status_code != expected_status:
|
||||
print msg
|
||||
self.assertEqual(rv.status_code, expected_status, msg)
|
||||
return test
|
||||
|
||||
|
|
|
@ -3,8 +3,9 @@ import logging
|
|||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
def render_snapshot(url):
|
||||
logger.info('Snapshotting url: %s' % url)
|
||||
|
|