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 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 || ''); } // Start the application code itself. quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) { $provide.factory('UserService', ['Restangular', function(Restangular) { var userResponse = { verified: false, anonymous: true, username: null, email: null, askForPassword: false, organizations: [] } var userService = {} userService.load = function(opt_callback) { var userFetch = Restangular.one('user/'); userFetch.get().then(function(loadedUser) { userResponse = loadedUser; if (!userResponse.anonymous) { mixpanel.identify(userResponse.username); mixpanel.people.set({ '$email': userResponse.email, '$username': userResponse.username, 'verified': userResponse.verified }); mixpanel.people.set_once({ '$created': new Date() }) } if (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.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', ['Restangular', 'KeyService', 'UserService', function(Restangular, KeyService, UserService) { var plans = null; var planDict = {}; var planService = {}; var listeners = []; 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.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; } var getPlans = Restangular.one('plans'); getPlans.get().then(function(data) { var i = 0; for(i = 0; i < data.user.length; i++) { planDict[data.user[i].stripeId] = data.user[i]; } for(i = 0; i < data.business.length; i++) { planDict[data.business[i].stripeId] = data.business[i]; } plans = data; callback(plans); }, function() { callback([]); }); }; 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.getPlans = function(callback) { planService.verifyLoaded(callback); }; planService.getPlan = function(planId, callback) { planService.verifyLoaded(function() { if (planDict[planId]) { callback(planDict[planId]); } }); }; planService.getMinimumPlan = function(privateCount, isBusiness, callback) { planService.verifyLoaded(function() { var planSource = plans.user; if (isBusiness) { planSource = plans.business; } for (var i = 0; i < planSource.length; i++) { var plan = planSource[i]; if (plan.privateRepos >= privateCount) { callback(plan); return; } } callback(null); }); }; 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'; }; planService.setSubscription = function(orgname, planId, success, failure, opt_token) { var subscriptionDetails = { plan: planId }; if (opt_token) { subscriptionDetails['token'] = opt_token.id; } var url = planService.getSubscriptionUrl(orgname); var createSubscriptionRequest = Restangular.one(url); createSubscriptionRequest.customPUT(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) { var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card'; var getCard = Restangular.one(url); getCard.customGET().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) { 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 }; 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); }); }); }; 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: true, reloadOnSearch: false}). when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl, fixFooter: true}). when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}). when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}). 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', 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}). when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}). 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) { $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('signinForm', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/signin-form.html', replace: false, transclude: true, restrict: 'C', scope: { 'redirectUrl': '=redirectUrl' }, controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) { $scope.githubClientId = KeyService.githubClientId; var appendMixpanelId = function() { if (mixpanel.get_distinct_id !== undefined) { $scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id(); } else { // Mixpanel not yet loaded, try again later $timeout(appendMixpanelId, 200); } }; appendMixpanelId(); $scope.signin = function() { var signinPost = Restangular.one('signin'); signinPost.customPOST($scope.user).then(function() { $scope.needsEmailVerification = false; $scope.invalidCredentials = false; // Redirect to the specified page or the landing page UserService.load(); $location.path($scope.redirectUrl ? $scope.redirectUrl : '/'); }, function(result) { $scope.needsEmailVerification = result.data.needsEmailVerification; $scope.invalidCredentials = result.data.invalidCredentials; }); }; } }; 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, Restangular) { $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('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('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' }, controller: function($scope, $element, $sce, Restangular) { $scope.loading = true; $scope.logs = null; $scope.kindsAllowed = null; $scope.chartVisible = true; 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': 'Change permission for user {username} in repository {repo} to {role}', 'delete_repo_permission': 'Remove permission for user {username} 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 update = function() { if (!$scope.visible || (!$scope.organization && !$scope.user)) { return; } $scope.loading = true; var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'logs') : getRestUrl('user/logs'); var loadLogs = Restangular.one(url); loadLogs.customGET().then(function(resp) { 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.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 description = logDescriptions[log.kind] || logTitles[log.kind] || log.kind; log.metadata['_ip'] = log.ip; for (var key in log.metadata) { if (log.metadata.hasOwnProperty(key)) { var markedDown = getMarkedDown(log.metadata[key].toString()); markedDown = markedDown.substr('

'.length, markedDown.length - '

'.length); description = description.replace('{' + key + '}', '' + markedDown + ''); } } return $sce.trustAsHtml(description); }; $scope.$watch('organization', update); $scope.$watch('user', update); $scope.$watch('visible', 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, Restangular) { $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; } var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', name) : getRestUrl('user/robots', name); var createRobot = Restangular.one(url); createRobot.customPUT().then(function(resp) { $scope.robots.push(resp); }, 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" } } }); }); }; $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) { 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; var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots') : 'user/robots'; var getRobots = Restangular.one(url); getRobots.customGET($scope.obj).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('organizationHeader', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/organization-header.html', replace: false, transclude: true, restrict: 'C', scope: { 'organization': '=organization', 'teamName': '=teamName' }, 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 = '
'; template += '' template += '' + datum.repo.namespace +'/' + datum.repo.name + '' if (datum.repo.description) { template += '' + getFirstTextLine(datum.repo.description) + '' } template += '
' return template; } }); element.on('typeahead:selected', function (e, datum) { element.typeahead('setQuery', ''); document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name; }); } }; return directiveDefinitionObject; }); quayApp.directive('headerBar', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/header-bar.html', replace: false, transclude: false, restrict: 'C', scope: { }, controller: function($scope, $element, $location, UserService, Restangular) { $scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) { $scope.user = currentUser; }, true); $scope.signout = function() { var signoutPost = Restangular.one('signout'); signoutPost.customPOST().then(function() { UserService.load(); $location.path('/'); }); }; $scope.appLinkTarget = function() { if ($("div[ng-view]").length === 0) { return "_self"; } return ""; }; } }; return directiveDefinitionObject; }); quayApp.directive('entitySearch', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/entity-search.html', replace: false, transclude: false, restrict: 'C', scope: { 'namespace': '=namespace', 'inputTitle': '=inputTitle', 'entitySelected': '=entitySelected', 'includeTeams': '=includeTeams' }, controller: function($scope, $element) { if (!$scope.entitySelected) { return; } number++; var input = $element[0].firstChild; $scope.namespace = $scope.namespace || ''; $(input).typeahead({ name: 'entities' + number, remote: { url: '/api/entities/%QUERY', replace: function (url, uriEncodedQuery) { url = url.replace('%QUERY', uriEncodedQuery); url += '?namespace=' + encodeURIComponent($scope.namespace); if ($scope.includeTeams) { url += '&includeTeams=true' } return url; }, filter: function(data) { var datums = []; for (var i = 0; i < data.results.length; ++i) { var entity = data.results[i]; datums.push({ 'value': entity.name, 'tokens': [entity.name], 'entity': entity }); } return datums; } }, template: function (datum) { template = '
'; if (datum.entity.kind == 'user' && !datum.entity.is_robot) { template += ''; } else if (datum.entity.kind == 'user' && datum.entity.is_robot) { template += ''; } else if (datum.entity.kind == 'team') { template += ''; } template += '' + datum.value + ''; if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member && datum.kind == 'user') { template += '
This user is outside your organization
'; } template += '
'; return template; }, }); $(input).on('typeahead:selected', function(e, datum) { $(input).typeahead('setQuery', ''); $scope.entitySelected(datum.entity); }); $scope.$watch('inputTitle', function(title) { input.setAttribute('placeholder', title); }); } }; return directiveDefinitionObject; }); quayApp.directive('roleGroup', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/role-group.html', replace: false, transclude: false, restrict: 'C', scope: { 'roles': '=roles', 'currentRole': '=currentRole', 'roleChanged': '&roleChanged' }, controller: function($scope, $element) { $scope.setRole = function(role) { if ($scope.currentRole == role) { return; } if ($scope.roleChanged) { $scope.roleChanged({'role': role}); } else { $scope.currentRole = role; } }; } }; return directiveDefinitionObject; }); quayApp.directive('billingOptions', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/billing-options.html', replace: false, transclude: false, restrict: 'C', scope: { 'user': '=user', 'organization': '=organization' }, controller: function($scope, $element, PlanService, Restangular) { $scope.invoice_email = false; $scope.currentCard = null; // Listen to plan changes. PlanService.registerListener(this, function(plan) { if (plan && plan.price > 0) { update(); } }); $scope.$on('$destroy', function() { PlanService.unregisterListener(this); }); $scope.changeCard = function() { var previousCard = $scope.currentCard; $scope.changingCard = true; var callbacks = { 'opened': function() { $scope.changingCard = true; }, 'closed': function() { $scope.changingCard = false; }, 'started': function() { $scope.currentCard = null; }, 'success': function(resp) { $scope.currentCard = resp.card; $scope.changingCard = false; }, 'failure': function(resp) { $scope.changingCard = false; $scope.currentCard = previousCard; if (!PlanService.isCardError(resp)) { $('#cannotchangecardModal').modal({}); } } }; PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks); }; $scope.getCreditImage = function(creditInfo) { if (!creditInfo || !creditInfo.type) { return 'credit.png'; } var kind = creditInfo.type.toLowerCase() || 'credit'; var supported = { 'american express': 'amex', 'credit': 'credit', 'diners club': 'diners', 'discover': 'discover', 'jcb': 'jcb', 'mastercard': 'mastercard', 'visa': 'visa' }; kind = supported[kind] || 'credit'; return kind + '.png'; }; var update = function() { if (!$scope.user && !$scope.organization) { return; } $scope.obj = $scope.user ? $scope.user : $scope.organization; $scope.invoice_email = $scope.obj.invoice_email; // Load the credit card information. PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) { $scope.currentCard = card; }); }; var save = function() { $scope.working = true; var url = $scope.organization ? getRestUrl('organization', $scope.organization.name) : 'user/'; var conductSave = Restangular.one(url); conductSave.customPUT($scope.obj).then(function(resp) { $scope.working = false; }); }; var checkSave = function() { if (!$scope.obj) { return; } if ($scope.obj.invoice_email != $scope.invoice_email) { $scope.obj.invoice_email = $scope.invoice_email; save(); } }; $scope.$watch('invoice_email', checkSave); $scope.$watch('organization', update); $scope.$watch('user', update); } }; return directiveDefinitionObject; }); quayApp.directive('planManager', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/plan-manager.html', replace: false, transclude: false, restrict: 'C', scope: { 'user': '=user', 'organization': '=organization', 'readyForPlan': '&readyForPlan', 'planChanged': '&planChanged' }, controller: function($scope, $element, PlanService, Restangular) { var hasSubscription = false; $scope.getActiveSubClass = function() { return 'active'; }; $scope.changeSubscription = function(planId) { if ($scope.planChanging) { return; } var callbacks = { 'opening': function() { $scope.planChanging = true; }, 'started': function() { $scope.planChanging = true; }, 'opened': function() { $scope.planChanging = true; }, 'closed': function() { $scope.planChanging = false; }, 'success': subscribedToPlan, 'failure': function(resp) { $scope.planChanging = false; } }; PlanService.changePlan($scope, $scope.organization, planId, callbacks); }; $scope.cancelSubscription = function() { $scope.changeSubscription(getFreePlan()); }; var subscribedToPlan = function(sub) { $scope.subscription = sub; if (sub.plan != getFreePlan()) { hasSubscription = true; } PlanService.getPlan(sub.plan, function(subscribedPlan) { $scope.subscribedPlan = subscribedPlan; $scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos; if ($scope.planChanged) { $scope.planChanged({ 'plan': subscribedPlan }); } if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) { $scope.limit = 'over'; } else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) { $scope.limit = 'at'; } else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) { $scope.limit = 'near'; } else { $scope.limit = 'none'; } if (!$scope.chart) { $scope.chart = new RepositoryUsageChart(); $scope.chart.draw('repository-usage-chart'); } $scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0); $scope.planChanging = false; $scope.planLoading = false; }); }; var getFreePlan = function() { for (var i = 0; i < $scope.plans.length; ++i) { if ($scope.plans[i].price == 0) { return $scope.plans[i].stripeId; } } return 'free'; }; var update = function() { $scope.planLoading = true; if (!$scope.plans) { return; } PlanService.getSubscription($scope.organization, subscribedToPlan, function() { // User/Organization has no subscription. subscribedToPlan({ 'plan': getFreePlan() }); }); }; var loadPlans = function() { if ($scope.plans || $scope.loadingPlans) { return; } if (!$scope.user && !$scope.organization) { return; } $scope.loadingPlans = true; PlanService.getPlans(function(plans) { $scope.plans = plans[$scope.organization ? 'business' : 'user']; update(); if ($scope.readyForPlan) { var planRequested = $scope.readyForPlan(); if (planRequested && planRequested != getFreePlan()) { $scope.changeSubscription(planRequested); } } }); }; // Start the initial download. $scope.planLoading = true; loadPlans(); $scope.$watch('organization', loadPlans); $scope.$watch('user', loadPlans); } }; return directiveDefinitionObject; }); quayApp.directive('namespaceSelector', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/namespace-selector.html', replace: false, transclude: false, restrict: 'C', scope: { 'user': '=user', 'namespace': '=namespace', 'requireCreate': '=requireCreate' }, controller: function($scope, $element, $routeParams, $cookieStore) { $scope.namespaces = {}; $scope.initialize = function(user) { var namespaces = {}; namespaces[user.username] = user; if (user.organizations) { for (var i = 0; i < user.organizations.length; ++i) { namespaces[user.organizations[i].name] = user.organizations[i]; } } var initialNamespace = $routeParams['namespace'] || $cookieStore.get('quay.currentnamespace') || $scope.user.username; $scope.namespaces = namespaces; $scope.setNamespace($scope.namespaces[initialNamespace]); }; $scope.setNamespace = function(namespaceObj) { if (!namespaceObj) { namespaceObj = $scope.namespaces[$scope.user.username]; } if ($scope.requireCreate && !namespaceObj.can_create_repo) { namespaceObj = $scope.namespaces[$scope.user.username]; } var newNamespace = namespaceObj.name || namespaceObj.username; $scope.namespaceObj = namespaceObj; $scope.namespace = newNamespace; $cookieStore.put('quay.currentnamespace', newNamespace); }; $scope.$watch('user', function(user) { $scope.user = user; $scope.initialize(user); }); } }; return directiveDefinitionObject; }); quayApp.directive('buildStatus', function () { var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/build-status.html', replace: false, transclude: false, restrict: 'C', scope: { 'build': '=build' }, controller: function($scope, $element) { $scope.getBuildProgress = function(buildInfo) { switch (buildInfo.status) { case 'building': return (buildInfo.current_command / buildInfo.total_commands) * 100; break; case 'pushing': var imagePercentDecimal = (buildInfo.image_completion_percent / 100); return ((buildInfo.current_image + imagePercentDecimal) / buildInfo.total_images) * 100; break; case 'complete': return 100; break; case 'initializing': case 'starting': case 'waiting': return 0; break; } return -1; }; $scope.getBuildMessage = function(buildInfo) { switch (buildInfo.status) { case 'initializing': return 'Starting Dockerfile build'; break; case 'starting': case 'waiting': case 'building': return 'Building image from Dockerfile'; break; case 'pushing': return 'Pushing image built from Dockerfile'; break; case 'complete': return 'Dockerfile build completed and pushed'; break; case 'error': return 'Dockerfile build failed: ' + buildInfo.message; break; } }; } }; return directiveDefinitionObject; }); // Note: ngBlur is not yet in Angular stable, so we add it manaully here. quayApp.directive('ngBlur', function() { return function( scope, elem, attrs ) { elem.bind('blur', function() { scope.$apply(attrs.ngBlur); }); }; }); quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', '$http', function($location, $rootScope, Restangular, UserService, $http) { Restangular.setErrorInterceptor(function(response) { if (response.status == 401) { $('#sessionexpiredModal').modal({}); return false; } return true; }); $rootScope.$on('$routeChangeSuccess', function (event, current, previous) { if (current.$$route.title) { $rootScope.title = current.$$route.title; } if (current.$$route.description) { $rootScope.description = current.$$route.description; } else { $rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.'; } $rootScope.fixFooter = !!current.$$route.fixFooter; }); var initallyChecked = false; window.__isLoading = function() { if (!initallyChecked) { initallyChecked = true; return true; } return $http.pendingRequests.length > 0; }; }]);