2396 lines
74 KiB
JavaScript
2396 lines
74 KiB
JavaScript
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 || '');
|
|
}
|
|
|
|
// Start the application code itself.
|
|
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies', 'ngSanitize', 'angular-md5'], 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, """)
|
|
.replace(/'/g, "'");
|
|
|
|
return $sanitize(adjusted);
|
|
};
|
|
|
|
return utilService;
|
|
}]);
|
|
|
|
$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) {
|
|
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
|
|
};
|
|
|
|
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 formatMethodName = function(endpointName) {
|
|
var formatted = '';
|
|
for (var i = 0; i < endpointName.length; ++i) {
|
|
var c = endpointName[i];
|
|
if (c == '_') {
|
|
c = endpointName[i + 1].toUpperCase();
|
|
i++;
|
|
}
|
|
|
|
formatted += c;
|
|
}
|
|
|
|
return formatted;
|
|
};
|
|
|
|
var buildUrl = function(path, parameters) {
|
|
// We already have /api/ on the URLs, so remove them from the paths.
|
|
path = path.substr('/api/'.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);
|
|
var colon = varName.indexOf(':');
|
|
var isPathVar = false;
|
|
if (colon > 0) {
|
|
isPathVar = true;
|
|
varName = varName.substr(colon + 1);
|
|
}
|
|
|
|
if (!parameters[varName]) {
|
|
throw new Error('Missing parameter: ' + varName);
|
|
}
|
|
|
|
url += isPathVar ? parameters[varName] : encodeURI(parameters[varName]);
|
|
i = end;
|
|
continue;
|
|
}
|
|
|
|
url += c;
|
|
}
|
|
|
|
return url;
|
|
};
|
|
|
|
var getGenericMethodName = function(userMethodName) {
|
|
return formatMethodName(userMethodName.replace('_user', ''));
|
|
};
|
|
|
|
var buildMethodsForEndpoint = function(endpoint) {
|
|
var method = endpoint.methods[0].toLowerCase();
|
|
var methodName = formatMethodName(endpoint['name']);
|
|
apiService[methodName] = function(opt_options, opt_parameters) {
|
|
return Restangular.one(buildUrl(endpoint['path'], opt_parameters))['custom' + method.toUpperCase()](opt_options);
|
|
};
|
|
|
|
if (method == 'get') {
|
|
apiService[methodName + 'AsResource'] = function(opt_parameters) {
|
|
return getResource(buildUrl(endpoint['path'], opt_parameters));
|
|
};
|
|
}
|
|
|
|
if (endpoint['user_method']) {
|
|
apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters) {
|
|
if (orgname) {
|
|
if (orgname.name) {
|
|
orgname = orgname.name;
|
|
}
|
|
|
|
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {});
|
|
return apiService[methodName](opt_options, params);
|
|
} else {
|
|
return apiService[formatMethodName(endpoint['user_method'])](opt_options, opt_parameters);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
|
|
// Construct the methods for each API endpoint.
|
|
if (!window.__endpoints) {
|
|
return apiService;
|
|
}
|
|
|
|
for (var i = 0; i < window.__endpoints.length; ++i) {
|
|
var endpoint = window.__endpoints[i];
|
|
buildMethodsForEndpoint(endpoint);
|
|
}
|
|
|
|
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) {
|
|
ApiService.getLoggedInUser().then(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();
|
|
}
|
|
});
|
|
};
|
|
|
|
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.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 {
|
|
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 = [];
|
|
|
|
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 (isBusiness && !planService.isOrgCompatible(plan)) {
|
|
continue;
|
|
}
|
|
|
|
if (plan.privateRepos >= privateCount) {
|
|
callback(plan);
|
|
return;
|
|
}
|
|
}
|
|
|
|
callback(null);
|
|
});
|
|
};
|
|
|
|
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 && !cardInfo.last4) {
|
|
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
|
|
return;
|
|
}
|
|
|
|
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
|
|
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/', {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('/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('/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', controller: SecurityCtrl}).
|
|
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
|
|
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/');
|
|
});
|
|
|
|
|
|
quayApp.directive('entityReference', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/entity-reference.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'name': '=name',
|
|
'orgname': '=orgname',
|
|
'team': '=team',
|
|
'isrobot': '=isrobot'
|
|
},
|
|
controller: function($scope, $element, UserService) {
|
|
$scope.getIsAdmin = function(orgname) {
|
|
return UserService.isNamespaceAdmin(orgname);
|
|
};
|
|
|
|
$scope.getPrefix = function(name) {
|
|
if (!name) { return ''; }
|
|
var plus = name.indexOf('+');
|
|
return name.substr(0, plus + 1);
|
|
};
|
|
|
|
$scope.getShortenedName = function(name) {
|
|
if (!name) { return ''; }
|
|
var plus = name.indexOf('+');
|
|
return name.substr(plus + 1);
|
|
};
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
|
|
quayApp.directive('markdownView', function () {
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
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('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('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('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) {
|
|
$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 logDescriptions = {
|
|
'account_change_plan': 'Change plan',
|
|
'account_change_cc': 'Update credit card',
|
|
'account_change_password': 'Change password',
|
|
'account_convert': 'Convert account to organization',
|
|
'create_robot': 'Create Robot Account: {robot}',
|
|
'delete_robot': 'Delete Robot Account: {robot}',
|
|
'create_repo': 'Create Repository: {repo}',
|
|
'push_repo': 'Push to repository: {repo}',
|
|
'pull_repo': function(metadata) {
|
|
if (metadata.token) {
|
|
return 'Pull repository {repo} via token {token}';
|
|
} else if (metadata.username) {
|
|
return 'Pull repository {repo} by {username}';
|
|
} else {
|
|
return 'Public pull of repository {repo} by {_ip}';
|
|
}
|
|
},
|
|
'delete_repo': 'Delete repository: {repo}',
|
|
'change_repo_permission': function(metadata) {
|
|
if (metadata.username) {
|
|
return 'Change permission for user {username} in repository {repo} to {role}';
|
|
} else if (metadata.team) {
|
|
return 'Change permission for team {team} in repository {repo} to {role}';
|
|
} else if (metadata.token) {
|
|
return 'Change permission for token {token} in repository {repo} to {role}';
|
|
}
|
|
},
|
|
'delete_repo_permission': function(metadata) {
|
|
if (metadata.username) {
|
|
return 'Remove permission for user {username} from repository {repo}';
|
|
} else if (metadata.team) {
|
|
return 'Remove permission for team {team} from repository {repo}';
|
|
} else if (metadata.token) {
|
|
return 'Remove permission for token {token} from repository {repo}';
|
|
}
|
|
},
|
|
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
|
|
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
|
|
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
|
|
'add_repo_webhook': 'Add webhook in repository {repo}',
|
|
'delete_repo_webhook': 'Delete webhook in repository {repo}',
|
|
'set_repo_description': 'Change description for repository {repo}: {description}',
|
|
'build_dockerfile': 'Build image from Dockerfile for repository {repo}',
|
|
'org_create_team': 'Create team: {team}',
|
|
'org_delete_team': 'Delete team: {team}',
|
|
'org_add_team_member': 'Add member {member} to team {team}',
|
|
'org_remove_team_member': 'Remove member {member} from team {team}',
|
|
'org_set_team_description': 'Change description of team {team}: {description}',
|
|
'org_set_team_role': 'Change permission of team {team} to {role}'
|
|
};
|
|
|
|
var logKinds = {
|
|
'account_change_plan': 'Change plan',
|
|
'account_change_cc': 'Update credit card',
|
|
'account_change_password': 'Change password',
|
|
'account_convert': 'Convert account to organization',
|
|
'create_robot': 'Create Robot Account',
|
|
'delete_robot': 'Delete Robot Account',
|
|
'create_repo': 'Create Repository',
|
|
'push_repo': 'Push to repository',
|
|
'pull_repo': 'Pull repository',
|
|
'delete_repo': 'Delete repository',
|
|
'change_repo_permission': 'Change repository permission',
|
|
'delete_repo_permission': 'Remove user permission from repository',
|
|
'change_repo_visibility': 'Change repository visibility',
|
|
'add_repo_accesstoken': 'Create access token',
|
|
'delete_repo_accesstoken': 'Delete access token',
|
|
'add_repo_webhook': 'Add webhook',
|
|
'delete_repo_webhook': 'Delete webhook',
|
|
'set_repo_description': 'Change repository description',
|
|
'build_dockerfile': 'Build image from Dockerfile',
|
|
'org_create_team': 'Create team',
|
|
'org_delete_team': 'Delete team',
|
|
'org_add_team_member': 'Add team member',
|
|
'org_remove_team_member': 'Remove team member',
|
|
'org_set_team_description': 'Change team description',
|
|
'org_set_team_role': 'Change team permission'
|
|
};
|
|
|
|
var 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/' + 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',
|
|
'team': 'group',
|
|
'token': 'key',
|
|
'repo': 'hdd',
|
|
'robot': 'wrench'
|
|
};
|
|
|
|
log.metadata['_ip'] = log.ip ? log.ip : null;
|
|
|
|
var description = logDescriptions[log.kind] || log.kind;
|
|
if (typeof description != 'string') {
|
|
description = description(log.metadata);
|
|
}
|
|
|
|
for (var key in log.metadata) {
|
|
if (log.metadata.hasOwnProperty(key)) {
|
|
var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)';
|
|
var markedDown = getMarkedDown(value);
|
|
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
|
|
|
|
var icon = fieldIcons[key];
|
|
if (icon) {
|
|
markedDown = '<i class="fa fa-' + icon + '"></i>' + markedDown;
|
|
}
|
|
|
|
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
|
|
}
|
|
}
|
|
return $sce.trustAsHtml(description);
|
|
};
|
|
|
|
$scope.$watch('organization', update);
|
|
$scope.$watch('user', update);
|
|
$scope.$watch('repository', update);
|
|
$scope.$watch('visible', update);
|
|
$scope.$watch('performer', update);
|
|
$scope.$watch('logStartDate', update);
|
|
$scope.$watch('logEndDate', update);
|
|
}
|
|
};
|
|
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
quayApp.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) {
|
|
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
|
|
$scope.robots = null;
|
|
$scope.loading = false;
|
|
$scope.shownRobot = null;
|
|
$scope.showRobotCounter = 0;
|
|
|
|
$scope.showRobot = function(info) {
|
|
$scope.shownRobot = info;
|
|
$scope.showRobotCounter++;
|
|
};
|
|
|
|
$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; }
|
|
|
|
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) {
|
|
for (var i = 0; i < $scope.robots.length; ++i) {
|
|
if ($scope.robots[i].name == info.name) {
|
|
$scope.robots.splice(i, 1);
|
|
return;
|
|
}
|
|
}
|
|
}, function() {
|
|
bootbox.dialog({
|
|
"message": 'The selected robot account could not be deleted',
|
|
"title": "Cannot delete robot account",
|
|
"buttons": {
|
|
"close": {
|
|
"label": "Close",
|
|
"className": "btn-primary"
|
|
}
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
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;
|
|
});
|
|
};
|
|
|
|
$scope.$watch('organization', update);
|
|
$scope.$watch('user', update);
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
|
|
quayApp.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();
|
|
}, 10);
|
|
};
|
|
|
|
$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;
|
|
});
|
|
|
|
|
|
quayApp.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;
|
|
});
|
|
|
|
|
|
quayApp.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;
|
|
});
|
|
|
|
|
|
quayApp.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;
|
|
});
|
|
|
|
|
|
quayApp.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;
|
|
});
|
|
|
|
|
|
quayApp.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) {
|
|
var searchToken = 0;
|
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
|
++searchToken;
|
|
}, true);
|
|
|
|
var element = $($element[0].childNodes[0]);
|
|
element.typeahead({
|
|
name: 'repositories',
|
|
remote: {
|
|
url: '/api/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;
|
|
}
|
|
},
|
|
template: 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">' + getFirstTextLine(datum.repo.description) + '</span>'
|
|
}
|
|
|
|
template += '</div>'
|
|
return template;
|
|
}
|
|
});
|
|
|
|
element.on('typeahead:selected', function (e, datum) {
|
|
element.typeahead('setQuery', '');
|
|
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name;
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
|
|
quayApp.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) {
|
|
$scope.overPlan = false;
|
|
|
|
var checkOverPlan = function() {
|
|
if ($scope.user.anonymous) {
|
|
$scope.overPlan = false;
|
|
return;
|
|
}
|
|
|
|
ApiService.getUserPrivateCount().then(function(resp) {
|
|
$scope.overPlan = resp.privateCount > resp.reposAllowed;
|
|
});
|
|
};
|
|
|
|
// Monitor any user changes and place the current user into the scope.
|
|
UserService.updateUserIn($scope, checkOverPlan);
|
|
|
|
// Monitor any plan changes.
|
|
PlanService.registerListener(this, checkOverPlan);
|
|
|
|
$scope.signout = function() {
|
|
ApiService.logout().then(function() {
|
|
UserService.load();
|
|
$location.path('/');
|
|
});
|
|
};
|
|
|
|
$scope.appLinkTarget = function() {
|
|
if ($("div[ng-view]").length === 0) {
|
|
return "_self";
|
|
}
|
|
return "";
|
|
};
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
|
|
quayApp.directive('entitySearch', function () {
|
|
var number = 0;
|
|
var directiveDefinitionObject = {
|
|
priority: 0,
|
|
templateUrl: '/static/directives/entity-search.html',
|
|
replace: false,
|
|
transclude: false,
|
|
restrict: 'C',
|
|
scope: {
|
|
'namespace': '=namespace',
|
|
'inputTitle': '=inputTitle',
|
|
'entitySelected': '=entitySelected',
|
|
'includeTeams': '=includeTeams',
|
|
'isOrganization': '=isOrganization'
|
|
},
|
|
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
|
$scope.lazyLoading = true;
|
|
$scope.isAdmin = false;
|
|
|
|
$scope.lazyLoad = function() {
|
|
if (!$scope.namespace || !$scope.lazyLoading) { return; }
|
|
|
|
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
|
|
|
|
if ($scope.isOrganization && $scope.includeTeams) {
|
|
ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
|
|
$scope.teams = resp.teams;
|
|
});
|
|
}
|
|
|
|
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
|
|
$scope.robots = resp.robots;
|
|
$scope.lazyLoading = false;
|
|
}, function() {
|
|
$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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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.is_organization) {
|
|
entity['is_org_member'] = true;
|
|
}
|
|
|
|
$scope.entitySelected(entity);
|
|
};
|
|
|
|
if (!$scope.entitySelected) { return; }
|
|
|
|
number++;
|
|
|
|
var input = $element[0].firstChild.firstChild;
|
|
$(input).typeahead({
|
|
name: 'entities' + number,
|
|
remote: {
|
|
url: '/api/entities/%QUERY',
|
|
replace: function (url, uriEncodedQuery) {
|
|
var namespace = $scope.namespace || '';
|
|
url = url.replace('%QUERY', uriEncodedQuery);
|
|
url += '?namespace=' + encodeURIComponent(namespace);
|
|
if ($scope.includeTeams) {
|
|
url += '&includeTeams=true'
|
|
}
|
|
return url;
|
|
},
|
|
filter: function(data) {
|
|
var datums = [];
|
|
for (var i = 0; i < data.results.length; ++i) {
|
|
var entity = data.results[i];
|
|
datums.push({
|
|
'value': entity.name,
|
|
'tokens': [entity.name],
|
|
'entity': entity
|
|
});
|
|
}
|
|
return datums;
|
|
}
|
|
},
|
|
template: 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>';
|
|
}
|
|
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('typeahead:selected', function(e, datum) {
|
|
$(input).typeahead('setQuery', '');
|
|
$scope.$apply(function() {
|
|
$scope.entitySelected(datum.entity);
|
|
});
|
|
});
|
|
|
|
$scope.$watch('inputTitle', function(title) {
|
|
input.setAttribute('placeholder', title);
|
|
});
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
|
|
quayApp.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;
|
|
});
|
|
|
|
|
|
quayApp.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.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;
|
|
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
|
|
$scope.working = false;
|
|
});
|
|
};
|
|
|
|
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;
|
|
});
|
|
|
|
|
|
quayApp.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) {
|
|
var hasSubscription = false;
|
|
|
|
$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) {
|
|
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);
|
|
};
|
|
|
|
$scope.cancelSubscription = function() {
|
|
$scope.changeSubscription(PlanService.getFreePlan());
|
|
};
|
|
|
|
var subscribedToPlan = function(sub) {
|
|
$scope.subscription = sub;
|
|
|
|
if (sub.plan != PlanService.getFreePlan()) {
|
|
hasSubscription = true;
|
|
}
|
|
|
|
PlanService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
|
|
$scope.subscribedPlan = subscribedPlan;
|
|
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
|
|
|
if ($scope.planChanged) {
|
|
$scope.planChanged({ 'plan': subscribedPlan });
|
|
}
|
|
|
|
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
|
$scope.limit = 'over';
|
|
} else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) {
|
|
$scope.limit = 'at';
|
|
} else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) {
|
|
$scope.limit = 'near';
|
|
} else {
|
|
$scope.limit = 'none';
|
|
}
|
|
|
|
if (!$scope.chart) {
|
|
$scope.chart = new RepositoryUsageChart();
|
|
$scope.chart.draw('repository-usage-chart');
|
|
}
|
|
|
|
$scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0);
|
|
|
|
$scope.planChanging = false;
|
|
$scope.planLoading = false;
|
|
});
|
|
};
|
|
|
|
var update = function() {
|
|
$scope.planLoading = true;
|
|
if (!$scope.plans) { return; }
|
|
|
|
PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
|
|
// User/Organization has no subscription.
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
};
|
|
|
|
// Start the initial download.
|
|
$scope.planLoading = true;
|
|
loadPlans();
|
|
|
|
$scope.$watch('organization', loadPlans);
|
|
$scope.$watch('user', loadPlans);
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
|
|
|
|
quayApp.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, 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);
|
|
}
|
|
};
|
|
|
|
$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;
|
|
});
|
|
|
|
|
|
quayApp.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) {
|
|
$scope.getBuildProgress = function(buildInfo) {
|
|
switch (buildInfo.status) {
|
|
case 'building':
|
|
return (buildInfo.current_command / buildInfo.total_commands) * 100;
|
|
break;
|
|
|
|
case 'pushing':
|
|
return buildInfo.push_completion * 100;
|
|
break;
|
|
|
|
case 'complete':
|
|
return 100;
|
|
break;
|
|
|
|
case 'initializing':
|
|
case 'starting':
|
|
case 'waiting':
|
|
return 0;
|
|
break;
|
|
}
|
|
|
|
return -1;
|
|
};
|
|
|
|
$scope.getBuildMessage = function(buildInfo) {
|
|
switch (buildInfo.status) {
|
|
case 'initializing':
|
|
return 'Starting Dockerfile build';
|
|
break;
|
|
|
|
case 'starting':
|
|
case 'waiting':
|
|
case 'building':
|
|
return 'Building image from Dockerfile';
|
|
break;
|
|
|
|
case 'pushing':
|
|
return 'Pushing image built from Dockerfile';
|
|
break;
|
|
|
|
case 'complete':
|
|
return 'Dockerfile build completed and pushed';
|
|
break;
|
|
|
|
case 'error':
|
|
return 'Dockerfile build failed: ' + buildInfo.message;
|
|
break;
|
|
}
|
|
};
|
|
}
|
|
};
|
|
return directiveDefinitionObject;
|
|
});
|
|
|
|
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
|
|
quayApp.directive('ngBlur', function() {
|
|
return function( scope, elem, attrs ) {
|
|
elem.bind('blur', function() {
|
|
scope.$apply(attrs.ngBlur);
|
|
});
|
|
};
|
|
});
|
|
|
|
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanService', '$http', '$timeout',
|
|
function($location, $rootScope, Restangular, UserService, PlanService, $http, $timeout) {
|
|
// Handle session expiration.
|
|
Restangular.setErrorInterceptor(function(response) {
|
|
if (response.status == 401) {
|
|
$('#sessionexpiredModal').modal({});
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
// Check if we need to redirect based on a previously chosen plan.
|
|
PlanService.handleNotedPlan();
|
|
|
|
var changeTab = function(activeTab, opt_timeout) {
|
|
var checkCount = 0;
|
|
|
|
$timeout(function() {
|
|
if (checkCount > 5) { return; }
|
|
checkCount++;
|
|
|
|
$('a[data-toggle="tab"]').each(function(index) {
|
|
var tabName = this.getAttribute('data-target').substr(1);
|
|
if (tabName != activeTab) {
|
|
return;
|
|
}
|
|
|
|
if (this.clientWidth == 0) {
|
|
changeTab(activeTab, 500);
|
|
return;
|
|
}
|
|
|
|
clickElement(this);
|
|
});
|
|
}, opt_timeout);
|
|
};
|
|
|
|
var resetDefaultTab = function() {
|
|
$timeout(function() {
|
|
$('a[data-toggle="tab"]').each(function(index) {
|
|
if (index == 0) {
|
|
clickElement(this);
|
|
}
|
|
});
|
|
});
|
|
};
|
|
|
|
$rootScope.$on('$routeUpdate', function(){
|
|
if ($location.search()['tab']) {
|
|
changeTab($location.search()['tab']);
|
|
} else {
|
|
resetDefaultTab();
|
|
}
|
|
});
|
|
|
|
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
|
if (current.$$route.title) {
|
|
$rootScope.title = current.$$route.title;
|
|
}
|
|
|
|
if (current.$$route.description) {
|
|
$rootScope.description = current.$$route.description;
|
|
} else {
|
|
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
|
|
}
|
|
|
|
$rootScope.fixFooter = !!current.$$route.fixFooter;
|
|
$rootScope.current = current.$$route;
|
|
});
|
|
|
|
$rootScope.$on('$viewContentLoaded', function(event, current) {
|
|
var activeTab = $location.search()['tab'];
|
|
|
|
// Setup deep linking of tabs. This will change the search field of the URL whenever a tab
|
|
// is changed in the UI.
|
|
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
|
var tabName = e.target.getAttribute('data-target').substr(1);
|
|
$rootScope.$apply(function() {
|
|
var isDefaultTab = $('a[data-toggle="tab"]')[0] == e.target;
|
|
var data = isDefaultTab ? {} : {'tab': tabName};
|
|
$location.search(data);
|
|
});
|
|
|
|
e.preventDefault();
|
|
});
|
|
|
|
if (activeTab) {
|
|
changeTab(activeTab);
|
|
}
|
|
});
|
|
|
|
var initallyChecked = false;
|
|
window.__isLoading = function() {
|
|
if (!initallyChecked) {
|
|
initallyChecked = true;
|
|
return true;
|
|
}
|
|
return $http.pendingRequests.length > 0;
|
|
};
|
|
}]);
|