From 56bb46ffb2c236474d666f3e097d89f05fd99613 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Thu, 26 Dec 2013 17:45:16 -0500 Subject: [PATCH] - Make the discovery information be preloaded via a bootstrap.js file before angular runs - Have ApiService generate all the api methods specified by the API discovery information - Change all call sites (except for a select few when it does not make sense) to use ApiService --- endpoints/api.py | 53 ++++++- static/js/app.js | 330 +++++++++++++++++++++++---------------- static/js/bootstrap.js | 13 ++ static/js/controllers.js | 220 +++++++++++++++++--------- templates/base.html | 17 ++ test/specs.py | 2 +- 6 files changed, 423 insertions(+), 212 deletions(-) create mode 100644 static/js/bootstrap.js diff --git a/endpoints/api.py b/endpoints/api.py index b3e6e9901..e0c663ae2 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -69,6 +69,17 @@ def internal_api_call(f): return decorated_view +def org_api_call(user_call_name): + def internal_decorator(f): + @wraps(f) + def decorated_view(*args, **kwargs): + return f(*args, **kwargs) + + decorated_view.__user_call = user_call_name + return decorated_view + + return internal_decorator + @app.errorhandler(model.DataModelException) def handle_dme(ex): return make_response(ex.message, 400) @@ -79,6 +90,32 @@ def handle_dme_key_error(ex): return make_response(ex.message, 400) +@app.route('/api/discovery') +def discovery(): + routes = [] + for rule in app.url_map.iter_rules(): + if rule.rule.startswith('/api/'): + endpoint_method = globals()[rule.endpoint] + is_internal = '__internal_call' in dir(endpoint_method) + is_org_api = '__user_call' in dir(endpoint_method) + methods = list(rule.methods.difference(['HEAD', 'OPTIONS'])) + + route = { + 'name': rule.endpoint, + 'methods': methods, + 'path': rule.rule, + 'parameters': list(rule.arguments) + } + + if is_org_api: + route['user_method'] = endpoint_method.__user_call + + routes.append(route) + + return jsonify({ + 'endpoints': routes + }) + @app.route('/api/') @internal_api_call def welcome(): @@ -438,6 +475,7 @@ def get_organization(orgname): @app.route('/api/organization/', methods=['PUT']) @api_login_required +@org_api_call('change_user_details') def change_organization_details(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1466,6 +1504,7 @@ def get_user_card(): @app.route('/api/organization//card', methods=['GET']) @api_login_required @internal_api_call +@org_api_call('get_user_card') def get_org_card(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1488,6 +1527,7 @@ def set_user_card(): @app.route('/api/organization//card', methods=['POST']) @api_login_required +@org_api_call('set_user_card') def set_org_card(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1642,6 +1682,7 @@ def list_user_invoices(): @app.route('/api/organization//invoices', methods=['GET']) @api_login_required +@org_api_call('list_user_invoices') def list_org_invoices(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1679,6 +1720,7 @@ def get_invoices(customer_id): @app.route('/api/organization//plan', methods=['PUT']) @api_login_required @internal_api_call +@org_api_call('update_user_subscription') def update_org_subscription(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1694,7 +1736,7 @@ def update_org_subscription(orgname): @app.route('/api/user/plan', methods=['GET']) @api_login_required @internal_api_call -def get_subscription(): +def get_user_subscription(): user = current_user.db_user() private_repos = model.get_private_repo_count(user.username) @@ -1713,6 +1755,7 @@ def get_subscription(): @app.route('/api/organization//plan', methods=['GET']) @api_login_required @internal_api_call +@org_api_call('get_user_subscription') def get_org_subscription(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1751,6 +1794,7 @@ def get_user_robots(): @app.route('/api/organization//robots', methods=['GET']) @api_login_required +@org_api_call('get_user_robots') def get_org_robots(orgname): permission = OrganizationMemberPermission(orgname) if permission.can(): @@ -1764,7 +1808,7 @@ def get_org_robots(orgname): @app.route('/api/user/robots/', methods=['PUT']) @api_login_required -def create_robot(robot_shortname): +def create_user_robot(robot_shortname): parent = current_user.db_user() robot, password = model.create_robot(robot_shortname, parent) resp = jsonify(robot_view(robot.username, password)) @@ -1776,6 +1820,7 @@ def create_robot(robot_shortname): @app.route('/api/organization//robots/', methods=['PUT']) @api_login_required +@org_api_call('create_user_robot') def create_org_robot(orgname, robot_shortname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1791,7 +1836,7 @@ def create_org_robot(orgname, robot_shortname): @app.route('/api/user/robots/', methods=['DELETE']) @api_login_required -def delete_robot(robot_shortname): +def delete_user_robot(robot_shortname): parent = current_user.db_user() model.delete_robot(format_robot_username(parent.username, robot_shortname)) log_action('delete_robot', parent.username, {'robot': robot_shortname}) @@ -1801,6 +1846,7 @@ def delete_robot(robot_shortname): @app.route('/api/organization//robots/', methods=['DELETE']) @api_login_required +@org_api_call('delete_user_robot') def delete_org_robot(orgname, robot_shortname): permission = AdministerOrganizationPermission(orgname) if permission.can(): @@ -1848,6 +1894,7 @@ def list_repo_logs(namespace, repository): @app.route('/api/organization//logs', methods=['GET']) @api_login_required +@org_api_call('list_user_logs') def list_org_logs(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): diff --git a/static/js/app.js b/static/js/app.js index a03f5a989..414c05357 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,6 +1,18 @@ 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 getFirstTextLine(commentString) { if (!commentString) { return ''; } @@ -34,11 +46,8 @@ function getFirstTextLine(commentString) { return ''; } -function createRobotAccount(Restangular, is_org, orgname, name, callback) { - var url = is_org ? getRestUrl('organization', orgname, 'robots', name) : - getRestUrl('user/robots', name); - var createRobot = Restangular.one(url); - createRobot.customPUT().then(callback, function(resp) { +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", @@ -52,14 +61,18 @@ function createRobotAccount(Restangular, is_org, orgname, name, callback) { }); } -function createOrganizationTeam(Restangular, orgname, teamname, callback) { - var createTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); +function createOrganizationTeam(ApiService, orgname, teamname, callback) { var data = { 'name': teamname, 'role': 'member' }; + + var params = { + 'orgname': orgname, + 'teamname': teamname + }; - createTeam.customPOST(data).then(callback, function() { + ApiService.updateOrganizationTeam(data, params).then(callback, function() { bootbox.dialog({ "message": resp.data ? resp.data : 'The team could not be created', "title": "Cannot create team", @@ -73,17 +86,6 @@ function createOrganizationTeam(Restangular, orgname, teamname, callback) { }); } -function getRestUrl(args) { - var url = ''; - for (var i = 0; i < arguments.length; ++i) { - if (i > 0) { - url += '/'; - } - url += encodeURI(arguments[i]) - } - return url; -} - function getMarkedDown(string) { return Markdown.getSanitizingConverter().makeHtml(string || ''); } @@ -91,6 +93,137 @@ function getMarkedDown(string) { // Start the application code itself. quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide, cfpLoadingBarProvider) { cfpLoadingBarProvider.includeSpinner = false; + + $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 = {}; @@ -113,7 +246,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest return cookieService; }]); - $provide.factory('UserService', ['Restangular', 'CookieService', function(Restangular, CookieService) { + $provide.factory('UserService', ['ApiService', 'CookieService', function(ApiService, CookieService) { var userResponse = { verified: false, anonymous: true, @@ -139,8 +272,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest }; userService.load = function(opt_callback) { - var userFetch = Restangular.one('user/'); - userFetch.get().then(function(loadedUser) { + ApiService.getLoggedInUser().then(function(loadedUser) { userResponse = loadedUser; if (!userResponse.anonymous) { @@ -198,48 +330,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest return userService; }]); - - $provide.factory('ApiService', ['Restangular', function(Restangular) { - var apiService = {} - apiService.at = function(locationPieces) { - var location = getRestUrl.apply(this, arguments); - var info = { - 'url': location, - 'caller': Restangular.one(location), - 'withOptions': function(options) { - info.options = options; - return info; - }, - 'get': function(processor, opt_errorHandler) { - var options = info.options; - var caller = info.caller; - var result = { - 'loading': true, - 'value': null, - 'hasError': false - }; - - caller.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 info; - }; - - return apiService; - }]); - $provide.factory('KeyService', ['$location', function($location) { var keyService = {} @@ -254,8 +344,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest return keyService; }]); - $provide.factory('PlanService', ['Restangular', 'KeyService', 'UserService', 'CookieService', - function(Restangular, KeyService, UserService, CookieService) { + $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', + function(KeyService, UserService, CookieService, ApiService) { var plans = null; var planDict = {}; var planService = {}; @@ -353,8 +443,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest return; } - var getPlans = Restangular.one('plans'); - getPlans.get().then(function(data) { + ApiService.listPlans().then(function(data) { var i = 0; for(i = 0; i < data.plans.length; i++) { planDict[data.plans[i].stripeId] = data.plans[i]; @@ -412,13 +501,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest }; planService.getSubscription = function(orgname, success, failure) { - var url = planService.getSubscriptionUrl(orgname); - var getSubscription = Restangular.one(url); - getSubscription.get().then(success, failure); - }; - - planService.getSubscriptionUrl = function(orgname) { - return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan'; + ApiService.getSubscription(orgname).then(success, failure); }; planService.setSubscription = function(orgname, planId, success, failure, opt_token) { @@ -430,9 +513,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest subscriptionDetails['token'] = opt_token.id; } - var url = planService.getSubscriptionUrl(orgname); - var createSubscriptionRequest = Restangular.one(url); - createSubscriptionRequest.customPUT(subscriptionDetails).then(function(resp) { + ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) { success(resp); planService.getPlan(planId, function(plan) { for (var i = 0; i < listeners.length; ++i) { @@ -443,9 +524,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest }; planService.getCardInfo = function(orgname, callback) { - var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; - var getCard = Restangular.one(url); - getCard.customGET().then(function(resp) { + ApiService.getCard(orgname).then(function(resp) { callback(resp.card); }, function() { callback({'is_valid': false}); @@ -492,12 +571,10 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest 'token': token.id }; - var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; - var changeCardRequest = Restangular.one(url); - changeCardRequest.customPOST(cardInfo).then(callbacks['success'], function(resp) { - planService.handleCardError(resp); - callbacks['failure'](resp); - }); + ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) { + planService.handleCardError(resp); + callbacks['failure'](resp); + }); }); }; @@ -739,10 +816,9 @@ quayApp.directive('userSetup', function () { 'signInStarted': '&signInStarted', 'signedIn': '&signedIn' }, - controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { + controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $scope.sendRecovery = function() { - var signinPost = Restangular.one('recovery'); - signinPost.customPOST($scope.recovery).then(function() { + ApiService.requestRecoveryEmail($scope.recovery).then(function() { $scope.invalidEmail = false; $scope.sent = true; }, function(result) { @@ -772,7 +848,7 @@ quayApp.directive('signinForm', function () { 'signInStarted': '&signInStarted', 'signedIn': '&signedIn' }, - controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { + controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $scope.showGithub = function() { $scope.markStarted(); @@ -799,8 +875,7 @@ quayApp.directive('signinForm', function () { $scope.signin = function() { $scope.markStarted(); - var signinPost = Restangular.one('signin'); - signinPost.customPOST($scope.user).then(function() { + ApiService.signinUser($scope.user).then(function() { $scope.needsEmailVerification = false; $scope.invalidCredentials = false; @@ -840,7 +915,7 @@ quayApp.directive('signupForm', function () { scope: { }, - controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { + controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $('.form-signup').popover(); angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) { @@ -857,8 +932,7 @@ quayApp.directive('signupForm', function () { $('.form-signup').popover('hide'); $scope.registering = true; - var newUserPost = Restangular.one('user/'); - newUserPost.customPOST($scope.newUser).then(function() { + ApiService.createNewUser($scope.newUser).then(function() { $scope.awaitingConfirmation = true; $scope.registering = false; @@ -911,7 +985,7 @@ quayApp.directive('dockerAuthDialog', function () { 'shown': '=shown', 'counter': '=counter' }, - controller: function($scope, $element, Restangular) { + controller: function($scope, $element) { $scope.isDownloadSupported = function() { try { return !!new Blob(); } catch(e){} return false; @@ -981,7 +1055,7 @@ quayApp.directive('billingInvoices', function () { 'user': '=user', 'visible': '=visible' }, - controller: function($scope, $element, $sce, Restangular) { + controller: function($scope, $element, $sce, ApiService) { $scope.loading = false; $scope.invoiceExpanded = {}; @@ -1000,13 +1074,7 @@ quayApp.directive('billingInvoices', function () { $scope.loading = true; - var url = getRestUrl('user/invoices'); - if ($scope.organization) { - url = getRestUrl('organization', $scope.organization.name, 'invoices'); - } - - var getInvoices = Restangular.one(url); - getInvoices.get().then(function(resp) { + ApiService.listInvoices($scope.organization).then(function(resp) { $scope.invoices = resp.invoices; $scope.loading = false; }); @@ -1036,7 +1104,7 @@ quayApp.directive('logsView', function () { 'repository': '=repository', 'performer': '=performer' }, - controller: function($scope, $element, $sce, Restangular) { + controller: function($scope, $element, $sce, Restangular, ApiService) { $scope.loading = true; $scope.logs = null; $scope.kindsAllowed = null; @@ -1152,6 +1220,8 @@ quayApp.directive('logsView', function () { $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'); @@ -1255,7 +1325,7 @@ quayApp.directive('robotsManager', function () { 'organization': '=organization', 'user': '=user' }, - controller: function($scope, $element, Restangular) { + controller: function($scope, $element, ApiService) { $scope.ROBOT_PATTERN = ROBOT_PATTERN; $scope.robots = null; $scope.loading = false; @@ -1280,7 +1350,7 @@ quayApp.directive('robotsManager', function () { $scope.createRobot = function(name) { if (!name) { return; } - createRobotAccount(Restangular, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name, + createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name, function(created) { $scope.robots.push(created); }); @@ -1288,11 +1358,7 @@ quayApp.directive('robotsManager', function () { $scope.deleteRobot = function(info) { var shortName = $scope.getShortenedName(info.name); - var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', shortName) : - getRestUrl('user/robots', shortName); - - var deleteRobot = Restangular.one(url); - deleteRobot.customDELETE().then(function(resp) { + 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); @@ -1318,9 +1384,7 @@ quayApp.directive('robotsManager', function () { if ($scope.loading) { return; } $scope.loading = true; - var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots') : 'user/robots'; - var getRobots = Restangular.one(url); - getRobots.customGET($scope.obj).then(function(resp) { + ApiService.getRobots($scope.organization).then(function(resp) { $scope.robots = resp.robots; $scope.loading = false; }); @@ -1553,7 +1617,7 @@ quayApp.directive('headerBar', function () { restrict: 'C', scope: { }, - controller: function($scope, $element, $location, UserService, PlanService, Restangular) { + controller: function($scope, $element, $location, UserService, PlanService, ApiService) { $scope.overPlan = false; var checkOverPlan = function() { @@ -1562,8 +1626,7 @@ quayApp.directive('headerBar', function () { return; } - var checkPrivate = Restangular.one('user/private'); - checkPrivate.customGET().then(function(resp) { + ApiService.getUserPrivateCount().then(function(resp) { $scope.overPlan = resp.privateCount > resp.reposAllowed; }); }; @@ -1575,10 +1638,9 @@ quayApp.directive('headerBar', function () { PlanService.registerListener(this, checkOverPlan); $scope.signout = function() { - var signoutPost = Restangular.one('signout'); - signoutPost.customPOST().then(function() { - UserService.load(); - $location.path('/'); + ApiService.logout().then(function() { + UserService.load(); + $location.path('/'); }); }; @@ -1609,7 +1671,7 @@ quayApp.directive('entitySearch', function () { 'includeTeams': '=includeTeams', 'isOrganization': '=isOrganization' }, - controller: function($scope, $element, Restangular, UserService) { + controller: function($scope, $element, Restangular, UserService, ApiService) { $scope.lazyLoading = true; $scope.isAdmin = false; @@ -1619,16 +1681,12 @@ quayApp.directive('entitySearch', function () { $scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace); if ($scope.isOrganization && $scope.includeTeams) { - var url = getRestUrl('organization', $scope.namespace); - var getOrganization = Restangular.one(url); - getOrganization.customGET().then(function(resp) { + ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) { $scope.teams = resp.teams; }); } - var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; - var getRobots = Restangular.one(url); - getRobots.customGET().then(function(resp) { + ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) { $scope.robots = resp.robots; $scope.lazyLoading = false; }, function() { @@ -1648,7 +1706,7 @@ quayApp.directive('entitySearch', function () { return; } - createOrganizationTeam(Restangular, $scope.namespace, teamname, function(created) { + createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) { $scope.setEntity(created.name, 'team', false); $scope.teams[teamname] = created; }); @@ -1667,7 +1725,7 @@ quayApp.directive('entitySearch', function () { return; } - createRobotAccount(Restangular, $scope.isOrganization, $scope.namespace, robotname, function(created) { + createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) { $scope.setEntity(created.name, 'user', true); $scope.robots.push(created); }); @@ -1793,7 +1851,7 @@ quayApp.directive('billingOptions', function () { 'user': '=user', 'organization': '=organization' }, - controller: function($scope, $element, PlanService, Restangular) { + controller: function($scope, $element, PlanService, ApiService) { $scope.invoice_email = false; $scope.currentCard = null; @@ -1863,9 +1921,7 @@ quayApp.directive('billingOptions', function () { var save = function() { $scope.working = true; - var url = $scope.organization ? getRestUrl('organization', $scope.organization.name) : 'user/'; - var conductSave = Restangular.one(url); - conductSave.customPUT($scope.obj).then(function(resp) { + ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) { $scope.working = false; }); }; @@ -1900,7 +1956,7 @@ quayApp.directive('planManager', function () { 'readyForPlan': '&readyForPlan', 'planChanged': '&planChanged' }, - controller: function($scope, $element, PlanService, Restangular) { + controller: function($scope, $element, PlanService, ApiService) { var hasSubscription = false; $scope.isPlanVisible = function(plan, subscribedPlan) { diff --git a/static/js/bootstrap.js b/static/js/bootstrap.js new file mode 100644 index 000000000..4293c6651 --- /dev/null +++ b/static/js/bootstrap.js @@ -0,0 +1,13 @@ + $.ajax({ + type: 'GET', + async: false, + url: '/api/discovery', + success: function(data) { + window.__endpoints = data.endpoints; + }, + error: function() { + setTimeout(function() { + $('#couldnotloadModal').modal({}); + }, 250); + } + }); \ No newline at end of file diff --git a/static/js/controllers.js b/static/js/controllers.js index 2054f3e84..c7403a7cd 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -67,14 +67,15 @@ function RepoListCtrl($scope, Restangular, UserService, ApiService) { } var options = {'public': false, 'sort': true, 'namespace': namespace}; - $scope.user_repositories = ApiService.at('repository').withOptions(options).get(function(resp) { + + $scope.user_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { return resp.repositories; }); }; var loadPublicRepos = function() { var options = {'public': true, 'private': false, 'sort': true, 'limit': 10}; - $scope.public_repositories = ApiService.at('repository').withOptions(options).get(function(resp) { + $scope.public_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { return resp.repositories; }); }; @@ -118,7 +119,7 @@ function LandingCtrl($scope, UserService, ApiService) { } var options = {'limit': 4, 'public': false, 'sort': true, 'namespace': namespace }; - $scope.my_repositories = ApiService.at('repository').withOptions(options).get(function(resp) { + $scope.my_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) { return resp.repositories; }); }; @@ -169,7 +170,8 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo }; $scope.loadImageChanges = function(image) { - $scope.currentImageChangeResource = ApiService.at('repository', namespace, name, 'image', image.id, 'changes').get(function(ci) { + var params = {'repository': namespace + '/' + name, 'image_id': image.id}; + $scope.currentImageChangeResource = ApiService.getImageChangesAsResource(params).get(function(ci) { $scope.currentImageChanges = ci; }); }; @@ -240,8 +242,9 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo }; var fetchRepository = function() { + var params = {'repository': namespace + '/' + name}; $rootScope.title = 'Loading Repository...'; - $scope.repository = ApiService.at('repository', namespace, name).get(function(repo) { + $scope.repository = ApiService.getRepoAsResource(params).get(function(repo) { // Set the repository object. $scope.repo = repo; @@ -283,6 +286,7 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo }; var getBuildInfo = function(repo) { + // Note: We use restangular manually here because we need to turn off the loading bar. var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/'); buildInfo.withHttpConfig({ 'ignoreLoadingBar': true @@ -312,7 +316,8 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo }; var listImages = function() { - $scope.imageHistory = ApiService.at('repository', namespace, name, 'image').get(function(resp) { + var params = {'repository': namespace + '/' + name}; + $scope.imageHistory = ApiService.listRepositoryImagesAsResource(params).get(function(resp) { // Dispose of any existing tree. if ($scope.tree) { $scope.tree.dispose(); @@ -443,8 +448,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope 'friendlyName': $scope.newToken.friendlyName }; - var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); - permissionPost.customPOST(friendlyName).then(function(newToken) { + var params = {'repository': namespace + '/' + name}; + ApiService.createToken(friendlyName, params).then(function(newToken) { $scope.newToken.friendlyName = ''; $scope.createTokenForm.$setPristine(); $scope.tokens[newToken.code] = newToken; @@ -452,8 +457,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }; $scope.deleteToken = function(tokenCode) { - var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode); - deleteAction.customDELETE().then(function() { + var params = { + 'repository': namespace + '/' + name, + 'code': tokenCode + }; + + ApiService.deleteToken(null, params).then(function() { delete $scope.tokens[tokenCode]; }); }; @@ -463,8 +472,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope 'role': newAccess }; - var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode); - deleteAction.customPUT(role).then(function(updated) { + var params = { + 'repository': namespace + '/' + name, + 'code': tokenCode + }; + + ApiService.changeToken(role, params).then(function(updated) { $scope.tokens[updated.code] = updated; }); }; @@ -486,8 +499,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope var visibility = { 'visibility': newAccess }; - var visibilityPost = Restangular.one('repository/' + namespace + '/' + name + '/changevisibility'); - visibilityPost.customPOST(visibility).then(function() { + + var params = { + 'repository': namespace + '/' + name + }; + + ApiService.changeRepoVisibility(visibility, params).then(function() { $scope.repo.is_public = newAccess == 'public'; }, function() { $('#cannotchangeModal').modal({}); @@ -501,8 +518,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope $scope.deleteRepo = function() { $('#confirmdeleteModal').modal('hide'); - var deleteAction = Restangular.one('repository/' + namespace + '/' + name); - deleteAction.customDELETE().then(function() { + var params = { + 'repository': namespace + '/' + name + }; + + ApiService.deleteRepository(null, params).then(function() { $scope.repo = null; setTimeout(function() { @@ -514,8 +534,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }; $scope.loadWebhooks = function() { + var params = { + 'repository': namespace + '/' + name + }; + $scope.newWebhook = {}; - $scope.webhooksResource = ApiService.at('repository', namespace, name, 'webhook').get(function(resp) { + $scope.webhooksResource = ApiService.listWebhooksAsResource(params).get(function(resp) { $scope.webhooks = resp.webhooks; return $scope.webhooks; }); @@ -526,8 +550,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope return; } - var newWebhook = Restangular.one('repository/' + namespace + '/' + name + '/webhook/'); - newWebhook.customPOST($scope.newWebhook).then(function(resp) { + var params = { + 'repository': namespace + '/' + name + }; + + ApiService.createWebhook($scope.newWebhook, params).then(function(resp) { $scope.webhooks.push(resp); $scope.newWebhook.url = ''; $scope.createWebhookForm.$setPristine(); @@ -535,15 +562,22 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }; $scope.deleteWebhook = function(webhook) { - var deleteWebhookReq = Restangular.one('repository/' + namespace + '/' + name + '/webhook/' + webhook.public_id); - deleteWebhookReq.customDELETE().then(function(resp) { + var params = { + 'repository': namespace + '/' + name, + 'public_id': webhook.public_id + }; + + ApiService.deleteWebhook(null, params).then(function(resp) { $scope.webhooks.splice($scope.webhooks.indexOf(webhook), 1); }); }; var fetchTokens = function() { - var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/'); - tokensFetch.get().then(function(resp) { + var params = { + 'repository': namespace + '/' + name + }; + + ApiService.listRepoTokens(null, params).then(function(resp) { $scope.tokens = resp.tokens; }, function() { $scope.tokens = null; @@ -560,7 +594,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope }; var fetchRepository = function() { - $scope.repository = ApiService.at('repository', namespace, name).get(function(repo) { + var params = { + 'repository': namespace + '/' + name + }; + + $scope.repository = ApiService.getRepoAsResource(params).get(function(repo) { $scope.repo = repo; $rootScope.title = 'Settings - ' + namespace + '/' + name; @@ -580,7 +618,7 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope fetchRepository(); } -function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, UserService, KeyService, $routeParams) { +function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, KeyService, $routeParams) { if ($routeParams['migrate']) { $('#migrateTab').tab('show') } @@ -645,8 +683,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us 'plan': $scope.org.plan.stripeId }; - var convertAccount = Restangular.one('user/convert'); - convertAccount.customPOST(data).then(function(resp) { + ApiService.convertUserToOrganization(data).then(function(resp) { UserService.load(); $location.path('/'); }, function(resp) { @@ -663,8 +700,8 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us $('.form-change-pw').popover('hide'); $scope.updatingUser = true; $scope.changePasswordSuccess = false; - var changePasswordPost = Restangular.one('user/'); - changePasswordPost.customPUT($scope.user).then(function() { + + ApiService.changeUserDetails($scope.user).then(function() { $scope.updatingUser = false; $scope.changePasswordSuccess = true; @@ -686,7 +723,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us }; } -function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, Restangular) { +function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService) { var namespace = $routeParams.namespace; var name = $routeParams.name; var imageid = $routeParams.image; @@ -742,7 +779,12 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, R }; var fetchImage = function() { - $scope.image = ApiService.at('repository', namespace, name, 'image', imageid).get(function(image) { + var params = { + 'repository': namespace + '/' + name, + 'image_id': imageid + }; + + $scope.image = ApiService.getImageAsResource(params).get(function(image) { $scope.repo = { 'name': name, 'namespace': namespace @@ -762,8 +804,12 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, R }; var fetchChanges = function() { - var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes'); - changesFetch.get().then(function(changes) { + var params = { + 'repository': namespace + '/' + name, + 'image_id': imageid + }; + + ApiService.getImageChanges(null, params).then(function(changes) { var combinedChanges = []; var addCombinedChanges = function(c, kind) { for (var i = 0; i < c.length; ++i) { @@ -791,7 +837,7 @@ function V1Ctrl($scope, $location, UserService) { UserService.updateUserIn($scope); } -function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangular, PlanService) { +function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService) { UserService.updateUserIn($scope); $scope.repo = { @@ -822,8 +868,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula if (isUserNamespace) { // Load the user's subscription information in case they want to create a private // repository. - var checkPrivateAllowed = Restangular.one('user/private'); - checkPrivateAllowed.get().then(function(resp) { + ApiService.getUserPrivateCount().then(function(resp) { if (resp.privateCount + 1 > resp.reposAllowed) { PlanService.getMinimumPlan(resp.privateCount + 1, false, function(minimum) { $scope.planRequired = minimum; @@ -836,8 +881,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula $scope.checkingPlan = false; }); } else { - var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private'); - checkPrivateAllowed.get().then(function(resp) { + ApiService.getOrganizationPrivateAllowed(null, {'orgname': namespace}).then(function(resp) { $scope.planRequired = resp.privateAllowed ? null : {}; $scope.checkingPlan = false; }, function() { @@ -868,8 +912,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula 'description': repo.description }; - var createPost = Restangular.one('repository'); - createPost.customPOST(data).then(function(created) { + ApiService.createRepo(data).then(function(created) { $scope.creating = false; $scope.created = created; @@ -912,9 +955,12 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula 'file_id': fileId }; - var startBuildCall = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/'); - startBuildCall.customPOST(data).then(function(resp) { - $location.path('/repository/' + repo.namespace + '/' + repo.name); + var params = { + 'repository': repo.namespace + '/' + repo.name + }; + + ApiService.requestRepoBuild(data, params).then(function(resp) { + $location.path('/repository/' + params.repository); }, function() { $('#couldnotbuildModal').modal(); }); @@ -963,8 +1009,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula 'mimeType': mimeType }; - var getUploadUrl = Restangular.one('filedrop/'); - getUploadUrl.customPOST(data).then(function(resp) { + var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) { conductUpload(repo, file, resp.url, resp.file_id, mimeType); }, function() { $('#couldnotbuildModal').modal(); @@ -992,7 +1037,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula }; } -function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) { +function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) { var orgname = $routeParams.orgname; $scope.TEAM_PATTERN = TEAM_PATTERN; @@ -1008,10 +1053,14 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) var previousRole = $scope.organization.teams[teamname].role; $scope.organization.teams[teamname].role = role; - var updateTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); + var params = { + 'orgname': orgname, + 'teamname': teamname + }; + var data = $scope.organization.teams[teamname]; - updateTeam.customPUT(data).then(function(resp) { + ApiService.updateOrganizationTeam(data, params).then(function(resp) { }, function(resp) { $scope.organization.teams[teamname].role = previousRole; $scope.roleError = resp.data || ''; @@ -1032,7 +1081,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) return; } - createOrganizationTeam(Restangular, orgname, teamname, function(created) { + createOrganizationTeam(ApiService, orgname, teamname, function(created) { $scope.organization.teams[teamname] = created; }); }; @@ -1047,8 +1096,12 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) if (!$scope.currentDeleteTeam) { return; } var teamname = $scope.currentDeleteTeam; - var deleteAction = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); - deleteAction.customDELETE().then(function() { + var params = { + 'orgname': orgname, + 'teamname': teamname + }; + + ApiService.deleteOrganizationTeam(null, params).then(function() { delete $scope.organization.teams[teamname]; $scope.currentDeleteTeam = null; }, function() { @@ -1058,7 +1111,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) }; var loadOrganization = function() { - $scope.orgResource = ApiService.at('organization', orgname).get(function(org) { + $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { $scope.organization = org; $rootScope.title = orgname; $rootScope.description = 'Viewing organization ' + orgname; @@ -1109,9 +1162,12 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService $scope.loadMembers = function() { if ($scope.membersFound) { return; } $scope.membersLoading = true; - - var getMembers = Restangular.one(getRestUrl('organization', orgname, 'members')); - getMembers.get().then(function(resp) { + + var params = { + 'orgname': orgname + }; + + ApiService.getOrganizationMembers(null, params).then(function(resp) { var membersArray = []; for (var key in resp.members) { if (resp.members.hasOwnProperty(key)) { @@ -1125,7 +1181,7 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService }; var loadOrganization = function() { - $scope.orgResource = ApiService.at('organization', orgname).get(function(org) { + $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { if (org && org.is_admin) { $scope.organization = org; $rootScope.title = orgname + ' (Admin)'; @@ -1155,8 +1211,13 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) $scope.addNewMember = function(member) { if ($scope.members[member.name]) { return; } - var addMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', member.name)); - addMember.customPOST().then(function(resp) { + var params = { + 'orgname': orgname, + 'teamname': teamname, + 'membername': member.name + }; + + ApiService.updateOrganizationTeamMember(null, params).then(function(resp) { $scope.members[member.name] = resp; }, function() { $('#cannotChangeMembersModal').modal({}); @@ -1164,8 +1225,13 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) }; $scope.removeMember = function(username) { - var removeMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', username)); - removeMember.customDELETE().then(function(resp) { + var params = { + 'orgname': orgname, + 'teamname': teamname, + 'membername': username + }; + + ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) { delete $scope.members[username]; }, function() { $('#cannotChangeMembersModal').modal({}); @@ -1175,16 +1241,20 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) $scope.updateForDescription = function(content) { $scope.organization.teams[teamname].description = content; - var updateTeam = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname)); - var data = $scope.organization.teams[teamname]; - updateTeam.customPUT(data).then(function(resp) { + var params = { + 'orgname': orgname, + 'teamname': teamname + }; + + var teaminfo = $scope.organization.teams[teamname]; + ApiService.updateOrganizationTeam(teaminfo, params).then(function(resp) { }, function() { $('#cannotChangeTeamModal').modal({}); }); }; var loadOrganization = function() { - $scope.orgResource = ApiService.at('organization', orgname).get(function(org) { + $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { $scope.organization = org; $scope.team = $scope.organization.teams[teamname]; $rootScope.title = teamname + ' (' + $scope.orgname + ')'; @@ -1195,7 +1265,12 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) }; var loadMembers = function() { - $scope.membersResource = ApiService.at('organization', $scope.orgname, 'team', teamname, 'members').get(function(resp) { + var params = { + 'orgname': orgname, + 'teamname': teamname + }; + + $scope.membersResource = ApiService.getOrganizationTeamMembersAsResource(params).get(function(resp) { $scope.members = resp.members; $scope.canEditMembers = resp.can_edit; return resp.members; @@ -1208,11 +1283,10 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) function OrgsCtrl($scope, UserService) { UserService.updateUserIn($scope); - browserchrome.update(); } -function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, Restangular) { +function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService) { UserService.updateUserIn($scope); var requested = $routeParams['plan']; @@ -1252,8 +1326,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan 'email': org.email }; - var createPost = Restangular.one('organization/'); - createPost.customPOST(data).then(function(created) { + ApiService.createOrganization(data).then(function(created) { $scope.created = created; // Reset the organizations list. @@ -1300,14 +1373,19 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul $scope.ready = false; var loadOrganization = function() { - $scope.orgResource = ApiService.at('organization', orgname).get(function(org) { + $scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) { $scope.organization = org; return org; }); }; var loadMemberInfo = function() { - $scope.memberResource = ApiService.at('organization', $scope.orgname, 'members', membername).get(function(resp) { + var params = { + 'orgname': orgname, + 'membername': membername + }; + + $scope.memberResource = ApiService.getOrganizationMemberAsResource(params).get(function(resp) { $scope.memberInfo = resp.member; $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')'; diff --git a/templates/base.html b/templates/base.html index 8940e9d33..a46822933 100644 --- a/templates/base.html +++ b/templates/base.html @@ -65,6 +65,7 @@ {% endblock %} + @@ -122,6 +123,22 @@ var isProd = document.location.hostname === 'quay.io'; + + + {% if request.host == 'quay.io' %}