var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function clickElement(el){
// From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements
var ev = document.createEvent("MouseEvent");
ev.initMouseEvent(
"click",
true /* bubble */, true /* cancelable */,
window, null,
0, 0, 0, 0, /* coordinates */
false, false, false, false, /* modifier keys */
0 /*left*/, null);
el.dispatchEvent(ev);
}
function getFirstTextLine(commentString) {
if (!commentString) { return ''; }
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return getMarkedDown(lines[i]);
}
return '';
}
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'The robot account could not be created',
"title": "Cannot create robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
var data = {
'name': teamname,
'role': 'member'
};
var params = {
'orgname': orgname,
'teamname': teamname
};
ApiService.updateOrganizationTeam(data, params).then(callback, function() {
bootbox.dialog({
"message": resp.data ? resp.data : 'The team could not be created',
"title": "Cannot create team",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
}
function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) {
cfpLoadingBarProvider.includeSpinner = false;
$provide.factory('UtilService', ['$sanitize', function($sanitize) {
var utilService = {};
utilService.textToSafeHtml = function(text) {
var adjusted = text.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
return $sanitize(adjusted);
};
return utilService;
}]);
$provide.factory('TriggerDescriptionBuilder', ['UtilService', '$sanitize', function(UtilService, $sanitize) {
var builderService = {};
builderService.getDescription = function(name, config) {
switch (name) {
case 'github':
var source = $sanitize(UtilService.textToSafeHtml(config['build_source']));
var desc = ' Push to Github Repository ';
desc += '' + source + '';
desc += '
Dockerfile folder: //' + UtilService.textToSafeHtml(config['subdir']);
return desc;
default:
return 'Unknown';
}
};
return builderService;
}]);
$provide.factory('ImageMetadataService', ['UtilService', function(UtilService) {
var metadataService = {};
metadataService.getFormattedCommand = function(image) {
if (!image || !image.command || !image.command.length) {
return '';
}
var getCommandStr = function(command) {
// Handle /bin/sh commands specially.
if (command.length > 2 && command[0] == '/bin/sh' && command[1] == '-c') {
return command[2];
}
return command.join(' ');
};
return getCommandStr(image.command);
};
metadataService.getEscapedFormattedCommand = function(image) {
return UtilService.textToSafeHtml(metadataService.getFormattedCommand(image));
};
return metadataService;
}]);
$provide.factory('ApiService', ['Restangular', function(Restangular) {
var apiService = {};
var getResource = function(path, opt_background) {
var resource = {};
resource.url = path;
resource.withOptions = function(options) {
this.options = options;
return this;
};
resource.get = function(processor, opt_errorHandler) {
var options = this.options;
var performer = Restangular.one(this.url);
var result = {
'loading': true,
'value': null,
'hasError': false
};
if (opt_background) {
performer.withHttpConfig({
'ignoreLoadingBar': true
});
}
performer.get(options).then(function(resp) {
result.value = processor(resp);
result.loading = false;
}, function(resp) {
result.hasError = true;
result.loading = false;
if (opt_errorHandler) {
opt_errorHandler(resp);
}
});
return result;
};
return resource;
};
var buildUrl = function(path, parameters) {
// We already have /api/v1/ on the URLs, so remove them from the paths.
path = path.substr('/api/v1/'.length, path.length);
var url = '';
for (var i = 0; i < path.length; ++i) {
var c = path[i];
if (c == '{') {
var end = path.indexOf('}', i);
var varName = path.substr(i + 1, end - i - 1);
if (!parameters[varName]) {
throw new Error('Missing parameter: ' + varName);
}
url += parameters[varName];
i = end;
continue;
}
url += c;
}
return url;
};
var getGenericOperationName = function(userOperationName) {
return userOperationName.replace('User', '');
};
var getMatchingUserOperationName = function(orgOperationName, method, userRelatedResource) {
if (userRelatedResource) {
var operations = userRelatedResource['operations'];
for (var i = 0; i < operations.length; ++i) {
var operation = operations[i];
if (operation['method'].toLowerCase() == method) {
return operation['nickname'];
}
}
}
throw new Error('Could not find user operation matching org operation: ' + orgOperationName);
};
var buildMethodsForEndpointResource = function(endpointResource, resourceMap) {
var name = endpointResource['name'];
var operations = endpointResource['operations'];
for (var i = 0; i < operations.length; ++i) {
var operation = operations[i];
buildMethodsForOperation(operation, endpointResource, resourceMap);
}
};
var buildMethodsForOperation = function(operation, resource, resourceMap) {
var method = operation['method'].toLowerCase();
var operationName = operation['nickname'];
var path = resource['path'];
// Add the operation itself.
apiService[operationName] = function(opt_options, opt_parameters, opt_background) {
var one = Restangular.one(buildUrl(path, opt_parameters));
if (opt_background) {
one.withHttpConfig({
'ignoreLoadingBar': true
});
}
return one['custom' + method.toUpperCase()](opt_options);
};
// If the method for the operation is a GET, add an operationAsResource method.
if (method == 'get') {
apiService[operationName + 'AsResource'] = function(opt_parameters, opt_background) {
return getResource(buildUrl(path, opt_parameters), opt_background);
};
}
// If the resource has a user-related resource, then make a generic operation for this operation
// that can call both the user and the organization versions of the operation, depending on the
// parameters given.
if (resource['quayUserRelated']) {
var userOperationName = getMatchingUserOperationName(operationName, method, resourceMap[resource['quayUserRelated']]);
var genericOperationName = getGenericOperationName(userOperationName);
apiService[genericOperationName] = function(orgname, opt_options, opt_parameters, opt_background) {
if (orgname) {
if (orgname.name) {
orgname = orgname.name;
}
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {}, opt_background);
return apiService[operationName](opt_options, params);
} else {
return apiService[userOperationName](opt_options, opt_parameters, opt_background);
}
};
}
};
if (!window.__endpoints) {
return apiService;
}
var resourceMap = {};
// Build the map of resource names to their objects.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
resourceMap[endpointResource['name']] = endpointResource;
}
// Construct the methods for each API endpoint.
for (var i = 0; i < window.__endpoints.length; ++i) {
var endpointResource = window.__endpoints[i];
buildMethodsForEndpointResource(endpointResource, resourceMap);
}
return apiService;
}]);
$provide.factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) {
var cookieService = {};
cookieService.putPermanent = function(name, value) {
document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/";
};
cookieService.putSession = function(name, value) {
$cookies[name] = value;
};
cookieService.clear = function(name) {
$cookies[name] = '';
};
cookieService.get = function(name) {
return $cookies[name];
};
return cookieService;
}]);
$provide.factory('UserService', ['ApiService', 'CookieService', function(ApiService, CookieService) {
var userResponse = {
verified: false,
anonymous: true,
username: null,
email: null,
askForPassword: false,
organizations: [],
logins: []
}
var userService = {}
userService.hasEverLoggedIn = function() {
return CookieService.get('quay.loggedin') == 'true';
};
userService.updateUserIn = function(scope, opt_callback) {
scope.$watch(function () { return userService.currentUser(); }, function (currentUser) {
scope.user = currentUser;
if (opt_callback) {
opt_callback(currentUser);
}
}, true);
};
userService.load = function(opt_callback) {
var handleUserResponse = function(loadedUser) {
userResponse = loadedUser;
if (!userResponse.anonymous) {
mixpanel.identify(userResponse.username);
mixpanel.people.set({
'$email': userResponse.email,
'$username': userResponse.username,
'verified': userResponse.verified
});
mixpanel.people.set_once({
'$created': new Date()
})
if (window.olark !== undefined) {
olark('api.visitor.getDetails', function(details) {
if (details.fullName === null) {
olark('api.visitor.updateFullName', {fullName: userResponse.username});
}
});
olark('api.visitor.updateEmailAddress', {emailAddress: userResponse.email});
olark('api.chat.updateVisitorStatus', {snippet: 'username: ' + userResponse.username});
}
CookieService.putPermanent('quay.loggedin', 'true');
}
if (opt_callback) {
opt_callback();
}
};
ApiService.getLoggedInUser().then(function(loadedUser) {
handleUserResponse(loadedUser);
}, function() {
handleUserResponse({'anonymous': true});
});
};
userService.getOrganization = function(name) {
if (!userResponse || !userResponse.organizations) { return null; }
for (var i = 0; i < userResponse.organizations.length; ++i) {
var org = userResponse.organizations[i];
if (org.name == name) {
return org;
}
}
return null;
};
userService.isNamespaceAdmin = function(namespace) {
if (namespace == userResponse.username) {
return true;
}
var org = userService.getOrganization(namespace);
if (!org) {
return false;
}
return org.is_org_admin;
};
userService.isKnownNamespace = function(namespace) {
if (namespace == userResponse.username) {
return true;
}
var org = userService.getOrganization(namespace);
return !!org;
};
userService.currentUser = function() {
return userResponse;
};
// Load the user the first time.
userService.load();
return userService;
}]);
$provide.factory('KeyService', ['$location', function($location) {
var keyService = {}
if ($location.host() === 'quay.io') {
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
keyService['githubClientId'] = '5a8c08b06c48d89d4d1e';
keyService['githubRedirectUri'] = 'https://quay.io/oauth2/github/callback';
} else if($location.host() === 'staging.quay.io') {
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
keyService['githubClientId'] = '4886304accbc444f0471';
keyService['githubRedirectUri'] = 'https://staging.quay.io/oauth2/github/callback';
} else {
keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
keyService['githubClientId'] = 'cfbc4aca88e5c1b40679';
keyService['githubRedirectUri'] = 'http://localhost:5000/oauth2/github/callback';
}
return keyService;
}]);
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
function(KeyService, UserService, CookieService, ApiService) {
var plans = null;
var planDict = {};
var planService = {};
var listeners = [];
var previousSubscribeFailure = false;
planService.getFreePlan = function() {
return 'free';
};
planService.registerListener = function(obj, callback) {
listeners.push({'obj': obj, 'callback': callback});
};
planService.unregisterListener = function(obj) {
for (var i = 0; i < listeners.length; ++i) {
if (listeners[i].obj == obj) {
listeners.splice(i, 1);
break;
}
}
};
planService.notePlan = function(planId) {
CookieService.putSession('quay.notedplan', planId);
};
planService.isOrgCompatible = function(plan) {
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
};
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
planService.handleNotedPlan = function() {
var planId = planService.getAndResetNotedPlan();
if (!planId) { return; }
UserService.load(function() {
if (UserService.currentUser().anonymous) {
return;
}
planService.getPlan(planId, function(plan) {
if (planService.isOrgCompatible(plan)) {
document.location = '/organizations/new/?plan=' + planId;
} else {
document.location = '/user?plan=' + planId;
}
});
});
};
planService.getAndResetNotedPlan = function() {
var planId = CookieService.get('quay.notedplan');
CookieService.clear('quay.notedplan');
return planId;
};
planService.handleCardError = function(resp) {
if (!planService.isCardError(resp)) { return; }
bootbox.dialog({
"message": resp.data.carderror,
"title": "Credit card issue",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
planService.isCardError = function(resp) {
return resp && resp.data && resp.data.carderror;
};
planService.verifyLoaded = function(callback) {
if (plans) {
callback(plans);
return;
}
ApiService.listPlans().then(function(data) {
var i = 0;
for(i = 0; i < data.plans.length; i++) {
planDict[data.plans[i].stripeId] = data.plans[i];
}
plans = data.plans;
callback(plans);
}, function() { callback([]); });
};
planService.getPlans = function(callback, opt_includePersonal) {
planService.verifyLoaded(function() {
var filtered = [];
for (var i = 0; i < plans.length; ++i) {
var plan = plans[i];
if (plan['deprecated']) { continue; }
if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
filtered.push(plan);
}
callback(filtered);
});
};
planService.getPlan = function(planId, callback) {
planService.getPlanIncludingDeprecated(planId, function(plan) {
if (!plan['deprecated']) {
callback(plan);
}
});
};
planService.getPlanIncludingDeprecated = function(planId, callback) {
planService.verifyLoaded(function() {
if (planDict[planId]) {
callback(planDict[planId]);
}
});
};
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.getPlans(function(plans) {
for (var i = 0; i < plans.length; i++) {
var plan = plans[i];
if (plan.privateRepos >= privateCount) {
callback(plan);
return;
}
}
callback(null);
}, /* include personal */!isBusiness);
};
planService.getSubscription = function(orgname, success, failure) {
ApiService.getSubscription(orgname).then(success, failure);
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
var subscriptionDetails = {
plan: planId
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
success(resp);
planService.getPlan(planId, function(plan) {
for (var i = 0; i < listeners.length; ++i) {
listeners[i]['callback'](plan);
}
});
}, failure);
};
planService.getCardInfo = function(orgname, callback) {
ApiService.getCard(orgname).then(function(resp) {
callback(resp.card);
}, function() {
callback({'is_valid': false});
});
};
planService.changePlan = function($scope, orgname, planId, callbacks) {
if (callbacks['started']) {
callbacks['started']();
}
planService.getPlan(planId, function(plan) {
if (orgname && !planService.isOrgCompatible(plan)) { return; }
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
return;
}
previousSubscribeFailure = false;
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
previousSubscribeFailure = true;
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
});
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
var cardInfo = {
'token': token.id
};
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
};
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
currency: 'usd',
name: 'Update credit card',
description: 'Enter your credit card number',
panelLabel: 'Update',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
};
planService.getEmail = function(orgname) {
var email = null;
if (UserService.currentUser()) {
email = UserService.currentUser().email;
if (orgname) {
org = UserService.getOrganization(orgname);
if (org) {
emaiil = org.email;
}
}
}
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) {
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
mixpanel.track('plan_subscribe');
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
});
};
planService.getPlan(planId, function(planDetails) {
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
amount: planDetails.price,
currency: 'usd',
name: 'Quay ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: 'Subscribe',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
});
};
return planService;
}]);
}).
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);
});
}
};
}).
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);
});
};
}).
config(['$routeProvider', '$locationProvider', '$analyticsProvider',
function($routeProvider, $locationProvider, $analyticsProvider) {
$analyticsProvider.virtualPageviews(true);
$locationProvider.html5Mode(true);
// WARNING WARNING WARNING
// If you add a route here, you must add a corresponding route in thr endpoints/web.py
// index rule to make sure that deep links directly deep into the app continue to work.
// WARNING WARNING WARNING
$routeProvider.
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
fixFooter: false, reloadOnSearch: false}).
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
fixFooter: false}).
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/build', {templateUrl: '/static/partials/repo-build.html', controller:RepoBuildCtrl, reloadOnSearch: false}).
when('/repository/:namespace/:name/build/:buildid/buildpack', {templateUrl: '/static/partials/build-package.html', controller:BuildPackageCtrl, reloadOnSearch: false}).
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html',
controller: GuideCtrl}).
when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html',
controller: TutorialCtrl}).
when('/contact/', {title: 'Contact Us', description:'Different ways for you to get a hold of us when you need us most.', templateUrl: '/static/partials/contact.html',
controller: ContactCtrl}).
when('/about/', {title: 'About Us', description:'Information about the Quay.io team and the company.', templateUrl: '/static/partials/about.html'}).
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
templateUrl: '/static/partials/security.html'}).
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html'}).
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on Quay.io',
templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl, reloadOnSearch: false}).
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}).
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
otherwise({redirectTo: '/'});
}]).
config(function(RestangularProvider) {
RestangularProvider.setBaseUrl('/api/v1/');
});
quayApp.directive('entityReference', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/entity-reference.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'entity': '=entity',
'namespace': '=namespace'
},
controller: function($scope, $element, UserService, $sanitize) {
$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=' + $sanitize(name);
}
return '/organization/' + org['name'] + '/admin?tab=robots&showRobot=' + $sanitize(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;
});
quayApp.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) {
// TODO: display the application information here.
}, function() {
bootbox.dialog({
"message": 'The application could not be found; it might have been deleted.',
"title": "Cannot find application",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
}
};
return directiveDefinitionObject;
});
quayApp.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) {
$scope.getMarkedDown = function(content, firstLineOnly) {
if (firstLineOnly) {
content = getFirstTextLine(content);
}
return $sce.trustAsHtml(getMarkedDown(content));
};
}
};
return directiveDefinitionObject;
});
quayApp.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;
});
quayApp.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;
});
quayApp.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) {
var number = $rootScope.__copyBoxIdCounter || 0;
$rootScope.__copyBoxIdCounter = number + 1;
$scope.inputId = "copy-box-input-" + number;
var button = $($element).find('.input-group-addon');
var input = $($element).find('input');
input.attr('id', $scope.inputId);
button.attr('data-clipboard-target', $scope.inputId);
var clip = new ZeroClipboard($(button), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function(e) {
var message = $(this.parentNode.parentNode.parentNode).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);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('userSetup', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/user-setup.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'redirectUrl': '=redirectUrl',
'signInStarted': '&signInStarted',
'signedIn': '&signedIn'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
$scope.sendRecovery = function() {
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
$scope.invalidRecovery = false;
$scope.errorMessage = '';
$scope.sent = true;
}, function(result) {
$scope.invalidRecovery = true;
$scope.errorMessage = result.data;
$scope.sent = false;
});
};
$scope.hasSignedIn = function() {
return UserService.hasEverLoggedIn();
};
}
};
return directiveDefinitionObject;
});
quayApp.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, ApiService, KeyService, UserService) {
$scope.showGithub = function() {
$scope.markStarted();
var mixpanelDistinctIdClause = '';
if (mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
var url = 'https://github.com/login/oauth/authorize?client_id=' + encodeURIComponent(KeyService.githubClientId) +
'&scope=user:email' + mixpanelDistinctIdClause;
document.location = url;
}, 250);
};
$scope.markStarted = function() {
if ($scope.signInStarted != null) {
$scope.signInStarted();
}
};
$scope.signin = function() {
$scope.markStarted();
ApiService.signinUser($scope.user).then(function() {
$scope.needsEmailVerification = false;
$scope.invalidCredentials = false;
if ($scope.signedIn != null) {
$scope.signedIn();
}
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() {
if ($scope.redirectUrl == $location.path()) {
return;
}
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
}, 500);
}, function(result) {
$scope.needsEmailVerification = result.data.needsEmailVerification;
$scope.invalidCredentials = result.data.invalidCredentials;
});
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('signupForm', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/signup-form.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
$('.form-signup').popover();
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
var mixpanelId = loadedMixpanel.get_distinct_id();
$scope.github_state_clause = '&state=' + mixpanelId;
});
$scope.githubClientId = KeyService.githubClientId;
$scope.awaitingConfirmation = false;
$scope.registering = false;
$scope.register = function() {
$('.form-signup').popover('hide');
$scope.registering = true;
ApiService.createNewUser($scope.newUser).then(function() {
$scope.awaitingConfirmation = true;
$scope.registering = false;
mixpanel.alias($scope.newUser.username);
}, function(result) {
$scope.registering = false;
$scope.registerError = result.data.message;
$timeout(function() {
$('.form-signup').popover('show');
});
});
};
}
};
return directiveDefinitionObject;
});
quayApp.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;
});
quayApp.directive('dockerAuthDialog', function () {
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'
},
controller: function($scope, $element) {
$scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){}
return false;
};
$scope.downloadCfg = function() {
var auth = $.base64.encode($scope.username + ":" + $scope.token);
config = {
"https://quay.io/v1/": {
"auth": auth,
"email": ""
}
};
var file = JSON.stringify(config, null, ' ');
var blob = new Blob([file]);
saveAs(blob, '.dockercfg');
};
var show = function(r) {
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;
});
quayApp.filter('reverse', function() {
return function(items) {
return items.slice().reverse();
};
});
quayApp.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];
}
});
quayApp.filter('visibleLogFilter', function () {
return function (logs, allowed) {
if (!allowed) {
return logs;
}
var filtered = [];
angular.forEach(logs, function (log) {
if (allowed[log.kind]) {
filtered.push(log);
}
});
return filtered;
};
});
quayApp.directive('billingInvoices', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/billing-invoices.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user',
'visible': '=visible'
},
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.visible || !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('visible', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('logsView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/logs-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user',
'visible': '=visible',
'repository': '=repository',
'performer': '=performer'
},
controller: function($scope, $element, $sce, Restangular, ApiService, TriggerDescriptionBuilder) {
$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}',
'pull_repo': function(metadata) {
if (metadata.token) {
return 'Pull repository {repo} via token {token}';
} else if (metadata.username) {
return 'Pull repository {repo} by {username}';
} else {
return 'Public pull of repository {repo} by {_ip}';
}
},
'delete_repo': 'Delete repository: {repo}',
'change_repo_permission': function(metadata) {
if (metadata.username) {
return 'Change permission for user {username} in repository {repo} to {role}';
} else if (metadata.team) {
return 'Change permission for team {team} in repository {repo} to {role}';
} else if (metadata.token) {
return 'Change permission for token {token} in repository {repo} to {role}';
}
},
'delete_repo_permission': function(metadata) {
if (metadata.username) {
return 'Remove permission for user {username} from repository {repo}';
} else if (metadata.team) {
return 'Remove permission for team {team} from repository {repo}';
} else if (metadata.token) {
return 'Remove permission for token {token} from repository {repo}';
}
},
'delete_tag': 'Tag {tag} deleted 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}',
'add_repo_webhook': 'Add webhook in repository {repo}',
'delete_repo_webhook': 'Delete webhook in repository {repo}',
'set_repo_description': 'Change description for repository {repo}: {description}',
'build_dockerfile': function(metadata) {
if (metadata.trigger_id) {
var triggerDescription = TriggerDescriptionBuilder.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_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 = TriggerDescriptionBuilder.getDescription(
metadata['service'], metadata['config']);
return 'Setup build trigger - ' + triggerDescription;
},
'delete_repo_trigger': function(metadata) {
var triggerDescription = TriggerDescriptionBuilder.getDescription(
metadata['service'], metadata['config']);
return 'Delete build trigger - ' + triggerDescription;
}
};
var logKinds = {
'account_change_plan': 'Change plan',
'account_change_cc': 'Update credit card',
'account_change_password': 'Change password',
'account_convert': 'Convert account to organization',
'create_robot': 'Create Robot Account',
'delete_robot': 'Delete Robot Account',
'create_repo': 'Create Repository',
'push_repo': 'Push to repository',
'pull_repo': 'Pull repository',
'delete_repo': 'Delete repository',
'change_repo_permission': 'Change repository permission',
'delete_repo_permission': 'Remove user permission from repository',
'change_repo_visibility': 'Change repository visibility',
'add_repo_accesstoken': 'Create access token',
'delete_repo_accesstoken': 'Delete access token',
'add_repo_webhook': 'Add webhook',
'delete_repo_webhook': 'Delete webhook',
'set_repo_description': 'Change repository description',
'build_dockerfile': 'Build image from Dockerfile',
'delete_tag': 'Delete Tag',
'org_create_team': 'Create team',
'org_delete_team': 'Delete team',
'org_add_team_member': 'Add team member',
'org_remove_team_member': 'Remove team member',
'org_set_team_description': 'Change team description',
'org_set_team_role': 'Change team permission',
'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'
};
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;
if (!$scope.visible || !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 = getRestUrl('user/logs');
if ($scope.organization) {
url = getRestUrl('organization', $scope.organization.name, 'logs');
}
if ($scope.repository) {
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
}
url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate));
url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate));
if ($scope.performer) {
url += '&performer=' + encodeURIComponent($scope.performer.username);
}
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) {
var fieldIcons = {
'username': 'user',
'activating_username': 'user',
'delegate_user': 'user',
'delegate_team': 'group',
'team': 'group',
'token': 'key',
'repo': 'hdd-o',
'robot': 'wrench',
'tag': 'tag',
'role': 'th-large',
'original_role': 'th-large'
};
log.metadata['_ip'] = log.ip ? log.ip : null;
var description = logDescriptions[log.kind] || log.kind;
if (typeof description != 'string') {
description = description(log.metadata);
}
for (var key in log.metadata) {
if (log.metadata.hasOwnProperty(key)) {
var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)';
var markedDown = getMarkedDown(value);
markedDown = markedDown.substr('
'.length, markedDown.length - '
'.length); var icon = fieldIcons[key]; if (icon) { markedDown = '' + markedDown; } description = description.replace('{' + key + '}', '' + markedDown + '
');
}
}
return $sce.trustAsHtml(description.replace('\n', '