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', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) { $provide.factory('UserService', ['Restangular', 'PlanService', function(Restangular, PlanService) { var userResponse = { verified: false, anonymous: true, username: null, email: null, askForPassword: false, organizations: [] } var userService = {} var currentSubscription = null; userService.load = function() { 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() }) } }); }; userService.resetCurrentSubscription = function() { currentSubscription = null; }; userService.getCurrentSubscription = function(callback, failure) { if (currentSubscription) { callback(currentSubscription); } PlanService.getSubscription(null, function(sub) { currentSubscription = sub; callback(sub); }, failure); }; 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', function(Restangular, KeyService) { var plans = null; var planDict = {}; var planService = {} 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(organization, success, failure) { var url = planService.getSubscriptionUrl(organization); 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(success, failure); }; planService.changePlan = function($scope, orgname, planId, hasExistingSubscription, started, success, failure) { if (!hasExistingSubscription) { planService.showSubscribeDialog($scope, orgname, planId, started, success, failure); return; } started(); planService.setSubscription(orgname, planId, success, failure); }; planService.showSubscribeDialog = function($scope, orgname, planId, started, success, failure) { var submitToken = function(token) { mixpanel.track('plan_subscribe'); $scope.$apply(function() { started(); planService.setSubscription(orgname, planId, success, failure); }); }; planService.getPlan(planId, function(planDetails) { StripeCheckout.open({ key: KeyService.stripePublishableKey, address: false, 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' }); }); }; 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, reloadOnSearch: false}). when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl}). 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', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}). when('/user/', {title: 'Account Settings', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}). when('/guide/', {title: 'Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}). when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}). when('/signin/', {title: 'Sign In', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}). when('/new/', {title: 'Create new repository', templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}). when('/organizations/', {title: 'Organizations', templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}). when('/organizations/new/', {title: 'New Organization', 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('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) { $scope.getMarkedDown = function(content, firstLineOnly) { if (firstLineOnly) { content = getFirstTextLine(content); } return 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: true, restrict: 'C', scope: { 'plans': '=plans', 'currentPlan': '=currentPlan' }, controller: function($scope, $element) { $scope.setPlan = function(plan) { $scope.currentPlan = plan; }; } }; 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('entitySearch', function () { var number = 0; var directiveDefinitionObject = { priority: 0, templateUrl: '/static/directives/entity-search.html', replace: false, transclude: false, restrict: 'C', scope: { 'organization': '=organization', 'inputTitle': '=inputTitle', 'entitySelected': '=entitySelected' }, controller: function($scope, $element) { if (!$scope.entitySelected) { return; } number++; var input = $element[0].firstChild; $scope.organization = $scope.organization || ''; $(input).typeahead({ name: 'entities' + number, remote: { url: '/api/entities/%QUERY', replace: function (url, uriEncodedQuery) { url = url.replace('%QUERY', uriEncodedQuery); if ($scope.organization) { url += '?organization=' + encodeURIComponent($scope.organization); } 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 = '