Merge branch 'pagesnew' into star
This commit is contained in:
commit
3dbaafbd60
148 changed files with 10997 additions and 10293 deletions
63
auth/auth.py
63
auth/auth.py
|
@ -6,6 +6,7 @@ from datetime import datetime
|
|||
from flask import request, session
|
||||
from flask.ext.principal import identity_changed, Identity
|
||||
from flask.ext.login import current_user
|
||||
from flask.sessions import SecureCookieSessionInterface, BadSignature
|
||||
from base64 import b64decode
|
||||
|
||||
import scopes
|
||||
|
@ -22,6 +23,9 @@ from util.http import abort
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SIGNATURE_PREFIX = 'signature='
|
||||
|
||||
|
||||
def _load_user_from_cookie():
|
||||
if not current_user.is_anonymous():
|
||||
try:
|
||||
|
@ -69,7 +73,7 @@ def _validate_and_apply_oauth_token(token):
|
|||
identity_changed.send(app, identity=new_identity)
|
||||
|
||||
|
||||
def process_basic_auth(auth):
|
||||
def _process_basic_auth(auth):
|
||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||
if normalized[0].lower() != 'basic' or len(normalized) != 2:
|
||||
logger.debug('Invalid basic auth format.')
|
||||
|
@ -127,44 +131,41 @@ def process_basic_auth(auth):
|
|||
logger.debug('Basic auth present but could not be validated.')
|
||||
|
||||
|
||||
def process_token(auth):
|
||||
def generate_signed_token(grants):
|
||||
ser = SecureCookieSessionInterface().get_signing_serializer(app)
|
||||
data_to_sign = {
|
||||
'grants': grants,
|
||||
}
|
||||
|
||||
encrypted = ser.dumps(data_to_sign)
|
||||
return '{0}{1}'.format(SIGNATURE_PREFIX, encrypted)
|
||||
|
||||
|
||||
def _process_signed_grant(auth):
|
||||
normalized = [part.strip() for part in auth.split(' ') if part]
|
||||
if normalized[0].lower() != 'token' or len(normalized) != 2:
|
||||
logger.debug('Not an auth token: %s' % auth)
|
||||
logger.debug('Not a token: %s', auth)
|
||||
return
|
||||
|
||||
token_details = normalized[1].split(',')
|
||||
if not normalized[1].startswith(SIGNATURE_PREFIX):
|
||||
logger.debug('Not a signed grant token: %s', auth)
|
||||
return
|
||||
|
||||
if len(token_details) != 1:
|
||||
logger.warning('Invalid token format: %s' % auth)
|
||||
abort(401, message='Invalid token format: %(auth)s', issue='invalid-auth-token', auth=auth)
|
||||
|
||||
def safe_get(lst, index, default_value):
|
||||
try:
|
||||
return lst[index]
|
||||
except IndexError:
|
||||
return default_value
|
||||
|
||||
token_vals = {val[0]: safe_get(val, 1, '') for val in
|
||||
(detail.split('=') for detail in token_details)}
|
||||
|
||||
if 'signature' not in token_vals:
|
||||
logger.warning('Token does not contain signature: %s' % auth)
|
||||
abort(401, message='Token does not contain a valid signature: %(auth)s',
|
||||
issue='invalid-auth-token', auth=auth)
|
||||
encrypted = normalized[1][len(SIGNATURE_PREFIX):]
|
||||
ser = SecureCookieSessionInterface().get_signing_serializer(app)
|
||||
|
||||
try:
|
||||
token_data = model.load_token_data(token_vals['signature'])
|
||||
|
||||
except model.InvalidTokenException:
|
||||
logger.warning('Token could not be validated: %s', token_vals['signature'])
|
||||
abort(401, message='Token could not be validated: %(auth)s', issue='invalid-auth-token',
|
||||
token_data = ser.loads(encrypted, max_age=app.config['SIGNED_GRANT_EXPIRATION_SEC'])
|
||||
except BadSignature:
|
||||
logger.warning('Signed grant could not be validated: %s', encrypted)
|
||||
abort(401, message='Signed grant could not be validated: %(auth)s', issue='invalid-auth-token',
|
||||
auth=auth)
|
||||
|
||||
logger.debug('Successfully validated token: %s', token_data.code)
|
||||
set_validated_token(token_data)
|
||||
logger.debug('Successfully validated signed grant with data: %s', token_data)
|
||||
|
||||
identity_changed.send(app, identity=Identity(token_data.code, 'token'))
|
||||
loaded_identity = Identity(None, 'signed_grant')
|
||||
loaded_identity.provides.update(token_data['grants'])
|
||||
identity_changed.send(app, identity=loaded_identity)
|
||||
|
||||
|
||||
def process_oauth(func):
|
||||
|
@ -192,8 +193,8 @@ def process_auth(func):
|
|||
|
||||
if auth:
|
||||
logger.debug('Validating auth header: %s' % auth)
|
||||
process_token(auth)
|
||||
process_basic_auth(auth)
|
||||
_process_signed_grant(auth)
|
||||
_process_basic_auth(auth)
|
||||
else:
|
||||
logger.debug('No auth header.')
|
||||
|
||||
|
|
|
@ -57,6 +57,14 @@ SCOPE_MAX_USER_ROLES.update({
|
|||
})
|
||||
|
||||
|
||||
def repository_read_grant(namespace, repository):
|
||||
return _RepositoryNeed(namespace, repository, 'read')
|
||||
|
||||
|
||||
def repository_write_grant(namespace, repository):
|
||||
return _RepositoryNeed(namespace, repository, 'write')
|
||||
|
||||
|
||||
class QuayDeferredPermissionUser(Identity):
|
||||
def __init__(self, uuid, auth_type, scopes):
|
||||
super(QuayDeferredPermissionUser, self).__init__(uuid, auth_type)
|
||||
|
@ -226,6 +234,11 @@ class ViewTeamPermission(Permission):
|
|||
team_member, admin_org)
|
||||
|
||||
|
||||
class AlwaysFailPermission(Permission):
|
||||
def can(self):
|
||||
return False
|
||||
|
||||
|
||||
@identity_loaded.connect_via(app)
|
||||
def on_identity_loaded(sender, identity):
|
||||
logger.debug('Identity loaded: %s' % identity)
|
||||
|
@ -249,5 +262,8 @@ def on_identity_loaded(sender, identity):
|
|||
logger.debug('Delegate token added permission: {0}'.format(repo_grant))
|
||||
identity.provides.add(repo_grant)
|
||||
|
||||
elif identity.auth_type == 'signed_grant':
|
||||
logger.debug('Loaded signed grants identity')
|
||||
|
||||
else:
|
||||
logger.error('Unknown identity auth type: %s', identity.auth_type)
|
||||
|
|
|
@ -16,6 +16,11 @@ gzip_types text/plain text/xml text/css
|
|||
text/javascript application/x-javascript
|
||||
application/octet-stream;
|
||||
|
||||
map $proxy_protocol_addr $proper_forwarded_for {
|
||||
"" $proxy_add_x_forwarded_for;
|
||||
default $proxy_protocol_addr;
|
||||
}
|
||||
|
||||
upstream web_app_server {
|
||||
server unix:/tmp/gunicorn_web.sock fail_timeout=0;
|
||||
}
|
||||
|
@ -33,3 +38,4 @@ upstream build_manager_controller_server {
|
|||
upstream build_manager_websocket_server {
|
||||
server localhost:8787;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,6 @@ include root-base.conf;
|
|||
|
||||
http {
|
||||
include http-base.conf;
|
||||
|
||||
include rate-limiting.conf;
|
||||
|
||||
server {
|
||||
|
|
|
@ -4,9 +4,7 @@ include root-base.conf;
|
|||
|
||||
http {
|
||||
include http-base.conf;
|
||||
|
||||
include hosted-http-base.conf;
|
||||
|
||||
include rate-limiting.conf;
|
||||
|
||||
server {
|
||||
|
@ -25,8 +23,7 @@ http {
|
|||
|
||||
server {
|
||||
include proxy-protocol.conf;
|
||||
|
||||
include proxy-server-base.conf;
|
||||
include server-base.conf;
|
||||
|
||||
listen 8443 default proxy_protocol;
|
||||
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
# vim: ft=nginx
|
||||
|
||||
client_body_temp_path /var/log/nginx/client_body 1 2;
|
||||
server_name _;
|
||||
|
||||
keepalive_timeout 5;
|
||||
|
||||
if ($args ~ "_escaped_fragment_") {
|
||||
rewrite ^ /snapshot$uri;
|
||||
}
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
|
||||
proxy_set_header Transfer-Encoding $http_transfer_encoding;
|
||||
|
||||
location / {
|
||||
proxy_pass http://web_app_server;
|
||||
|
||||
limit_req zone=webapp burst=25 nodelay;
|
||||
}
|
||||
|
||||
location /realtime {
|
||||
proxy_pass http://web_app_server;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /v1/repositories/ {
|
||||
proxy_buffering off;
|
||||
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_pass http://registry_app_server;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
|
||||
client_max_body_size 20G;
|
||||
|
||||
limit_req zone=repositories burst=5 nodelay;
|
||||
}
|
||||
|
||||
location /v1/ {
|
||||
proxy_buffering off;
|
||||
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_pass http://registry_app_server;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
|
||||
client_max_body_size 20G;
|
||||
}
|
||||
|
||||
location /c1/ {
|
||||
proxy_buffering off;
|
||||
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_pass http://verbs_app_server;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
|
||||
limit_req zone=api burst=5 nodelay;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
# checks for static file, if not found proxy to app
|
||||
alias /static/;
|
||||
}
|
||||
|
||||
location /v1/_ping {
|
||||
add_header Content-Type text/plain;
|
||||
add_header X-Docker-Registry-Version 0.6.0;
|
||||
add_header X-Docker-Registry-Standalone 0;
|
||||
return 200 'true';
|
||||
}
|
||||
|
||||
location ~ ^/b1/controller(/?)(.*) {
|
||||
proxy_pass http://build_manager_controller_server/$2;
|
||||
}
|
||||
|
||||
location ~ ^/b1/socket(/?)(.*) {
|
||||
proxy_pass http://build_manager_websocket_server/$2;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
|
@ -1,7 +1,16 @@
|
|||
# vim: ft=nginx
|
||||
|
||||
# Check the Authorization header and, if it is empty, use their proxy protocol
|
||||
# IP, else use the header as their unique identifier for rate limiting.
|
||||
# Enterprise users will never be using proxy protocol, thus the value will be
|
||||
# empty string. This means they will not get rate limited.
|
||||
map $http_authorization $registry_bucket {
|
||||
"" $proxy_protocol_addr;
|
||||
default $http_authorization;
|
||||
}
|
||||
|
||||
limit_req_zone $proxy_protocol_addr zone=webapp:10m rate=25r/s;
|
||||
limit_req_zone $proxy_protocol_addr zone=repositories:10m rate=1r/s;
|
||||
limit_req_zone $proxy_protocol_addr zone=api:10m rate=1r/s;
|
||||
limit_req_zone $registry_bucket zone=repositories:10m rate=1r/s;
|
||||
limit_req_status 429;
|
||||
limit_req_log_level warn;
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
client_body_temp_path /var/log/nginx/client_body 1 2;
|
||||
server_name _;
|
||||
|
||||
set_real_ip_from 172.17.0.0/16;
|
||||
real_ip_header X-Forwarded-For;
|
||||
|
||||
keepalive_timeout 5;
|
||||
|
||||
if ($args ~ "_escaped_fragment_") {
|
||||
rewrite ^ /snapshot$uri;
|
||||
}
|
||||
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-For $proper_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
|
@ -21,6 +18,8 @@ proxy_set_header Transfer-Encoding $http_transfer_encoding;
|
|||
|
||||
location / {
|
||||
proxy_pass http://web_app_server;
|
||||
|
||||
limit_req zone=webapp;
|
||||
}
|
||||
|
||||
location /realtime {
|
||||
|
@ -29,6 +28,18 @@ location /realtime {
|
|||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /v1/repositories/ {
|
||||
proxy_buffering off;
|
||||
|
||||
proxy_request_buffering off;
|
||||
|
||||
proxy_pass http://registry_app_server;
|
||||
proxy_read_timeout 2000;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
|
||||
limit_req zone=repositories;
|
||||
}
|
||||
|
||||
location /v1/ {
|
||||
proxy_buffering off;
|
||||
|
||||
|
@ -47,6 +58,8 @@ location /c1/ {
|
|||
|
||||
proxy_pass http://verbs_app_server;
|
||||
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
|
||||
|
||||
limit_req zone=api;
|
||||
}
|
||||
|
||||
location /static/ {
|
||||
|
|
|
@ -197,4 +197,7 @@ class DefaultConfig(object):
|
|||
SYSTEM_SERVICE_BLACKLIST = []
|
||||
|
||||
# Temporary tag expiration in seconds, this may actually be longer based on GC policy
|
||||
PUSH_TEMP_TAG_EXPIRATION_SEC = 60 * 60
|
||||
PUSH_TEMP_TAG_EXPIRATION_SEC = 60 * 60 # One hour per layer
|
||||
|
||||
# Signed registry grant token expiration in seconds
|
||||
SIGNED_GRANT_EXPIRATION_SEC = 60 * 60 * 24 # One day to complete a push/pull
|
||||
|
|
|
@ -9,12 +9,13 @@ from collections import OrderedDict
|
|||
from data import model
|
||||
from data.model import oauth
|
||||
from app import app, authentication, userevents, storage
|
||||
from auth.auth import process_auth
|
||||
from auth.auth import process_auth, generate_signed_token
|
||||
from auth.auth_context import get_authenticated_user, get_validated_token, get_validated_oauth_token
|
||||
from util.names import parse_repository_name
|
||||
from util.useremails import send_confirmation_email
|
||||
from auth.permissions import (ModifyRepositoryPermission, UserAdminPermission,
|
||||
ReadRepositoryPermission, CreateRepositoryPermission)
|
||||
ReadRepositoryPermission, CreateRepositoryPermission,
|
||||
AlwaysFailPermission, repository_read_grant, repository_write_grant)
|
||||
|
||||
from util.http import abort
|
||||
from endpoints.trackhelper import track_and_log
|
||||
|
@ -26,7 +27,13 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
index = Blueprint('index', __name__)
|
||||
|
||||
def generate_headers(role='read'):
|
||||
|
||||
class GrantType(object):
|
||||
READ_REPOSITORY = 'read'
|
||||
WRITE_REPOSITORY = 'write'
|
||||
|
||||
|
||||
def generate_headers(scope=GrantType.READ_REPOSITORY):
|
||||
def decorator_method(f):
|
||||
@wraps(f)
|
||||
def wrapper(namespace, repository, *args, **kwargs):
|
||||
|
@ -35,12 +42,6 @@ def generate_headers(role='read'):
|
|||
# Setting session namespace and repository
|
||||
session['namespace'] = namespace
|
||||
session['repository'] = repository
|
||||
|
||||
if get_authenticated_user():
|
||||
session['username'] = get_authenticated_user().username
|
||||
else:
|
||||
session.pop('username', None)
|
||||
|
||||
# We run our index and registry on the same hosts for now
|
||||
registry_server = urlparse.urlparse(request.url).netloc
|
||||
response.headers['X-Docker-Endpoints'] = registry_server
|
||||
|
@ -48,16 +49,23 @@ def generate_headers(role='read'):
|
|||
has_token_request = request.headers.get('X-Docker-Token', '')
|
||||
|
||||
if has_token_request:
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if repo:
|
||||
token = model.create_access_token(repo, role, 'pushpull-token')
|
||||
token_str = 'signature=%s' % token.code
|
||||
response.headers['WWW-Authenticate'] = token_str
|
||||
response.headers['X-Docker-Token'] = token_str
|
||||
else:
|
||||
logger.info('Token request in non-existing repo: %s/%s' %
|
||||
(namespace, repository))
|
||||
permission = AlwaysFailPermission()
|
||||
grants = []
|
||||
if scope == GrantType.READ_REPOSITORY:
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
grants.append(repository_read_grant(namespace, repository))
|
||||
elif scope == GrantType.WRITE_REPOSITORY:
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
grants.append(repository_write_grant(namespace, repository))
|
||||
|
||||
if permission.can():
|
||||
# Generate a signed grant which expires here
|
||||
signature = generate_signed_token(grants)
|
||||
response.headers['WWW-Authenticate'] = signature
|
||||
response.headers['X-Docker-Token'] = signature
|
||||
else:
|
||||
logger.warning('Registry request with invalid credentials on repository: %s/%s',
|
||||
namespace, repository)
|
||||
return response
|
||||
return wrapper
|
||||
return decorator_method
|
||||
|
@ -186,7 +194,7 @@ def update_user(username):
|
|||
@index.route('/repositories/<path:repository>', methods=['PUT'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(role='write')
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
def create_repository(namespace, repository):
|
||||
logger.debug('Parsing image descriptions')
|
||||
image_descriptions = json.loads(request.data.decode('utf8'))
|
||||
|
@ -228,7 +236,7 @@ def create_repository(namespace, repository):
|
|||
@index.route('/repositories/<path:repository>/images', methods=['PUT'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(role='write')
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
def update_images(namespace, repository):
|
||||
permission = ModifyRepositoryPermission(namespace, repository)
|
||||
|
||||
|
@ -273,7 +281,7 @@ def update_images(namespace, repository):
|
|||
@index.route('/repositories/<path:repository>/images', methods=['GET'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(role='read')
|
||||
@generate_headers(scope=GrantType.READ_REPOSITORY)
|
||||
def get_repository_images(namespace, repository):
|
||||
permission = ReadRepositoryPermission(namespace, repository)
|
||||
|
||||
|
@ -307,7 +315,7 @@ def get_repository_images(namespace, repository):
|
|||
@index.route('/repositories/<path:repository>/images', methods=['DELETE'])
|
||||
@process_auth
|
||||
@parse_repository_name
|
||||
@generate_headers(role='write')
|
||||
@generate_headers(scope=GrantType.WRITE_REPOSITORY)
|
||||
def delete_repository_images(namespace, repository):
|
||||
abort(501, 'Not Implemented', issue='not-implemented')
|
||||
|
||||
|
|
|
@ -455,14 +455,15 @@ def put_image_json(namespace, repository, image_id):
|
|||
issue='invalid-request', image_id=image_id)
|
||||
|
||||
logger.debug('Looking up repo image')
|
||||
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if repo is None:
|
||||
abort(404, 'Repository does not exist: %(namespace)s/%(repository)s', issue='no-repo',
|
||||
namespace=namespace, repository=repository)
|
||||
|
||||
repo_image = model.get_repo_image_extended(namespace, repository, image_id)
|
||||
if not repo_image:
|
||||
logger.debug('Image not found, creating image')
|
||||
repo = model.get_repository(namespace, repository)
|
||||
if repo is None:
|
||||
abort(404, 'Repository does not exist: %(namespace)s/%(repository)s', issue='no-repo',
|
||||
namespace=namespace, repository=repository)
|
||||
|
||||
username = get_authenticated_user() and get_authenticated_user().username
|
||||
repo_image = model.find_create_or_link_image(image_id, repo, username, {},
|
||||
store.preferred_locations[0])
|
||||
|
|
|
@ -173,6 +173,12 @@ def security():
|
|||
return index('')
|
||||
|
||||
|
||||
@web.route('/__exp/<expname>')
|
||||
@no_cache
|
||||
def exp(expname):
|
||||
return index('')
|
||||
|
||||
|
||||
@web.route('/v1')
|
||||
@web.route('/v1/')
|
||||
@no_cache
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<div class="prototype-manager-element">
|
||||
<div class="quay-spinner" ng-show="loading"></div>
|
||||
|
||||
<div class="container" ng-show="!loading">
|
||||
<div class="cor-container" ng-show="!loading">
|
||||
<div class="alert alert-info">
|
||||
Default permissions provide a means of specifying <span class="context-tooltip" data-title="By default, all repositories have the creating user added as an 'Admin'" bs-tooltip="tooltip.title">additional</span> permissions that should be granted automatically to a repository <strong>when it is created</strong>.
|
||||
</div>
|
||||
|
|
38
static/js/angular-route-builder.js
vendored
Normal file
38
static/js/angular-route-builder.js
vendored
Normal file
|
@ -0,0 +1,38 @@
|
|||
var AngularRouteBuilder = function(routeProvider, pages, profiles, currentProfile) {
|
||||
this.routeProvider = routeProvider;
|
||||
this.pages = pages;
|
||||
this.profiles = profiles;
|
||||
|
||||
for (var i = 0; i < profiles.length; ++i) {
|
||||
var current = profiles[i];
|
||||
if (current.id == currentProfile) {
|
||||
this.profiles = this.profiles.slice(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
AngularRouteBuilder.prototype.otherwise = function(options) {
|
||||
this.routeProvider.otherwise(options);
|
||||
};
|
||||
|
||||
AngularRouteBuilder.prototype.route = function(path, pagename) {
|
||||
// Lookup the page, matching our lists of profiles.
|
||||
var pair = this.pages.get(pagename, this.profiles);
|
||||
if (!pair) {
|
||||
throw Error('Unknown page: ' + pagename);
|
||||
}
|
||||
|
||||
// Create the route.
|
||||
var foundProfile = pair[0];
|
||||
var page = pair[1];
|
||||
var templateUrl = foundProfile.templatePath + page.templateName;
|
||||
|
||||
var options = jQuery.extend({}, page.flags || {});
|
||||
options['templateUrl'] = templateUrl;
|
||||
options['reloadOnSearch'] = false;
|
||||
options['controller'] = page.controller;
|
||||
|
||||
this.routeProvider.when(path, options);
|
||||
return this;
|
||||
};
|
6968
static/js/app.js
6968
static/js/app.js
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,272 +0,0 @@
|
|||
function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize,
|
||||
ansi2html, AngularViewArray, AngularPollChannel) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
// Watch for changes to the current parameter.
|
||||
$scope.$on('$routeUpdate', function(){
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.builds = null;
|
||||
$scope.pollChannel = null;
|
||||
$scope.buildDialogShowCounter = 0;
|
||||
|
||||
$scope.showNewBuildDialog = function() {
|
||||
$scope.buildDialogShowCounter++;
|
||||
};
|
||||
|
||||
$scope.handleBuildStarted = function(newBuild) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
$scope.builds.unshift(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
};
|
||||
|
||||
$scope.adjustLogHeight = function() {
|
||||
var triggerOffset = 0;
|
||||
if ($scope.currentBuild && $scope.currentBuild.trigger) {
|
||||
triggerOffset = 85;
|
||||
}
|
||||
$('.build-logs').height($(window).height() - 415 - triggerOffset);
|
||||
};
|
||||
|
||||
$scope.askRestartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal({});
|
||||
};
|
||||
|
||||
$scope.askCancelBuild = function(build) {
|
||||
bootbox.confirm('Are you sure you want to cancel this build?', function(r) {
|
||||
if (r) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.cancelRepoBuild(null, params).then(function() {
|
||||
if (!$scope.builds) { return; }
|
||||
$scope.builds.splice($.inArray(build, $scope.builds), 1);
|
||||
|
||||
if ($scope.builds.length) {
|
||||
$scope.currentBuild = $scope.builds[0];
|
||||
} else {
|
||||
$scope.currentBuild = null;
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot cancel build'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restartBuild = function(build) {
|
||||
$('#confirmRestartBuildModal').modal('hide');
|
||||
|
||||
var subdirectory = '';
|
||||
if (build['job_config']) {
|
||||
subdirectory = build['job_config']['build_subdir'] || '';
|
||||
}
|
||||
|
||||
var data = {
|
||||
'file_id': build['resource_key'],
|
||||
'subdirectory': subdirectory,
|
||||
'docker_tags': build['job_config']['docker_tags']
|
||||
};
|
||||
|
||||
if (build['pull_robot']) {
|
||||
data['pull_robot'] = build['pull_robot']['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(newBuild) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
$scope.builds.unshift(newBuild);
|
||||
$scope.setCurrentBuild(newBuild['id'], true);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.hasLogs = function(container) {
|
||||
return container.logs.hasEntries;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuild = function(buildId, opt_updateURL) {
|
||||
if (!$scope.builds) { return; }
|
||||
|
||||
// Find the build.
|
||||
for (var i = 0; i < $scope.builds.length; ++i) {
|
||||
if ($scope.builds[i].id == buildId) {
|
||||
$scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.processANSI = function(message, container) {
|
||||
var filter = container.logs._filter = (container.logs._filter || ansi2html.create());
|
||||
|
||||
// Note: order is important here.
|
||||
var setup = filter.getSetupHtml();
|
||||
var stream = filter.addInputToStream(message);
|
||||
var teardown = filter.getTeardownHtml();
|
||||
return setup + stream + teardown;
|
||||
};
|
||||
|
||||
$scope.setCurrentBuildInternal = function(index, build, opt_updateURL) {
|
||||
if (build == $scope.currentBuild) { return; }
|
||||
|
||||
$scope.logEntries = null;
|
||||
$scope.logStartIndex = null;
|
||||
$scope.currentParentEntry = null;
|
||||
|
||||
$scope.currentBuild = build;
|
||||
|
||||
if (opt_updateURL) {
|
||||
if (build) {
|
||||
$location.search('current', build.id);
|
||||
} else {
|
||||
$location.search('current', null);
|
||||
}
|
||||
}
|
||||
|
||||
// Timeout needed to ensure the log element has been created
|
||||
// before its height is adjusted.
|
||||
setTimeout(function() {
|
||||
$scope.adjustLogHeight();
|
||||
}, 1);
|
||||
|
||||
// Stop any existing polling.
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
// Create a new channel for polling the build status and logs.
|
||||
var conductStatusAndLogRequest = function(callback) {
|
||||
getBuildStatusAndLogs(build, callback);
|
||||
};
|
||||
|
||||
$scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */);
|
||||
$scope.pollChannel.start();
|
||||
};
|
||||
|
||||
var processLogs = function(logs, startIndex, endIndex) {
|
||||
if (!$scope.logEntries) { $scope.logEntries = []; }
|
||||
|
||||
// If the start index given is less than that requested, then we've received a larger
|
||||
// pool of logs, and we need to only consider the new ones.
|
||||
if (startIndex < $scope.logStartIndex) {
|
||||
logs = logs.slice($scope.logStartIndex - startIndex);
|
||||
}
|
||||
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var entry = logs[i];
|
||||
var type = entry['type'] || 'entry';
|
||||
if (type == 'command' || type == 'phase' || type == 'error') {
|
||||
entry['logs'] = AngularViewArray.create();
|
||||
entry['index'] = $scope.logStartIndex + i;
|
||||
|
||||
$scope.logEntries.push(entry);
|
||||
$scope.currentParentEntry = entry;
|
||||
} else if ($scope.currentParentEntry) {
|
||||
$scope.currentParentEntry['logs'].push(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return endIndex;
|
||||
};
|
||||
|
||||
var getBuildStatusAndLogs = function(build, callback) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': build.id
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
if (build != $scope.currentBuild) { callback(false); return; }
|
||||
|
||||
// Note: We use extend here rather than replacing as Angular is depending on the
|
||||
// root build object to remain the same object.
|
||||
var matchingBuilds = $.grep($scope.builds, function(elem) {
|
||||
return elem['id'] == resp['id']
|
||||
});
|
||||
|
||||
var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null;
|
||||
if (currentBuild) {
|
||||
currentBuild = $.extend(true, currentBuild, resp);
|
||||
} else {
|
||||
currentBuild = resp;
|
||||
$scope.builds.push(currentBuild);
|
||||
}
|
||||
|
||||
// Load the updated logs for the build.
|
||||
var options = {
|
||||
'start': $scope.logStartIndex
|
||||
};
|
||||
|
||||
ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) {
|
||||
if (build != $scope.currentBuild) { callback(false); return; }
|
||||
|
||||
// Process the logs we've received.
|
||||
$scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']);
|
||||
|
||||
// If the build status is an error, open the last two log entries.
|
||||
if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) {
|
||||
var openLogEntries = function(entry) {
|
||||
if (entry.logs) {
|
||||
entry.logs.setVisible(true);
|
||||
}
|
||||
};
|
||||
|
||||
openLogEntries($scope.logEntries[$scope.logEntries.length - 2]);
|
||||
openLogEntries($scope.logEntries[$scope.logEntries.length - 1]);
|
||||
}
|
||||
|
||||
// If the build phase is an error or a complete, then we mark the channel
|
||||
// as closed.
|
||||
callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete');
|
||||
}, function() {
|
||||
callback(false);
|
||||
});
|
||||
}, function() {
|
||||
callback(false);
|
||||
});
|
||||
};
|
||||
|
||||
var fetchRepository = function() {
|
||||
var params = {'repository': namespace + '/' + name};
|
||||
$rootScope.title = 'Loading Repository...';
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
if (!repo.can_write) {
|
||||
$rootScope.title = 'Unknown builds';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$rootScope.title = 'Repository Builds';
|
||||
$scope.repo = repo;
|
||||
|
||||
getBuildInfo();
|
||||
});
|
||||
};
|
||||
|
||||
var getBuildInfo = function(repo) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.getRepoBuilds(null, params).then(function(resp) {
|
||||
$scope.builds = resp.builds;
|
||||
|
||||
if ($location.search().current) {
|
||||
$scope.setCurrentBuild($location.search().current, false);
|
||||
} else if ($scope.builds.length > 0) {
|
||||
$scope.setCurrentBuild($scope.builds[0].id, true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
fetchRepository();
|
||||
}
|
|
@ -1,282 +0,0 @@
|
|||
function SetupCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, CoreDialog) {
|
||||
if (!Features.SUPER_USERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$';
|
||||
|
||||
$scope.validateHostname = function(hostname) {
|
||||
if (hostname.indexOf('127.0.0.1') == 0 || hostname.indexOf('localhost') == 0) {
|
||||
return 'Please specify a non-localhost hostname. "localhost" will refer to the container, not your machine.'
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// Note: The values of the enumeration are important for isStepFamily. For example,
|
||||
// *all* states under the "configuring db" family must start with "config-db".
|
||||
$scope.States = {
|
||||
// Loading the state of the product.
|
||||
'LOADING': 'loading',
|
||||
|
||||
// The configuration directory is missing.
|
||||
'MISSING_CONFIG_DIR': 'missing-config-dir',
|
||||
|
||||
// The config.yaml exists but it is invalid.
|
||||
'INVALID_CONFIG': 'config-invalid',
|
||||
|
||||
// DB is being configured.
|
||||
'CONFIG_DB': 'config-db',
|
||||
|
||||
// DB information is being validated.
|
||||
'VALIDATING_DB': 'config-db-validating',
|
||||
|
||||
// DB information is being saved to the config.
|
||||
'SAVING_DB': 'config-db-saving',
|
||||
|
||||
// A validation error occurred with the database.
|
||||
'DB_ERROR': 'config-db-error',
|
||||
|
||||
// Database is being setup.
|
||||
'DB_SETUP': 'setup-db',
|
||||
|
||||
// Database setup has succeeded.
|
||||
'DB_SETUP_SUCCESS': 'setup-db-success',
|
||||
|
||||
// An error occurred when setting up the database.
|
||||
'DB_SETUP_ERROR': 'setup-db-error',
|
||||
|
||||
// The container is being restarted for the database changes.
|
||||
'DB_RESTARTING': 'setup-db-restarting',
|
||||
|
||||
// A superuser is being configured.
|
||||
'CREATE_SUPERUSER': 'create-superuser',
|
||||
|
||||
// The superuser is being created.
|
||||
'CREATING_SUPERUSER': 'create-superuser-creating',
|
||||
|
||||
// An error occurred when setting up the superuser.
|
||||
'SUPERUSER_ERROR': 'create-superuser-error',
|
||||
|
||||
// The superuser was created successfully.
|
||||
'SUPERUSER_CREATED': 'create-superuser-created',
|
||||
|
||||
// General configuration is being setup.
|
||||
'CONFIG': 'config',
|
||||
|
||||
// The configuration is fully valid.
|
||||
'VALID_CONFIG': 'valid-config',
|
||||
|
||||
// The container is being restarted for the configuration changes.
|
||||
'CONFIG_RESTARTING': 'config-restarting',
|
||||
|
||||
// The product is ready for use.
|
||||
'READY': 'ready'
|
||||
}
|
||||
|
||||
$scope.csrf_token = window.__token;
|
||||
$scope.currentStep = $scope.States.LOADING;
|
||||
$scope.errors = {};
|
||||
$scope.stepProgress = [];
|
||||
$scope.hasSSL = false;
|
||||
$scope.hostname = null;
|
||||
|
||||
$scope.$watch('currentStep', function(currentStep) {
|
||||
$scope.stepProgress = $scope.getProgress(currentStep);
|
||||
|
||||
switch (currentStep) {
|
||||
case $scope.States.CONFIG:
|
||||
$('#setupModal').modal('hide');
|
||||
break;
|
||||
|
||||
case $scope.States.MISSING_CONFIG_DIR:
|
||||
$scope.showMissingConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.INVALID_CONFIG:
|
||||
$scope.showInvalidConfigDialog();
|
||||
break;
|
||||
|
||||
case $scope.States.DB_SETUP:
|
||||
$scope.performDatabaseSetup();
|
||||
// Fall-through.
|
||||
|
||||
case $scope.States.CREATE_SUPERUSER:
|
||||
case $scope.States.DB_RESTARTING:
|
||||
case $scope.States.CONFIG_DB:
|
||||
case $scope.States.VALID_CONFIG:
|
||||
case $scope.States.READY:
|
||||
$('#setupModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
$scope.restartContainer = function(state) {
|
||||
$scope.currentStep = state;
|
||||
ContainerService.restartContainer(function() {
|
||||
$scope.checkStatus()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showSuperuserPanel = function() {
|
||||
$('#setupModal').modal('hide');
|
||||
var prefix = $scope.hasSSL ? 'https' : 'http';
|
||||
var hostname = $scope.hostname;
|
||||
window.location = prefix + '://' + hostname + '/superuser';
|
||||
};
|
||||
|
||||
$scope.configurationSaved = function(config) {
|
||||
$scope.hasSSL = config['PREFERRED_URL_SCHEME'] == 'https';
|
||||
$scope.hostname = config['SERVER_HOSTNAME'];
|
||||
$scope.currentStep = $scope.States.VALID_CONFIG;
|
||||
};
|
||||
|
||||
$scope.getProgress = function(step) {
|
||||
var isStep = $scope.isStep;
|
||||
var isStepFamily = $scope.isStepFamily;
|
||||
var States = $scope.States;
|
||||
|
||||
return [
|
||||
isStepFamily(step, States.CONFIG_DB),
|
||||
isStepFamily(step, States.DB_SETUP),
|
||||
isStep(step, States.DB_RESTARTING),
|
||||
isStepFamily(step, States.CREATE_SUPERUSER),
|
||||
isStep(step, States.CONFIG),
|
||||
isStep(step, States.VALID_CONFIG),
|
||||
isStep(step, States.CONFIG_RESTARTING),
|
||||
isStep(step, States.READY)
|
||||
];
|
||||
};
|
||||
|
||||
$scope.isStepFamily = function(step, family) {
|
||||
if (!step) { return false; }
|
||||
return step.indexOf(family) == 0;
|
||||
};
|
||||
|
||||
$scope.isStep = function(step) {
|
||||
for (var i = 1; i < arguments.length; ++i) {
|
||||
if (arguments[i] == step) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.showInvalidConfigDialog = function() {
|
||||
var message = "The <code>config.yaml</code> file found in <code>conf/stack</code> could not be parsed."
|
||||
var title = "Invalid configuration file";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
|
||||
$scope.showMissingConfigDialog = function() {
|
||||
var message = "A volume should be mounted into the container at <code>/conf/stack</code>: " +
|
||||
"<br><br><pre>docker run -v /path/to/config:/conf/stack</pre>" +
|
||||
"<br>Once fixed, restart the container. For more information, " +
|
||||
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
|
||||
"Read the Setup Guide</a>"
|
||||
|
||||
var title = "Missing configuration volume";
|
||||
CoreDialog.fatal(title, message);
|
||||
};
|
||||
|
||||
$scope.parseDbUri = function(value) {
|
||||
if (!value) { return null; }
|
||||
|
||||
// Format: mysql+pymysql://<username>:<url escaped password>@<hostname>/<database_name>
|
||||
var uri = URI(value);
|
||||
return {
|
||||
'kind': uri.protocol(),
|
||||
'username': uri.username(),
|
||||
'password': uri.password(),
|
||||
'server': uri.host(),
|
||||
'database': uri.path() ? uri.path().substr(1) : ''
|
||||
};
|
||||
};
|
||||
|
||||
$scope.serializeDbUri = function(fields) {
|
||||
if (!fields['server']) { return ''; }
|
||||
|
||||
try {
|
||||
if (!fields['server']) { return ''; }
|
||||
if (!fields['database']) { return ''; }
|
||||
|
||||
var uri = URI();
|
||||
uri = uri && uri.host(fields['server']);
|
||||
uri = uri && uri.protocol(fields['kind']);
|
||||
uri = uri && uri.username(fields['username']);
|
||||
uri = uri && uri.password(fields['password']);
|
||||
uri = uri && uri.path('/' + (fields['database'] || ''));
|
||||
uri = uri && uri.toString();
|
||||
} catch (ex) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return uri;
|
||||
};
|
||||
|
||||
$scope.createSuperUser = function() {
|
||||
$scope.currentStep = $scope.States.CREATING_SUPERUSER;
|
||||
ApiService.scCreateInitialSuperuser($scope.superUser, null).then(function(resp) {
|
||||
UserService.load();
|
||||
$scope.checkStatus();
|
||||
}, function(resp) {
|
||||
$scope.currentStep = $scope.States.SUPERUSER_ERROR;
|
||||
$scope.errors.SuperuserCreationError = ApiService.getErrorMessage(resp, 'Could not create superuser');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performDatabaseSetup = function() {
|
||||
$scope.currentStep = $scope.States.DB_SETUP;
|
||||
ApiService.scSetupDatabase(null, null).then(function(resp) {
|
||||
if (resp['error']) {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_ERROR;
|
||||
$scope.errors.DatabaseSetupError = resp['error'];
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_SETUP_SUCCESS;
|
||||
}
|
||||
}, ApiService.errorDisplay('Could not setup database. Please report this to support.'))
|
||||
};
|
||||
|
||||
$scope.validateDatabase = function() {
|
||||
$scope.currentStep = $scope.States.VALIDATING_DB;
|
||||
$scope.databaseInvalid = null;
|
||||
|
||||
var data = {
|
||||
'config': {
|
||||
'DB_URI': $scope.databaseUri
|
||||
},
|
||||
'hostname': window.location.host
|
||||
};
|
||||
|
||||
var params = {
|
||||
'service': 'database'
|
||||
};
|
||||
|
||||
ApiService.scValidateConfig(data, params).then(function(resp) {
|
||||
var status = resp.status;
|
||||
|
||||
if (status) {
|
||||
$scope.currentStep = $scope.States.SAVING_DB;
|
||||
ApiService.scUpdateConfig(data, null).then(function(resp) {
|
||||
$scope.checkStatus();
|
||||
}, ApiService.errorDisplay('Cannot update config. Please report this to support'));
|
||||
} else {
|
||||
$scope.currentStep = $scope.States.DB_ERROR;
|
||||
$scope.errors.DatabaseValidationError = resp.reason;
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot validate database. Please report this to support'));
|
||||
};
|
||||
|
||||
$scope.checkStatus = function() {
|
||||
ContainerService.checkStatus(function(resp) {
|
||||
$scope.currentStep = resp['status'];
|
||||
}, $scope.hasSSL);
|
||||
};
|
||||
|
||||
// Load the initial status.
|
||||
$scope.checkStatus();
|
||||
}
|
|
@ -1,229 +0,0 @@
|
|||
function SuperUserAdminCtrl($scope, $timeout, ApiService, Features, UserService, ContainerService, AngularPollChannel, CoreDialog) {
|
||||
if (!Features.SUPER_USERS) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.configStatus = null;
|
||||
$scope.requiresRestart = null;
|
||||
$scope.logsCounter = 0;
|
||||
$scope.newUser = {};
|
||||
$scope.createdUser = null;
|
||||
$scope.systemUsage = null;
|
||||
$scope.debugServices = null;
|
||||
$scope.debugLogs = null;
|
||||
$scope.pollChannel = null;
|
||||
$scope.logsScrolled = false;
|
||||
$scope.csrf_token = encodeURIComponent(window.__token);
|
||||
$scope.dashboardActive = false;
|
||||
|
||||
$scope.setDashboardActive = function(active) {
|
||||
$scope.dashboardActive = active;
|
||||
};
|
||||
|
||||
$scope.configurationSaved = function() {
|
||||
$scope.requiresRestart = true;
|
||||
};
|
||||
|
||||
$scope.showCreateUser = function() {
|
||||
$scope.createdUser = null;
|
||||
$('#createUserModal').modal('show');
|
||||
};
|
||||
|
||||
$scope.viewSystemLogs = function(service) {
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
$scope.debugService = service;
|
||||
$scope.debugLogs = null;
|
||||
|
||||
$scope.pollChannel = AngularPollChannel.create($scope, $scope.loadServiceLogs, 2 * 1000 /* 2s */);
|
||||
$scope.pollChannel.start();
|
||||
};
|
||||
|
||||
$scope.loadServiceLogs = function(callback) {
|
||||
if (!$scope.debugService) { return; }
|
||||
|
||||
var params = {
|
||||
'service': $scope.debugService
|
||||
};
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot load system logs. Please contact support.',
|
||||
function() {
|
||||
callback(false);
|
||||
})
|
||||
|
||||
ApiService.getSystemLogs(null, params, /* background */true).then(function(resp) {
|
||||
$scope.debugLogs = resp['logs'];
|
||||
callback(true);
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.loadDebugServices = function() {
|
||||
if ($scope.pollChannel) {
|
||||
$scope.pollChannel.stop();
|
||||
}
|
||||
|
||||
$scope.debugService = null;
|
||||
|
||||
ApiService.listSystemLogServices().then(function(resp) {
|
||||
$scope.debugServices = resp['services'];
|
||||
}, ApiService.errorDisplay('Cannot load system logs. Please contact support.'))
|
||||
};
|
||||
|
||||
$scope.getUsage = function() {
|
||||
if ($scope.systemUsage) { return; }
|
||||
|
||||
ApiService.getSystemUsage().then(function(resp) {
|
||||
$scope.systemUsage = resp;
|
||||
}, ApiService.errorDisplay('Cannot load system usage. Please contact support.'))
|
||||
}
|
||||
|
||||
$scope.loadUsageLogs = function() {
|
||||
$scope.logsCounter++;
|
||||
};
|
||||
|
||||
$scope.loadUsers = function() {
|
||||
if ($scope.users) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loadUsersInternal();
|
||||
};
|
||||
|
||||
$scope.loadUsersInternal = function() {
|
||||
ApiService.listAllUsers().then(function(resp) {
|
||||
$scope.users = resp['users'];
|
||||
$scope.showInterface = true;
|
||||
}, function(resp) {
|
||||
$scope.users = [];
|
||||
$scope.usersError = resp['data']['message'] || resp['data']['error_description'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showChangePassword = function(user) {
|
||||
$scope.userToChange = user;
|
||||
$('#changePasswordModal').modal({});
|
||||
};
|
||||
|
||||
$scope.createUser = function() {
|
||||
$scope.creatingUser = true;
|
||||
$scope.createdUser = null;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create user', function() {
|
||||
$scope.creatingUser = false;
|
||||
$('#createUserModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.createInstallUser($scope.newUser, null).then(function(resp) {
|
||||
$scope.creatingUser = false;
|
||||
$scope.newUser = {};
|
||||
$scope.createdUser = resp;
|
||||
$scope.loadUsersInternal();
|
||||
}, errorHandler)
|
||||
};
|
||||
|
||||
$scope.showDeleteUser = function(user) {
|
||||
if (user.username == UserService.currentUser().username) {
|
||||
bootbox.dialog({
|
||||
"message": 'Cannot delete yourself!',
|
||||
"title": "Cannot delete user",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.userToDelete = user;
|
||||
$('#confirmDeleteUserModal').modal({});
|
||||
};
|
||||
|
||||
$scope.changeUserPassword = function(user) {
|
||||
$('#changePasswordModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
var data = {
|
||||
'password': user.password
|
||||
};
|
||||
|
||||
ApiService.changeInstallUser(data, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, ApiService.errorDisplay('Could not change user'));
|
||||
};
|
||||
|
||||
$scope.deleteUser = function(user) {
|
||||
$('#confirmDeleteUserModal').modal('hide');
|
||||
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
ApiService.deleteInstallUser(null, params).then(function(resp) {
|
||||
$scope.loadUsersInternal();
|
||||
}, ApiService.errorDisplay('Cannot delete user'));
|
||||
};
|
||||
|
||||
$scope.sendRecoveryEmail = function(user) {
|
||||
var params = {
|
||||
'username': user.username
|
||||
};
|
||||
|
||||
ApiService.sendInstallUserRecoveryEmail(null, params).then(function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": "A recovery email has been sent to " + resp['email'],
|
||||
"title": "Recovery email sent",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}, ApiService.errorDisplay('Cannot send recovery email'))
|
||||
};
|
||||
|
||||
$scope.restartContainer = function() {
|
||||
$('#restartingContainerModal').modal({
|
||||
keyboard: false,
|
||||
backdrop: 'static'
|
||||
});
|
||||
|
||||
ContainerService.restartContainer(function() {
|
||||
$scope.checkStatus()
|
||||
});
|
||||
};
|
||||
|
||||
$scope.checkStatus = function() {
|
||||
ContainerService.checkStatus(function(resp) {
|
||||
$('#restartingContainerModal').modal('hide');
|
||||
$scope.configStatus = resp['status'];
|
||||
$scope.requiresRestart = resp['requires_restart'];
|
||||
|
||||
if ($scope.configStatus == 'ready') {
|
||||
$scope.loadUsers();
|
||||
} else {
|
||||
var message = "Installation of this product has not yet been completed." +
|
||||
"<br><br>Please read the " +
|
||||
"<a href='https://coreos.com/docs/enterprise-registry/initial-setup/'>" +
|
||||
"Setup Guide</a>"
|
||||
|
||||
var title = "Installation Incomplete";
|
||||
CoreDialog.fatal(title, message);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Load the initial status.
|
||||
$scope.checkStatus();
|
||||
}
|
|
@ -548,6 +548,8 @@ angular.module("core-ui", [])
|
|||
},
|
||||
controller: function($rootScope, $scope, $element) {
|
||||
$scope.$watch('progress', function(progress) {
|
||||
if (!progress) { return; }
|
||||
|
||||
var index = 0;
|
||||
for (var i = 0; i < progress.length; ++i) {
|
||||
if (progress[i]) {
|
||||
|
|
14
static/js/directives/fallback-src.js
Normal file
14
static/js/directives/fallback-src.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Adds a fallback-src attribute, which is used as the source for an <img> tag if the main
|
||||
* image fails to load.
|
||||
*/
|
||||
angular.module('quay').directive('fallbackSrc', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function postLink(scope, element, attributes) {
|
||||
element.bind('error', function() {
|
||||
angular.element(this).attr("src", attributes.fallbackSrc);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
18
static/js/directives/file-present.js
Normal file
18
static/js/directives/file-present.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* Sets the 'filePresent' value on the scope if a file on the marked <input type="file"> exists.
|
||||
*/
|
||||
angular.module('quay').directive("filePresent", [function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
scope: {
|
||||
'filePresent': "="
|
||||
},
|
||||
link: function (scope, element, attributes) {
|
||||
element.bind("change", function (changeEvent) {
|
||||
scope.$apply(function() {
|
||||
scope.filePresent = changeEvent.target.files.length > 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}]);
|
12
static/js/directives/filters/bytes.js
Normal file
12
static/js/directives/filters/bytes.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* Filter which displays bytes with suffixes.
|
||||
*/
|
||||
angular.module('quay').filter('bytes', function() {
|
||||
return function(bytes, precision) {
|
||||
if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown';
|
||||
if (typeof precision === 'undefined') precision = 1;
|
||||
var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'],
|
||||
number = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number];
|
||||
}
|
||||
});
|
23
static/js/directives/filters/regex.js
Normal file
23
static/js/directives/filters/regex.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* Regular expression filter.
|
||||
*/
|
||||
angular.module('quay').filter('regex', function() {
|
||||
return function(input, regex) {
|
||||
if (!regex) { return []; }
|
||||
|
||||
try {
|
||||
var patt = new RegExp(regex);
|
||||
} catch (ex) {
|
||||
return [];
|
||||
}
|
||||
|
||||
var out = [];
|
||||
for (var i = 0; i < input.length; ++i){
|
||||
var m = input[i].match(patt);
|
||||
if (m && m[0].length == input[i].length) {
|
||||
out.push(input[i]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
};
|
||||
});
|
8
static/js/directives/filters/reverse.js
Normal file
8
static/js/directives/filters/reverse.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/**
|
||||
* Reversing filter.
|
||||
*/
|
||||
angular.module('quay').filter('reverse', function() {
|
||||
return function(items) {
|
||||
return items.slice().reverse();
|
||||
};
|
||||
});
|
19
static/js/directives/filters/visible-log-filter.js
Normal file
19
static/js/directives/filters/visible-log-filter.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* Filter for hiding logs that don't meet the allowed predicate.
|
||||
*/
|
||||
angular.module('quay').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;
|
||||
};
|
||||
});
|
37
static/js/directives/focusable-popover-content.js
Normal file
37
static/js/directives/focusable-popover-content.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* An element which, when used to display content inside a popover, hide the popover once
|
||||
* the content loses focus.
|
||||
*/
|
||||
angular.module('quay').directive('focusablePopoverContent', ['$timeout', '$popover', function ($timeout, $popover) {
|
||||
return {
|
||||
restrict: "A",
|
||||
link: function (scope, element, attrs) {
|
||||
$body = $('body');
|
||||
var hide = function() {
|
||||
$body.off('click');
|
||||
|
||||
if (!scope) { return; }
|
||||
scope.$apply(function() {
|
||||
if (!scope || !$scope.$hide) { return; }
|
||||
scope.$hide();
|
||||
});
|
||||
};
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
$body.off('click');
|
||||
});
|
||||
|
||||
$timeout(function() {
|
||||
$body.on('click', function(evt) {
|
||||
var target = evt.target;
|
||||
var isPanelMember = $(element).has(target).length > 0 || target == element;
|
||||
if (!isPanelMember) {
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
$(element).find('input').focus();
|
||||
}, 100);
|
||||
}
|
||||
};
|
||||
}]);
|
16
static/js/directives/match.js
Normal file
16
static/js/directives/match.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* Adds a 'match' attribute that ensures that a form field's value matches another field's
|
||||
* value.
|
||||
*/
|
||||
angular.module('quay').directive('match', function($parse) {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elem, attrs, ctrl) {
|
||||
scope.$watch(function() {
|
||||
return $parse(attrs.match)(scope) === ctrl.$modelValue;
|
||||
}, function(currentValue) {
|
||||
ctrl.$setValidity('mismatch', currentValue);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
7
static/js/directives/ng-blur.js
Normal file
7
static/js/directives/ng-blur.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
angular.module('quay').directive('ngBlur', function() {
|
||||
return function( scope, elem, attrs ) {
|
||||
elem.bind('blur', function() {
|
||||
scope.$apply(attrs.ngBlur);
|
||||
});
|
||||
};
|
||||
});
|
15
static/js/directives/ng-if-media.js
Normal file
15
static/js/directives/ng-if-media.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
/**
|
||||
* Adds an ng-if-media attribute that evaluates a media query and, if false, removes the element.
|
||||
*/
|
||||
angular.module('quay').directive('ngIfMedia', function ($animate, AngularHelper) {
|
||||
return {
|
||||
transclude: 'element',
|
||||
priority: 600,
|
||||
terminal: true,
|
||||
restrict: 'A',
|
||||
link: AngularHelper.buildConditionalLinker($animate, 'ngIfMedia', function(value) {
|
||||
return window.matchMedia(value).matches;
|
||||
})
|
||||
};
|
||||
});
|
||||
|
10
static/js/directives/ng-visible.js
Normal file
10
static/js/directives/ng-visible.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* Adds an ng-visible attribute that hides an element if the expression evaluates to false.
|
||||
*/
|
||||
angular.module('quay').directive('ngVisible', function () {
|
||||
return function (scope, element, attr) {
|
||||
scope.$watch(attr.ngVisible, function (visible) {
|
||||
element.css('visibility', visible ? 'visible' : 'hidden');
|
||||
});
|
||||
};
|
||||
});
|
20
static/js/directives/onresize.js
Normal file
20
static/js/directives/onresize.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* Adds an onresize event attribtue that gets invokved when the size of the window changes.
|
||||
*/
|
||||
angular.module('quay').directive('onresize', function ($window, $parse) {
|
||||
return function (scope, element, attr) {
|
||||
var fn = $parse(attr.onresize);
|
||||
|
||||
var notifyResized = function() {
|
||||
scope.$apply(function () {
|
||||
fn(scope);
|
||||
});
|
||||
};
|
||||
|
||||
angular.element($window).on('resize', null, notifyResized);
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
angular.element($window).off('resize', null, notifyResized);
|
||||
});
|
||||
};
|
||||
});
|
170
static/js/directives/quay-layout.js
Normal file
170
static/js/directives/quay-layout.js
Normal file
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Directives which show, hide, include or otherwise mutate the DOM based on Features and Config.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Adds a quay-require attribute includes an element in the DOM iff the features specified are true.
|
||||
*/
|
||||
angular.module('quay').directive('quayRequire', function ($animate, Features, AngularHelper) {
|
||||
return {
|
||||
transclude: 'element',
|
||||
priority: 600,
|
||||
terminal: true,
|
||||
restrict: 'A',
|
||||
link: AngularHelper.buildConditionalLinker($animate, 'quayRequire', function(value) {
|
||||
return Features.matchesFeatures(value);
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds a quay-show attribute that shows the element only if the attribute evaluates to true.
|
||||
* The Features and Config services are added into the scope's context automatically.
|
||||
*/
|
||||
angular.module('quay').directive('quayShow', function($animate, Features, Config) {
|
||||
return {
|
||||
priority: 590,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl, $transclude) {
|
||||
$scope.Features = Features;
|
||||
$scope.Config = Config;
|
||||
$scope.$watch($attr.quayShow, function(result) {
|
||||
$animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* Adds a quay-section attribute that adds an 'active' class to the element if the current URL
|
||||
* matches the given section.
|
||||
*/
|
||||
angular.module('quay').directive('quaySection', function($animate, $location, $rootScope) {
|
||||
return {
|
||||
priority: 590,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl, $transclude) {
|
||||
var update = function() {
|
||||
var result = $location.path().indexOf('/' + $attr.quaySection) == 0;
|
||||
$animate[!result ? 'removeClass' : 'addClass']($element, 'active');
|
||||
};
|
||||
|
||||
$scope.$watch(function(){
|
||||
return $location.path();
|
||||
}, update);
|
||||
|
||||
$scope.$watch($attr.quaySection, update);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds a quay-classes attribute that performs like ng-class, but with Features and Config also
|
||||
* available in the scope automatically.
|
||||
*/
|
||||
angular.module('quay').directive('quayClasses', function(Features, Config) {
|
||||
return {
|
||||
priority: 580,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl, $transclude) {
|
||||
|
||||
// Borrowed from ngClass.
|
||||
function flattenClasses(classVal) {
|
||||
if(angular.isArray(classVal)) {
|
||||
return classVal.join(' ');
|
||||
} else if (angular.isObject(classVal)) {
|
||||
var classes = [], i = 0;
|
||||
angular.forEach(classVal, function(v, k) {
|
||||
if (v) {
|
||||
classes.push(k);
|
||||
}
|
||||
});
|
||||
return classes.join(' ');
|
||||
}
|
||||
|
||||
return classVal;
|
||||
}
|
||||
|
||||
function removeClass(classVal) {
|
||||
$attr.$removeClass(flattenClasses(classVal));
|
||||
}
|
||||
|
||||
|
||||
function addClass(classVal) {
|
||||
$attr.$addClass(flattenClasses(classVal));
|
||||
}
|
||||
|
||||
$scope.$watch($attr.quayClasses, function(result) {
|
||||
var scopeVals = {
|
||||
'Features': Features,
|
||||
'Config': Config
|
||||
};
|
||||
|
||||
for (var expr in result) {
|
||||
if (!result.hasOwnProperty(expr)) { continue; }
|
||||
|
||||
// Evaluate the expression with the entire features list added.
|
||||
var value = $scope.$eval(expr, scopeVals);
|
||||
if (value) {
|
||||
addClass(result[expr]);
|
||||
} else {
|
||||
removeClass(result[expr]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
/**
|
||||
* Adds a quay-include attribtue that adds a template solely if the expression evaluates to true.
|
||||
* Automatically adds the Features and Config services to the scope.
|
||||
*/
|
||||
angular.module('quay').directive('quayInclude', function($compile, $templateCache, $http, Features, Config) {
|
||||
return {
|
||||
priority: 595,
|
||||
restrict: 'A',
|
||||
link: function($scope, $element, $attr, ctrl) {
|
||||
var getTemplate = function(templateName) {
|
||||
var templateUrl = '/static/partials/' + templateName;
|
||||
return $http.get(templateUrl, {cache: $templateCache});
|
||||
};
|
||||
|
||||
var result = $scope.$eval($attr.quayInclude);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
var scopeVals = {
|
||||
'Features': Features,
|
||||
'Config': Config
|
||||
};
|
||||
|
||||
var templatePath = null;
|
||||
for (var expr in result) {
|
||||
if (!result.hasOwnProperty(expr)) { continue; }
|
||||
|
||||
// Evaluate the expression with the entire features list added.
|
||||
var value = $scope.$eval(expr, scopeVals);
|
||||
if (value) {
|
||||
templatePath = result[expr];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!templatePath) {
|
||||
return;
|
||||
}
|
||||
|
||||
var promise = getTemplate(templatePath).success(function(html) {
|
||||
$element.html(html);
|
||||
}).then(function (response) {
|
||||
$element.replaceWith($compile($element.html())($scope));
|
||||
if ($attr.onload) {
|
||||
$scope.$eval($attr.onload);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
18
static/js/directives/ui/application-info.js
Normal file
18
static/js/directives/ui/application-info.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which shows information about a registered OAuth application.
|
||||
*/
|
||||
angular.module('quay').directive('applicationInfo', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-info.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'application': '=application'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
56
static/js/directives/ui/application-manager.js
Normal file
56
static/js/directives/ui/application-manager.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* Element for managing the applications of an organization.
|
||||
*/
|
||||
angular.module('quay').directive('applicationManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'makevisible': '=makevisible'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.applications = [];
|
||||
|
||||
$scope.createApplication = function(appName) {
|
||||
if (!appName) { return; }
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'name': appName
|
||||
};
|
||||
|
||||
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
||||
$scope.applications.push(resp);
|
||||
}, ApiService.errorDisplay('Cannot create application'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.organization || !$scope.makevisible) { return; }
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
ApiService.getOrganizationApplications(null, params).then(function(resp) {
|
||||
$scope.loading = false;
|
||||
$scope.applications = resp['applications'] || [];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('makevisible', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
36
static/js/directives/ui/application-reference.js
Normal file
36
static/js/directives/ui/application-reference.js
Normal file
|
@ -0,0 +1,36 @@
|
|||
/**
|
||||
* An element which shows information about an OAuth application and provides a clickable link
|
||||
* for displaying a dialog with further information. Unlike application-info, this element is
|
||||
* intended for the *owner* of the application (since it requires the client ID).
|
||||
*/
|
||||
angular.module('quay').directive('applicationReference', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-reference.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'title': '=title',
|
||||
'clientId': '=clientId'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, $modal) {
|
||||
$scope.showAppDetails = function() {
|
||||
var params = {
|
||||
'client_id': $scope.clientId
|
||||
};
|
||||
|
||||
ApiService.getApplicationInformation(null, params).then(function(resp) {
|
||||
$scope.applicationInfo = resp;
|
||||
$modal({
|
||||
title: 'Application Information',
|
||||
scope: $scope,
|
||||
template: '/static/directives/application-reference-dialog.html',
|
||||
show: true
|
||||
});
|
||||
}, ApiService.errorDisplay('Application could not be found'));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
34
static/js/directives/ui/avatar.js
Normal file
34
static/js/directives/ui/avatar.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
/**
|
||||
* An element which displays an avatar for the given {email,name} or hash.
|
||||
*/
|
||||
angular.module('quay').directive('avatar', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/avatar.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'hash': '=hash',
|
||||
'email': '=email',
|
||||
'name': '=name',
|
||||
'size': '=size'
|
||||
},
|
||||
controller: function($scope, $element, AvatarService) {
|
||||
$scope.AvatarService = AvatarService;
|
||||
|
||||
var refreshHash = function() {
|
||||
if (!$scope.name && !$scope.email) { return; }
|
||||
$scope._hash = AvatarService.computeHash($scope.email, $scope.name);
|
||||
};
|
||||
|
||||
$scope.$watch('hash', function(hash) {
|
||||
$scope._hash = hash;
|
||||
});
|
||||
|
||||
$scope.$watch('name', refreshHash);
|
||||
$scope.$watch('email', refreshHash);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
49
static/js/directives/ui/billing-invoice.js
Normal file
49
static/js/directives/ui/billing-invoice.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* Element for displaying the list of billing invoices for the user or organization.
|
||||
*/
|
||||
angular.module('quay').directive('billingInvoices', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/billing-invoices.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user',
|
||||
'makevisible': '=makevisible'
|
||||
},
|
||||
controller: function($scope, $element, $sce, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.invoiceExpanded = {};
|
||||
|
||||
$scope.toggleInvoice = function(id) {
|
||||
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
||||
var hasValidOrg = !!$scope.organization;
|
||||
var isValid = hasValidUser || hasValidOrg;
|
||||
|
||||
if (!$scope.makevisible || !isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
ApiService.listInvoices($scope.organization).then(function(resp) {
|
||||
$scope.invoices = resp.invoices;
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
$scope.$watch('makevisible', update);
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
113
static/js/directives/ui/billing-options.js
Normal file
113
static/js/directives/ui/billing-options.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
/**
|
||||
* An element which displays the billing options for a user or an organization.
|
||||
*/
|
||||
angular.module('quay').directive('billingOptions', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/billing-options.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'organization': '=organization'
|
||||
},
|
||||
controller: function($scope, $element, PlanService, ApiService) {
|
||||
$scope.invoice_email = false;
|
||||
$scope.currentCard = null;
|
||||
|
||||
// Listen to plan changes.
|
||||
PlanService.registerListener(this, function(plan) {
|
||||
if (plan && plan.price > 0) {
|
||||
update();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
PlanService.unregisterListener(this);
|
||||
});
|
||||
|
||||
$scope.isExpiringSoon = function(cardInfo) {
|
||||
var current = new Date();
|
||||
var expires = new Date(cardInfo.exp_year, cardInfo.exp_month, 1);
|
||||
var difference = expires - current;
|
||||
return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */);
|
||||
};
|
||||
|
||||
$scope.changeCard = function() {
|
||||
var previousCard = $scope.currentCard;
|
||||
$scope.changingCard = true;
|
||||
var callbacks = {
|
||||
'opened': function() { $scope.changingCard = true; },
|
||||
'closed': function() { $scope.changingCard = false; },
|
||||
'started': function() { $scope.currentCard = null; },
|
||||
'success': function(resp) {
|
||||
$scope.currentCard = resp.card;
|
||||
$scope.changingCard = false;
|
||||
},
|
||||
'failure': function(resp) {
|
||||
$scope.changingCard = false;
|
||||
$scope.currentCard = previousCard;
|
||||
|
||||
if (!PlanService.isCardError(resp)) {
|
||||
$('#cannotchangecardModal').modal({});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks);
|
||||
};
|
||||
|
||||
$scope.getCreditImage = function(creditInfo) {
|
||||
if (!creditInfo || !creditInfo.type) { return 'credit.png'; }
|
||||
|
||||
var kind = creditInfo.type.toLowerCase() || 'credit';
|
||||
var supported = {
|
||||
'american express': 'amex',
|
||||
'credit': 'credit',
|
||||
'diners club': 'diners',
|
||||
'discover': 'discover',
|
||||
'jcb': 'jcb',
|
||||
'mastercard': 'mastercard',
|
||||
'visa': 'visa'
|
||||
};
|
||||
|
||||
kind = supported[kind] || 'credit';
|
||||
return kind + '.png';
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.user && !$scope.organization) { return; }
|
||||
$scope.obj = $scope.user ? $scope.user : $scope.organization;
|
||||
$scope.invoice_email = $scope.obj.invoice_email;
|
||||
|
||||
// Load the credit card information.
|
||||
PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) {
|
||||
$scope.currentCard = card;
|
||||
});
|
||||
};
|
||||
|
||||
var save = function() {
|
||||
$scope.working = true;
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Could not change user details');
|
||||
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
|
||||
$scope.working = false;
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var checkSave = function() {
|
||||
if (!$scope.obj) { return; }
|
||||
if ($scope.obj.invoice_email != $scope.invoice_email) {
|
||||
$scope.obj.invoice_email = $scope.invoice_email;
|
||||
save();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('invoice_email', checkSave);
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
26
static/js/directives/ui/build-log-command.js
Normal file
26
static/js/directives/ui/build-log-command.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* An element which displays a command in a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogCommand', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-command.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'command': '=command'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getWithoutStep = function(fullTitle) {
|
||||
var colon = fullTitle.indexOf(':');
|
||||
if (colon <= 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $.trim(fullTitle.substring(colon + 1));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
62
static/js/directives/ui/build-log-error.js
Normal file
62
static/js/directives/ui/build-log-error.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/**
|
||||
* An element which displays a build error in a nice format.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogError', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-error.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'error': '=error',
|
||||
'entries': '=entries'
|
||||
},
|
||||
controller: function($scope, $element, Config) {
|
||||
$scope.isInternalError = function() {
|
||||
var entry = $scope.entries[$scope.entries.length - 1];
|
||||
return entry && entry.data && entry.data['internal_error'];
|
||||
};
|
||||
|
||||
$scope.getLocalPullInfo = function() {
|
||||
if ($scope.entries.__localpull !== undefined) {
|
||||
return $scope.entries.__localpull;
|
||||
}
|
||||
|
||||
var localInfo = {
|
||||
'isLocal': false
|
||||
};
|
||||
|
||||
// Find the 'pulling' phase entry, and then extra any metadata found under
|
||||
// it.
|
||||
for (var i = 0; i < $scope.entries.length; ++i) {
|
||||
var entry = $scope.entries[i];
|
||||
if (entry.type == 'phase' && entry.message == 'pulling') {
|
||||
for (var j = 0; j < entry.logs.length(); ++j) {
|
||||
var log = entry.logs.get(j);
|
||||
if (log.data && log.data.phasestep == 'login') {
|
||||
localInfo['login'] = log.data;
|
||||
}
|
||||
|
||||
if (log.data && log.data.phasestep == 'pull') {
|
||||
var repo_url = log.data['repo_url'];
|
||||
var repo_and_tag = repo_url.substring(Config.SERVER_HOSTNAME.length + 1);
|
||||
var tagIndex = repo_and_tag.lastIndexOf(':');
|
||||
var repo = repo_and_tag.substring(0, tagIndex);
|
||||
|
||||
localInfo['repo_url'] = repo_url;
|
||||
localInfo['repo'] = repo;
|
||||
|
||||
localInfo['isLocal'] = repo_url.indexOf(Config.SERVER_HOSTNAME + '/') == 0;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $scope.entries.__localpull = localInfo;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
18
static/js/directives/ui/build-log-phase.js
Normal file
18
static/js/directives/ui/build-log-phase.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which displays the phase of a build nicely.
|
||||
*/
|
||||
angular.module('quay').directive('buildLogPhase', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-log-phase.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
61
static/js/directives/ui/build-message.js
Normal file
61
static/js/directives/ui/build-message.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* An element which displays a user-friendly message for the current phase of a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildMessage', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-message.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'phase': '=phase'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getBuildMessage = function (phase) {
|
||||
switch (phase) {
|
||||
case 'cannot_load':
|
||||
return 'Cannot load build status - Please report this error';
|
||||
|
||||
case 'starting':
|
||||
case 'initializing':
|
||||
return 'Starting Dockerfile build';
|
||||
|
||||
case 'waiting':
|
||||
return 'Waiting for available build worker';
|
||||
|
||||
case 'unpacking':
|
||||
return 'Unpacking build package';
|
||||
|
||||
case 'pulling':
|
||||
return 'Pulling base image';
|
||||
|
||||
case 'building':
|
||||
return 'Building image from Dockerfile';
|
||||
|
||||
case 'checking-cache':
|
||||
return 'Looking up cached images';
|
||||
|
||||
case 'priming-cache':
|
||||
return 'Priming cache for build';
|
||||
|
||||
case 'build-scheduled':
|
||||
return 'Preparing build node';
|
||||
|
||||
case 'pushing':
|
||||
return 'Pushing image built from Dockerfile';
|
||||
|
||||
case 'complete':
|
||||
return 'Dockerfile build completed and pushed';
|
||||
|
||||
case 'error':
|
||||
return 'Dockerfile build failed';
|
||||
|
||||
case 'internalerror':
|
||||
return 'An internal system error occurred while building; the build will be retried in the next few minutes.';
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
53
static/js/directives/ui/build-progress.js
Normal file
53
static/js/directives/ui/build-progress.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
/**
|
||||
* An element which displays a progressbar for the given build.
|
||||
*/
|
||||
angular.module('quay').directive('buildProgress', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-progress.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.getPercentage = function(buildInfo) {
|
||||
switch (buildInfo.phase) {
|
||||
case 'pulling':
|
||||
return buildInfo.status.pull_completion * 100;
|
||||
break;
|
||||
|
||||
case 'building':
|
||||
return (buildInfo.status.current_command / buildInfo.status.total_commands) * 100;
|
||||
break;
|
||||
|
||||
case 'pushing':
|
||||
return buildInfo.status.push_completion * 100;
|
||||
break;
|
||||
|
||||
case 'priming-cache':
|
||||
return buildInfo.status.cache_completion * 100;
|
||||
break;
|
||||
|
||||
case 'complete':
|
||||
return 100;
|
||||
break;
|
||||
|
||||
case 'initializing':
|
||||
case 'checking-cache':
|
||||
case 'starting':
|
||||
case 'waiting':
|
||||
case 'cannot_load':
|
||||
case 'unpacking':
|
||||
return 0;
|
||||
break;
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
18
static/js/directives/ui/build-status.js
Normal file
18
static/js/directives/ui/build-status.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which displays the status of a build.
|
||||
*/
|
||||
angular.module('quay').directive('buildStatus', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/build-status.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'build': '=build'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
76
static/js/directives/ui/copy-box.js
Normal file
76
static/js/directives/ui/copy-box.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
$.fn.clipboardCopy = function() {
|
||||
if (__zeroClipboardSupported) {
|
||||
(new ZeroClipboard($(this)));
|
||||
return true;
|
||||
}
|
||||
|
||||
this.hide();
|
||||
return false;
|
||||
};
|
||||
|
||||
// Initialize the clipboard system.
|
||||
(function () {
|
||||
__zeroClipboardSupported = true;
|
||||
|
||||
ZeroClipboard.config({
|
||||
'swfPath': 'static/lib/ZeroClipboard.swf'
|
||||
});
|
||||
|
||||
ZeroClipboard.on("error", function(e) {
|
||||
__zeroClipboardSupported = false;
|
||||
});
|
||||
|
||||
ZeroClipboard.on('aftercopy', function(e) {
|
||||
var container = e.target.parentNode.parentNode.parentNode;
|
||||
var message = $(container).find('.clipboard-copied-message')[0];
|
||||
|
||||
// Resets the animation.
|
||||
var elem = message;
|
||||
elem.style.display = 'none';
|
||||
elem.classList.remove('animated');
|
||||
|
||||
// Show the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'inline-block';
|
||||
elem.classList.add('animated');
|
||||
}, 10);
|
||||
|
||||
// Reset the notification.
|
||||
setTimeout(function() {
|
||||
elem.style.display = 'none';
|
||||
}, 5000);
|
||||
});
|
||||
})();
|
||||
|
||||
/**
|
||||
* An element which displays a textfield with a "Copy to Clipboard" icon next to it. Note
|
||||
* that this method depends on the clipboard copying library in the lib/ folder.
|
||||
*/
|
||||
angular.module('quay').directive('copyBox', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/copy-box.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'value': '=value',
|
||||
'hoveringMessage': '=hoveringMessage',
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
$scope.disabled = false;
|
||||
|
||||
var number = $rootScope.__copyBoxIdCounter || 0;
|
||||
$rootScope.__copyBoxIdCounter = number + 1;
|
||||
$scope.inputId = "copy-box-input-" + number;
|
||||
|
||||
var button = $($element).find('.copy-icon');
|
||||
var input = $($element).find('input');
|
||||
|
||||
input.attr('id', $scope.inputId);
|
||||
button.attr('data-clipboard-target', $scope.inputId);
|
||||
$scope.disabled = !button.clipboardCopy();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
144
static/js/directives/ui/create-external-notification-dialog.js
Normal file
144
static/js/directives/ui/create-external-notification-dialog.js
Normal file
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* An element which displays a dialog to register a new external notification on a repository.
|
||||
*/
|
||||
angular.module('quay').directive('createExternalNotificationDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/create-external-notification-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'counter': '=counter',
|
||||
'notificationCreated': '¬ificationCreated'
|
||||
},
|
||||
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
|
||||
$scope.currentEvent = null;
|
||||
$scope.currentMethod = null;
|
||||
$scope.status = '';
|
||||
$scope.currentConfig = {};
|
||||
$scope.clearCounter = 0;
|
||||
$scope.unauthorizedEmail = false;
|
||||
|
||||
$scope.events = ExternalNotificationData.getSupportedEvents();
|
||||
$scope.methods = ExternalNotificationData.getSupportedMethods();
|
||||
|
||||
$scope.getPattern = function(field) {
|
||||
return new RegExp(field.regex);
|
||||
};
|
||||
|
||||
$scope.setEvent = function(event) {
|
||||
$scope.currentEvent = event;
|
||||
};
|
||||
|
||||
$scope.setMethod = function(method) {
|
||||
$scope.currentConfig = {};
|
||||
$scope.currentMethod = method;
|
||||
$scope.unauthorizedEmail = false;
|
||||
};
|
||||
|
||||
$scope.createNotification = function() {
|
||||
if (!$scope.currentConfig.email) {
|
||||
$scope.performCreateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.status = 'checking-email';
|
||||
$scope.checkEmailAuthorization();
|
||||
};
|
||||
|
||||
$scope.checkEmailAuthorization = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'email': $scope.currentConfig.email
|
||||
};
|
||||
|
||||
ApiService.checkRepoEmailAuthorized(null, params).then(function(resp) {
|
||||
$scope.handleEmailCheck(resp.confirmed);
|
||||
}, function(resp) {
|
||||
$scope.handleEmailCheck(false);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.performCreateNotification = function() {
|
||||
$scope.status = 'creating';
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'event': $scope.currentEvent.id,
|
||||
'method': $scope.currentMethod.id,
|
||||
'config': $scope.currentConfig
|
||||
};
|
||||
|
||||
ApiService.createRepoNotification(data, params).then(function(resp) {
|
||||
$scope.status = '';
|
||||
$scope.notificationCreated({'notification': resp});
|
||||
$('#createNotificationModal').modal('hide');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.handleEmailCheck = function(isAuthorized) {
|
||||
if (isAuthorized) {
|
||||
$scope.performCreateNotification();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.status == 'authorizing-email-sent') {
|
||||
$scope.watchEmail();
|
||||
} else {
|
||||
$scope.status = 'unauthorized-email';
|
||||
}
|
||||
|
||||
$scope.unauthorizedEmail = true;
|
||||
};
|
||||
|
||||
$scope.sendAuthEmail = function() {
|
||||
$scope.status = 'authorizing-email';
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'email': $scope.currentConfig.email
|
||||
};
|
||||
|
||||
ApiService.sendAuthorizeRepoEmail(null, params).then(function(resp) {
|
||||
$scope.status = 'authorizing-email-sent';
|
||||
$scope.watchEmail();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.watchEmail = function() {
|
||||
// TODO: change this to SSE?
|
||||
$timeout(function() {
|
||||
$scope.checkEmailAuthorization();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
$scope.getHelpUrl = function(field, config) {
|
||||
var helpUrl = field['help_url'];
|
||||
if (!helpUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return StringBuilderService.buildUrl(helpUrl, config);
|
||||
};
|
||||
|
||||
$scope.$watch('counter', function(counter) {
|
||||
if (counter) {
|
||||
$scope.clearCounter++;
|
||||
$scope.status = '';
|
||||
$scope.currentEvent = null;
|
||||
$scope.currentMethod = null;
|
||||
$scope.unauthorizedEmail = false;
|
||||
$('#createNotificationModal').modal({});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
26
static/js/directives/ui/delete-ui.js
Normal file
26
static/js/directives/ui/delete-ui.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* A two-step delete button that slides into view when clicked.
|
||||
*/
|
||||
angular.module('quay').directive('deleteUi', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/delete-ui.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'deleteTitle': '=deleteTitle',
|
||||
'buttonTitle': '=buttonTitle',
|
||||
'performDelete': '&performDelete'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.buttonTitleInternal = $scope.buttonTitle || 'Delete';
|
||||
|
||||
$element.children().attr('tabindex', 0);
|
||||
$scope.focus = function() {
|
||||
$element[0].firstChild.focus();
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
86
static/js/directives/ui/docker-auth-dialog.js
Normal file
86
static/js/directives/ui/docker-auth-dialog.js
Normal file
|
@ -0,0 +1,86 @@
|
|||
/**
|
||||
* An element which displays a dialog with docker auth credentials for an entity.
|
||||
*/
|
||||
angular.module('quay').directive('dockerAuthDialog', function (Config) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/docker-auth-dialog.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'username': '=username',
|
||||
'token': '=token',
|
||||
'shown': '=shown',
|
||||
'counter': '=counter',
|
||||
'supportsRegenerate': '@supportsRegenerate',
|
||||
'regenerate': '®enerate'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var updateCommand = function() {
|
||||
var escape = function(v) {
|
||||
if (!v) { return v; }
|
||||
return v.replace('$', '\\$');
|
||||
};
|
||||
$scope.command = 'docker login -e="." -u="' + escape($scope.username) +
|
||||
'" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME'];
|
||||
};
|
||||
|
||||
$scope.$watch('username', updateCommand);
|
||||
$scope.$watch('token', updateCommand);
|
||||
|
||||
$scope.regenerating = true;
|
||||
|
||||
$scope.askRegenerate = function() {
|
||||
bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) {
|
||||
if (resp) {
|
||||
$scope.regenerating = true;
|
||||
$scope.regenerate({'username': $scope.username, 'token': $scope.token});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.isDownloadSupported = function() {
|
||||
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
|
||||
if (isSafari) {
|
||||
// Doesn't work properly in Safari, sadly.
|
||||
return false;
|
||||
}
|
||||
|
||||
try { return !!new Blob(); } catch(e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.downloadCfg = function() {
|
||||
var auth = $.base64.encode($scope.username + ":" + $scope.token);
|
||||
config = {}
|
||||
config[Config['SERVER_HOSTNAME']] = {
|
||||
"auth": auth,
|
||||
"email": ""
|
||||
};
|
||||
|
||||
var file = JSON.stringify(config, null, ' ');
|
||||
var blob = new Blob([file]);
|
||||
saveAs(blob, '.dockercfg');
|
||||
};
|
||||
|
||||
var show = function(r) {
|
||||
$scope.regenerating = false;
|
||||
|
||||
if (!$scope.shown || !$scope.username || !$scope.token) {
|
||||
$('#dockerauthmodal').modal('hide');
|
||||
return;
|
||||
}
|
||||
|
||||
$('#copyClipboard').clipboardCopy();
|
||||
$('#dockerauthmodal').modal({});
|
||||
};
|
||||
|
||||
$scope.$watch('counter', show);
|
||||
$scope.$watch('shown', show);
|
||||
$scope.$watch('username', show);
|
||||
$scope.$watch('token', show);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
45
static/js/directives/ui/dockerfile-build-dialog.js
Normal file
45
static/js/directives/ui/dockerfile-build-dialog.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* An element which displays a dialog for manually starting a dockerfile build.
|
||||
*/
|
||||
angular.module('quay').directive('dockerfileBuildDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'showNow': '=showNow',
|
||||
'buildStarted': '&buildStarted'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
$scope.startCounter = 0;
|
||||
|
||||
$scope.handleBuildStarted = function(build) {
|
||||
$('#dockerfilebuildModal').modal('hide');
|
||||
if ($scope.buildStarted) {
|
||||
$scope.buildStarted({'build': build});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.handleBuildFailed = function(message) {
|
||||
$scope.errorMessage = message;
|
||||
};
|
||||
|
||||
$scope.startBuild = function() {
|
||||
$scope.errorMessage = null;
|
||||
$scope.startCounter++;
|
||||
};
|
||||
|
||||
$scope.$watch('showNow', function(sn) {
|
||||
if (sn && $scope.repository) {
|
||||
$('#dockerfilebuildModal').modal({});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
199
static/js/directives/ui/dockerfile-build-form.js
Normal file
199
static/js/directives/ui/dockerfile-build-form.js
Normal file
|
@ -0,0 +1,199 @@
|
|||
/**
|
||||
* An element which displays a form for manually starting a dockerfile build.
|
||||
*/
|
||||
angular.module('quay').directive('dockerfileBuildForm', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-build-form.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'startNow': '=startNow',
|
||||
'isReady': '=isReady',
|
||||
'uploadFailed': '&uploadFailed',
|
||||
'uploadStarted': '&uploadStarted',
|
||||
'buildStarted': '&buildStarted',
|
||||
'buildFailed': '&buildFailed',
|
||||
'missingFile': '&missingFile',
|
||||
'uploading': '=uploading',
|
||||
'building': '=building'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.internal = {'hasDockerfile': false};
|
||||
$scope.pull_entity = null;
|
||||
$scope.is_public = true;
|
||||
|
||||
var handleBuildFailed = function(message) {
|
||||
message = message || 'Dockerfile build failed to start';
|
||||
|
||||
var result = false;
|
||||
if ($scope.buildFailed) {
|
||||
result = $scope.buildFailed({'message': message});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Cannot start Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleUploadFailed = function(message) {
|
||||
message = message || 'Error with file upload';
|
||||
|
||||
var result = false;
|
||||
if ($scope.uploadFailed) {
|
||||
result = $scope.uploadFailed({'message': message});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": message,
|
||||
"title": "Cannot upload file for Dockerfile build",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var handleMissingFile = function() {
|
||||
var result = false;
|
||||
if ($scope.missingFile) {
|
||||
result = $scope.missingFile({});
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
bootbox.dialog({
|
||||
"message": 'A Dockerfile or an archive containing a Dockerfile is required',
|
||||
"title": "Missing Dockerfile",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var startBuild = function(fileId) {
|
||||
$scope.building = true;
|
||||
|
||||
var repo = $scope.repository;
|
||||
var data = {
|
||||
'file_id': fileId
|
||||
};
|
||||
|
||||
if (!$scope.is_public && $scope.pull_entity) {
|
||||
data['pull_robot'] = $scope.pull_entity['name'];
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name,
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
|
||||
if ($scope.buildStarted) {
|
||||
$scope.buildStarted({'build': resp});
|
||||
}
|
||||
}, function(resp) {
|
||||
$scope.building = false;
|
||||
$scope.uploading = false;
|
||||
|
||||
handleBuildFailed(resp.message);
|
||||
});
|
||||
};
|
||||
|
||||
var conductUpload = function(file, url, fileId, mimeType) {
|
||||
if ($scope.uploadStarted) {
|
||||
$scope.uploadStarted({});
|
||||
}
|
||||
|
||||
var request = new XMLHttpRequest();
|
||||
request.open('PUT', url, true);
|
||||
request.setRequestHeader('Content-Type', mimeType);
|
||||
request.onprogress = function(e) {
|
||||
$scope.$apply(function() {
|
||||
var percentLoaded;
|
||||
if (e.lengthComputable) {
|
||||
$scope.upload_progress = (e.loaded / e.total) * 100;
|
||||
}
|
||||
});
|
||||
};
|
||||
request.onerror = function() {
|
||||
$scope.$apply(function() {
|
||||
handleUploadFailed();
|
||||
});
|
||||
};
|
||||
request.onreadystatechange = function() {
|
||||
var state = request.readyState;
|
||||
if (state == 4) {
|
||||
$scope.$apply(function() {
|
||||
startBuild(fileId);
|
||||
$scope.uploading = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
request.send(file);
|
||||
};
|
||||
|
||||
var startFileUpload = function(repo) {
|
||||
$scope.uploading = true;
|
||||
$scope.uploading_progress = 0;
|
||||
|
||||
var uploader = $('#file-drop')[0];
|
||||
if (uploader.files.length == 0) {
|
||||
handleMissingFile();
|
||||
$scope.uploading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var file = uploader.files[0];
|
||||
$scope.upload_file = file.name;
|
||||
|
||||
var mimeType = file.type || 'application/octet-stream';
|
||||
var data = {
|
||||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
conductUpload(file, resp.url, resp.file_id, mimeType);
|
||||
}, function() {
|
||||
handleUploadFailed('Could not retrieve upload URL');
|
||||
});
|
||||
};
|
||||
|
||||
var checkIsReady = function() {
|
||||
$scope.isReady = $scope.internal.hasDockerfile && ($scope.is_public || $scope.pull_entity);
|
||||
};
|
||||
|
||||
$scope.$watch('pull_entity', checkIsReady);
|
||||
$scope.$watch('is_public', checkIsReady);
|
||||
$scope.$watch('internal.hasDockerfile', checkIsReady);
|
||||
|
||||
$scope.$watch('startNow', function() {
|
||||
if ($scope.startNow && $scope.repository && !$scope.uploading && !$scope.building) {
|
||||
startFileUpload();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
68
static/js/directives/ui/dockerfile-command.js
Normal file
68
static/js/directives/ui/dockerfile-command.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
/**
|
||||
* An element which displays a Dockerfile command nicely formatted, with optional link to the
|
||||
* image (for FROM commands that link to us or to the DockerHub).
|
||||
*/
|
||||
angular.module('quay').directive('dockerfileCommand', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-command.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'command': '=command'
|
||||
},
|
||||
controller: function($scope, $element, UtilService, Config) {
|
||||
var registryHandlers = {
|
||||
'quay.io': function(pieces) {
|
||||
var rnamespace = pieces[pieces.length - 2];
|
||||
var rname = pieces[pieces.length - 1].split(':')[0];
|
||||
return '/repository/' + rnamespace + '/' + rname + '/';
|
||||
},
|
||||
|
||||
'': function(pieces) {
|
||||
var rnamespace = pieces.length == 1 ? '_' : 'u/' + pieces[0];
|
||||
var rname = pieces[pieces.length - 1].split(':')[0];
|
||||
return 'https://registry.hub.docker.com/' + rnamespace + '/' + rname + '/';
|
||||
}
|
||||
};
|
||||
|
||||
registryHandlers[Config.getDomain()] = registryHandlers['quay.io'];
|
||||
|
||||
var kindHandlers = {
|
||||
'FROM': function(title) {
|
||||
var pieces = title.split('/');
|
||||
var registry = pieces.length < 3 ? '' : pieces[0];
|
||||
if (!registryHandlers[registry]) {
|
||||
return title;
|
||||
}
|
||||
|
||||
return '<i class="fa fa-hdd-o"></i> <a href="' + registryHandlers[registry](pieces) + '" target="_blank">' + title + '</a>';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getCommandKind = function(title) {
|
||||
var space = title.indexOf(' ');
|
||||
return title.substring(0, space);
|
||||
};
|
||||
|
||||
$scope.getCommandTitleHtml = function(title) {
|
||||
var space = title.indexOf(' ');
|
||||
if (space <= 0) {
|
||||
return UtilService.textToSafeHtml(title);
|
||||
}
|
||||
|
||||
var kind = $scope.getCommandKind(title);
|
||||
var sanitized = UtilService.textToSafeHtml(title.substring(space + 1));
|
||||
|
||||
var handler = kindHandlers[kind || ''];
|
||||
if (handler) {
|
||||
return handler(sanitized);
|
||||
} else {
|
||||
return sanitized;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
38
static/js/directives/ui/dockerfile-view.js
Normal file
38
static/js/directives/ui/dockerfile-view.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
/**
|
||||
* An element which displays the contents of a Dockerfile in a nicely formatted way.
|
||||
*/
|
||||
angular.module('quay').directive('dockerfileView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dockerfile-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'contents': '=contents'
|
||||
},
|
||||
controller: function($scope, $element, UtilService) {
|
||||
$scope.$watch('contents', function(contents) {
|
||||
$scope.lines = [];
|
||||
|
||||
var lines = contents ? contents.split('\n') : [];
|
||||
for (var i = 0; i < lines.length; ++i) {
|
||||
var line = $.trim(lines[i]);
|
||||
var kind = 'text';
|
||||
if (line && line[0] == '#') {
|
||||
kind = 'comment';
|
||||
} else if (line.match(/^([A-Z]+\s)/)) {
|
||||
kind = 'command';
|
||||
}
|
||||
|
||||
var lineInfo = {
|
||||
'text': line,
|
||||
'kind': kind
|
||||
};
|
||||
$scope.lines.push(lineInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
174
static/js/directives/ui/dropdown-select.js
Normal file
174
static/js/directives/ui/dropdown-select.js
Normal file
|
@ -0,0 +1,174 @@
|
|||
/**
|
||||
* An element which displays a dropdown select box which is (optionally) editable. This box
|
||||
* is displayed with an <input> and a menu on the right.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelect', function ($compile) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/dropdown-select.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'selectedItem': '=selectedItem',
|
||||
'placeholder': '=placeholder',
|
||||
'lookaheadItems': '=lookaheadItems',
|
||||
|
||||
'allowCustomInput': '@allowCustomInput',
|
||||
|
||||
'handleItemSelected': '&handleItemSelected',
|
||||
'handleInput': '&handleInput',
|
||||
|
||||
'clearValue': '=clearValue'
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
if (!$rootScope.__dropdownSelectCounter) {
|
||||
$rootScope.__dropdownSelectCounter = 1;
|
||||
}
|
||||
|
||||
$scope.placeholder = $scope.placeholder || '';
|
||||
$scope.internalItem = null;
|
||||
|
||||
// Setup lookahead.
|
||||
var input = $($element).find('.lookahead-input');
|
||||
|
||||
$scope.$watch('clearValue', function(cv) {
|
||||
if (cv) {
|
||||
$scope.selectedItem = null;
|
||||
$(input).val('');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('selectedItem', function(item) {
|
||||
if ($scope.selectedItem == $scope.internalItem) {
|
||||
// The item has already been set due to an internal action.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.selectedItem != null) {
|
||||
$(input).val(item.toString());
|
||||
} else {
|
||||
$(input).val('');
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('lookaheadItems', function(items) {
|
||||
$(input).off();
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
var formattedItems = [];
|
||||
for (var i = 0; i < items.length; ++i) {
|
||||
var formattedItem = items[i];
|
||||
if (typeof formattedItem == 'string') {
|
||||
formattedItem = {
|
||||
'value': formattedItem
|
||||
};
|
||||
}
|
||||
formattedItems.push(formattedItem);
|
||||
}
|
||||
|
||||
var dropdownHound = new Bloodhound({
|
||||
name: 'dropdown-items-' + $rootScope.__dropdownSelectCounter,
|
||||
local: formattedItems,
|
||||
datumTokenizer: function(d) {
|
||||
return Bloodhound.tokenizers.whitespace(d.val || d.value || '');
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
dropdownHound.initialize();
|
||||
|
||||
$(input).typeahead({}, {
|
||||
source: dropdownHound.ttAdapter(),
|
||||
templates: {
|
||||
'suggestion': function (datum) {
|
||||
template = datum['template'] ? datum['template'](datum) : datum['value'];
|
||||
return template;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$(input).on('input', function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.internalItem = null;
|
||||
$scope.selectedItem = null;
|
||||
if ($scope.handleInput) {
|
||||
$scope.handleInput({'input': $(input).val()});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(input).on('typeahead:selected', function(e, datum) {
|
||||
$scope.$apply(function() {
|
||||
$scope.internalItem = datum['item'] || datum['value'];
|
||||
$scope.selectedItem = datum['item'] || datum['value'];
|
||||
if ($scope.handleItemSelected) {
|
||||
$scope.handleItemSelected({'datum': datum});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$rootScope.__dropdownSelectCounter++;
|
||||
});
|
||||
},
|
||||
link: function(scope, element, attrs) {
|
||||
var transcludedBlock = element.find('div.transcluded');
|
||||
var transcludedElements = transcludedBlock.children();
|
||||
|
||||
var iconContainer = element.find('div.dropdown-select-icon-transclude');
|
||||
var menuContainer = element.find('div.dropdown-select-menu-transclude');
|
||||
|
||||
angular.forEach(transcludedElements, function(elem) {
|
||||
if (angular.element(elem).hasClass('dropdown-select-icon')) {
|
||||
iconContainer.append(elem);
|
||||
} else if (angular.element(elem).hasClass('dropdown-select-menu')) {
|
||||
menuContainer.replaceWith(elem);
|
||||
}
|
||||
});
|
||||
|
||||
transcludedBlock.remove();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* An icon in the dropdown select. Only one icon will be displayed at a time.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelectIcon', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
require: '^dropdownSelect',
|
||||
templateUrl: '/static/directives/dropdown-select-icon.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* The menu for the dropdown select.
|
||||
*/
|
||||
angular.module('quay').directive('dropdownSelectMenu', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
require: '^dropdownSelect',
|
||||
templateUrl: '/static/directives/dropdown-select-menu.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
56
static/js/directives/ui/entity-reference.js
Normal file
56
static/js/directives/ui/entity-reference.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* An element which shows an icon and a name/title for an entity (user, org, robot, team),
|
||||
* optionally linking to that entity if applicable.
|
||||
*/
|
||||
angular.module('quay').directive('entityReference', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/entity-reference.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'entity': '=entity',
|
||||
'namespace': '=namespace',
|
||||
'showAvatar': '@showAvatar',
|
||||
'avatarSize': '@avatarSize'
|
||||
},
|
||||
controller: function($scope, $element, UserService, UtilService) {
|
||||
$scope.getIsAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.getRobotUrl = function(name) {
|
||||
var namespace = $scope.getPrefix(name);
|
||||
if (!namespace) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!$scope.getIsAdmin(namespace)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
var org = UserService.getOrganization(namespace);
|
||||
if (!org) {
|
||||
// This robot is owned by the user.
|
||||
return '/user/?tab=robots&showRobot=' + UtilService.textToSafeHtml(name);
|
||||
}
|
||||
|
||||
return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + UtilService.textToSafeHtml(name);
|
||||
};
|
||||
|
||||
$scope.getPrefix = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus);
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
if (!name) { return ''; }
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
361
static/js/directives/ui/entity-search.js
Normal file
361
static/js/directives/ui/entity-search.js
Normal file
|
@ -0,0 +1,361 @@
|
|||
|
||||
/**
|
||||
* An element which displays a box to search for an entity (org, user, robot, team). This control
|
||||
* allows for filtering of the entities found and whether to allow selection by e-mail.
|
||||
*/
|
||||
angular.module('quay').directive('entitySearch', function () {
|
||||
var number = 0;
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/entity-search.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
require: '?ngModel',
|
||||
link: function(scope, element, attr, ctrl) {
|
||||
scope.ngModel = ctrl;
|
||||
},
|
||||
scope: {
|
||||
'namespace': '=namespace',
|
||||
'placeholder': '=placeholder',
|
||||
|
||||
// Default: ['user', 'team', 'robot']
|
||||
'allowedEntities': '=allowedEntities',
|
||||
|
||||
'currentEntity': '=currentEntity',
|
||||
|
||||
'entitySelected': '&entitySelected',
|
||||
'emailSelected': '&emailSelected',
|
||||
|
||||
// When set to true, the contents of the control will be cleared as soon
|
||||
// as an entity is selected.
|
||||
'autoClear': '=autoClear',
|
||||
|
||||
// Set this property to immediately clear the contents of the control.
|
||||
'clearValue': '=clearValue',
|
||||
|
||||
// Whether e-mail addresses are allowed.
|
||||
'allowEmails': '=allowEmails',
|
||||
'emailMessage': '@emailMessage',
|
||||
|
||||
// True if the menu should pull right.
|
||||
'pullRight': '@pullRight'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, Restangular, UserService, ApiService, UtilService, Config, CreateService) {
|
||||
$scope.lazyLoading = true;
|
||||
|
||||
$scope.teams = null;
|
||||
$scope.robots = null;
|
||||
|
||||
$scope.isAdmin = false;
|
||||
$scope.isOrganization = false;
|
||||
|
||||
$scope.includeTeams = true;
|
||||
$scope.includeRobots = true;
|
||||
$scope.includeOrgs = false;
|
||||
|
||||
$scope.currentEntityInternal = $scope.currentEntity;
|
||||
|
||||
var isSupported = function(kind, opt_array) {
|
||||
return $.inArray(kind, opt_array || $scope.allowedEntities || ['user', 'team', 'robot']) >= 0;
|
||||
};
|
||||
|
||||
$scope.lazyLoad = function() {
|
||||
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
||||
|
||||
// Reset the cached teams and robots.
|
||||
$scope.teams = null;
|
||||
$scope.robots = null;
|
||||
|
||||
// Load the organization's teams (if applicable).
|
||||
if ($scope.isOrganization && isSupported('team')) {
|
||||
// Note: We load the org here again so that we always have the fully up-to-date
|
||||
// teams list.
|
||||
ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
|
||||
$scope.teams = resp.teams;
|
||||
});
|
||||
}
|
||||
|
||||
// Load the user/organization's robots (if applicable).
|
||||
if ($scope.isAdmin && isSupported('robot')) {
|
||||
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
|
||||
$scope.robots = resp.robots;
|
||||
$scope.lazyLoading = false;
|
||||
}, function() {
|
||||
$scope.lazyLoading = false;
|
||||
});
|
||||
} else {
|
||||
$scope.lazyLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createTeam = function() {
|
||||
if (!$scope.isAdmin) { return; }
|
||||
|
||||
bootbox.prompt('Enter the name of the new team', function(teamname) {
|
||||
if (!teamname) { return; }
|
||||
|
||||
var regex = new RegExp(TEAM_PATTERN);
|
||||
if (!regex.test(teamname)) {
|
||||
bootbox.alert('Invalid team name');
|
||||
return;
|
||||
}
|
||||
|
||||
CreateService.createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) {
|
||||
$scope.setEntity(created.name, 'team', false);
|
||||
$scope.teams[teamname] = created;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.createRobot = function() {
|
||||
if (!$scope.isAdmin) { return; }
|
||||
|
||||
bootbox.prompt('Enter the name of the new robot account', function(robotname) {
|
||||
if (!robotname) { return; }
|
||||
|
||||
var regex = new RegExp(ROBOT_PATTERN);
|
||||
if (!regex.test(robotname)) {
|
||||
bootbox.alert('Invalid robot account name');
|
||||
return;
|
||||
}
|
||||
|
||||
CreateService.createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) {
|
||||
$scope.setEntity(created.name, 'user', true);
|
||||
$scope.robots.push(created);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.setEntity = function(name, kind, is_robot) {
|
||||
var entity = {
|
||||
'name': name,
|
||||
'kind': kind,
|
||||
'is_robot': is_robot
|
||||
};
|
||||
|
||||
if ($scope.isOrganization) {
|
||||
entity['is_org_member'] = true;
|
||||
}
|
||||
|
||||
$scope.setEntityInternal(entity, false);
|
||||
};
|
||||
|
||||
$scope.clearEntityInternal = function() {
|
||||
$scope.currentEntityInternal = null;
|
||||
$scope.currentEntity = null;
|
||||
$scope.entitySelected({'entity': null});
|
||||
if ($scope.ngModel) {
|
||||
$scope.ngModel.$setValidity('entity', false);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setEntityInternal = function(entity, updateTypeahead) {
|
||||
if (updateTypeahead) {
|
||||
$(input).typeahead('val', $scope.autoClear ? '' : entity.name);
|
||||
} else {
|
||||
$(input).val($scope.autoClear ? '' : entity.name);
|
||||
}
|
||||
|
||||
if (!$scope.autoClear) {
|
||||
$scope.currentEntityInternal = entity;
|
||||
$scope.currentEntity = entity;
|
||||
}
|
||||
|
||||
$scope.entitySelected({'entity': entity});
|
||||
if ($scope.ngModel) {
|
||||
$scope.ngModel.$setValidity('entity', !!entity);
|
||||
}
|
||||
};
|
||||
|
||||
// Setup the typeahead.
|
||||
var input = $element[0].firstChild.firstChild;
|
||||
|
||||
(function() {
|
||||
// Create the bloodhound search query system.
|
||||
$rootScope.__entity_search_counter = (($rootScope.__entity_search_counter || 0) + 1);
|
||||
var entitySearchB = new Bloodhound({
|
||||
name: 'entities' + $rootScope.__entity_search_counter,
|
||||
remote: {
|
||||
url: '/api/v1/entities/%QUERY',
|
||||
replace: function (url, uriEncodedQuery) {
|
||||
var namespace = $scope.namespace || '';
|
||||
url = url.replace('%QUERY', uriEncodedQuery);
|
||||
url += '?namespace=' + encodeURIComponent(namespace);
|
||||
if ($scope.isOrganization && isSupported('team')) {
|
||||
url += '&includeTeams=true'
|
||||
}
|
||||
if (isSupported('org')) {
|
||||
url += '&includeOrgs=true'
|
||||
}
|
||||
return url;
|
||||
},
|
||||
filter: function(data) {
|
||||
var datums = [];
|
||||
for (var i = 0; i < data.results.length; ++i) {
|
||||
var entity = data.results[i];
|
||||
|
||||
var found = 'user';
|
||||
if (entity.kind == 'user') {
|
||||
found = entity.is_robot ? 'robot' : 'user';
|
||||
} else if (entity.kind == 'team') {
|
||||
found = 'team';
|
||||
} else if (entity.kind == 'org') {
|
||||
found = 'org';
|
||||
}
|
||||
|
||||
if (!isSupported(found)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
datums.push({
|
||||
'value': entity.name,
|
||||
'tokens': [entity.name],
|
||||
'entity': entity
|
||||
});
|
||||
}
|
||||
return datums;
|
||||
}
|
||||
},
|
||||
datumTokenizer: function(d) {
|
||||
return Bloodhound.tokenizers.whitespace(d.val);
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
entitySearchB.initialize();
|
||||
|
||||
// Setup the typeahead.
|
||||
$(input).typeahead({
|
||||
'highlight': true
|
||||
}, {
|
||||
source: entitySearchB.ttAdapter(),
|
||||
templates: {
|
||||
'empty': function(info) {
|
||||
// Only display the empty dialog if the server load has finished.
|
||||
if (info.resultKind == 'remote') {
|
||||
var val = $(input).val();
|
||||
if (!val) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (UtilService.isEmailAddress(val)) {
|
||||
if ($scope.allowEmails) {
|
||||
return '<div class="tt-message">' + $scope.emailMessage + '</div>';
|
||||
} else {
|
||||
return '<div class="tt-empty">A ' + Config.REGISTRY_TITLE_SHORT + ' username (not an e-mail address) must be specified</div>';
|
||||
}
|
||||
}
|
||||
|
||||
var classes = [];
|
||||
|
||||
if (isSupported('user')) { classes.push('users'); }
|
||||
if (isSupported('org')) { classes.push('organizations'); }
|
||||
if ($scope.isAdmin && isSupported('robot')) { classes.push('robot accounts'); }
|
||||
if ($scope.isOrganization && isSupported('team')) { classes.push('teams'); }
|
||||
|
||||
if (classes.length > 1) {
|
||||
classes[classes.length - 1] = 'or ' + classes[classes.length - 1];
|
||||
} else if (classes.length == 0) {
|
||||
return '<div class="tt-empty">No matching entities found</div>';
|
||||
}
|
||||
|
||||
var class_string = '';
|
||||
for (var i = 0; i < classes.length; ++i) {
|
||||
if (i > 0) {
|
||||
if (i == classes.length - 1) {
|
||||
class_string += ' or ';
|
||||
} else {
|
||||
class_string += ', ';
|
||||
}
|
||||
}
|
||||
|
||||
class_string += classes[i];
|
||||
}
|
||||
|
||||
return '<div class="tt-empty">No matching ' + Config.REGISTRY_TITLE_SHORT + ' ' + class_string + ' found</div>';
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
'suggestion': function (datum) {
|
||||
template = '<div class="entity-mini-listing">';
|
||||
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
|
||||
template += '<i class="fa fa-user fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
|
||||
template += '<i class="fa fa-wrench fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'team') {
|
||||
template += '<i class="fa fa-group fa-lg"></i>';
|
||||
} else if (datum.entity.kind == 'org') {
|
||||
template += '<i class="fa">' + AvatarService.getAvatar(datum.entity.avatar, 16) + '</i>';
|
||||
}
|
||||
|
||||
template += '<span class="name">' + datum.value + '</span>';
|
||||
|
||||
if (datum.entity.is_org_member === false && datum.entity.kind == 'user') {
|
||||
template += '<i class="fa fa-exclamation-triangle" title="User is outside the organization"></i>';
|
||||
}
|
||||
|
||||
template += '</div>';
|
||||
return template;
|
||||
}}
|
||||
});
|
||||
|
||||
$(input).on('keypress', function(e) {
|
||||
var val = $(input).val();
|
||||
var code = e.keyCode || e.which;
|
||||
if (code == 13 && $scope.allowEmails && UtilService.isEmailAddress(val)) {
|
||||
$scope.$apply(function() {
|
||||
$scope.emailSelected({'email': val});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$(input).on('input', function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.clearEntityInternal();
|
||||
});
|
||||
});
|
||||
|
||||
$(input).on('typeahead:selected', function(e, datum) {
|
||||
$scope.$apply(function() {
|
||||
$scope.setEntityInternal(datum.entity, true);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
$scope.$watch('clearValue', function() {
|
||||
if (!input) { return; }
|
||||
|
||||
$(input).typeahead('val', '');
|
||||
$scope.clearEntityInternal();
|
||||
});
|
||||
|
||||
$scope.$watch('placeholder', function(title) {
|
||||
input.setAttribute('placeholder', title);
|
||||
});
|
||||
|
||||
$scope.$watch('allowedEntities', function(allowed) {
|
||||
if (!allowed) { return; }
|
||||
$scope.includeTeams = isSupported('team', allowed);
|
||||
$scope.includeRobots = isSupported('robot', allowed);
|
||||
});
|
||||
|
||||
$scope.$watch('namespace', function(namespace) {
|
||||
if (!namespace) { return; }
|
||||
$scope.isAdmin = UserService.isNamespaceAdmin(namespace);
|
||||
$scope.isOrganization = !!UserService.getOrganization(namespace);
|
||||
});
|
||||
|
||||
$scope.$watch('currentEntity', function(entity) {
|
||||
if ($scope.currentEntityInternal != entity) {
|
||||
if (entity) {
|
||||
$scope.setEntityInternal(entity, false);
|
||||
} else {
|
||||
$scope.clearEntityInternal();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
40
static/js/directives/ui/external-login-button.js
Normal file
40
static/js/directives/ui/external-login-button.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* An element which displays a button for logging into the application via an external service.
|
||||
*/
|
||||
angular.module('quay').directive('externalLoginButton', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/external-login-button.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'signInStarted': '&signInStarted',
|
||||
'redirectUrl': '=redirectUrl',
|
||||
'provider': '@provider',
|
||||
'action': '@action'
|
||||
},
|
||||
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
|
||||
$scope.signingIn = false;
|
||||
$scope.isEnterprise = KeyService.isEnterprise;
|
||||
|
||||
$scope.startSignin = function(service) {
|
||||
$scope.signInStarted({'service': service});
|
||||
|
||||
var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login');
|
||||
|
||||
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
|
||||
var redirectURL = $scope.redirectUrl || window.location.toString();
|
||||
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
|
||||
|
||||
// Needed to ensure that UI work done by the started callback is finished before the location
|
||||
// changes.
|
||||
$scope.signingIn = true;
|
||||
$timeout(function() {
|
||||
document.location = url;
|
||||
}, 250);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
59
static/js/directives/ui/external-notification-view.js
Normal file
59
static/js/directives/ui/external-notification-view.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
/**
|
||||
* An element which displays controls and information about a defined external notification on
|
||||
* a repository.
|
||||
*/
|
||||
angular.module('quay').directive('externalNotificationView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/external-notification-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'notification': '=notification',
|
||||
'notificationDeleted': '¬ificationDeleted'
|
||||
},
|
||||
controller: function($scope, $element, ExternalNotificationData, ApiService) {
|
||||
$scope.deleteNotification = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'uuid': $scope.notification.uuid
|
||||
};
|
||||
|
||||
ApiService.deleteRepoNotification(null, params).then(function() {
|
||||
$scope.notificationDeleted({'notification': $scope.notification});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.testNotification = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'uuid': $scope.notification.uuid
|
||||
};
|
||||
|
||||
ApiService.testRepoNotification(null, params).then(function() {
|
||||
bootbox.dialog({
|
||||
"title": "Test Notification Queued",
|
||||
"message": "A test version of this notification has been queued and should appear shortly",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('notification', function(notification) {
|
||||
if (notification) {
|
||||
$scope.eventInfo = ExternalNotificationData.getEventInfo(notification.event);
|
||||
$scope.methodInfo = ExternalNotificationData.getMethodInfo(notification.method);
|
||||
$scope.config = notification.config;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
45
static/js/directives/ui/header-bar.js
Normal file
45
static/js/directives/ui/header-bar.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
|
||||
/**
|
||||
* The application header bar.
|
||||
*/
|
||||
angular.module('quay').directive('headerBar', function () {
|
||||
var number = 0;
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/header-bar.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element, $location, UserService, PlanService, ApiService, NotificationService, Config) {
|
||||
$scope.notificationService = NotificationService;
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.signout = function() {
|
||||
ApiService.logout().then(function() {
|
||||
UserService.load();
|
||||
$location.path('/');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.appLinkTarget = function() {
|
||||
if ($("div[ng-view]").length === 0) {
|
||||
return "_self";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
|
||||
$scope.getEnterpriseLogo = function() {
|
||||
if (!Config.ENTERPRISE_LOGO_URL) {
|
||||
return '/static/img/quay-logo.png';
|
||||
}
|
||||
|
||||
return Config.ENTERPRISE_LOGO_URL;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
106
static/js/directives/ui/location-view.js
Normal file
106
static/js/directives/ui/location-view.js
Normal file
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* An element which displays a small flag representing the given location, as well as a ping
|
||||
* latency gauge for that location.
|
||||
*/
|
||||
angular.module('quay').directive('locationView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/location-view.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'location': '=location'
|
||||
},
|
||||
controller: function($rootScope, $scope, $element, $http, PingService) {
|
||||
var LOCATIONS = {
|
||||
'local_us': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States' },
|
||||
'local_eu': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' },
|
||||
|
||||
's3_us_east_1': { 'country': 'US', 'data': 'quay-registry.s3.amazonaws.com', 'title': 'United States (East)' },
|
||||
's3_us_west_1': { 'country': 'US', 'data': 'quay-registry-cali.s3.amazonaws.com', 'title': 'United States (West)' },
|
||||
|
||||
's3_eu_west_1': { 'country': 'EU', 'data': 'quay-registry-eu.s3-eu-west-1.amazonaws.com', 'title': 'Europe' },
|
||||
|
||||
's3_ap_southeast_1': { 'country': 'SG', 'data': 'quay-registry-singapore.s3-ap-southeast-1.amazonaws.com', 'title': 'Singapore' },
|
||||
's3_ap_southeast_2': { 'country': 'AU', 'data': 'quay-registry-sydney.s3-ap-southeast-2.amazonaws.com', 'title': 'Australia' },
|
||||
|
||||
// 's3_ap_northeast-1': { 'country': 'JP', 'data': 's3-ap-northeast-1.amazonaws.com', 'title': 'Japan' },
|
||||
// 's3_sa_east1': { 'country': 'BR', 'data': 's3-east-1.amazonaws.com', 'title': 'Sao Paulo' }
|
||||
};
|
||||
|
||||
$scope.locationPing = null;
|
||||
$scope.locationPingClass = null;
|
||||
|
||||
$scope.getLocationTooltip = function(location, ping) {
|
||||
var tip = $scope.getLocationTitle(location) + '<br>';
|
||||
if (ping == null) {
|
||||
tip += '(Loading)';
|
||||
} else if (ping < 0) {
|
||||
tip += '<br><b>Note: Could not contact server</b>';
|
||||
} else {
|
||||
tip += 'Estimated Ping: ' + (ping ? ping + 'ms' : '(Loading)');
|
||||
}
|
||||
return tip;
|
||||
};
|
||||
|
||||
$scope.getLocationTitle = function(location) {
|
||||
if (!LOCATIONS[location]) {
|
||||
return '(Unknown)';
|
||||
}
|
||||
return 'Image data is located in ' + LOCATIONS[location]['title'];
|
||||
};
|
||||
|
||||
$scope.getLocationImage = function(location) {
|
||||
if (!LOCATIONS[location]) {
|
||||
return 'unknown.png';
|
||||
}
|
||||
return LOCATIONS[location]['country'] + '.png';
|
||||
};
|
||||
|
||||
$scope.getLocationPing = function(location) {
|
||||
var url = 'https://' + LOCATIONS[location]['data'] + '/okay.txt';
|
||||
PingService.pingUrl($scope, url, function(ping, success, count) {
|
||||
if (count == 3 || !success) {
|
||||
$scope.locationPing = success ? ping : -1;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('location', function(location) {
|
||||
if (!location) { return; }
|
||||
$scope.getLocationPing(location);
|
||||
});
|
||||
|
||||
$scope.$watch('locationPing', function(locationPing) {
|
||||
if (locationPing == null) {
|
||||
$scope.locationPingClass = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (locationPing < 0) {
|
||||
$scope.locationPingClass = 'error';
|
||||
return;
|
||||
}
|
||||
|
||||
if (locationPing < 100) {
|
||||
$scope.locationPingClass = 'good';
|
||||
return;
|
||||
}
|
||||
|
||||
if (locationPing < 250) {
|
||||
$scope.locationPingClass = 'fair';
|
||||
return;
|
||||
}
|
||||
|
||||
if (locationPing < 500) {
|
||||
$scope.locationPingClass = 'barely';
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.locationPingClass = 'poor';
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
336
static/js/directives/ui/logs-view.js
Normal file
336
static/js/directives/ui/logs-view.js
Normal file
|
@ -0,0 +1,336 @@
|
|||
/**
|
||||
* Element which displays usage logs for the given entity.
|
||||
*/
|
||||
angular.module('quay').directive('logsView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/logs-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user',
|
||||
'makevisible': '=makevisible',
|
||||
'repository': '=repository',
|
||||
'performer': '=performer',
|
||||
'allLogs': '@allLogs'
|
||||
},
|
||||
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerService,
|
||||
StringBuilderService, ExternalNotificationData, UtilService) {
|
||||
$scope.loading = true;
|
||||
$scope.logs = null;
|
||||
$scope.kindsAllowed = null;
|
||||
$scope.chartVisible = true;
|
||||
$scope.logsPath = '';
|
||||
|
||||
var datetime = new Date();
|
||||
$scope.logStartDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate() - 7);
|
||||
$scope.logEndDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate());
|
||||
|
||||
var defaultPermSuffix = function(metadata) {
|
||||
if (metadata.activating_username) {
|
||||
return ', when creating user is {activating_username}';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
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}',
|
||||
'repo_verb': function(metadata) {
|
||||
var prefix = '';
|
||||
if (metadata.verb == 'squash') {
|
||||
prefix = 'Pull of squashed tag {tag}'
|
||||
}
|
||||
|
||||
if (metadata.token) {
|
||||
if (metadata.token_type == 'build-worker') {
|
||||
prefix += ' by <b>build worker</b>';
|
||||
} else {
|
||||
prefix += ' via token';
|
||||
}
|
||||
} else if (metadata.username) {
|
||||
prefix += ' by {username}';
|
||||
} else {
|
||||
prefix += ' by {_ip}';
|
||||
}
|
||||
|
||||
return prefix;
|
||||
},
|
||||
'pull_repo': function(metadata) {
|
||||
if (metadata.token) {
|
||||
var prefix = 'Pull of repository'
|
||||
if (metadata.token_type == 'build-worker') {
|
||||
prefix += ' by <b>build worker</b>';
|
||||
} else {
|
||||
prefix += ' via token';
|
||||
}
|
||||
return prefix;
|
||||
} 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}';
|
||||
}
|
||||
},
|
||||
'delete_tag': 'Tag {tag} deleted in repository {repo} by user {username}',
|
||||
'create_tag': 'Tag {tag} created in repository {repo} on image {image} by user {username}',
|
||||
'move_tag': 'Tag {tag} moved from image {original_image} to image {image} in repository {repo} by user {username}',
|
||||
'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}',
|
||||
'set_repo_description': 'Change description for repository {repo}: {description}',
|
||||
'build_dockerfile': function(metadata) {
|
||||
if (metadata.trigger_id) {
|
||||
var triggerDescription = TriggerService.getDescription(
|
||||
metadata['service'], metadata['config']);
|
||||
return 'Build image from Dockerfile for repository {repo} triggered by ' + triggerDescription;
|
||||
}
|
||||
return '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_invite_team_member': function(metadata) {
|
||||
if (metadata.user) {
|
||||
return 'Invite {user} to team {team}';
|
||||
} else {
|
||||
return 'Invite {email} to team {team}';
|
||||
}
|
||||
},
|
||||
'org_delete_team_member_invite': function(metadata) {
|
||||
if (metadata.user) {
|
||||
return 'Rescind invite of {user} to team {team}';
|
||||
} else {
|
||||
return 'Rescind invite of {email} to team {team}';
|
||||
}
|
||||
},
|
||||
|
||||
'org_team_member_invite_accepted': 'User {member}, invited by {inviter}, joined team {team}',
|
||||
'org_team_member_invite_declined': 'User {member}, invited by {inviter}, declined to join team {team}',
|
||||
|
||||
'org_set_team_description': 'Change description of team {team}: {description}',
|
||||
'org_set_team_role': 'Change permission of team {team} to {role}',
|
||||
'create_prototype_permission': function(metadata) {
|
||||
if (metadata.delegate_user) {
|
||||
return 'Create default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata);
|
||||
} else if (metadata.delegate_team) {
|
||||
return 'Create default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata);
|
||||
}
|
||||
},
|
||||
'modify_prototype_permission': function(metadata) {
|
||||
if (metadata.delegate_user) {
|
||||
return 'Modify default permission: {role} (from {original_role}) for {delegate_user}' + defaultPermSuffix(metadata);
|
||||
} else if (metadata.delegate_team) {
|
||||
return 'Modify default permission: {role} (from {original_role}) for {delegate_team}' + defaultPermSuffix(metadata);
|
||||
}
|
||||
},
|
||||
'delete_prototype_permission': function(metadata) {
|
||||
if (metadata.delegate_user) {
|
||||
return 'Delete default permission: {role} for {delegate_user}' + defaultPermSuffix(metadata);
|
||||
} else if (metadata.delegate_team) {
|
||||
return 'Delete default permission: {role} for {delegate_team}' + defaultPermSuffix(metadata);
|
||||
}
|
||||
},
|
||||
'setup_repo_trigger': function(metadata) {
|
||||
var triggerDescription = TriggerService.getDescription(
|
||||
metadata['service'], metadata['config']);
|
||||
return 'Setup build trigger - ' + triggerDescription;
|
||||
},
|
||||
'delete_repo_trigger': function(metadata) {
|
||||
var triggerDescription = TriggerService.getDescription(
|
||||
metadata['service'], metadata['config']);
|
||||
return 'Delete build trigger - ' + triggerDescription;
|
||||
},
|
||||
'create_application': 'Create application {application_name} with client ID {client_id}',
|
||||
'update_application': 'Update application to {application_name} for client ID {client_id}',
|
||||
'delete_application': 'Delete application {application_name} with client ID {client_id}',
|
||||
'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' +
|
||||
'with client ID {client_id}',
|
||||
|
||||
'add_repo_notification': function(metadata) {
|
||||
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
||||
return 'Add notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||
},
|
||||
|
||||
'delete_repo_notification': function(metadata) {
|
||||
var eventData = ExternalNotificationData.getEventInfo(metadata.event);
|
||||
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
|
||||
},
|
||||
|
||||
'regenerate_robot_token': 'Regenerated token for robot {robot}',
|
||||
|
||||
// Note: These are deprecated.
|
||||
'add_repo_webhook': 'Add webhook in repository {repo}',
|
||||
'delete_repo_webhook': 'Delete webhook in repository {repo}'
|
||||
};
|
||||
|
||||
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',
|
||||
'repo_verb': 'Pull Repo Verb',
|
||||
'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',
|
||||
'set_repo_description': 'Change repository description',
|
||||
'build_dockerfile': 'Build image from Dockerfile',
|
||||
'delete_tag': 'Delete Tag',
|
||||
'create_tag': 'Create Tag',
|
||||
'move_tag': 'Move Tag',
|
||||
'org_create_team': 'Create team',
|
||||
'org_delete_team': 'Delete team',
|
||||
'org_add_team_member': 'Add team member',
|
||||
'org_invite_team_member': 'Invite team member',
|
||||
'org_delete_team_member_invite': 'Rescind team member invitation',
|
||||
'org_remove_team_member': 'Remove team member',
|
||||
'org_team_member_invite_accepted': 'Team invite accepted',
|
||||
'org_team_member_invite_declined': 'Team invite declined',
|
||||
'org_set_team_description': 'Change team description',
|
||||
'org_set_team_role': 'Change team permission',
|
||||
'create_prototype_permission': 'Create default permission',
|
||||
'modify_prototype_permission': 'Modify default permission',
|
||||
'delete_prototype_permission': 'Delete default permission',
|
||||
'setup_repo_trigger': 'Setup build trigger',
|
||||
'delete_repo_trigger': 'Delete build trigger',
|
||||
'create_application': 'Create Application',
|
||||
'update_application': 'Update Application',
|
||||
'delete_application': 'Delete Application',
|
||||
'reset_application_client_secret': 'Reset Client Secret',
|
||||
'add_repo_notification': 'Add repository notification',
|
||||
'delete_repo_notification': 'Delete repository notification',
|
||||
'regenerate_robot_token': 'Regenerate Robot Token',
|
||||
|
||||
// Note: these are deprecated.
|
||||
'add_repo_webhook': 'Add webhook',
|
||||
'delete_repo_webhook': 'Delete webhook'
|
||||
};
|
||||
|
||||
var getDateString = function(date) {
|
||||
return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear();
|
||||
};
|
||||
|
||||
var getOffsetDate = function(date, days) {
|
||||
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
||||
var hasValidOrg = !!$scope.organization;
|
||||
var hasValidRepo = $scope.repository && $scope.repository.namespace;
|
||||
var isValid = hasValidUser || hasValidOrg || hasValidRepo || $scope.allLogs;
|
||||
|
||||
if (!$scope.makevisible || !isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
var twoWeeksAgo = getOffsetDate($scope.logEndDate, -14);
|
||||
if ($scope.logStartDate > $scope.logEndDate || $scope.logStartDate < twoWeeksAgo) {
|
||||
$scope.logStartDate = twoWeeksAgo;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
// Note: We construct the URLs here manually because we also use it for the download
|
||||
// path.
|
||||
var url = UtilService.getRestUrl('user/logs');
|
||||
if ($scope.organization) {
|
||||
url = UtilService.getRestUrl('organization', $scope.organization.name, 'logs');
|
||||
}
|
||||
if ($scope.repository) {
|
||||
url = UtilService.getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
||||
}
|
||||
|
||||
if ($scope.allLogs) {
|
||||
url = UtilService.getRestUrl('superuser', 'logs')
|
||||
}
|
||||
|
||||
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
|
||||
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));
|
||||
|
||||
if ($scope.performer) {
|
||||
url += '&performer=' + encodeURIComponent($scope.performer.name);
|
||||
}
|
||||
|
||||
var loadLogs = Restangular.one(url);
|
||||
loadLogs.customGET().then(function(resp) {
|
||||
$scope.logsPath = '/api/v1/' + url;
|
||||
|
||||
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.logStartDate, $scope.logEndDate);
|
||||
$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) {
|
||||
log.metadata['_ip'] = log.ip ? log.ip : null;
|
||||
return StringBuilderService.buildString(logDescriptions[log.kind] || log.kind, log.metadata);
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
$scope.$watch('repository', update);
|
||||
$scope.$watch('makevisible', update);
|
||||
$scope.$watch('performer', update);
|
||||
$scope.$watch('logStartDate', update);
|
||||
$scope.$watch('logEndDate', update);
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
61
static/js/directives/ui/manual-trigger-build-dialog.js
Normal file
61
static/js/directives/ui/manual-trigger-build-dialog.js
Normal file
|
@ -0,0 +1,61 @@
|
|||
/**
|
||||
* An element which displays a dialog for manually trigger a build.
|
||||
*/
|
||||
angular.module('quay').directive('manualTriggerBuildDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/manual-trigger-build-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'counter': '=counter',
|
||||
'trigger': '=trigger',
|
||||
'startBuild': '&startBuild'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, TriggerService) {
|
||||
$scope.parameters = {};
|
||||
$scope.fieldOptions = {};
|
||||
|
||||
$scope.startTrigger = function() {
|
||||
$('#startTriggerDialog').modal('hide');
|
||||
$scope.startBuild({
|
||||
'trigger': $scope.trigger,
|
||||
'parameters': $scope.parameters
|
||||
});
|
||||
};
|
||||
|
||||
$scope.show = function() {
|
||||
$scope.parameters = {};
|
||||
$scope.fieldOptions = {};
|
||||
|
||||
var parameters = TriggerService.getRunParameters($scope.trigger.service);
|
||||
for (var i = 0; i < parameters.length; ++i) {
|
||||
var parameter = parameters[i];
|
||||
if (parameter['type'] == 'option') {
|
||||
// Load the values for this parameter.
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id,
|
||||
'field_name': parameter['name']
|
||||
};
|
||||
|
||||
ApiService.listTriggerFieldValues(null, params).then(function(resp) {
|
||||
$scope.fieldOptions[parameter['name']] = resp['values'];
|
||||
});
|
||||
}
|
||||
}
|
||||
$scope.runParameters = parameters;
|
||||
|
||||
$('#startTriggerDialog').modal('show');
|
||||
};
|
||||
|
||||
$scope.$watch('counter', function(counter) {
|
||||
if (counter) {
|
||||
$scope.show();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
49
static/js/directives/ui/markdown-input.js
Normal file
49
static/js/directives/ui/markdown-input.js
Normal file
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* An element which allows for entry of markdown content and previewing its rendering.
|
||||
*/
|
||||
angular.module('quay').directive('markdownInput', function () {
|
||||
var counter = 0;
|
||||
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/markdown-input.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'content': '=content',
|
||||
'canWrite': '=canWrite',
|
||||
'contentChanged': '=contentChanged',
|
||||
'fieldTitle': '=fieldTitle'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
var elm = $element[0];
|
||||
|
||||
$scope.id = (counter++);
|
||||
|
||||
$scope.editContent = function() {
|
||||
if (!$scope.canWrite) { return; }
|
||||
|
||||
if (!$scope.markdownDescriptionEditor) {
|
||||
var converter = Markdown.getSanitizingConverter();
|
||||
var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
|
||||
editor.run();
|
||||
$scope.markdownDescriptionEditor = editor;
|
||||
}
|
||||
|
||||
$('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
|
||||
$(elm).find('.modal').modal({});
|
||||
};
|
||||
|
||||
$scope.saveContent = function() {
|
||||
$scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
|
||||
$(elm).find('.modal').modal('hide');
|
||||
|
||||
if ($scope.contentChanged) {
|
||||
$scope.contentChanged($scope.content);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
25
static/js/directives/ui/markdown-view.js
Normal file
25
static/js/directives/ui/markdown-view.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
/**
|
||||
* An element which displays its content processed as markdown.
|
||||
*/
|
||||
angular.module('quay').directive('markdownView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/markdown-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'content': '=content',
|
||||
'firstLineOnly': '=firstLineOnly'
|
||||
},
|
||||
controller: function($scope, $element, $sce, UtilService) {
|
||||
$scope.getMarkedDown = function(content, firstLineOnly) {
|
||||
if (firstLineOnly) {
|
||||
return $sce.trustAsHtml(UtilService.getFirstMarkdownLineAsText(content));
|
||||
}
|
||||
return $sce.trustAsHtml(UtilService.getMarkedDown(content));
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
74
static/js/directives/ui/namespace-selector.js
Normal file
74
static/js/directives/ui/namespace-selector.js
Normal file
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* An element which displays a dropdown namespace selector or, if there is only a single namespace,
|
||||
* that namespace.
|
||||
*/
|
||||
angular.module('quay').directive('namespaceSelector', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/namespace-selector.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'namespace': '=namespace',
|
||||
'requireCreate': '=requireCreate'
|
||||
},
|
||||
controller: function($scope, $element, $routeParams, $location, CookieService) {
|
||||
$scope.namespaces = {};
|
||||
|
||||
$scope.initialize = function(user) {
|
||||
var preferredNamespace = user.username;
|
||||
var namespaces = {};
|
||||
namespaces[user.username] = user;
|
||||
if (user.organizations) {
|
||||
for (var i = 0; i < user.organizations.length; ++i) {
|
||||
namespaces[user.organizations[i].name] = user.organizations[i];
|
||||
if (user.organizations[i].preferred_namespace) {
|
||||
preferredNamespace = user.organizations[i].name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var initialNamespace = $routeParams['namespace'] || CookieService.get('quay.namespace') ||
|
||||
preferredNamespace || $scope.user.username;
|
||||
$scope.namespaces = namespaces;
|
||||
$scope.setNamespace($scope.namespaces[initialNamespace]);
|
||||
};
|
||||
|
||||
$scope.setNamespace = function(namespaceObj) {
|
||||
if (!namespaceObj) {
|
||||
namespaceObj = $scope.namespaces[$scope.user.username];
|
||||
}
|
||||
|
||||
if ($scope.requireCreate && !namespaceObj.can_create_repo) {
|
||||
namespaceObj = $scope.namespaces[$scope.user.username];
|
||||
}
|
||||
|
||||
var newNamespace = namespaceObj.name || namespaceObj.username;
|
||||
$scope.namespaceObj = namespaceObj;
|
||||
$scope.namespace = newNamespace;
|
||||
|
||||
if (newNamespace) {
|
||||
CookieService.putPermanent('quay.namespace', newNamespace);
|
||||
|
||||
if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) {
|
||||
$location.search({'namespace': newNamespace});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('namespace', function(namespace) {
|
||||
if ($scope.namespaceObj && namespace && namespace != $scope.namespaceObj.username) {
|
||||
$scope.setNamespace($scope.namespaces[namespace]);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('user', function(user) {
|
||||
$scope.user = user;
|
||||
$scope.initialize(user);
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
69
static/js/directives/ui/notification-view.js
Normal file
69
static/js/directives/ui/notification-view.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/**
|
||||
* An element which displays an application notification's information.
|
||||
*/
|
||||
angular.module('quay').directive('notificationView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/notification-view.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'notification': '=notification',
|
||||
'parent': '=parent'
|
||||
},
|
||||
controller: function($scope, $element, $window, $location, UserService, NotificationService, ApiService) {
|
||||
var stringStartsWith = function (str, prefix) {
|
||||
return str.slice(0, prefix.length) == prefix;
|
||||
};
|
||||
|
||||
$scope.getMessage = function(notification) {
|
||||
return NotificationService.getMessage(notification);
|
||||
};
|
||||
|
||||
$scope.getAvatar = function(orgname) {
|
||||
var organization = UserService.getOrganization(orgname);
|
||||
return organization['avatar'] || '';
|
||||
};
|
||||
|
||||
$scope.parseDate = function(dateString) {
|
||||
return Date.parse(dateString);
|
||||
};
|
||||
|
||||
$scope.showNotification = function() {
|
||||
var url = NotificationService.getPage($scope.notification);
|
||||
if (url) {
|
||||
if (stringStartsWith(url, 'http://') || stringStartsWith(url, 'https://')) {
|
||||
$window.location.href = url;
|
||||
} else {
|
||||
var parts = url.split('?')
|
||||
$location.path(parts[0]);
|
||||
|
||||
if (parts.length > 1) {
|
||||
$location.search(parts[1]);
|
||||
}
|
||||
|
||||
$scope.parent.$hide();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.dismissNotification = function(notification) {
|
||||
NotificationService.dismissNotification(notification);
|
||||
};
|
||||
|
||||
$scope.canDismiss = function(notification) {
|
||||
return NotificationService.canDismiss(notification);
|
||||
};
|
||||
|
||||
$scope.getClass = function(notification) {
|
||||
return NotificationService.getClass(notification);
|
||||
};
|
||||
|
||||
$scope.getActions = function(notification) {
|
||||
return NotificationService.getActions(notification);
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
19
static/js/directives/ui/notifications-bubble.js
Normal file
19
static/js/directives/ui/notifications-bubble.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* An element which displays the number and kind of application notifications. If there are no
|
||||
* notifications, then the element is hidden/empty.
|
||||
*/
|
||||
angular.module('quay').directive('notificationsBubble', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/notifications-bubble.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, UserService, NotificationService) {
|
||||
$scope.notificationService = NotificationService;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
20
static/js/directives/ui/organization-header.js
Normal file
20
static/js/directives/ui/organization-header.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* An element which displays an organization header, optionally with trancluded content.
|
||||
*/
|
||||
angular.module('quay').directive('organizationHeader', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/organization-header.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'teamName': '=teamName',
|
||||
'clickable': '=clickable'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
112
static/js/directives/ui/plan-manager.js
Normal file
112
static/js/directives/ui/plan-manager.js
Normal file
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* Element for managing subscriptions.
|
||||
*/
|
||||
angular.module('quay').directive('planManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/plan-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'user': '=user',
|
||||
'organization': '=organization',
|
||||
'readyForPlan': '&readyForPlan',
|
||||
'planChanged': '&planChanged'
|
||||
},
|
||||
controller: function($scope, $element, PlanService, ApiService) {
|
||||
$scope.isExistingCustomer = false;
|
||||
|
||||
$scope.parseDate = function(timestamp) {
|
||||
return new Date(timestamp * 1000);
|
||||
};
|
||||
|
||||
$scope.isPlanVisible = function(plan, subscribedPlan) {
|
||||
if (plan['deprecated']) {
|
||||
return plan == subscribedPlan;
|
||||
}
|
||||
|
||||
if ($scope.organization && !PlanService.isOrgCompatible(plan)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.changeSubscription = function(planId, opt_async) {
|
||||
if ($scope.planChanging) { return; }
|
||||
|
||||
var callbacks = {
|
||||
'opening': function() { $scope.planChanging = true; },
|
||||
'started': function() { $scope.planChanging = true; },
|
||||
'opened': function() { $scope.planChanging = true; },
|
||||
'closed': function() { $scope.planChanging = false; },
|
||||
'success': subscribedToPlan,
|
||||
'failure': function(resp) {
|
||||
$scope.planChanging = false;
|
||||
}
|
||||
};
|
||||
|
||||
PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
|
||||
};
|
||||
|
||||
$scope.cancelSubscription = function() {
|
||||
$scope.changeSubscription(PlanService.getFreePlan());
|
||||
};
|
||||
|
||||
var subscribedToPlan = function(sub) {
|
||||
$scope.subscription = sub;
|
||||
$scope.isExistingCustomer = !!sub['isExistingCustomer'];
|
||||
|
||||
PlanService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
|
||||
$scope.subscribedPlan = subscribedPlan;
|
||||
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
||||
|
||||
if ($scope.planChanged) {
|
||||
$scope.planChanged({ 'plan': subscribedPlan });
|
||||
}
|
||||
|
||||
$scope.planChanging = false;
|
||||
$scope.planLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
$scope.planLoading = true;
|
||||
if (!$scope.plans) { return; }
|
||||
|
||||
PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
|
||||
$scope.isExistingCustomer = false;
|
||||
subscribedToPlan({ 'plan': PlanService.getFreePlan() });
|
||||
});
|
||||
};
|
||||
|
||||
var loadPlans = function() {
|
||||
if ($scope.plans || $scope.loadingPlans) { return; }
|
||||
if (!$scope.user && !$scope.organization) { return; }
|
||||
|
||||
$scope.loadingPlans = true;
|
||||
PlanService.verifyLoaded(function(plans) {
|
||||
$scope.plans = plans;
|
||||
update();
|
||||
|
||||
if ($scope.readyForPlan) {
|
||||
var planRequested = $scope.readyForPlan();
|
||||
if (planRequested && planRequested != PlanService.getFreePlan()) {
|
||||
$scope.changeSubscription(planRequested, /* async */true);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Start the initial download.
|
||||
$scope.planLoading = true;
|
||||
loadPlans();
|
||||
|
||||
$scope.$watch('organization', loadPlans);
|
||||
$scope.$watch('user', loadPlans);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
23
static/js/directives/ui/plans-table.js
Normal file
23
static/js/directives/ui/plans-table.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* An element which shows a table of all the defined subscription plans and allows one to be
|
||||
* highlighted.
|
||||
*/
|
||||
angular.module('quay').directive('plansTable', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/plans-table.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'plans': '=plans',
|
||||
'currentPlan': '=currentPlan'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.setPlan = function(plan) {
|
||||
$scope.currentPlan = plan;
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
48
static/js/directives/ui/popup-input-button.js
Normal file
48
static/js/directives/ui/popup-input-button.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/**
|
||||
* An element which, when clicked, displays a popup input dialog to accept a text value.
|
||||
*/
|
||||
angular.module('quay').directive('popupInputButton', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/popup-input-button.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'placeholder': '=placeholder',
|
||||
'pattern': '=pattern',
|
||||
'submitted': '&submitted'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.popupShown = function() {
|
||||
setTimeout(function() {
|
||||
var box = $('#input-box');
|
||||
box[0].value = '';
|
||||
box.focus();
|
||||
}, 40);
|
||||
};
|
||||
|
||||
$scope.getRegexp = function(pattern) {
|
||||
if (!pattern) {
|
||||
pattern = '.*';
|
||||
}
|
||||
return new RegExp(pattern);
|
||||
};
|
||||
|
||||
$scope.inputSubmit = function() {
|
||||
var box = $('#input-box');
|
||||
if (box.hasClass('ng-invalid')) { return; }
|
||||
|
||||
var entered = box[0].value;
|
||||
if (!entered) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scope.submitted) {
|
||||
$scope.submitted({'value': entered});
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
123
static/js/directives/ui/prototype-manager.js
vendored
Normal file
123
static/js/directives/ui/prototype-manager.js
vendored
Normal file
|
@ -0,0 +1,123 @@
|
|||
/**
|
||||
* Element for managing the prototype permissions for an organization.
|
||||
*/
|
||||
angular.module('quay').directive('prototypeManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/prototype-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.activatingForNew = null;
|
||||
$scope.delegateForNew = null;
|
||||
$scope.clearCounter = 0;
|
||||
$scope.newForWholeOrg = true;
|
||||
|
||||
$scope.roles = [
|
||||
{ 'id': 'read', 'title': 'Read', 'kind': 'success' },
|
||||
{ 'id': 'write', 'title': 'Write', 'kind': 'success' },
|
||||
{ 'id': 'admin', 'title': 'Admin', 'kind': 'primary' }
|
||||
];
|
||||
|
||||
$scope.setRole = function(role, prototype) {
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'prototypeid': prototype.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'id': prototype.id,
|
||||
'role': role
|
||||
};
|
||||
|
||||
ApiService.updateOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||
prototype.role = role;
|
||||
}, ApiService.errorDisplay('Cannot modify permission'));
|
||||
};
|
||||
|
||||
$scope.comparePrototypes = function(p) {
|
||||
return p.activating_user ? p.activating_user.name : ' ';
|
||||
};
|
||||
|
||||
$scope.setRoleForNew = function(role) {
|
||||
$scope.newRole = role;
|
||||
};
|
||||
|
||||
$scope.setNewForWholeOrg = function(value) {
|
||||
$scope.newForWholeOrg = value;
|
||||
};
|
||||
|
||||
$scope.showAddDialog = function() {
|
||||
$scope.activatingForNew = null;
|
||||
$scope.delegateForNew = null;
|
||||
$scope.newRole = 'read';
|
||||
$scope.clearCounter++;
|
||||
$scope.newForWholeOrg = true;
|
||||
$('#addPermissionDialogModal').modal({});
|
||||
};
|
||||
|
||||
$scope.createPrototype = function() {
|
||||
$scope.loading = true;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'delegate': $scope.delegateForNew,
|
||||
'role': $scope.newRole
|
||||
};
|
||||
|
||||
if (!$scope.newForWholeOrg) {
|
||||
data['activating_user'] = $scope.activatingForNew;
|
||||
}
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot create permission',
|
||||
function(resp) {
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
});
|
||||
|
||||
ApiService.createOrganizationPrototypePermission(data, params).then(function(resp) {
|
||||
$scope.prototypes.push(resp);
|
||||
$scope.loading = false;
|
||||
$('#addPermissionDialogModal').modal('hide');
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
$scope.deletePrototype = function(prototype) {
|
||||
$scope.loading = true;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name,
|
||||
'prototypeid': prototype.id
|
||||
};
|
||||
|
||||
ApiService.deleteOrganizationPrototypePermission(null, params).then(function(resp) {
|
||||
$scope.prototypes.splice($scope.prototypes.indexOf(prototype), 1);
|
||||
$scope.loading = false;
|
||||
}, ApiService.errorDisplay('Cannot delete permission'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.organization) { return; }
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
var params = {'orgname': $scope.organization.name};
|
||||
|
||||
$scope.loading = true;
|
||||
ApiService.getOrganizationPrototypePermissions(null, params).then(function(resp) {
|
||||
$scope.prototypes = resp.prototypes;
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
50
static/js/directives/ui/ps-usage-graph.js
Normal file
50
static/js/directives/ui/ps-usage-graph.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* An element which displays charts and graphs representing the current installation of the
|
||||
* application. This control requires superuser access and *must be disabled when not visible*.
|
||||
*/
|
||||
angular.module('quay').directive('psUsageGraph', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/ps-usage-graph.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isEnabled': '=isEnabled'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.counter = -1;
|
||||
$scope.data = null;
|
||||
|
||||
var source = null;
|
||||
|
||||
var connect = function() {
|
||||
if (source) { return; }
|
||||
source = new EventSource('/realtime/ps');
|
||||
source.onmessage = function(e) {
|
||||
$scope.$apply(function() {
|
||||
$scope.counter++;
|
||||
$scope.data = JSON.parse(e.data);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
var disconnect = function() {
|
||||
if (!source) { return; }
|
||||
source.close();
|
||||
source = null;
|
||||
};
|
||||
|
||||
$scope.$watch('isEnabled', function(value) {
|
||||
if (value) {
|
||||
connect();
|
||||
} else {
|
||||
disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$on("$destroy", disconnect);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
16
static/js/directives/ui/quay-spinner.js
Normal file
16
static/js/directives/ui/quay-spinner.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* A spinner.
|
||||
*/
|
||||
angular.module('quay').directive('quaySpinner', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/spinner.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
20
static/js/directives/ui/registry-name.js
Normal file
20
static/js/directives/ui/registry-name.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* An element which displays the name of the registry (optionally the short name).
|
||||
*/
|
||||
angular.module('quay').directive('registryName', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/registry-name.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'isShort': '=isShort'
|
||||
},
|
||||
controller: function($scope, $element, Config) {
|
||||
$scope.name = $scope.isShort ? Config.REGISTRY_TITLE_SHORT : Config.REGISTRY_TITLE;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
22
static/js/directives/ui/repo-breadcrumb.js
Normal file
22
static/js/directives/ui/repo-breadcrumb.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which shows the breadcrumbs for a repository, including subsections such as an
|
||||
* an image or a generic subsection.
|
||||
*/
|
||||
angular.module('quay').directive('repoBreadcrumb', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/repo-breadcrumb.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repo': '=repo',
|
||||
'image': '=image',
|
||||
'subsection': '=subsection',
|
||||
'subsectionIcon': '=subsectionIcon'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
18
static/js/directives/ui/repo-circle.js
Normal file
18
static/js/directives/ui/repo-circle.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
/**
|
||||
* An element which shows a repository icon (inside a circle).
|
||||
*/
|
||||
angular.module('quay').directive('repoCircle', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/repo-circle.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repo': '=repo'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -1,4 +1,8 @@
|
|||
quayApp.directive('repoListGrid', function() {
|
||||
/**
|
||||
* An element that displays a list of repositories in a grid.
|
||||
*
|
||||
*/
|
||||
angular.module('quay').directive('repoListGrid', function() {
|
||||
return {
|
||||
templateUrl: '/static/directives/repo-list-grid.html',
|
||||
priority: 0,
|
76
static/js/directives/ui/repo-search.js
Normal file
76
static/js/directives/ui/repo-search.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
/**
|
||||
* An element which displays a repository search box.
|
||||
*/
|
||||
angular.module('quay').directive('repoSearch', function () {
|
||||
var number = 0;
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/repo-search.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element, $location, UserService, Restangular, UtilService) {
|
||||
var searchToken = 0;
|
||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||
++searchToken;
|
||||
}, true);
|
||||
|
||||
var repoHound = new Bloodhound({
|
||||
name: 'repositories',
|
||||
remote: {
|
||||
url: '/api/v1/find/repository?query=%QUERY',
|
||||
replace: function (url, uriEncodedQuery) {
|
||||
url = url.replace('%QUERY', uriEncodedQuery);
|
||||
url += '&cb=' + searchToken;
|
||||
return url;
|
||||
},
|
||||
filter: function(data) {
|
||||
var datums = [];
|
||||
for (var i = 0; i < data.repositories.length; ++i) {
|
||||
var repo = data.repositories[i];
|
||||
datums.push({
|
||||
'value': repo.name,
|
||||
'tokens': [repo.name, repo.namespace],
|
||||
'repo': repo
|
||||
});
|
||||
}
|
||||
return datums;
|
||||
}
|
||||
},
|
||||
datumTokenizer: function(d) {
|
||||
return Bloodhound.tokenizers.whitespace(d.val);
|
||||
},
|
||||
queryTokenizer: Bloodhound.tokenizers.whitespace
|
||||
});
|
||||
repoHound.initialize();
|
||||
|
||||
var element = $($element[0].childNodes[0]);
|
||||
element.typeahead({ 'highlight': true }, {
|
||||
source: repoHound.ttAdapter(),
|
||||
templates: {
|
||||
'suggestion': function (datum) {
|
||||
template = '<div class="repo-mini-listing">';
|
||||
template += '<i class="fa fa-hdd-o fa-lg"></i>'
|
||||
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
|
||||
if (datum.repo.description) {
|
||||
template += '<span class="description">' + UtilService.getFirstMarkdownLineAsText(datum.repo.description) + '</span>'
|
||||
}
|
||||
|
||||
template += '</div>'
|
||||
return template;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
element.on('typeahead:selected', function (e, datum) {
|
||||
element.typeahead('val', '');
|
||||
$scope.$apply(function() {
|
||||
$location.path('/repository/' + datum.repo.namespace + '/' + datum.repo.name);
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
20
static/js/directives/ui/resource-view.js
Normal file
20
static/js/directives/ui/resource-view.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
/**
|
||||
* An element which displays either a resource (if present) or an error message if the resource
|
||||
* failed to load.
|
||||
*/
|
||||
angular.module('quay').directive('resourceView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/resource-view.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'resource': '=resource',
|
||||
'errorMessage': '=errorMessage'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
102
static/js/directives/ui/robots-manager.js
Normal file
102
static/js/directives/ui/robots-manager.js
Normal file
|
@ -0,0 +1,102 @@
|
|||
/**
|
||||
* Element for managing the robots owned by an organization or a user.
|
||||
*/
|
||||
angular.module('quay').directive('robotsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/robots-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, $routeParams, CreateService) {
|
||||
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
|
||||
$scope.robots = null;
|
||||
$scope.loading = false;
|
||||
$scope.shownRobot = null;
|
||||
$scope.showRobotCounter = 0;
|
||||
|
||||
$scope.regenerateToken = function(username) {
|
||||
if (!username) { return; }
|
||||
|
||||
var shortName = $scope.getShortenedName(username);
|
||||
ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
|
||||
var index = $scope.findRobotIndexByName(username);
|
||||
if (index >= 0) {
|
||||
$scope.robots.splice(index, 1);
|
||||
$scope.robots.push(updated);
|
||||
}
|
||||
$scope.shownRobot = updated;
|
||||
}, ApiService.errorDisplay('Cannot regenerate robot account token'));
|
||||
};
|
||||
|
||||
$scope.showRobot = function(info) {
|
||||
$scope.shownRobot = info;
|
||||
$scope.showRobotCounter++;
|
||||
};
|
||||
|
||||
$scope.findRobotIndexByName = function(name) {
|
||||
for (var i = 0; i < $scope.robots.length; ++i) {
|
||||
if ($scope.robots[i].name == name) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
$scope.getShortenedName = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(plus + 1);
|
||||
};
|
||||
|
||||
$scope.getPrefix = function(name) {
|
||||
var plus = name.indexOf('+');
|
||||
return name.substr(0, plus);
|
||||
};
|
||||
|
||||
$scope.createRobot = function(name) {
|
||||
if (!name) { return; }
|
||||
|
||||
CreateService.createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
|
||||
function(created) {
|
||||
$scope.robots.push(created);
|
||||
});
|
||||
};
|
||||
|
||||
$scope.deleteRobot = function(info) {
|
||||
var shortName = $scope.getShortenedName(info.name);
|
||||
ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
|
||||
var index = $scope.findRobotIndexByName(info.name);
|
||||
if (index >= 0) {
|
||||
$scope.robots.splice(index, 1);
|
||||
}
|
||||
}, ApiService.errorDisplay('Cannot delete robot account'));
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.user && !$scope.organization) { return; }
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
ApiService.getRobots($scope.organization).then(function(resp) {
|
||||
$scope.robots = resp.robots;
|
||||
$scope.loading = false;
|
||||
|
||||
if ($routeParams.showRobot) {
|
||||
var index = $scope.findRobotIndexByName($routeParams.showRobot);
|
||||
if (index >= 0) {
|
||||
$scope.showRobot($scope.robots[index]);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
29
static/js/directives/ui/role-group.js
Normal file
29
static/js/directives/ui/role-group.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
/**
|
||||
* An element which displays a set of roles, and highlights the current role. This control also
|
||||
* allows the current role to be changed.
|
||||
*/
|
||||
angular.module('quay').directive('roleGroup', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/role-group.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'roles': '=roles',
|
||||
'currentRole': '=currentRole',
|
||||
'roleChanged': '&roleChanged'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.setRole = function(role) {
|
||||
if ($scope.currentRole == role) { return; }
|
||||
if ($scope.roleChanged) {
|
||||
$scope.roleChanged({'role': role});
|
||||
} else {
|
||||
$scope.currentRole = role;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
136
static/js/directives/ui/setup-trigger-dialog.js
Normal file
136
static/js/directives/ui/setup-trigger-dialog.js
Normal file
|
@ -0,0 +1,136 @@
|
|||
/**
|
||||
* An element which displays a dialog for setting up a build trigger.
|
||||
*/
|
||||
angular.module('quay').directive('setupTriggerDialog', function () {
|
||||
var directiveDefinitionObject = {
|
||||
templateUrl: '/static/directives/setup-trigger-dialog.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger',
|
||||
'counter': '=counter',
|
||||
'canceled': '&canceled',
|
||||
'activated': '&activated'
|
||||
},
|
||||
controller: function($scope, $element, ApiService, UserService) {
|
||||
var modalSetup = false;
|
||||
|
||||
$scope.state = {};
|
||||
$scope.nextStepCounter = -1;
|
||||
$scope.currentView = 'config';
|
||||
|
||||
$scope.show = function() {
|
||||
if (!$scope.trigger || !$scope.repository) { return; }
|
||||
|
||||
$scope.currentView = 'config';
|
||||
$('#setupTriggerModal').modal({});
|
||||
|
||||
if (!modalSetup) {
|
||||
$('#setupTriggerModal').on('hidden.bs.modal', function () {
|
||||
if (!$scope.trigger || $scope.trigger['is_active']) { return; }
|
||||
|
||||
$scope.nextStepCounter = -1;
|
||||
$scope.$apply(function() {
|
||||
$scope.cancelSetupTrigger();
|
||||
});
|
||||
});
|
||||
|
||||
modalSetup = true;
|
||||
$scope.nextStepCounter = 0;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.isNamespaceAdmin = function(namespace) {
|
||||
return UserService.isNamespaceAdmin(namespace);
|
||||
};
|
||||
|
||||
$scope.cancelSetupTrigger = function() {
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
};
|
||||
|
||||
$scope.hide = function() {
|
||||
$('#setupTriggerModal').modal('hide');
|
||||
};
|
||||
|
||||
$scope.checkAnalyze = function(isValid) {
|
||||
$scope.currentView = 'analyzing';
|
||||
$scope.pullInfo = {
|
||||
'is_public': true
|
||||
};
|
||||
|
||||
if (!isValid) {
|
||||
$scope.currentView = 'analyzed';
|
||||
return;
|
||||
}
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger.config
|
||||
};
|
||||
|
||||
ApiService.analyzeBuildTrigger(data, params).then(function(resp) {
|
||||
$scope.currentView = 'analyzed';
|
||||
|
||||
if (resp['status'] == 'analyzed') {
|
||||
if (resp['robots'] && resp['robots'].length > 0) {
|
||||
$scope.pullInfo['pull_entity'] = resp['robots'][0];
|
||||
} else {
|
||||
$scope.pullInfo['pull_entity'] = null;
|
||||
}
|
||||
|
||||
$scope.pullInfo['is_public'] = false;
|
||||
}
|
||||
|
||||
$scope.pullInfo['analysis'] = resp;
|
||||
}, ApiService.errorDisplay('Cannot load Dockerfile information'));
|
||||
};
|
||||
|
||||
$scope.activate = function() {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
var data = {
|
||||
'config': $scope.trigger['config']
|
||||
};
|
||||
|
||||
if ($scope.pullInfo['pull_entity']) {
|
||||
data['pull_robot'] = $scope.pullInfo['pull_entity']['name'];
|
||||
}
|
||||
|
||||
$scope.currentView = 'activating';
|
||||
|
||||
var errorHandler = ApiService.errorDisplay('Cannot activate build trigger', function(resp) {
|
||||
$scope.hide();
|
||||
$scope.canceled({'trigger': $scope.trigger});
|
||||
});
|
||||
|
||||
ApiService.activateBuildTrigger(data, params).then(function(resp) {
|
||||
$scope.hide();
|
||||
$scope.trigger['is_active'] = true;
|
||||
$scope.trigger['pull_robot'] = resp['pull_robot'];
|
||||
$scope.activated({'trigger': $scope.trigger});
|
||||
}, errorHandler);
|
||||
};
|
||||
|
||||
var check = function() {
|
||||
if ($scope.counter && $scope.trigger && $scope.repository) {
|
||||
$scope.show();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.$watch('trigger', check);
|
||||
$scope.$watch('counter', check);
|
||||
$scope.$watch('repository', check);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
99
static/js/directives/ui/signin-form.js
Normal file
99
static/js/directives/ui/signin-form.js
Normal file
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* An element which displays the sign in form.
|
||||
*/
|
||||
angular.module('quay').directive('signinForm', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/signin-form.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'redirectUrl': '=redirectUrl',
|
||||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
|
||||
$scope.tryAgainSoon = 0;
|
||||
$scope.tryAgainInterval = null;
|
||||
$scope.signingIn = false;
|
||||
|
||||
$scope.markStarted = function() {
|
||||
$scope.signingIn = true;
|
||||
if ($scope.signInStarted != null) {
|
||||
$scope.signInStarted();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cancelInterval = function() {
|
||||
$scope.tryAgainSoon = 0;
|
||||
|
||||
if ($scope.tryAgainInterval) {
|
||||
$interval.cancel($scope.tryAgainInterval);
|
||||
}
|
||||
|
||||
$scope.tryAgainInterval = null;
|
||||
};
|
||||
|
||||
$scope.$watch('user.username', function() {
|
||||
$scope.cancelInterval();
|
||||
});
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
$scope.cancelInterval();
|
||||
});
|
||||
|
||||
$scope.signin = function() {
|
||||
if ($scope.tryAgainSoon > 0) { return; }
|
||||
|
||||
$scope.markStarted();
|
||||
$scope.cancelInterval();
|
||||
|
||||
ApiService.signinUser($scope.user).then(function() {
|
||||
$scope.signingIn = false;
|
||||
$scope.needsEmailVerification = false;
|
||||
$scope.invalidCredentials = false;
|
||||
|
||||
if ($scope.signedIn != null) {
|
||||
$scope.signedIn();
|
||||
}
|
||||
|
||||
// Load the newly created user.
|
||||
UserService.load();
|
||||
|
||||
// Redirect to the specified page or the landing page
|
||||
// Note: The timeout of 500ms is needed to ensure dialogs containing sign in
|
||||
// forms get removed before the location changes.
|
||||
$timeout(function() {
|
||||
var redirectUrl = $scope.redirectUrl;
|
||||
if (redirectUrl == $location.path() || redirectUrl == null) {
|
||||
return;
|
||||
}
|
||||
window.location = (redirectUrl ? redirectUrl : '/');
|
||||
}, 500);
|
||||
}, function(result) {
|
||||
$scope.signingIn = false;
|
||||
|
||||
if (result.status == 429 /* try again later */) {
|
||||
$scope.needsEmailVerification = false;
|
||||
$scope.invalidCredentials = false;
|
||||
|
||||
$scope.cancelInterval();
|
||||
|
||||
$scope.tryAgainSoon = result.headers('Retry-After');
|
||||
$scope.tryAgainInterval = $interval(function() {
|
||||
$scope.tryAgainSoon--;
|
||||
if ($scope.tryAgainSoon <= 0) {
|
||||
$scope.cancelInterval();
|
||||
}
|
||||
}, 1000, $scope.tryAgainSoon);
|
||||
} else {
|
||||
$scope.needsEmailVerification = result.data.needsEmailVerification;
|
||||
$scope.invalidCredentials = result.data.invalidCredentials;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
51
static/js/directives/ui/signup-form.js
Normal file
51
static/js/directives/ui/signup-form.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
/**
|
||||
* An element which displays the sign up form.
|
||||
*/
|
||||
angular.module('quay').directive('signupForm', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/signup-form.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'inviteCode': '=inviteCode',
|
||||
|
||||
'userRegistered': '&userRegistered'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
|
||||
$('.form-signup').popover();
|
||||
|
||||
$scope.awaitingConfirmation = false;
|
||||
$scope.registering = false;
|
||||
|
||||
$scope.register = function() {
|
||||
UIService.hidePopover('#signupButton');
|
||||
$scope.registering = true;
|
||||
|
||||
if ($scope.inviteCode) {
|
||||
$scope.newUser['invite_code'] = $scope.inviteCode;
|
||||
}
|
||||
|
||||
ApiService.createNewUser($scope.newUser).then(function(resp) {
|
||||
$scope.registering = false;
|
||||
$scope.awaitingConfirmation = !!resp['awaiting_verification'];
|
||||
|
||||
if (Config.MIXPANEL_KEY) {
|
||||
mixpanel.alias($scope.newUser.username);
|
||||
}
|
||||
|
||||
$scope.userRegistered({'username': $scope.newUser.username});
|
||||
|
||||
if (!$scope.awaitingConfirmation) {
|
||||
document.location = '/';
|
||||
}
|
||||
}, function(result) {
|
||||
$scope.registering = false;
|
||||
UIService.showFormError('#signupButton', result);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
120
static/js/directives/ui/step-view.js
Normal file
120
static/js/directives/ui/step-view.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
/**
|
||||
* An element which displays the steps of the wizard-like dialog, changing them as each step
|
||||
* is completed.
|
||||
*/
|
||||
angular.module('quay').directive('stepView', function ($compile) {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/step-view.html',
|
||||
replace: true,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'nextStepCounter': '=nextStepCounter',
|
||||
'currentStepValid': '=currentStepValid',
|
||||
|
||||
'stepsCompleted': '&stepsCompleted'
|
||||
},
|
||||
controller: function($scope, $element, $rootScope) {
|
||||
this.currentStepIndex = -1;
|
||||
this.steps = [];
|
||||
this.watcher = null;
|
||||
|
||||
this.getCurrentStep = function() {
|
||||
return this.steps[this.currentStepIndex];
|
||||
};
|
||||
|
||||
this.reset = function() {
|
||||
this.currentStepIndex = -1;
|
||||
for (var i = 0; i < this.steps.length; ++i) {
|
||||
this.steps[i].element.hide();
|
||||
}
|
||||
|
||||
$scope.currentStepValid = false;
|
||||
};
|
||||
|
||||
this.next = function() {
|
||||
if (this.currentStepIndex >= 0) {
|
||||
var currentStep = this.getCurrentStep();
|
||||
if (!currentStep || !currentStep.scope) { return; }
|
||||
|
||||
if (!currentStep.scope.completeCondition) {
|
||||
return;
|
||||
}
|
||||
|
||||
currentStep.element.hide();
|
||||
|
||||
if (this.unwatch) {
|
||||
this.unwatch();
|
||||
this.unwatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
this.currentStepIndex++;
|
||||
|
||||
if (this.currentStepIndex < this.steps.length) {
|
||||
var currentStep = this.getCurrentStep();
|
||||
currentStep.element.show();
|
||||
currentStep.scope.load()
|
||||
|
||||
this.unwatch = currentStep.scope.$watch('completeCondition', function(cc) {
|
||||
$scope.currentStepValid = !!cc;
|
||||
});
|
||||
} else {
|
||||
$scope.stepsCompleted();
|
||||
}
|
||||
};
|
||||
|
||||
this.register = function(scope, element) {
|
||||
element.hide();
|
||||
|
||||
this.steps.push({
|
||||
'scope': scope,
|
||||
'element': element
|
||||
});
|
||||
};
|
||||
|
||||
var that = this;
|
||||
$scope.$watch('nextStepCounter', function(nsc) {
|
||||
if (nsc >= 0) {
|
||||
that.next();
|
||||
} else {
|
||||
that.reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
/**
|
||||
* A step in the step view.
|
||||
*/
|
||||
angular.module('quay').directive('stepViewStep', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 1,
|
||||
require: '^stepView',
|
||||
templateUrl: '/static/directives/step-view-step.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'completeCondition': '=completeCondition',
|
||||
'loadCallback': '&loadCallback',
|
||||
'loadMessage': '@loadMessage'
|
||||
},
|
||||
link: function(scope, element, attrs, controller) {
|
||||
controller.register(scope, element);
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.load = function() {
|
||||
$scope.loading = true;
|
||||
$scope.loadCallback({'callback': function() {
|
||||
$scope.loading = false;
|
||||
}});
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
127
static/js/directives/ui/tag-specific-image-view.js
Normal file
127
static/js/directives/ui/tag-specific-image-view.js
Normal file
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* An element which displays those images which belong to the specified tag *only*. If an image
|
||||
* is shared between more than a single tag in the repository, then it is not displayed.
|
||||
*/
|
||||
angular.module('quay').directive('tagSpecificImagesView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/tag-specific-images-view.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'tag': '=tag',
|
||||
'images': '=images',
|
||||
'imageCutoff': '=imageCutoff'
|
||||
},
|
||||
controller: function($scope, $element, UtilService) {
|
||||
$scope.getFirstTextLine = UtilService.getFirstMarkdownLineAsText;
|
||||
|
||||
$scope.hasImages = false;
|
||||
$scope.tagSpecificImages = [];
|
||||
|
||||
$scope.getImageListingClasses = function(image) {
|
||||
var classes = '';
|
||||
if (image.ancestors.length > 1) {
|
||||
classes += 'child ';
|
||||
}
|
||||
|
||||
var currentTag = $scope.repository.tags[$scope.tag];
|
||||
if (image.id == currentTag.image_id) {
|
||||
classes += 'tag-image ';
|
||||
}
|
||||
|
||||
return classes;
|
||||
};
|
||||
|
||||
var forAllTagImages = function(tag, callback, opt_cutoff) {
|
||||
if (!tag) { return; }
|
||||
|
||||
if (!$scope.imageByDockerId) {
|
||||
$scope.imageByDockerId = [];
|
||||
for (var i = 0; i < $scope.images.length; ++i) {
|
||||
var currentImage = $scope.images[i];
|
||||
$scope.imageByDockerId[currentImage.id] = currentImage;
|
||||
}
|
||||
}
|
||||
|
||||
var tag_image = $scope.imageByDockerId[tag.image_id];
|
||||
if (!tag_image) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(tag_image);
|
||||
|
||||
var ancestors = tag_image.ancestors.split('/').reverse();
|
||||
for (var i = 0; i < ancestors.length; ++i) {
|
||||
var image = $scope.imageByDockerId[ancestors[i]];
|
||||
if (image) {
|
||||
if (image == opt_cutoff) {
|
||||
return;
|
||||
}
|
||||
|
||||
callback(image);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var refresh = function() {
|
||||
if (!$scope.repository || !$scope.tag || !$scope.images) {
|
||||
$scope.tagSpecificImages = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var tag = $scope.repository.tags[$scope.tag];
|
||||
if (!tag) {
|
||||
$scope.tagSpecificImages = [];
|
||||
return;
|
||||
}
|
||||
|
||||
var getIdsForTag = function(currentTag) {
|
||||
var ids = {};
|
||||
forAllTagImages(currentTag, function(image) {
|
||||
ids[image.id] = true;
|
||||
}, $scope.imageCutoff);
|
||||
return ids;
|
||||
};
|
||||
|
||||
// Remove any IDs that match other tags.
|
||||
var toDelete = getIdsForTag(tag);
|
||||
for (var currentTagName in $scope.repository.tags) {
|
||||
var currentTag = $scope.repository.tags[currentTagName];
|
||||
if (currentTag != tag) {
|
||||
for (var id in getIdsForTag(currentTag)) {
|
||||
delete toDelete[id];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the matching list of images.
|
||||
var images = [];
|
||||
for (var i = 0; i < $scope.images.length; ++i) {
|
||||
var image = $scope.images[i];
|
||||
if (toDelete[image.id]) {
|
||||
images.push(image);
|
||||
}
|
||||
}
|
||||
|
||||
images.sort(function(a, b) {
|
||||
var result = new Date(b.created) - new Date(a.created);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return b.sort_index - a.sort_index;
|
||||
});
|
||||
|
||||
$scope.tagSpecificImages = images;
|
||||
};
|
||||
|
||||
$scope.$watch('repository', refresh);
|
||||
$scope.$watch('tag', refresh);
|
||||
$scope.$watch('images', refresh);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
35
static/js/directives/ui/tour-content.js
Normal file
35
static/js/directives/ui/tour-content.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
/**
|
||||
* An element which implements a frame for content in the application tour, making sure to
|
||||
* chromify any marked elements found. Note that this directive relies on the browserchrome library
|
||||
* in the lib/ folder.
|
||||
*/
|
||||
angular.module('quay').directive('tourContent', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/tour-content.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'kind': '=kind'
|
||||
},
|
||||
controller: function($scope, $element, $timeout, UserService) {
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.chromify = function() {
|
||||
browserchrome.update();
|
||||
};
|
||||
|
||||
$scope.$watch('kind', function(kind) {
|
||||
$timeout(function() {
|
||||
$scope.chromify();
|
||||
});
|
||||
});
|
||||
},
|
||||
link: function($scope, $element, $attr, ctrl) {
|
||||
$scope.chromify();
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
21
static/js/directives/ui/trigger-description.js
Normal file
21
static/js/directives/ui/trigger-description.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
/**
|
||||
* An element which displays information about a build trigger.
|
||||
*/
|
||||
angular.module('quay').directive('triggerDescription', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/trigger-description.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'trigger': '=trigger',
|
||||
'short': '=short'
|
||||
},
|
||||
controller: function($scope, $element, KeyService, TriggerService) {
|
||||
$scope.KeyService = KeyService;
|
||||
$scope.TriggerService = TriggerService;
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
219
static/js/directives/ui/trigger-setup-github.js
Normal file
219
static/js/directives/ui/trigger-setup-github.js
Normal file
|
@ -0,0 +1,219 @@
|
|||
/**
|
||||
* An element which displays github-specific setup information for its build triggers.
|
||||
*/
|
||||
angular.module('quay').directive('triggerSetupGithub', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/trigger-setup-github.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'repository': '=repository',
|
||||
'trigger': '=trigger',
|
||||
|
||||
'nextStepCounter': '=nextStepCounter',
|
||||
'currentStepValid': '=currentStepValid',
|
||||
|
||||
'analyze': '&analyze'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.analyzeCounter = 0;
|
||||
$scope.setupReady = false;
|
||||
$scope.refs = null;
|
||||
$scope.branchNames = null;
|
||||
$scope.tagNames = null;
|
||||
|
||||
$scope.state = {
|
||||
'currentRepo': null,
|
||||
'branchTagFilter': '',
|
||||
'hasBranchTagFilter': false,
|
||||
'isInvalidLocation': true,
|
||||
'currentLocation': null
|
||||
};
|
||||
|
||||
$scope.isMatching = function(kind, name, filter) {
|
||||
try {
|
||||
var patt = new RegExp(filter);
|
||||
} catch (ex) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var fullname = (kind + '/' + name);
|
||||
var m = fullname.match(patt);
|
||||
return m && m[0].length == fullname.length;
|
||||
}
|
||||
|
||||
$scope.addRef = function(kind, name) {
|
||||
if ($scope.isMatching(kind, name, $scope.state.branchTagFilter)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var newFilter = kind + '/' + name;
|
||||
var existing = $scope.state.branchTagFilter;
|
||||
if (existing) {
|
||||
$scope.state.branchTagFilter = '(' + existing + ')|(' + newFilter + ')';
|
||||
} else {
|
||||
$scope.state.branchTagFilter = newFilter;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.stepsCompleted = function() {
|
||||
$scope.analyze({'isValid': !$scope.state.isInvalidLocation});
|
||||
};
|
||||
|
||||
$scope.loadRepositories = function(callback) {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
ApiService.listTriggerBuildSources(null, params).then(function(resp) {
|
||||
$scope.orgs = resp['sources'];
|
||||
setupTypeahead();
|
||||
callback();
|
||||
}, ApiService.errorDisplay('Cannot load repositories'));
|
||||
};
|
||||
|
||||
$scope.loadBranchesAndTags = function(callback) {
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger['id'],
|
||||
'field_name': 'refs'
|
||||
};
|
||||
|
||||
ApiService.listTriggerFieldValues($scope.trigger['config'], params).then(function(resp) {
|
||||
$scope.refs = resp['values'];
|
||||
$scope.branchNames = [];
|
||||
$scope.tagNames = [];
|
||||
|
||||
for (var i = 0; i < $scope.refs.length; ++i) {
|
||||
var ref = $scope.refs[i];
|
||||
if (ref.kind == 'branch') {
|
||||
$scope.branchNames.push(ref.name);
|
||||
} else {
|
||||
$scope.tagNames.push(ref.name);
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
}, ApiService.errorDisplay('Cannot load branch and tag names'));
|
||||
};
|
||||
|
||||
$scope.loadLocations = function(callback) {
|
||||
$scope.locations = null;
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger.id
|
||||
};
|
||||
|
||||
ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
|
||||
if (resp['status'] == 'error') {
|
||||
callback(resp['message'] || 'Could not load Dockerfile locations');
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.locations = resp['subdir'] || [];
|
||||
|
||||
// Select a default location (if any).
|
||||
if ($scope.locations.length > 0) {
|
||||
$scope.setLocation($scope.locations[0]);
|
||||
} else {
|
||||
$scope.state.currentLocation = null;
|
||||
$scope.state.isInvalidLocation = resp['subdir'].indexOf('') < 0;
|
||||
$scope.trigger.$ready = true;
|
||||
}
|
||||
|
||||
callback();
|
||||
}, ApiService.errorDisplay('Cannot load locations'));
|
||||
}
|
||||
|
||||
$scope.handleLocationInput = function(location) {
|
||||
$scope.state.isInvalidLocation = $scope.locations.indexOf(location) < 0;
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.trigger.$ready = true;
|
||||
};
|
||||
|
||||
$scope.handleLocationSelected = function(datum) {
|
||||
$scope.setLocation(datum['value']);
|
||||
};
|
||||
|
||||
$scope.setLocation = function(location) {
|
||||
$scope.state.currentLocation = location;
|
||||
$scope.state.isInvalidLocation = false;
|
||||
$scope.trigger['config']['subdir'] = location || '';
|
||||
$scope.trigger.$ready = true;
|
||||
};
|
||||
|
||||
$scope.selectRepo = function(repo, org) {
|
||||
$scope.state.currentRepo = {
|
||||
'repo': repo,
|
||||
'avatar_url': org['info']['avatar_url'],
|
||||
'toString': function() {
|
||||
return this.repo;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
$scope.selectRepoInternal = function(currentRepo) {
|
||||
$scope.trigger.$ready = false;
|
||||
|
||||
var params = {
|
||||
'repository': $scope.repository.namespace + '/' + $scope.repository.name,
|
||||
'trigger_uuid': $scope.trigger['id']
|
||||
};
|
||||
|
||||
var repo = currentRepo['repo'];
|
||||
$scope.trigger['config'] = {
|
||||
'build_source': repo,
|
||||
'subdir': ''
|
||||
};
|
||||
};
|
||||
|
||||
var setupTypeahead = function() {
|
||||
var repos = [];
|
||||
for (var i = 0; i < $scope.orgs.length; ++i) {
|
||||
var org = $scope.orgs[i];
|
||||
var orepos = org['repos'];
|
||||
for (var j = 0; j < orepos.length; ++j) {
|
||||
var repoValue = {
|
||||
'repo': orepos[j],
|
||||
'avatar_url': org['info']['avatar_url'],
|
||||
'toString': function() {
|
||||
return this.repo;
|
||||
}
|
||||
};
|
||||
var datum = {
|
||||
'name': orepos[j],
|
||||
'org': org,
|
||||
'value': orepos[j],
|
||||
'title': orepos[j],
|
||||
'item': repoValue
|
||||
};
|
||||
repos.push(datum);
|
||||
}
|
||||
}
|
||||
|
||||
$scope.repoLookahead = repos;
|
||||
};
|
||||
|
||||
$scope.$watch('state.currentRepo', function(repo) {
|
||||
if (repo) {
|
||||
$scope.selectRepoInternal(repo);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.$watch('state.branchTagFilter', function(bf) {
|
||||
if (!$scope.trigger) { return; }
|
||||
|
||||
if ($scope.state.hasBranchTagFilter) {
|
||||
$scope.trigger['config']['branchtag_regex'] = bf;
|
||||
} else {
|
||||
delete $scope.trigger['config']['branchtag_regex'];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
22
static/js/directives/ui/twitter-view.js
Normal file
22
static/js/directives/ui/twitter-view.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
/**
|
||||
* An element which displays a twitter message and author information.
|
||||
*/
|
||||
angular.module('quay').directive('twitterView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/twitter-view.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'avatarUrl': '@avatarUrl',
|
||||
'authorName': '@authorName',
|
||||
'authorUser': '@authorUser',
|
||||
'messageUrl': '@messageUrl',
|
||||
'messageDate': '@messageDate'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
50
static/js/directives/ui/usage-chart.js
Normal file
50
static/js/directives/ui/usage-chart.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
/**
|
||||
* An element which displays a donut chart, along with warnings if the limit is close to being
|
||||
* reached.
|
||||
*/
|
||||
angular.module('quay').directive('usageChart', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/usage-chart.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'current': '=current',
|
||||
'total': '=total',
|
||||
'limit': '=limit',
|
||||
'usageTitle': '@usageTitle'
|
||||
},
|
||||
controller: function($scope, $element) {
|
||||
$scope.limit = "";
|
||||
|
||||
var chart = null;
|
||||
|
||||
var update = function() {
|
||||
if ($scope.current == null || $scope.total == null) { return; }
|
||||
if (!chart) {
|
||||
chart = new UsageChart();
|
||||
chart.draw('usage-chart-element');
|
||||
}
|
||||
|
||||
var current = $scope.current || 0;
|
||||
var total = $scope.total || 0;
|
||||
if (current > total) {
|
||||
$scope.limit = 'over';
|
||||
} else if (current == total) {
|
||||
$scope.limit = 'at';
|
||||
} else if (current >= total * 0.7) {
|
||||
$scope.limit = 'near';
|
||||
} else {
|
||||
$scope.limit = 'none';
|
||||
}
|
||||
|
||||
chart.update($scope.current, $scope.total);
|
||||
};
|
||||
|
||||
$scope.$watch('current', update);
|
||||
$scope.$watch('total', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
47
static/js/directives/ui/user-setup.js
Normal file
47
static/js/directives/ui/user-setup.js
Normal file
|
@ -0,0 +1,47 @@
|
|||
/**
|
||||
* An element which displays a box for the user to sign in, sign up and recover their account.
|
||||
*/
|
||||
angular.module('quay').directive('userSetup', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/user-setup.html',
|
||||
replace: false,
|
||||
transclude: true,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'redirectUrl': '=redirectUrl',
|
||||
|
||||
'inviteCode': '=inviteCode',
|
||||
|
||||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn',
|
||||
'userRegistered': '&userRegistered'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||
$scope.sendRecovery = function() {
|
||||
$scope.sendingRecovery = true;
|
||||
|
||||
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
|
||||
$scope.invalidRecovery = false;
|
||||
$scope.errorMessage = '';
|
||||
$scope.sent = true;
|
||||
$scope.sendingRecovery = false;
|
||||
}, function(resp) {
|
||||
$scope.invalidRecovery = true;
|
||||
$scope.errorMessage = ApiService.getErrorMessage(resp, 'Cannot send recovery email');
|
||||
$scope.sent = false;
|
||||
$scope.sendingRecovery = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.handleUserRegistered = function(username) {
|
||||
$scope.userRegistered({'username': username});
|
||||
};
|
||||
|
||||
$scope.hasSignedIn = function() {
|
||||
return UserService.hasEverLoggedIn();
|
||||
};
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
|
@ -521,6 +521,11 @@ ImageHistoryTree.prototype.pruneUnreferenced_ = function(node) {
|
|||
}
|
||||
node.children = surviving_children;
|
||||
}
|
||||
|
||||
if (!node.tags) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return (node.children.length == 0 && node.tags.length == 0);
|
||||
};
|
||||
|
||||
|
|
11
static/js/pages/about.js
Normal file
11
static/js/pages/about.js
Normal file
|
@ -0,0 +1,11 @@
|
|||
(function() {
|
||||
/**
|
||||
* About page.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('about', 'about.html', null, {
|
||||
'title': 'About Us',
|
||||
'description': 'About Quay.io'
|
||||
});
|
||||
}]);
|
||||
}())
|
145
static/js/pages/build-package.js
Normal file
145
static/js/pages/build-package.js
Normal file
|
@ -0,0 +1,145 @@
|
|||
(function() {
|
||||
/**
|
||||
* Page which displays a build package for a specific build.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('build-package', 'build-package.html', BuildPackageCtrl);
|
||||
}]);
|
||||
|
||||
function BuildPackageCtrl($scope, Restangular, ApiService, DataFileService, $routeParams, $rootScope, $location, $timeout) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var buildid = $routeParams.buildid;
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'build_uuid': buildid
|
||||
};
|
||||
|
||||
$scope.initializeTree = function() {
|
||||
if ($scope.drawn) {
|
||||
$scope.tree.notifyResized();
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.drawn = true;
|
||||
$timeout(function() {
|
||||
$scope.tree.draw('file-tree-container');
|
||||
}, 10);
|
||||
};
|
||||
|
||||
var determineDockerfilePath = function() {
|
||||
var dockerfilePath = 'Dockerfile';
|
||||
if ($scope.repobuild['job_config']) {
|
||||
var dockerfileFolder = ($scope.repobuild['job_config']['build_subdir'] || '');
|
||||
if (dockerfileFolder[0] == '/') {
|
||||
dockerfileFolder = dockerfileFolder.substr(1);
|
||||
}
|
||||
if (dockerfileFolder && dockerfileFolder[dockerfileFolder.length - 1] != '/') {
|
||||
dockerfileFolder += '/';
|
||||
}
|
||||
dockerfilePath = dockerfileFolder + 'Dockerfile';
|
||||
}
|
||||
return dockerfilePath;
|
||||
};
|
||||
|
||||
var processBuildPack = function(uint8array) {
|
||||
var archiveread = function(files) {
|
||||
var getpath = function(file) {
|
||||
return file.path;
|
||||
};
|
||||
|
||||
var findFile = function(path) {
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
var file = files[i];
|
||||
if (file.path == path) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
$scope.tree = new FileTree($.map(files, getpath));
|
||||
$($scope.tree).bind('fileClicked', function(e) {
|
||||
var file = findFile(e.path);
|
||||
if (file && file.canRead) {
|
||||
saveAs(file.toBlob(), file.name);
|
||||
}
|
||||
});
|
||||
|
||||
var dockerfilePath = determineDockerfilePath();
|
||||
var dockerfile = findFile(dockerfilePath);
|
||||
if (dockerfile && dockerfile.canRead) {
|
||||
DataFileService.blobToString(dockerfile.toBlob(), function(result) {
|
||||
$scope.$apply(function() {
|
||||
$scope.dockerFilePath = dockerfilePath || 'Dockerfile';
|
||||
$scope.dockerFileContents = result;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
$scope.loaded = true;
|
||||
};
|
||||
|
||||
var notarchive = function() {
|
||||
DataFileService.arrayToString(uint8array, function(r) {
|
||||
$scope.dockerFilePath = 'Dockerfile';
|
||||
$scope.dockerFileContents = r;
|
||||
$scope.loaded = true;
|
||||
});
|
||||
};
|
||||
|
||||
setTimeout(function() {
|
||||
$scope.$apply(function() {
|
||||
DataFileService.readDataArrayAsPossibleArchive(uint8array, archiveread, notarchive);
|
||||
});
|
||||
}, 0);
|
||||
};
|
||||
|
||||
var downloadBuildPack = function(url) {
|
||||
$scope.downloadProgress = 0;
|
||||
$scope.downloading = true;
|
||||
startDownload(url);
|
||||
};
|
||||
|
||||
var startDownload = function(url) {
|
||||
var onprogress = function(p) {
|
||||
$scope.downloadProgress = p * 100;
|
||||
};
|
||||
|
||||
var onerror = function() {
|
||||
$scope.downloading = false;
|
||||
$scope.downloadError = true;
|
||||
};
|
||||
|
||||
var onloaded = function(uint8array) {
|
||||
$scope.downloading = false;
|
||||
processBuildPack(uint8array);
|
||||
};
|
||||
|
||||
DataFileService.downloadDataFileAsArrayBuffer($scope, url, onprogress, onerror, onloaded);
|
||||
};
|
||||
|
||||
var getBuildInfo = function() {
|
||||
$scope.repository_build = ApiService.getRepoBuildStatus(null, params, true).then(function(resp) {
|
||||
if (!resp['is_writer']) {
|
||||
$rootScope.title = 'Unknown build';
|
||||
$scope.accessDenied = true;
|
||||
return;
|
||||
}
|
||||
|
||||
$rootScope.title = 'Repository Build Pack - ' + resp['display_name'];
|
||||
$scope.repobuild = resp;
|
||||
$scope.repo = {
|
||||
'namespace': namespace,
|
||||
'name': name
|
||||
};
|
||||
|
||||
downloadBuildPack(resp['archive_url']);
|
||||
return resp;
|
||||
});
|
||||
};
|
||||
|
||||
getBuildInfo();
|
||||
}
|
||||
})();
|
40
static/js/pages/confirm-invite.js
Normal file
40
static/js/pages/confirm-invite.js
Normal file
|
@ -0,0 +1,40 @@
|
|||
(function() {
|
||||
/**
|
||||
* Page for confirming an invite to a team.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('confirm-invite', 'confirm-invite.html', ConfirmInviteCtrl, {
|
||||
'title': 'Confirm Invitation'
|
||||
});
|
||||
}]);
|
||||
|
||||
function ConfirmInviteCtrl($scope, $location, UserService, ApiService, NotificationService) {
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
$scope.loading = false;
|
||||
$scope.inviteCode = $location.search()['code'] || '';
|
||||
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
if (!user.anonymous && !$scope.loading) {
|
||||
// Make sure to not redirect now that we have logged in. We'll conduct the redirect
|
||||
// manually.
|
||||
$scope.redirectUrl = null;
|
||||
$scope.loading = true;
|
||||
|
||||
var params = {
|
||||
'code': $location.search()['code']
|
||||
};
|
||||
|
||||
ApiService.acceptOrganizationTeamInvite(null, params).then(function(resp) {
|
||||
NotificationService.update();
|
||||
UserService.load();
|
||||
$location.path('/organization/' + resp.org + '/teams/' + resp.team);
|
||||
}, function(resp) {
|
||||
$scope.loading = false;
|
||||
$scope.invalid = ApiService.getErrorMessage(resp, 'Invalid confirmation code');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.redirectUrl = window.location.href;
|
||||
}
|
||||
})();
|
55
static/js/pages/contact.js
Normal file
55
static/js/pages/contact.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
(function() {
|
||||
/**
|
||||
* Contact details page. The contacts are configurable.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('contact', 'contact.html', ContactCtrl, {
|
||||
'title': 'Contact Us'
|
||||
});
|
||||
}]);
|
||||
|
||||
function ContactCtrl($scope, Config) {
|
||||
$scope.Config = Config;
|
||||
$scope.colsize = Math.floor(12 / Config.CONTACT_INFO.length);
|
||||
|
||||
$scope.getKind = function(contactInfo) {
|
||||
var colon = contactInfo.indexOf(':');
|
||||
var scheme = contactInfo.substr(0, colon);
|
||||
if (scheme == 'https' || scheme == 'http') {
|
||||
if (contactInfo.indexOf('//twitter.com/') > 0) {
|
||||
return 'twitter';
|
||||
}
|
||||
|
||||
return 'url';
|
||||
}
|
||||
|
||||
return scheme;
|
||||
};
|
||||
|
||||
$scope.getTitle = function(contactInfo) {
|
||||
switch ($scope.getKind(contactInfo)) {
|
||||
case 'url':
|
||||
return contactInfo;
|
||||
|
||||
case 'twitter':
|
||||
var parts = contactInfo.split('/');
|
||||
return '@' + parts[parts.length - 1];
|
||||
|
||||
case 'tel':
|
||||
return contactInfo.substr('tel:'.length);
|
||||
|
||||
case 'irc':
|
||||
// irc://chat.freenode.net:6665/quayio
|
||||
var parts = contactInfo.substr('irc://'.length).split('/');
|
||||
var server = parts[0];
|
||||
if (server.indexOf('freenode') > 0) {
|
||||
server = 'Freenode';
|
||||
}
|
||||
return server + ': #' + parts[parts.length - 1];
|
||||
|
||||
case 'mailto':
|
||||
return contactInfo.substr('mailto:'.length);
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
19
static/js/pages/exp-new-layout.js
Normal file
19
static/js/pages/exp-new-layout.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
(function() {
|
||||
/**
|
||||
* Experiment enable page: New layout
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('exp-new-layout', 'exp-new-layout.html', ExpCtrl, {
|
||||
'newLayout': true
|
||||
});
|
||||
}]);
|
||||
|
||||
function ExpCtrl($scope, CookieService) {
|
||||
$scope.isEnabled = CookieService.get('quay.exp-new-layout') == 'true';
|
||||
|
||||
$scope.setEnabled = function(value) {
|
||||
$scope.isEnabled = value;
|
||||
CookieService.putPermanent('quay.exp-new-layout', value.toString());
|
||||
};
|
||||
}
|
||||
}())
|
133
static/js/pages/image-view.js
Normal file
133
static/js/pages/image-view.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
(function() {
|
||||
/**
|
||||
* Page to view the details of a single image.
|
||||
*/
|
||||
angular.module('quayPages').config(['pages', function(pages) {
|
||||
pages.create('image-view', 'image-view.html', ImageViewCtrl);
|
||||
}]);
|
||||
|
||||
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, ImageMetadataService) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var imageid = $routeParams.image;
|
||||
|
||||
$scope.getFormattedCommand = ImageMetadataService.getFormattedCommand;
|
||||
|
||||
$scope.parseDate = function(dateString) {
|
||||
return Date.parse(dateString);
|
||||
};
|
||||
|
||||
$scope.getFolder = function(filepath) {
|
||||
var index = filepath.lastIndexOf('/');
|
||||
if (index < 0) {
|
||||
return '';
|
||||
}
|
||||
return filepath.substr(0, index + 1);
|
||||
};
|
||||
|
||||
$scope.getFolders = function(filepath) {
|
||||
var index = filepath.lastIndexOf('/');
|
||||
if (index < 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return filepath.substr(0, index).split('/');
|
||||
};
|
||||
|
||||
$scope.getFilename = function(filepath) {
|
||||
var index = filepath.lastIndexOf('/');
|
||||
if (index < 0) {
|
||||
return filepath;
|
||||
}
|
||||
return filepath.substr(index + 1);
|
||||
};
|
||||
|
||||
$scope.setFolderFilter = function(folderPath, index) {
|
||||
var parts = folderPath.split('/');
|
||||
parts = parts.slice(0, index + 1);
|
||||
$scope.setFilter(parts.join('/'));
|
||||
};
|
||||
|
||||
$scope.setFilter = function(filter) {
|
||||
$scope.search = {};
|
||||
$scope.search['$'] = filter;
|
||||
document.getElementById('change-filter').value = filter;
|
||||
};
|
||||
|
||||
$scope.initializeTree = function() {
|
||||
if ($scope.tree) { return; }
|
||||
|
||||
$scope.tree = new ImageFileChangeTree($scope.image, $scope.combinedChanges);
|
||||
$timeout(function() {
|
||||
$scope.tree.draw('changes-tree-container');
|
||||
}, 10);
|
||||
};
|
||||
|
||||
var fetchRepository = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repo = repo;
|
||||
});
|
||||
};
|
||||
|
||||
var fetchImage = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'image_id': imageid
|
||||
};
|
||||
|
||||
$scope.image = ApiService.getImageAsResource(params).get(function(image) {
|
||||
if (!$scope.repo) {
|
||||
$scope.repo = {
|
||||
'name': name,
|
||||
'namespace': namespace,
|
||||
'is_public': true
|
||||
};
|
||||
}
|
||||
|
||||
$rootScope.title = 'View Image - ' + image.id;
|
||||
$rootScope.description = 'Viewing docker image ' + image.id + ' under repository ' + namespace + '/' + name +
|
||||
': Image changes tree and list view';
|
||||
|
||||
// Fetch the image's changes.
|
||||
fetchChanges();
|
||||
return image;
|
||||
});
|
||||
};
|
||||
|
||||
var fetchChanges = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'image_id': imageid
|
||||
};
|
||||
|
||||
ApiService.getImageChanges(null, params).then(function(changes) {
|
||||
var combinedChanges = [];
|
||||
var addCombinedChanges = function(c, kind) {
|
||||
for (var i = 0; i < c.length; ++i) {
|
||||
combinedChanges.push({
|
||||
'kind': kind,
|
||||
'file': c[i]
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
addCombinedChanges(changes.added, 'added');
|
||||
addCombinedChanges(changes.removed, 'removed');
|
||||
addCombinedChanges(changes.changed, 'changed');
|
||||
|
||||
$scope.combinedChanges = combinedChanges;
|
||||
$scope.imageChanges = changes;
|
||||
});
|
||||
};
|
||||
|
||||
// Fetch the repository.
|
||||
fetchRepository();
|
||||
|
||||
// Fetch the image.
|
||||
fetchImage();
|
||||
}
|
||||
})();
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue