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

Conflicts:
	data/model/legacy.py
	static/js/app.js
This commit is contained in:
Jake Moshenko 2014-09-12 11:03:30 -04:00
commit c5ca46a14b
70 changed files with 1566 additions and 630 deletions

View file

@ -153,6 +153,14 @@ quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoading
this.currentIndex_ = 0;
}
_ViewArray.prototype.length = function() {
return this.entries.length;
};
_ViewArray.prototype.get = function(index) {
return this.entries[index];
};
_ViewArray.prototype.push = function(elem) {
this.entries.push(elem);
this.hasEntries = true;
@ -384,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();
}
@ -446,6 +454,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];
@ -458,28 +489,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) {
@ -515,12 +533,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;
}
@ -558,6 +571,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 = {
'inviter': 'user',
@ -716,7 +764,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) {
@ -830,6 +878,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'];
@ -843,7 +962,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.
@ -1141,6 +1268,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'
}
]
}
];
@ -1396,10 +1571,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;
}]);
@ -1805,7 +2011,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',
@ -2339,6 +2545,45 @@ 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) {
var getRedirectUrl = function() {
return $scope.redirectUrl;
};
$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 = getRedirectUrl() || 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,
@ -2351,33 +2596,9 @@ quayApp.directive('signinForm', function () {
'signInStarted': '&signInStarted',
'signedIn': '&signedIn'
},
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) {
var getRedirectUrl = function() {
return $scope.redirectUrl;
};
$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 = getRedirectUrl() || 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) {
@ -2385,8 +2606,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;
@ -2410,8 +2652,23 @@ quayApp.directive('signinForm', function () {
window.location = (redirectUrl ? 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;
}
});
};
}
@ -2432,18 +2689,9 @@ quayApp.directive('signupForm', function () {
'userRegistered': '&userRegistered'
},
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;
@ -2545,7 +2793,11 @@ quayApp.directive('dockerAuthDialog', function (Config) {
},
controller: function($scope, $element) {
var updateCommand = function() {
$scope.command = 'docker login -e="." -u="' + $scope.username +
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'];
};
@ -4045,9 +4297,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() {
@ -4207,7 +4461,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) {
@ -4244,6 +4498,10 @@ quayApp.directive('namespaceSelector', function () {
if (newNamespace) {
CookieService.putPermanent('quay.namespace', newNamespace);
if ($routeParams['namespace'] && $routeParams['namespace'] != newNamespace) {
$location.search({'namespace': newNamespace});
}
}
};
@ -4288,9 +4546,48 @@ quayApp.directive('buildLogError', function () {
transclude: false,
restrict: 'C',
scope: {
'error': '=error'
'error': '=error',
'entries': '=entries'
},
controller: function($scope, $element) {
controller: function($scope, $element, Config) {
$scope.getLocalPullInfo = function() {
if ($scope.entries.__localpull !== undefined) {
return $scope.entries.__localpull;
}
var localInfo = {
'isLocal': false
};
// Find the 'pulling' phase entry, and then extra any metadata found under
// it.
for (var i = 0; i < $scope.entries.length; ++i) {
var entry = $scope.entries[i];
if (entry.type == 'phase' && entry.message == 'pulling') {
for (var j = 0; j < entry.logs.length(); ++j) {
var log = entry.logs.get(j);
if (log.data && log.data.phasestep == 'login') {
localInfo['login'] = log.data;
}
if (log.data && log.data.phasestep == 'pull') {
var repo_url = log.data['repo_url'];
var repo_and_tag = repo_url.substring(Config.SERVER_HOSTNAME.length + 1);
var tagIndex = repo_and_tag.lastIndexOf(':');
var repo = repo_and_tag.substring(0, tagIndex);
localInfo['repo_url'] = repo_url;
localInfo['repo'] = repo;
localInfo['isLocal'] = repo_url.indexOf(Config.SERVER_HOSTNAME + '/') == 0;
}
}
break;
}
}
return $scope.entries.__localpull = localInfo;
};
}
};
return directiveDefinitionObject;
@ -4326,6 +4623,9 @@ quayApp.directive('dropdownSelect', function ($compile) {
'selectedItem': '=selectedItem',
'placeholder': '=placeholder',
'lookaheadItems': '=lookaheadItems',
'allowCustomInput': '@allowCustomInput',
'handleItemSelected': '&handleItemSelected',
'handleInput': '&handleInput',
@ -5084,7 +5384,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 = '';
@ -5184,6 +5484,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++;
@ -5571,7 +5880,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)');
@ -5819,11 +6130,10 @@ 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 (response.status == 503) {