Merge remote-tracking branch 'origin/master' into pullfail

This commit is contained in:
Jake Moshenko 2014-09-12 10:36:38 -04:00
commit 5388633f9a
100 changed files with 2125 additions and 1008 deletions

View file

@ -1,6 +1,46 @@
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]{3,29}$';
$.fn.clipboardCopy = function() {
if (zeroClipboardSupported) {
(new ZeroClipboard($(this)));
return true;
}
this.hide();
return false;
};
var zeroClipboardSupported = true;
ZeroClipboard.config({
'swfPath': 'static/lib/ZeroClipboard.swf'
});
ZeroClipboard.on("error", function(e) {
zeroClipboardSupported = false;
});
ZeroClipboard.on('aftercopy', function(e) {
var container = e.target.parentNode.parentNode.parentNode;
var message = $(container).find('.clipboard-copied-message')[0];
// Resets the animation.
var elem = message;
elem.style.display = 'none';
elem.classList.remove('animated');
// Show the notification.
setTimeout(function() {
elem.style.display = 'inline-block';
elem.classList.add('animated');
}, 10);
// Reset the notification.
setTimeout(function() {
elem.style.display = 'none';
}, 5000);
});
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
@ -352,7 +392,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
var uiService = {};
uiService.hidePopover = function(elem) {
var popover = $('#signupButton').data('bs.popover');
var popover = $(elem).data('bs.popover');
if (popover) {
popover.hide();
}
@ -409,6 +449,29 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
var pingService = {};
var pingCache = {};
var invokeCallback = function($scope, pings, callback) {
if (pings[0] == -1) {
setTimeout(function() {
$scope.$apply(function() {
callback(-1, false, -1);
});
}, 0);
return;
}
var sum = 0;
for (var i = 0; i < pings.length; ++i) {
sum += pings[i];
}
// Report the average ping.
setTimeout(function() {
$scope.$apply(function() {
callback(Math.floor(sum / pings.length), true, pings.length);
});
}, 0);
};
var reportPingResult = function($scope, url, ping, callback) {
// Lookup the cached ping data, if any.
var cached = pingCache[url];
@ -421,28 +484,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
// If an error occurred, report it and done.
if (ping < 0) {
cached['pings'] = [-1];
setTimeout(function() {
$scope.$apply(function() {
callback(-1, false, -1);
});
}, 0);
invokeCallback($scope, pings, callback);
return;
}
// Otherwise, add the current ping and determine the average.
cached['pings'].push(ping);
var sum = 0;
for (var i = 0; i < cached['pings'].length; ++i) {
sum += cached['pings'][i];
}
// Report the average ping.
setTimeout(function() {
$scope.$apply(function() {
callback(Math.floor(sum / cached['pings'].length), true, cached['pings'].length);
});
}, 0);
// Invoke the callback.
invokeCallback($scope, cached['pings'], callback);
// Schedule another check if we've done less than three.
if (cached['pings'].length < 3) {
@ -478,12 +528,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
pingService.pingUrl = function($scope, url, callback) {
if (pingCache[url]) {
cached = pingCache[url];
setTimeout(function() {
$scope.$apply(function() {
callback(cached.result, cached.success);
});
}, 0);
invokeCallback($scope, pingCache[url]['pings'], callback);
return;
}
@ -521,6 +566,41 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
$provide.factory('StringBuilderService', ['$sce', 'UtilService', function($sce, UtilService) {
var stringBuilderService = {};
stringBuilderService.buildUrl = function(value_or_func, metadata) {
var url = value_or_func;
if (typeof url != 'string') {
url = url(metadata);
}
// Find the variables to be replaced.
var varNames = [];
for (var i = 0; i < url.length; ++i) {
var c = url[i];
if (c == '{') {
for (var j = i + 1; j < url.length; ++j) {
var d = url[j];
if (d == '}') {
varNames.push(url.substring(i + 1, j));
i = j;
break;
}
}
}
}
// Replace all variables found.
for (var i = 0; i < varNames.length; ++i) {
var varName = varNames[i];
if (!metadata[varName]) {
return null;
}
url = url.replace('{' + varName + '}', metadata[varName]);
}
return url;
};
stringBuilderService.buildString = function(value_or_func, metadata) {
var fieldIcons = {
'username': 'user',
@ -676,7 +756,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return config;
}]);
$provide.factory('ApiService', ['Restangular', function(Restangular) {
$provide.factory('ApiService', ['Restangular', '$q', function(Restangular, $q) {
var apiService = {};
var getResource = function(path, opt_background) {
@ -773,6 +853,77 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
}
};
var freshLoginFailCheck = function(opName, opArgs) {
return function(resp) {
var deferred = $q.defer();
// If the error is a fresh login required, show the dialog.
if (resp.status == 401 && resp.data['error_type'] == 'fresh_login_required') {
var verifyNow = function() {
var info = {
'password': $('#freshPassword').val()
};
$('#freshPassword').val('');
// Conduct the sign in of the user.
apiService.verifyUser(info).then(function() {
// On success, retry the operation. if it succeeds, then resolve the
// deferred promise with the result. Otherwise, reject the same.
apiService[opName].apply(apiService, opArgs).then(function(resp) {
deferred.resolve(resp);
}, function(resp) {
deferred.reject(resp);
});
}, function(resp) {
// Reject with the sign in error.
deferred.reject({'data': {'message': 'Invalid verification credentials'}});
});
};
var box = bootbox.dialog({
"message": 'It has been more than a few minutes since you last logged in, ' +
'so please verify your password to perform this sensitive operation:' +
'<form style="margin-top: 10px" action="javascript:void(0)">' +
'<input id="freshPassword" class="form-control" type="password" placeholder="Current Password">' +
'</form>',
"title": 'Please Verify',
"buttons": {
"verify": {
"label": "Verify",
"className": "btn-success",
"callback": verifyNow
},
"close": {
"label": "Cancel",
"className": "btn-default",
"callback": function() {
deferred.reject({'data': {'message': 'Verification canceled'}});
}
}
}
});
box.bind('shown.bs.modal', function(){
box.find("input").focus();
box.find("form").submit(function() {
if (!$('#freshPassword').val()) { return; }
box.modal('hide');
verifyNow();
});
});
// Return a new promise. We'll accept or reject it based on the result
// of the login.
return deferred.promise;
}
// Otherwise, we just 'raise' the error via the reject method on the promise.
return $q.reject(resp);
};
};
var buildMethodsForOperation = function(operation, resource, resourceMap) {
var method = operation['method'].toLowerCase();
var operationName = operation['nickname'];
@ -786,7 +937,15 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'ignoreLoadingBar': true
});
}
return one['custom' + method.toUpperCase()](opt_options);
var opObj = one['custom' + method.toUpperCase()](opt_options);
// If the operation requires_fresh_login, then add a specialized error handler that
// will defer the operation's result if sudo is requested.
if (operation['requires_fresh_login']) {
opObj = opObj.catch(freshLoginFailCheck(operationName, arguments));
}
return opObj;
};
// If the method for the operation is a GET, add an operationAsResource method.
@ -1084,6 +1243,54 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'title': 'Webhook URL'
}
]
},
{
'id': 'flowdock',
'title': 'Flowdock Team Notification',
'icon': 'flowdock-icon',
'fields': [
{
'name': 'flow_api_token',
'type': 'string',
'title': 'Flow API Token',
'help_url': 'https://www.flowdock.com/account/tokens'
}
]
},
{
'id': 'hipchat',
'title': 'HipChat Room Notification',
'icon': 'hipchat-icon',
'fields': [
{
'name': 'room_id',
'type': 'string',
'title': 'Room ID #'
},
{
'name': 'notification_token',
'type': 'string',
'title': 'Notification Token'
}
]
},
{
'id': 'slack',
'title': 'Slack Room Notification',
'icon': 'slack-icon',
'fields': [
{
'name': 'subdomain',
'type': 'string',
'title': 'Slack Subdomain'
},
{
'name': 'token',
'type': 'string',
'title': 'Token',
'help_url': 'https://{subdomain}.slack.com/services/new/incoming-webhook'
}
]
}
];
@ -1123,7 +1330,8 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'user': null,
'notifications': [],
'notificationClasses': [],
'notificationSummaries': []
'notificationSummaries': [],
'additionalNotifications': false
};
var pollTimerHandle = null;
@ -1219,7 +1427,9 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
'uuid': notification.id
};
ApiService.updateUserNotification(notification, params);
ApiService.updateUserNotification(notification, params, function() {
notificationService.update();
}, ApiService.errorDisplay('Could not update notification'));
var index = $.inArray(notification, notificationService.notifications);
if (index >= 0) {
@ -1276,6 +1486,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
ApiService.listUserNotifications().then(function(resp) {
notificationService.notifications = resp['notifications'];
notificationService.additionalNotifications = resp['additional'];
notificationService.notificationClasses = notificationService.getClasses(notificationService.notifications);
});
};
@ -1304,10 +1515,41 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
var keyService = {}
keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY'];
keyService['githubClientId'] = Config['GITHUB_CLIENT_ID'];
keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID'];
keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback');
keyService['googleLoginClientId'] = Config['GOOGLE_LOGIN_CLIENT_ID'];
keyService['googleRedirectUri'] = Config.getUrl('/oauth2/google/callback');
keyService['googleLoginUrl'] = 'https://accounts.google.com/o/oauth2/auth?response_type=code&';
keyService['githubLoginUrl'] = 'https://github.com/login/oauth/authorize?';
keyService['googleLoginScope'] = 'openid email';
keyService['githubLoginScope'] = 'user:email';
keyService.getExternalLoginUrl = function(service, action) {
var state_clause = '';
if (Config.MIXPANEL_KEY && window.mixpanel) {
if (mixpanel.get_distinct_id !== undefined) {
state_clause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
}
var client_id = keyService[service + 'LoginClientId'];
var scope = keyService[service + 'LoginScope'];
var redirect_uri = keyService[service + 'RedirectUri'];
if (action == 'attach') {
redirect_uri += '/attach';
}
var url = keyService[service + 'LoginUrl'] + 'client_id=' + client_id + '&scope=' + scope +
'&redirect_uri=' + redirect_uri + state_clause;
return url;
};
return keyService;
}]);
@ -1507,7 +1749,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
});
};
planService.changePlan = function($scope, orgname, planId, callbacks) {
planService.changePlan = function($scope, orgname, planId, callbacks, opt_async) {
if (!Features.BILLING) { return; }
if (callbacks['started']) {
@ -1520,7 +1762,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && (previousSubscribeFailure || !cardInfo.last4)) {
var title = cardInfo.last4 ? 'Subscribe' : 'Start Trial ({{amount}} plan)';
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title);
planService.showSubscribeDialog($scope, orgname, planId, callbacks, title, /* async */true);
return;
}
@ -1593,9 +1835,34 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title) {
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks, opt_title, opt_async) {
if (!Features.BILLING) { return; }
// If the async parameter is true and this is a browser that does not allow async popup of the
// Stripe dialog (such as Mobile Safari or IE), show a bootbox to show the dialog instead.
var isIE = navigator.appName.indexOf("Internet Explorer") != -1;
var isMobileSafari = navigator.userAgent.match(/(iPod|iPhone|iPad)/) && navigator.userAgent.match(/AppleWebKit/);
if (opt_async && (isIE || isMobileSafari)) {
bootbox.dialog({
"message": "Please click 'Subscribe' to continue",
"buttons": {
"subscribe": {
"label": "Subscribe",
"className": "btn-primary",
"callback": function() {
planService.showSubscribeDialog($scope, orgname, planId, callbacks, opt_title, false);
}
},
"close": {
"label": "Cancel",
"className": "btn-default"
}
}
});
return;
}
if (callbacks['opening']) {
callbacks['opening']();
}
@ -1688,7 +1955,7 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
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}).
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl, reloadOnSearch: false}).
when('/user/', {title: 'Account Settings', description:'Account settings for ' + title, templateUrl: '/static/partials/user-admin.html',
reloadOnSearch: false, controller: UserAdminCtrl}).
when('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for ' + title, templateUrl: '/static/partials/super-user.html',
@ -2114,6 +2381,8 @@ quayApp.directive('copyBox', function () {
'hoveringMessage': '=hoveringMessage'
},
controller: function($scope, $element, $rootScope) {
$scope.disabled = false;
var number = $rootScope.__copyBoxIdCounter || 0;
$rootScope.__copyBoxIdCounter = number + 1;
$scope.inputId = "copy-box-input-" + number;
@ -2123,27 +2392,7 @@ quayApp.directive('copyBox', function () {
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);
});
$scope.disabled = !button.clipboardCopy();
}
};
return directiveDefinitionObject;
@ -2185,6 +2434,41 @@ quayApp.directive('userSetup', function () {
});
quayApp.directive('externalLoginButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/external-login-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'signInStarted': '&signInStarted',
'redirectUrl': '=redirectUrl',
'provider': '@provider',
'action': '@action'
},
controller: function($scope, $timeout, $interval, ApiService, KeyService, CookieService, Features, Config) {
$scope.startSignin = function(service) {
$scope.signInStarted({'service': service});
var url = KeyService.getExternalLoginUrl(service, $scope.action || 'login');
// Save the redirect URL in a cookie so that we can redirect back after the service returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
document.location = url;
}, 250);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('signinForm', function () {
var directiveDefinitionObject = {
priority: 0,
@ -2197,29 +2481,9 @@ quayApp.directive('signinForm', function () {
'signInStarted': '&signInStarted',
'signedIn': '&signedIn'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
$scope.showGithub = function() {
if (!Features.GITHUB_LOGIN) { return; }
$scope.markStarted();
var mixpanelDistinctIdClause = '';
if (Config.MIXPANEL_KEY && mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id());
}
// Save the redirect URL in a cookie so that we can redirect back after GitHub returns to us.
var redirectURL = $scope.redirectUrl || window.location.toString();
CookieService.putPermanent('quay.redirectAfterLoad', redirectURL);
// Needed to ensure that UI work done by the started callback is finished before the location
// changes.
$timeout(function() {
var url = 'https://github.com/login/oauth/authorize?client_id=' + encodeURIComponent(KeyService.githubLoginClientId) +
'&scope=user:email' + mixpanelDistinctIdClause;
document.location = url;
}, 250);
};
controller: function($scope, $location, $timeout, $interval, ApiService, KeyService, UserService, CookieService, Features, Config) {
$scope.tryAgainSoon = 0;
$scope.tryAgainInterval = null;
$scope.markStarted = function() {
if ($scope.signInStarted != null) {
@ -2227,8 +2491,29 @@ quayApp.directive('signinForm', function () {
}
};
$scope.cancelInterval = function() {
$scope.tryAgainSoon = 0;
if ($scope.tryAgainInterval) {
$interval.cancel($scope.tryAgainInterval);
}
$scope.tryAgainInterval = null;
};
$scope.$watch('user.username', function() {
$scope.cancelInterval();
});
$scope.$on('$destroy', function() {
$scope.cancelInterval();
});
$scope.signin = function() {
if ($scope.tryAgainSoon > 0) { return; }
$scope.markStarted();
$scope.cancelInterval();
ApiService.signinUser($scope.user).then(function() {
$scope.needsEmailVerification = false;
@ -2250,8 +2535,23 @@ quayApp.directive('signinForm', function () {
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
}, 500);
}, function(result) {
$scope.needsEmailVerification = result.data.needsEmailVerification;
$scope.invalidCredentials = result.data.invalidCredentials;
if (result.status == 429 /* try again later */) {
$scope.needsEmailVerification = false;
$scope.invalidCredentials = false;
$scope.cancelInterval();
$scope.tryAgainSoon = result.headers('Retry-After');
$scope.tryAgainInterval = $interval(function() {
$scope.tryAgainSoon--;
if ($scope.tryAgainSoon <= 0) {
$scope.cancelInterval();
}
}, 1000, $scope.tryAgainSoon);
} else {
$scope.needsEmailVerification = result.data.needsEmailVerification;
$scope.invalidCredentials = result.data.invalidCredentials;
}
});
};
}
@ -2270,18 +2570,9 @@ quayApp.directive('signupForm', function () {
scope: {
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {
$('.form-signup').popover();
if (Config.MIXPANEL_KEY) {
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
var mixpanelId = loadedMixpanel.get_distinct_id();
$scope.github_state_clause = '&state=' + mixpanelId;
});
}
$scope.githubClientId = KeyService.githubLoginClientId;
$scope.awaitingConfirmation = false;
$scope.registering = false;
@ -2371,11 +2662,42 @@ quayApp.directive('dockerAuthDialog', function (Config) {
'username': '=username',
'token': '=token',
'shown': '=shown',
'counter': '=counter'
'counter': '=counter',
'supportsRegenerate': '@supportsRegenerate',
'regenerate': '&regenerate'
},
controller: function($scope, $element) {
controller: function($scope, $element) {
var updateCommand = function() {
var escape = function(v) {
if (!v) { return v; }
return v.replace('$', '\\$');
};
$scope.command = 'docker login -e="." -u="' + escape($scope.username) +
'" -p="' + $scope.token + '" ' + Config['SERVER_HOSTNAME'];
};
$scope.$watch('username', updateCommand);
$scope.$watch('token', updateCommand);
$scope.regenerating = true;
$scope.askRegenerate = function() {
bootbox.confirm('Are you sure you want to regenerate the token? All existing login credentials will become invalid', function(resp) {
if (resp) {
$scope.regenerating = true;
$scope.regenerate({'username': $scope.username, 'token': $scope.token});
}
});
};
$scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){}
var isSafari = /^((?!chrome).)*safari/i.test(navigator.userAgent);
if (isSafari) {
// Doesn't work properly in Safari, sadly.
return false;
}
try { return !!new Blob(); } catch(e) {}
return false;
};
@ -2393,6 +2715,8 @@ quayApp.directive('dockerAuthDialog', function (Config) {
};
var show = function(r) {
$scope.regenerating = false;
if (!$scope.shown || !$scope.username || !$scope.token) {
$('#dockerauthmodal').modal('hide');
return;
@ -2633,6 +2957,8 @@ quayApp.directive('logsView', function () {
return 'Delete notification of event "' + eventData['title'] + '" for repository {repo}';
},
'regenerate_robot_token': 'Regenerated token for robot {robot}',
// Note: These are deprecated.
'add_repo_webhook': 'Add webhook in repository {repo}',
'delete_repo_webhook': 'Delete webhook in repository {repo}'
@ -2676,6 +3002,7 @@ quayApp.directive('logsView', function () {
'reset_application_client_secret': 'Reset Client Secret',
'add_repo_notification': 'Add repository notification',
'delete_repo_notification': 'Delete repository notification',
'regenerate_robot_token': 'Regenerate Robot Token',
// Note: these are deprecated.
'add_repo_webhook': 'Add webhook',
@ -2847,6 +3174,20 @@ quayApp.directive('robotsManager', function () {
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
$scope.regenerateToken = function(username) {
if (!username) { return; }
var shortName = $scope.getShortenedName(username);
ApiService.regenerateRobotToken($scope.organization, null, {'robot_shortname': shortName}).then(function(updated) {
var index = $scope.findRobotIndexByName(username);
if (index >= 0) {
$scope.robots.splice(index, 1);
$scope.robots.push(updated);
}
$scope.shownRobot = updated;
}, ApiService.errorDisplay('Cannot regenerate robot account token'));
};
$scope.showRobot = function(info) {
$scope.shownRobot = info;
$scope.showRobotCounter++;
@ -3786,9 +4127,11 @@ quayApp.directive('billingOptions', function () {
var save = function() {
$scope.working = true;
var errorHandler = ApiService.errorDisplay('Could not change user details');
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
$scope.working = false;
});
}, errorHandler);
};
var checkSave = function() {
@ -3840,7 +4183,7 @@ quayApp.directive('planManager', function () {
return true;
};
$scope.changeSubscription = function(planId) {
$scope.changeSubscription = function(planId, opt_async) {
if ($scope.planChanging) { return; }
var callbacks = {
@ -3854,7 +4197,7 @@ quayApp.directive('planManager', function () {
}
};
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
PlanService.changePlan($scope, $scope.organization, planId, callbacks, opt_async);
};
$scope.cancelSubscription = function() {
@ -3917,7 +4260,7 @@ quayApp.directive('planManager', function () {
if ($scope.readyForPlan) {
var planRequested = $scope.readyForPlan();
if (planRequested && planRequested != PlanService.getFreePlan()) {
$scope.changeSubscription(planRequested);
$scope.changeSubscription(planRequested, /* async */true);
}
}
});
@ -3948,7 +4291,7 @@ quayApp.directive('namespaceSelector', function () {
'namespace': '=namespace',
'requireCreate': '=requireCreate'
},
controller: function($scope, $element, $routeParams, CookieService) {
controller: function($scope, $element, $routeParams, $location, CookieService) {
$scope.namespaces = {};
$scope.initialize = function(user) {
@ -3985,6 +4328,10 @@ quayApp.directive('namespaceSelector', function () {
if (newNamespace) {
CookieService.putPermanent('quay.namespace', newNamespace);
if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) {
$location.search({'namespace': newNamespace});
}
}
};
@ -4106,6 +4453,9 @@ quayApp.directive('dropdownSelect', function ($compile) {
'selectedItem': '=selectedItem',
'placeholder': '=placeholder',
'lookaheadItems': '=lookaheadItems',
'allowCustomInput': '@allowCustomInput',
'handleItemSelected': '&handleItemSelected',
'handleInput': '&handleInput',
@ -4864,7 +5214,7 @@ quayApp.directive('createExternalNotificationDialog', function () {
'counter': '=counter',
'notificationCreated': '&notificationCreated'
},
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout) {
controller: function($scope, $element, ExternalNotificationData, ApiService, $timeout, StringBuilderService) {
$scope.currentEvent = null;
$scope.currentMethod = null;
$scope.status = '';
@ -4964,6 +5314,15 @@ quayApp.directive('createExternalNotificationDialog', function () {
}, 1000);
};
$scope.getHelpUrl = function(field, config) {
var helpUrl = field['help_url'];
if (!helpUrl) {
return null;
}
return StringBuilderService.buildUrl(helpUrl, config);
};
$scope.$watch('counter', function(counter) {
if (counter) {
$scope.clearCounter++;
@ -5002,6 +5361,23 @@ quayApp.directive('twitterView', function () {
});
quayApp.directive('notificationsBubble', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/notifications-bubble.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, UserService, NotificationService) {
$scope.notificationService = NotificationService;
}
};
return directiveDefinitionObject;
});
quayApp.directive('notificationView', function () {
var directiveDefinitionObject = {
priority: 0,
@ -5330,7 +5706,9 @@ quayApp.directive('locationView', function () {
$scope.getLocationTooltip = function(location, ping) {
var tip = $scope.getLocationTitle(location) + '<br>';
if (ping < 0) {
if (ping == null) {
tip += '(Loading)';
} else if (ping < 0) {
tip += '<br><b>Note: Could not contact server</b>';
} else {
tip += 'Estimated Ping: ' + (ping ? ping + 'ms' : '(Loading)');
@ -5578,15 +5956,14 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
// Handle session expiration.
Restangular.setErrorInterceptor(function(response) {
if (response.status == 401) {
if (response.data['session_required'] == null || response.data['session_required'] === true) {
$('#sessionexpiredModal').modal({});
return false;
}
if (response.status == 401 && response.data['error_type'] == 'invalid_token' &&
response.data['session_required'] !== false) {
$('#sessionexpiredModal').modal({});
return false;
}
if (!Features.BILLING && response.status == 402) {
$('#overlicenseModal').modal({});
if (response.status == 503) {
$('#cannotContactService').modal({});
return false;
}

View file

@ -1,25 +1,3 @@
$.fn.clipboardCopy = function() {
var clip = new ZeroClipboard($(this), { 'moviePath': 'static/lib/ZeroClipboard.swf' });
clip.on('complete', function() {
// Resets the animation.
var elem = $('#clipboardCopied')[0];
if (!elem) {
return;
}
elem.style.display = 'none';
elem.classList.remove('animated');
// Show the notification.
setTimeout(function() {
if (!elem) { return; }
elem.style.display = 'inline-block';
elem.classList.add('animated');
}, 10);
});
};
function GuideCtrl() {
}
@ -545,16 +523,24 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
$scope.deleteTag = function(tagName) {
if (!$scope.repo.can_admin) { return; }
$('#confirmdeleteTagModal').modal('hide');
var params = {
'repository': namespace + '/' + name,
'tag': tagName
};
var errorHandler = ApiService.errorDisplay('Cannot delete tag', function() {
$('#confirmdeleteTagModal').modal('hide');
$scope.deletingTag = false;
});
$scope.deletingTag = true;
ApiService.deleteFullTag(null, params).then(function() {
loadViewInfo();
}, ApiService.errorDisplay('Cannot delete tag'));
$('#confirmdeleteTagModal').modal('hide');
$scope.deletingTag = false;
}, errorHandler);
};
$scope.getImagesForTagBySize = function(tag) {
@ -733,8 +719,6 @@ function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiServi
// Load the builds for this repository. If none are active it will cancel the poll.
startBuildInfoTimer(repo);
$('#copyClipboard').clipboardCopy();
});
};
@ -1661,13 +1645,19 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
UserService.updateUserIn($scope, function(user) {
$scope.cuser = jQuery.extend({}, user);
if (Features.GITHUB_LOGIN && $scope.cuser.logins) {
if ($scope.cuser.logins) {
for (var i = 0; i < $scope.cuser.logins.length; i++) {
if ($scope.cuser.logins[i].service == 'github') {
var githubId = $scope.cuser.logins[i].service_identifier;
$http.get('https://api.github.com/user/' + githubId).success(function(resp) {
$scope.githubLogin = resp.login;
});
var login = $scope.cuser.logins[i];
login.metadata = login.metadata || {};
if (login.service == 'github') {
$scope.hasGithubLogin = true;
$scope.githubLogin = login.metadata['service_username'];
}
if (login.service == 'google') {
$scope.hasGoogleLogin = true;
$scope.googleLogin = login.metadata['service_username'];
}
}
}
@ -1685,7 +1675,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.convertStep = 0;
$scope.org = {};
$scope.githubRedirectUri = KeyService.githubRedirectUri;
$scope.githubClientId = KeyService.githubLoginClientId;
$scope.authorizedApps = null;
$scope.logsShown = 0;
@ -1715,7 +1704,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
};
$scope.loadInvoices = function() {
if (!$scope.hasPaidBusinessPlan) { return; }
$scope.invoicesShown++;
};
@ -1784,7 +1772,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.changeEmailForm.$setPristine();
}, function(result) {
$scope.updatingUser = false;
UIService.showFormError('#changeEmailForm', result);
UIService.showFormError('#changeEmailForm', result);
});
};
@ -1794,7 +1782,8 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use
$scope.updatingUser = true;
$scope.changePasswordSuccess = false;
ApiService.changeUserDetails($scope.cuser).then(function() {
ApiService.changeUserDetails($scope.cuser).then(function(resp) {
$scope.updatingUser = false;
$scope.changePasswordSuccess = true;
@ -1901,9 +1890,6 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, I
// Fetch the image's changes.
fetchChanges();
$('#copyClipboard').clipboardCopy();
return image;
});
};
@ -2699,35 +2685,7 @@ function SuperUserAdminCtrl($scope, ApiService, Features, UserService) {
}, ApiService.errorDisplay('Cannot delete user'));
};
var seatUsageLoaded = function(usage) {
$scope.usageLoading = false;
if (usage.count > usage.allowed) {
$scope.limit = 'over';
} else if (usage.count == usage.allowed) {
$scope.limit = 'at';
} else if (usage.count >= usage.allowed * 0.7) {
$scope.limit = 'near';
} else {
$scope.limit = 'none';
}
if (!$scope.chart) {
$scope.chart = new UsageChart();
$scope.chart.draw('seat-usage-chart');
}
$scope.chart.update(usage.count, usage.allowed);
};
var loadSeatUsage = function() {
$scope.usageLoading = true;
ApiService.getSeatCount().then(function(resp) {
seatUsageLoaded(resp);
});
};
loadSeatUsage();
$scope.loadUsers();
}
function TourCtrl($scope, $location) {