var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; function getRestUrl(args) { var url = ''; for (var i = 0; i < arguments.length; ++i) { if (i > 0) { url += '/'; } url += encodeURI(arguments[i]) } return url; } function clickElement(el){ // From: http://stackoverflow.com/questions/16802795/click-not-working-in-mocha-phantomjs-on-certain-elements var ev = document.createEvent("MouseEvent"); ev.initMouseEvent( "click", true /* bubble */, true /* cancelable */, window, null, 0, 0, 0, 0, /* coordinates */ false, false, false, false, /* modifier keys */ 0 /*left*/, null); el.dispatchEvent(ev); } function getFirstTextLine(commentString) { if (!commentString) { return ''; } var lines = commentString.split('\n'); var MARKDOWN_CHARS = { '#': true, '-': true, '>': true, '`': true }; for (var i = 0; i < lines.length; ++i) { // Skip code lines. if (lines[i].indexOf(' ') == 0) { continue; } // Skip empty lines. if ($.trim(lines[i]).length == 0) { continue; } // Skip control lines. if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) { continue; } return getMarkedDown(lines[i]); } return ''; } function createRobotAccount(ApiService, is_org, orgname, name, callback) { ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) { bootbox.dialog({ "message": resp.data ? resp.data : 'The robot account could not be created', "title": "Cannot create robot account", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); }); } function createOrganizationTeam(ApiService, orgname, teamname, callback) { var data = { 'name': teamname, 'role': 'member' }; var params = { 'orgname': orgname, 'teamname': teamname }; ApiService.updateOrganizationTeam(data, params).then(callback, function() { bootbox.dialog({ "message": resp.data ? resp.data : 'The team could not be created', "title": "Cannot create team", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); }); } function getMarkedDown(string) { return Markdown.getSanitizingConverter().makeHtml(string || ''); } // Start the application code itself. quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], 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 = {}; cookieService.putPermanent = function(name, value) { document.cookie = escape(name) + "=" + escape(value) + "; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/"; }; cookieService.putSession = function(name, value) { $cookies[name] = value; }; cookieService.clear = function(name) { $cookies[name] = ''; }; cookieService.get = function(name) { return $cookies[name]; }; return cookieService; }]); $provide.factory('UserService', ['ApiService', 'CookieService', function(ApiService, CookieService) { var userResponse = { verified: false, anonymous: true, username: null, email: null, askForPassword: false, organizations: [] } var userService = {} userService.hasEverLoggedIn = function() { return CookieService.get('quay.loggedin') == 'true'; }; userService.updateUserIn = function(scope, opt_callback) { scope.$watch(function () { return userService.currentUser(); }, function (currentUser) { scope.user = currentUser; if (opt_callback) { opt_callback(currentUser); } }, true); }; userService.load = function(opt_callback) { ApiService.getLoggedInUser().then(function(loadedUser) { userResponse = loadedUser; if (!userResponse.anonymous) { mixpanel.identify(userResponse.username); mixpanel.people.set({ '$email': userResponse.email, '$username': userResponse.username, 'verified': userResponse.verified }); mixpanel.people.set_once({ '$created': new Date() }) CookieService.putPermanent('quay.loggedin', 'true'); } if (opt_callback) { opt_callback(); } }); }; userService.getOrganization = function(name) { if (!userResponse || !userResponse.organizations) { return null; } for (var i = 0; i < userResponse.organizations.length; ++i) { var org = userResponse.organizations[i]; if (org.name == name) { return org; } } return null; }; userService.isNamespaceAdmin = function(namespace) { if (namespace == userResponse.username) { return true; } var org = userService.getOrganization(namespace); if (!org) { return false; } return org.is_org_admin; }; userService.currentUser = function() { return userResponse; }; // Load the user the first time. userService.load(); return userService; }]); $provide.factory('KeyService', ['$location', function($location) { var keyService = {} if ($location.host() === 'quay.io') { keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu'; keyService['githubClientId'] = '5a8c08b06c48d89d4d1e'; } else { keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh'; keyService['githubClientId'] = 'cfbc4aca88e5c1b40679'; } return keyService; }]); $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', function(KeyService, UserService, CookieService, ApiService) { var plans = null; var planDict = {}; var planService = {}; var listeners = []; planService.getFreePlan = function() { return 'free'; }; planService.registerListener = function(obj, callback) { listeners.push({'obj': obj, 'callback': callback}); }; planService.unregisterListener = function(obj) { for (var i = 0; i < listeners.length; ++i) { if (listeners[i].obj == obj) { listeners.splice(i, 1); break; } } }; planService.notePlan = function(planId) { CookieService.putSession('quay.notedplan', planId); }; planService.isOrgCompatible = function(plan) { return plan['stripeId'] == planService.getFreePlan() || plan['bus_features']; }; planService.getMatchingBusinessPlan = function(callback) { planService.getPlans(function() { planService.getSubscription(null, function(sub) { var plan = planDict[sub.plan]; if (!plan) { planService.getMinimumPlan(0, true, callback); return; } var count = Math.max(sub.usedPrivateRepos, plan.privateRepos); planService.getMinimumPlan(count, true, callback); }, function() { planService.getMinimumPlan(0, true, callback); }); }); }; planService.handleNotedPlan = function() { var planId = planService.getAndResetNotedPlan(); if (!planId) { return; } UserService.load(function() { if (UserService.currentUser().anonymous) { return; } planService.getPlan(planId, function(plan) { if (planService.isOrgCompatible(plan)) { document.location = '/organizations/new/?plan=' + planId; } else { document.location = '/user?plan=' + planId; } }); }); }; planService.getAndResetNotedPlan = function() { var planId = CookieService.get('quay.notedplan'); CookieService.clear('quay.notedplan'); return planId; }; planService.handleCardError = function(resp) { if (!planService.isCardError(resp)) { return; } bootbox.dialog({ "message": resp.data.carderror, "title": "Credit card issue", "buttons": { "close": { "label": "Close", "className": "btn-primary" } } }); }; planService.isCardError = function(resp) { return resp && resp.data && resp.data.carderror; }; planService.verifyLoaded = function(callback) { if (plans) { callback(plans); return; } ApiService.listPlans().then(function(data) { var i = 0; for(i = 0; i < data.plans.length; i++) { planDict[data.plans[i].stripeId] = data.plans[i]; } plans = data.plans; callback(plans); }, function() { callback([]); }); }; planService.getPlans = function(callback, opt_includePersonal) { planService.verifyLoaded(function() { var filtered = []; for (var i = 0; i < plans.length; ++i) { var plan = plans[i]; if (plan['deprecated']) { continue; } if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; } filtered.push(plan); } callback(filtered); }); }; planService.getPlan = function(planId, callback) { planService.getPlanIncludingDeprecated(planId, function(plan) { if (!plan['deprecated']) { callback(plan); } }); }; planService.getPlanIncludingDeprecated = function(planId, callback) { planService.verifyLoaded(function() { if (planDict[planId]) { callback(planDict[planId]); } }); }; planService.getMinimumPlan = function(privateCount, isBusiness, callback) { planService.getPlans(function(plans) { for (var i = 0; i < plans.length; i++) { var plan = plans[i]; if (isBusiness && !planService.isOrgCompatible(plan)) { continue; } if (plan.privateRepos >= privateCount) { callback(plan); return; } } callback(null); }); }; planService.getSubscription = function(orgname, success, failure) { ApiService.getSubscription(orgname).then(success, failure); }; planService.setSubscription = function(orgname, planId, success, failure, opt_token) { var subscriptionDetails = { plan: planId }; if (opt_token) { subscriptionDetails['token'] = opt_token.id; } ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) { success(resp); planService.getPlan(planId, function(plan) { for (var i = 0; i < listeners.length; ++i) { listeners[i]['callback'](plan); } }); }, failure); }; planService.getCardInfo = function(orgname, callback) { ApiService.getCard(orgname).then(function(resp) { callback(resp.card); }, function() { callback({'is_valid': false}); }); }; planService.changePlan = function($scope, orgname, planId, callbacks) { if (callbacks['started']) { callbacks['started'](); } planService.getPlan(planId, function(plan) { if (orgname && !planService.isOrgCompatible(plan)) { return; } planService.getCardInfo(orgname, function(cardInfo) { if (plan.price > 0 && !cardInfo.last4) { planService.showSubscribeDialog($scope, orgname, planId, callbacks); return; } planService.setSubscription(orgname, planId, callbacks['success'], function(resp) { planService.handleCardError(resp); callbacks['failure'](resp); }); }); }); }; planService.changeCreditCard = function($scope, orgname, callbacks) { if (callbacks['opening']) { callbacks['opening'](); } var submitted = false; var submitToken = function(token) { if (submitted) { return; } submitted = true; $scope.$apply(function() { if (callbacks['started']) { callbacks['started'](); } var cardInfo = { 'token': token.id }; ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) { planService.handleCardError(resp); callbacks['failure'](resp); }); }); }; var email = planService.getEmail(orgname); StripeCheckout.open({ key: KeyService.stripePublishableKey, address: false, email: email, currency: 'usd', name: 'Update credit card', description: 'Enter your credit card number', panelLabel: 'Update', token: submitToken, image: 'static/img/quay-icon-stripe.png', opened: function() { $scope.$apply(function() { callbacks['opened']() }); }, closed: function() { $scope.$apply(function() { callbacks['closed']() }); } }); }; planService.getEmail = function(orgname) { var email = null; if (UserService.currentUser()) { email = UserService.currentUser().email; if (orgname) { org = UserService.getOrganization(orgname); if (org) { emaiil = org.email; } } } return email; }; planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) { if (callbacks['opening']) { callbacks['opening'](); } var submitted = false; var submitToken = function(token) { if (submitted) { return; } submitted = true; mixpanel.track('plan_subscribe'); $scope.$apply(function() { if (callbacks['started']) { callbacks['started'](); } planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token); }); }; planService.getPlan(planId, function(planDetails) { var email = planService.getEmail(orgname); StripeCheckout.open({ key: KeyService.stripePublishableKey, address: false, email: email, amount: planDetails.price, currency: 'usd', name: 'Quay ' + planDetails.title + ' Subscription', description: 'Up to ' + planDetails.privateRepos + ' private repositories', panelLabel: 'Subscribe', token: submitToken, image: 'static/img/quay-icon-stripe.png', opened: function() { $scope.$apply(function() { callbacks['opened']() }); }, closed: function() { $scope.$apply(function() { callbacks['closed']() }); } }); }); }; return planService; }]); }). directive('match', function($parse) { return { require: 'ngModel', link: function(scope, elem, attrs, ctrl) { scope.$watch(function() { return $parse(attrs.match)(scope) === ctrl.$modelValue; }, function(currentValue) { ctrl.$setValidity('mismatch', currentValue); }); } }; }). directive('onresize', function ($window, $parse) { return function (scope, element, attr) { var fn = $parse(attr.onresize); var notifyResized = function() { scope.$apply(function () { fn(scope); }); }; angular.element($window).on('resize', null, notifyResized); scope.$on('$destroy', function() { angular.element($window).off('resize', null, notifyResized); }); }; }). config(['$routeProvider', '$locationProvider', '$analyticsProvider', function($routeProvider, $locationProvider, $analyticsProvider) { $analyticsProvider.virtualPageviews(true); $locationProvider.html5Mode(true); // WARNING WARNING WARNING // If you add a route here, you must add a corresponding route in thr endpoints/web.py // index rule to make sure that deep links directly deep into the app continue to work. // WARNING WARNING WARNING $routeProvider. when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, fixFooter: false, reloadOnSearch: false}). when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, fixFooter: false}). when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl, reloadOnSearch: false}). when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl, reloadOnSearch: false}). when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html', reloadOnSearch: false, controller: UserAdminCtrl}). when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}). when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data', templateUrl: '/static/partials/security.html', controller: SecurityCtrl}). when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}). when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}). when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}). when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on Quay.io', templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}). when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}). when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl, reloadOnSearch: false}). when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}). when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}). when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}). when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}). otherwise({redirectTo: '/'}); }]). config(function(RestangularProvider) { RestangularProvider.setBaseUrl('/api/'); }); quayApp.directive('entityReference', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/entity-reference.html', replace: false, transclude: false, restrict: 'C', scope: { 'name': '=name', 'orgname': '=orgname', 'team': '=team', 'isrobot': '=isrobot' }, controller: function($scope, $element, UserService) { $scope.getIsAdmin = function(orgname) { return UserService.isNamespaceAdmin(orgname); }; $scope.getPrefix = function(name) { if (!name) { return ''; } var plus = name.indexOf('+'); return name.substr(0, plus + 1); }; $scope.getShortenedName = function(name) { if (!name) { return ''; } var plus = name.indexOf('+'); return name.substr(plus + 1); }; } }; return directiveDefinitionObject; }); quayApp.directive('markdownView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/markdown-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'content': '=content', 'firstLineOnly': '=firstLineOnly' }, controller: function($scope, $element, $sce) { $scope.getMarkedDown = function(content, firstLineOnly) { if (firstLineOnly) { content = getFirstTextLine(content); } return $sce.trustAsHtml(getMarkedDown(content)); }; } }; return directiveDefinitionObject; }); quayApp.directive('repoCircle', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/repo-circle.html', replace: false, transclude: false, restrict: 'C', scope: { 'repo': '=repo' }, controller: function($scope, $element) { } }; return directiveDefinitionObject; }); quayApp.directive('userSetup', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/user-setup.html', replace: false, transclude: true, restrict: 'C', scope: { 'redirectUrl': '=redirectUrl', 'signInStarted': '&signInStarted', 'signedIn': '&signedIn' }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $scope.sendRecovery = function() { ApiService.requestRecoveryEmail($scope.recovery).then(function() { $scope.invalidRecovery = false; $scope.errorMessage = ''; $scope.sent = true; }, function(result) { $scope.invalidRecovery = true; $scope.errorMessage = result.data; $scope.sent = false; }); }; $scope.hasSignedIn = function() { return UserService.hasEverLoggedIn(); }; } }; return directiveDefinitionObject; }); quayApp.directive('signinForm', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/signin-form.html', replace: false, transclude: true, restrict: 'C', scope: { 'redirectUrl': '=redirectUrl', 'signInStarted': '&signInStarted', 'signedIn': '&signedIn' }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $scope.showGithub = function() { $scope.markStarted(); var mixpanelDistinctIdClause = ''; if (mixpanel.get_distinct_id !== undefined) { $scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id()); } // Needed to ensure that UI work done by the started callback is finished before the location // changes. $timeout(function() { var url = 'https://github.com/login/oauth/authorize?client_id=' + encodeURIComponent(KeyService.githubClientId) + '&scope=user:email' + mixpanelDistinctIdClause; document.location = url; }, 250); }; $scope.markStarted = function() { if ($scope.signInStarted != null) { $scope.signInStarted(); } }; $scope.signin = function() { $scope.markStarted(); ApiService.signinUser($scope.user).then(function() { $scope.needsEmailVerification = false; $scope.invalidCredentials = false; if ($scope.signedIn != null) { $scope.signedIn(); } UserService.load(); // Redirect to the specified page or the landing page // Note: The timeout of 500ms is needed to ensure dialogs containing sign in // forms get removed before the location changes. $timeout(function() { if ($scope.redirectUrl == $location.path()) { return; } $location.path($scope.redirectUrl ? $scope.redirectUrl : '/'); }, 500); }, function(result) { $scope.needsEmailVerification = result.data.needsEmailVerification; $scope.invalidCredentials = result.data.invalidCredentials; }); }; } }; return directiveDefinitionObject; }); quayApp.directive('signupForm', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/signup-form.html', replace: false, transclude: true, restrict: 'C', scope: { }, controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) { $('.form-signup').popover(); angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) { var mixpanelId = loadedMixpanel.get_distinct_id(); $scope.github_state_clause = '&state=' + mixpanelId; }); $scope.githubClientId = KeyService.githubClientId; $scope.awaitingConfirmation = false; $scope.registering = false; $scope.register = function() { $('.form-signup').popover('hide'); $scope.registering = true; ApiService.createNewUser($scope.newUser).then(function() { $scope.awaitingConfirmation = true; $scope.registering = false; mixpanel.alias($scope.newUser.username); }, function(result) { $scope.registering = false; $scope.registerError = result.data.message; $timeout(function() { $('.form-signup').popover('show'); }); }); }; } }; return directiveDefinitionObject; }); quayApp.directive('plansTable', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/plans-table.html', replace: false, transclude: false, restrict: 'C', scope: { 'plans': '=plans', 'currentPlan': '=currentPlan' }, controller: function($scope, $element) { $scope.setPlan = function(plan) { $scope.currentPlan = plan; }; } }; return directiveDefinitionObject; }); quayApp.directive('dockerAuthDialog', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/docker-auth-dialog.html', replace: false, transclude: true, restrict: 'C', scope: { 'username': '=username', 'token': '=token', 'shown': '=shown', 'counter': '=counter' }, controller: function($scope, $element) { $scope.isDownloadSupported = function() { try { return !!new Blob(); } catch(e){} return false; }; $scope.downloadCfg = function() { var auth = $.base64.encode($scope.username + ":" + $scope.token); config = { "https://quay.io/v1/": { "auth": auth, "email": "" } }; var file = JSON.stringify(config, null, ' '); var blob = new Blob([file]); saveAs(blob, '.dockercfg'); }; var show = function(r) { if (!$scope.shown || !$scope.username || !$scope.token) { $('#dockerauthmodal').modal('hide'); return; } $('#copyClipboard').clipboardCopy(); $('#dockerauthmodal').modal({}); }; $scope.$watch('counter', show); $scope.$watch('shown', show); $scope.$watch('username', show); $scope.$watch('token', show); } }; return directiveDefinitionObject; }); quayApp.filter('bytes', function() { return function(bytes, precision) { if (!bytes || isNaN(parseFloat(bytes)) || !isFinite(bytes)) return 'Unknown'; if (typeof precision === 'undefined') precision = 1; var units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'], number = Math.floor(Math.log(bytes) / Math.log(1024)); return (bytes / Math.pow(1024, Math.floor(number))).toFixed(precision) + ' ' + units[number]; } }); quayApp.filter('visibleLogFilter', function () { return function (logs, allowed) { if (!allowed) { return logs; } var filtered = []; angular.forEach(logs, function (log) { if (allowed[log.kind]) { filtered.push(log); } }); return filtered; }; }); quayApp.directive('billingInvoices', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/billing-invoices.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'user': '=user', 'visible': '=visible' }, controller: function($scope, $element, $sce, ApiService) { $scope.loading = false; $scope.invoiceExpanded = {}; $scope.toggleInvoice = function(id) { $scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id]; }; var update = function() { var hasValidUser = !!$scope.user; var hasValidOrg = !!$scope.organization; var isValid = hasValidUser || hasValidOrg; if (!$scope.visible || !isValid) { return; } $scope.loading = true; ApiService.listInvoices($scope.organization).then(function(resp) { $scope.invoices = resp.invoices; $scope.loading = false; }); }; $scope.$watch('organization', update); $scope.$watch('user', update); $scope.$watch('visible', update); } }; return directiveDefinitionObject; }); quayApp.directive('logsView', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/logs-view.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'user': '=user', 'visible': '=visible', 'repository': '=repository', 'performer': '=performer' }, controller: function($scope, $element, $sce, Restangular, ApiService) { $scope.loading = true; $scope.logs = null; $scope.kindsAllowed = null; $scope.chartVisible = true; $scope.logsPath = ''; var datetime = new Date(); $scope.logStartDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate() - 7); $scope.logEndDate = new Date(datetime.getUTCFullYear(), datetime.getUTCMonth(), datetime.getUTCDate()); var logDescriptions = { 'account_change_plan': 'Change plan', 'account_change_cc': 'Update credit card', 'account_change_password': 'Change password', 'account_convert': 'Convert account to organization', 'create_robot': 'Create Robot Account: {robot}', 'delete_robot': 'Delete Robot Account: {robot}', 'create_repo': 'Create Repository: {repo}', 'push_repo': 'Push to repository: {repo}', 'pull_repo': function(metadata) { if (metadata.token) { return 'Pull repository {repo} via token {token}'; } else if (metadata.username) { return 'Pull repository {repo} by {username}'; } else { return 'Public pull of repository {repo} by {_ip}'; } }, 'delete_repo': 'Delete repository: {repo}', 'change_repo_permission': function(metadata) { if (metadata.username) { return 'Change permission for user {username} in repository {repo} to {role}'; } else if (metadata.team) { return 'Change permission for team {team} in repository {repo} to {role}'; } else if (metadata.token) { return 'Change permission for token {token} in repository {repo} to {role}'; } }, 'delete_repo_permission': function(metadata) { if (metadata.username) { return 'Remove permission for user {username} from repository {repo}'; } else if (metadata.team) { return 'Remove permission for team {team} from repository {repo}'; } else if (metadata.token) { return 'Remove permission for token {token} from repository {repo}'; } }, 'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}', 'add_repo_accesstoken': 'Create access token {token} in repository {repo}', 'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}', 'add_repo_webhook': 'Add webhook in repository {repo}', 'delete_repo_webhook': 'Delete webhook in repository {repo}', 'set_repo_description': 'Change description for repository {repo}: {description}', 'build_dockerfile': 'Build image from Dockerfile for repository {repo}', 'org_create_team': 'Create team: {team}', 'org_delete_team': 'Delete team: {team}', 'org_add_team_member': 'Add member {member} to team {team}', 'org_remove_team_member': 'Remove member {member} from team {team}', 'org_set_team_description': 'Change description of team {team}: {description}', 'org_set_team_role': 'Change permission of team {team} to {role}' }; var logKinds = { 'account_change_plan': 'Change plan', 'account_change_cc': 'Update credit card', 'account_change_password': 'Change password', 'account_convert': 'Convert account to organization', 'create_robot': 'Create Robot Account', 'delete_robot': 'Delete Robot Account', 'create_repo': 'Create Repository', 'push_repo': 'Push to repository', 'pull_repo': 'Pull repository', 'delete_repo': 'Delete repository', 'change_repo_permission': 'Change repository permission', 'delete_repo_permission': 'Remove user permission from repository', 'change_repo_visibility': 'Change repository visibility', 'add_repo_accesstoken': 'Create access token', 'delete_repo_accesstoken': 'Delete access token', 'add_repo_webhook': 'Add webhook', 'delete_repo_webhook': 'Delete webhook', 'set_repo_description': 'Change repository description', 'build_dockerfile': 'Build image from Dockerfile', 'org_create_team': 'Create team', 'org_delete_team': 'Delete team', 'org_add_team_member': 'Add team member', 'org_remove_team_member': 'Remove team member', 'org_set_team_description': 'Change team description', 'org_set_team_role': 'Change team permission' }; var getDateString = function(date) { return (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear(); }; var getOffsetDate = function(date, days) { return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days); }; var update = function() { var hasValidUser = !!$scope.user; var hasValidOrg = !!$scope.organization; var hasValidRepo = $scope.repository && $scope.repository.namespace; var isValid = hasValidUser || hasValidOrg || hasValidRepo; if (!$scope.visible || !isValid) { return; } var twoWeeksAgo = getOffsetDate($scope.logEndDate, -14); if ($scope.logStartDate > $scope.logEndDate || $scope.logStartDate < twoWeeksAgo) { $scope.logStartDate = twoWeeksAgo; } $scope.loading = true; // Note: We construct the URLs here manually because we also use it for the download // path. var url = getRestUrl('user/logs'); if ($scope.organization) { url = getRestUrl('organization', $scope.organization.name, 'logs'); } if ($scope.repository) { url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs'); } url += '?starttime=' + encodeURIComponent(getDateString($scope.logStartDate)); url += '&endtime=' + encodeURIComponent(getDateString($scope.logEndDate)); if ($scope.performer) { url += '&performer=' + encodeURIComponent($scope.performer.username); } var loadLogs = Restangular.one(url); loadLogs.customGET().then(function(resp) { $scope.logsPath = '/api/' + url; if (!$scope.chart) { $scope.chart = new LogUsageChart(logKinds); $($scope.chart).bind('filteringChanged', function(e) { $scope.$apply(function() { $scope.kindsAllowed = e.allowed; }); }); } $scope.chart.draw('bar-chart', resp.logs, $scope.logStartDate, $scope.logEndDate); $scope.kindsAllowed = null; $scope.logs = resp.logs; $scope.loading = false; }); }; $scope.toggleChart = function() { $scope.chartVisible = !$scope.chartVisible; }; $scope.isVisible = function(allowed, kind) { return allowed == null || allowed.hasOwnProperty(kind); }; $scope.getColor = function(kind) { return $scope.chart.getColor(kind); }; $scope.getDescription = function(log) { var fieldIcons = { 'username': 'user', 'team': 'group', 'token': 'key', 'repo': 'hdd', 'robot': 'wrench' }; log.metadata['_ip'] = log.ip ? log.ip : null; var description = logDescriptions[log.kind] || log.kind; if (typeof description != 'string') { description = description(log.metadata); } for (var key in log.metadata) { if (log.metadata.hasOwnProperty(key)) { var value = log.metadata[key] != null ? log.metadata[key].toString() : '(Unknown)'; var markedDown = getMarkedDown(value); markedDown = markedDown.substr('
'.length, markedDown.length - '
'.length); var icon = fieldIcons[key]; if (icon) { markedDown = '' + markedDown; } description = description.replace('{' + key + '}', '' + markedDown + '
');
}
}
return $sce.trustAsHtml(description);
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
$scope.$watch('repository', update);
$scope.$watch('visible', update);
$scope.$watch('performer', update);
$scope.$watch('logStartDate', update);
$scope.$watch('logEndDate', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('robotsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/robots-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user'
},
controller: function($scope, $element, ApiService) {
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
$scope.robots = null;
$scope.loading = false;
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
$scope.showRobot = function(info) {
$scope.shownRobot = info;
$scope.showRobotCounter++;
};
$scope.getShortenedName = function(name) {
var plus = name.indexOf('+');
return name.substr(plus + 1);
};
$scope.getPrefix = function(name) {
var plus = name.indexOf('+');
return name.substr(0, plus);
};
$scope.createRobot = function(name) {
if (!name) { return; }
createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
function(created) {
$scope.robots.push(created);
});
};
$scope.deleteRobot = function(info) {
var shortName = $scope.getShortenedName(info.name);
ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
for (var i = 0; i < $scope.robots.length; ++i) {
if ($scope.robots[i].name == info.name) {
$scope.robots.splice(i, 1);
return;
}
}
}, function() {
bootbox.dialog({
"message": 'The selected robot account could not be deleted',
"title": "Cannot delete robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
if ($scope.loading) { return; }
$scope.loading = true;
ApiService.getRobots($scope.organization).then(function(resp) {
$scope.robots = resp.robots;
$scope.loading = false;
});
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('popupInputButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/popup-input-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'placeholder': '=placeholder',
'pattern': '=pattern',
'submitted': '&submitted'
},
controller: function($scope, $element) {
$scope.popupShown = function() {
setTimeout(function() {
var box = $('#input-box');
box[0].value = '';
box.focus();
}, 10);
};
$scope.getRegexp = function(pattern) {
if (!pattern) {
pattern = '.*';
}
return new RegExp(pattern);
};
$scope.inputSubmit = function() {
var box = $('#input-box');
if (box.hasClass('ng-invalid')) { return; }
var entered = box[0].value;
if (!entered) {
return;
}
if ($scope.submitted) {
$scope.submitted({'value': entered});
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('resourceView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/resource-view.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'resource': '=resource',
'errorMessage': '=errorMessage'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('quaySpinner', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/spinner.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/organization-header.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'organization': '=organization',
'teamName': '=teamName',
'clickable': '=clickable'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('markdownInput', function () {
var counter = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/markdown-input.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'content': '=content',
'canWrite': '=canWrite',
'contentChanged': '=contentChanged',
'fieldTitle': '=fieldTitle'
},
controller: function($scope, $element) {
var elm = $element[0];
$scope.id = (counter++);
$scope.editContent = function() {
if (!$scope.canWrite) { return; }
if (!$scope.markdownDescriptionEditor) {
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
editor.run();
$scope.markdownDescriptionEditor = editor;
}
$('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
$(elm).find('.modal').modal({});
};
$scope.saveContent = function() {
$scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
$(elm).find('.modal').modal('hide');
if ($scope.contentChanged) {
$scope.contentChanged($scope.content);
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('repoSearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-search.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, Restangular) {
var searchToken = 0;
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
++searchToken;
}, true);
var element = $($element[0].childNodes[0]);
element.typeahead({
name: 'repositories',
remote: {
url: '/api/find/repository?query=%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
url += '&cb=' + searchToken;
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.repositories.length; ++i) {
var repo = data.repositories[i];
datums.push({
'value': repo.name,
'tokens': [repo.name, repo.namespace],
'repo': repo
});
}
return datums;
}
},
template: function (datum) {
template = '