Merge remote-tracking branch 'origin/master' into tagyourit
Conflicts: static/css/quay.css static/js/graphing.js static/partials/view-repo.html test/data/test.db
This commit is contained in:
		
						commit
						3f42d15335
					
				
					 132 changed files with 4266 additions and 1924 deletions
				
			
		
							
								
								
									
										631
									
								
								static/js/app.js
									
										
									
									
									
								
							
							
						
						
									
										631
									
								
								static/js/app.js
									
										
									
									
									
								
							|  | @ -102,7 +102,17 @@ function getMarkedDown(string) { | |||
|   return Markdown.getSanitizingConverter().makeHtml(string || ''); | ||||
| } | ||||
| 
 | ||||
| quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', 'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'ngAnimate'], function($provide, cfpLoadingBarProvider) { | ||||
| 
 | ||||
| quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'angular-tour', 'restangular', 'angularMoment', | ||||
|                     'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', | ||||
|                     'ngAnimate']; | ||||
| 
 | ||||
| if (window.__config && window.__config.MIXPANEL_KEY) { | ||||
|   quayDependencies.push('angulartics'); | ||||
|   quayDependencies.push('angulartics.mixpanel'); | ||||
| } | ||||
| 
 | ||||
| quayApp = angular.module('quay', quayDependencies, function($provide, cfpLoadingBarProvider) { | ||||
|     cfpLoadingBarProvider.includeSpinner = false; | ||||
| 
 | ||||
|     /** | ||||
|  | @ -325,6 +335,42 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|     }]); | ||||
| 
 | ||||
| 
 | ||||
|     $provide.factory('UIService', [function() { | ||||
|       var uiService = {}; | ||||
|        | ||||
|       uiService.hidePopover = function(elem) { | ||||
|         var popover = $('#signupButton').data('bs.popover'); | ||||
|         if (popover) { | ||||
|           popover.hide(); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       uiService.showPopover = function(elem, content) { | ||||
|         var popover = $(elem).data('bs.popover'); | ||||
|         if (!popover) { | ||||
|           $(elem).popover({'content': '-', 'placement': 'left'}); | ||||
|         } | ||||
| 
 | ||||
|         setTimeout(function() { | ||||
|           var popover = $(elem).data('bs.popover'); | ||||
|           popover.options.content = content; | ||||
|           popover.show(); | ||||
|         }, 500); | ||||
|       }; | ||||
|        | ||||
|       uiService.showFormError = function(elem, result) { | ||||
|         var message =  result.data['message'] || result.data['error_description'] || ''; | ||||
|         if (message) { | ||||
|           uiService.showPopover(elem, message); | ||||
|         } else { | ||||
|           uiService.hidePopover(elem); | ||||
|         } | ||||
|       }; | ||||
|        | ||||
|       return uiService; | ||||
|     }]); | ||||
| 
 | ||||
| 
 | ||||
|     $provide.factory('UtilService', ['$sanitize', function($sanitize) { | ||||
|       var utilService = {}; | ||||
|        | ||||
|  | @ -439,6 +485,63 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       return metadataService; | ||||
|     }]); | ||||
| 
 | ||||
|     $provide.factory('Features', [function() { | ||||
|       if (!window.__features) { | ||||
|         return {}; | ||||
|       } | ||||
| 
 | ||||
|       var features = window.__features; | ||||
|       features.getFeature = function(name, opt_defaultValue) { | ||||
|         var value = features[name]; | ||||
|         if (value == null) { | ||||
|           return opt_defaultValue; | ||||
|         } | ||||
|         return value; | ||||
|       }; | ||||
| 
 | ||||
|       features.hasFeature = function(name) { | ||||
|         return !!features.getFeature(name); | ||||
|       }; | ||||
|        | ||||
|       features.matchesFeatures = function(list) { | ||||
|         for (var i = 0; i < list.length; ++i) { | ||||
|           var value = features.getFeature(list[i]); | ||||
|           if (!value) { | ||||
|             return false; | ||||
|           } | ||||
|         } | ||||
|         return true; | ||||
|       }; | ||||
| 
 | ||||
|       return features; | ||||
|     }]); | ||||
| 
 | ||||
|     $provide.factory('Config', [function() { | ||||
|       if (!window.__config) { | ||||
|         return {}; | ||||
|       } | ||||
| 
 | ||||
|       var config = window.__config; | ||||
|       config.getDomain = function() { | ||||
|         return config['SERVER_HOSTNAME']; | ||||
|       }; | ||||
| 
 | ||||
|       config.getUrl = function(opt_path) { | ||||
|         var path = opt_path || ''; | ||||
|         return config['PREFERRED_URL_SCHEME'] + '://' + config['SERVER_HOSTNAME'] + path; | ||||
|       }; | ||||
| 
 | ||||
|       config.getValue = function(name, opt_defaultValue) { | ||||
|         var value = config[name]; | ||||
|         if (value == null) { | ||||
|           return opt_defaultValue; | ||||
|         } | ||||
|         return value; | ||||
|       }; | ||||
| 
 | ||||
|       return config; | ||||
|     }]); | ||||
| 
 | ||||
|     $provide.factory('ApiService', ['Restangular', function(Restangular) { | ||||
|       var apiService = {}; | ||||
| 
 | ||||
|  | @ -622,8 +725,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       return cookieService; | ||||
|     }]); | ||||
| 
 | ||||
|     $provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', | ||||
|       function(ApiService, CookieService, $rootScope) { | ||||
|     $provide.factory('UserService', ['ApiService', 'CookieService', '$rootScope', 'Config', | ||||
|                                      function(ApiService, CookieService, $rootScope, Config) { | ||||
|       var userResponse = { | ||||
|         verified: false, | ||||
|         anonymous: true, | ||||
|  | @ -653,15 +756,17 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|           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 (Config.MIXPANEL_KEY) { | ||||
|               mixpanel.identify(userResponse.username); | ||||
|               mixpanel.people.set({ | ||||
|                 '$email': userResponse.email, | ||||
|                 '$username': userResponse.username, | ||||
|                 'verified': userResponse.verified | ||||
|               }); | ||||
|               mixpanel.people.set_once({ | ||||
|                 '$created': new Date() | ||||
|               }) | ||||
|             } | ||||
| 
 | ||||
|             if (window.olark !== undefined) { | ||||
|               olark('api.visitor.getDetails', function(details) { | ||||
|  | @ -735,8 +840,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       return userService; | ||||
|     }]); | ||||
| 
 | ||||
|   $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', | ||||
|     function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService) { | ||||
|   $provide.factory('NotificationService', ['$rootScope', '$interval', 'UserService', 'ApiService', 'StringBuilderService', 'PlanService', 'UserService', 'Config', | ||||
|     function($rootScope, $interval, UserService, ApiService, StringBuilderService, PlanService, UserService, Config) { | ||||
|       var notificationService = { | ||||
|         'user': null, | ||||
|         'notifications': [], | ||||
|  | @ -830,28 +935,18 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       return notificationService; | ||||
|     }]); | ||||
| 
 | ||||
|     $provide.factory('KeyService', ['$location', function($location) { | ||||
|     $provide.factory('KeyService', ['$location', 'Config', function($location, Config) { | ||||
|       var keyService = {} | ||||
| 
 | ||||
|       if ($location.host() === 'quay.io') { | ||||
|         keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu'; | ||||
|         keyService['githubClientId'] = '5a8c08b06c48d89d4d1e'; | ||||
|         keyService['githubRedirectUri'] = 'https://quay.io/oauth2/github/callback'; | ||||
|       } else if($location.host() === 'staging.quay.io') { | ||||
|         keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu'; | ||||
|         keyService['githubClientId'] = '4886304accbc444f0471'; | ||||
|         keyService['githubRedirectUri'] = 'https://staging.quay.io/oauth2/github/callback'; | ||||
|       } else {         | ||||
|         keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh'; | ||||
|         keyService['githubClientId'] = 'cfbc4aca88e5c1b40679'; | ||||
|         keyService['githubRedirectUri'] = 'http://localhost:5000/oauth2/github/callback'; | ||||
|       } | ||||
| 
 | ||||
|       keyService['stripePublishableKey'] = Config['STRIPE_PUBLISHABLE_KEY']; | ||||
|       keyService['githubClientId'] = Config['GITHUB_CLIENT_ID']; | ||||
|       keyService['githubLoginClientId'] = Config['GITHUB_LOGIN_CLIENT_ID']; | ||||
|       keyService['githubRedirectUri'] = Config.getUrl('/oauth2/github/callback'); | ||||
|       return keyService; | ||||
|     }]); | ||||
| 
 | ||||
|     $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', | ||||
|         function(KeyService, UserService, CookieService, ApiService) { | ||||
|    | ||||
|     $provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService', 'Features', 'Config', | ||||
|                                      function(KeyService, UserService, CookieService, ApiService, Features, Config) { | ||||
|       var plans = null; | ||||
|       var planDict = {}; | ||||
|       var planService = {}; | ||||
|  | @ -877,7 +972,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.notePlan = function(planId) { | ||||
|         CookieService.putSession('quay.notedplan', planId); | ||||
|         if (Features.BILLING) { | ||||
|           CookieService.putSession('quay.notedplan', planId); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       planService.isOrgCompatible = function(plan) { | ||||
|  | @ -903,7 +1000,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
| 
 | ||||
|       planService.handleNotedPlan = function() { | ||||
|         var planId = planService.getAndResetNotedPlan(); | ||||
|         if (!planId) { return false; } | ||||
|         if (!planId || !Features.BILLING) { return false; } | ||||
| 
 | ||||
|         UserService.load(function() { | ||||
|           if (UserService.currentUser().anonymous) { | ||||
|  | @ -948,6 +1045,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.verifyLoaded = function(callback) { | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         if (plans) { | ||||
|           callback(plans); | ||||
|           return; | ||||
|  | @ -1007,10 +1106,14 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.getSubscription = function(orgname, success, failure) { | ||||
|          ApiService.getSubscription(orgname).then(success, failure); | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         ApiService.getSubscription(orgname).then(success, failure); | ||||
|       }; | ||||
| 
 | ||||
|       planService.setSubscription = function(orgname, planId, success, failure, opt_token) { | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         var subscriptionDetails = { | ||||
|           plan: planId | ||||
|         }; | ||||
|  | @ -1030,6 +1133,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.getCardInfo = function(orgname, callback) { | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         ApiService.getCard(orgname).then(function(resp) { | ||||
|           callback(resp.card); | ||||
|         }, function() { | ||||
|  | @ -1038,6 +1143,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.changePlan = function($scope, orgname, planId, callbacks) { | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         if (callbacks['started']) { | ||||
|           callbacks['started'](); | ||||
|         } | ||||
|  | @ -1063,6 +1170,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.changeCreditCard = function($scope, orgname, callbacks) { | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         if (callbacks['opening']) { | ||||
|           callbacks['opening'](); | ||||
|         } | ||||
|  | @ -1119,6 +1228,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }; | ||||
| 
 | ||||
|       planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) { | ||||
|         if (!Features.BILLING) { return; } | ||||
| 
 | ||||
|         if (callbacks['opening']) { | ||||
|           callbacks['opening'](); | ||||
|         } | ||||
|  | @ -1128,7 +1239,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|           if (submitted) { return; } | ||||
|           submitted = true; | ||||
| 
 | ||||
|           mixpanel.track('plan_subscribe'); | ||||
|           if (Config.MIXPANEL_KEY) { | ||||
|             mixpanel.track('plan_subscribe'); | ||||
|           } | ||||
| 
 | ||||
|           $scope.$apply(function() { | ||||
|             if (callbacks['started']) { | ||||
|  | @ -1146,7 +1259,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|             email:       email, | ||||
|             amount:      planDetails.price, | ||||
|             currency:    'usd', | ||||
|             name:        'Quay ' + planDetails.title + ' Subscription', | ||||
|             name:        'Quay.io ' + planDetails.title + ' Subscription', | ||||
|             description: 'Up to ' + planDetails.privateRepos + ' private repositories', | ||||
|             panelLabel:  'Subscribe', | ||||
|             token:       submitToken, | ||||
|  | @ -1189,10 +1302,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|       }); | ||||
|     }; | ||||
|   }). | ||||
|   config(['$routeProvider', '$locationProvider', '$analyticsProvider', | ||||
|     function($routeProvider, $locationProvider, $analyticsProvider) { | ||||
| 
 | ||||
|     $analyticsProvider.virtualPageviews(true); | ||||
|   config(['$routeProvider', '$locationProvider',  | ||||
|     function($routeProvider, $locationProvider) { | ||||
| 
 | ||||
|     $locationProvider.html5Mode(true); | ||||
| 
 | ||||
|  | @ -1213,6 +1324,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|                             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('/superuser/', {title: 'Superuser Admin Panel', description:'Admin panel for Quay.io', templateUrl: '/static/partials/super-user.html', | ||||
|                            reloadOnSearch: false, controller: SuperUserAdminCtrl}). | ||||
|       when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html', | ||||
|                        controller: GuideCtrl}). | ||||
|       when('/tutorial/', {title: 'Tutorial', description:'Interactive tutorial for using Quay.io', templateUrl: '/static/partials/tutorial.html', | ||||
|  | @ -1245,6 +1358,178 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu | |||
|     RestangularProvider.setBaseUrl('/api/v1/'); | ||||
|   }); | ||||
| 
 | ||||
|   if (window.__config && window.__config.MIXPANEL_KEY) { | ||||
|     quayApp.config(['$analyticsProvider', function($analyticsProvider) { | ||||
|       $analyticsProvider.virtualPageviews(true);       | ||||
|     }]); | ||||
|   } | ||||
| 
 | ||||
| 
 | ||||
| function buildConditionalLinker($animate, name, evaluator) { | ||||
|   // Based off of a solution found here: http://stackoverflow.com/questions/20325480/angularjs-whats-the-best-practice-to-add-ngif-to-a-directive-programmatically
 | ||||
|   return function ($scope, $element, $attr, ctrl, $transclude) { | ||||
|     var block; | ||||
|     var childScope; | ||||
|     var roles; | ||||
| 
 | ||||
|     $attr.$observe(name, function (value) { | ||||
|       if (evaluator($scope.$eval(value))) { | ||||
|         if (!childScope) { | ||||
|           childScope = $scope.$new(); | ||||
|           $transclude(childScope, function (clone) { | ||||
|             block = { | ||||
|               startNode: clone[0], | ||||
|               endNode: clone[clone.length++] = document.createComment(' end ' + name + ': ' + $attr[name] + ' ') | ||||
|             }; | ||||
|             $animate.enter(clone, $element.parent(), $element); | ||||
|           }); | ||||
|         } | ||||
|       } else { | ||||
|         if (childScope) { | ||||
|           childScope.$destroy(); | ||||
|           childScope = null; | ||||
|         } | ||||
| 
 | ||||
|         if (block) { | ||||
|           $animate.leave(getBlockElements(block)); | ||||
|           block = null; | ||||
|         } | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| quayApp.directive('quayRequire', function ($animate, Features) { | ||||
|   return { | ||||
|     transclude: 'element', | ||||
|     priority: 600, | ||||
|     terminal: true, | ||||
|     restrict: 'A', | ||||
|     link: buildConditionalLinker($animate, 'quayRequire', function(value) { | ||||
|       return Features.matchesFeatures(value); | ||||
|     }) | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('quayShow', function($animate, Features, Config) { | ||||
|   return { | ||||
|     priority: 590, | ||||
|     restrict: 'A', | ||||
|     link: function($scope, $element, $attr, ctrl, $transclude) { | ||||
|       $scope.Features = Features; | ||||
|       $scope.Config = Config; | ||||
|       $scope.$watch($attr.quayShow, function(result) { | ||||
|         $animate[!!result ? 'removeClass' : 'addClass']($element, 'ng-hide'); | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('quayClasses', function(Features, Config) { | ||||
|   return { | ||||
|     priority: 580, | ||||
|     restrict: 'A', | ||||
|     link: function($scope, $element, $attr, ctrl, $transclude) { | ||||
|        | ||||
|       // Borrowed from ngClass.
 | ||||
|       function flattenClasses(classVal) { | ||||
|         if(angular.isArray(classVal)) { | ||||
|           return classVal.join(' '); | ||||
|         } else if (angular.isObject(classVal)) { | ||||
|           var classes = [], i = 0; | ||||
|           angular.forEach(classVal, function(v, k) { | ||||
|             if (v) { | ||||
|               classes.push(k); | ||||
|             } | ||||
|           }); | ||||
|           return classes.join(' '); | ||||
|         } | ||||
|          | ||||
|         return classVal; | ||||
|       } | ||||
|        | ||||
|       function removeClass(classVal) { | ||||
|         $attr.$removeClass(flattenClasses(classVal)); | ||||
|       } | ||||
| 
 | ||||
| 
 | ||||
|       function addClass(classVal) { | ||||
|         $attr.$addClass(flattenClasses(classVal)); | ||||
|       } | ||||
| 
 | ||||
|       $scope.$watch($attr.quayClasses, function(result) { | ||||
|         var scopeVals = { | ||||
|           'Features': Features, | ||||
|           'Config': Config | ||||
|         }; | ||||
| 
 | ||||
|         for (var expr in result) { | ||||
|           if (!result.hasOwnProperty(expr)) { continue; } | ||||
| 
 | ||||
|           // Evaluate the expression with the entire features list added.
 | ||||
|           var value = $scope.$eval(expr, scopeVals); | ||||
|           if (value) { | ||||
|             addClass(result[expr]); | ||||
|           } else { | ||||
|             removeClass(result[expr]); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('quayInclude', function($compile, $templateCache, $http, Features, Config) { | ||||
|   return { | ||||
|     priority: 595, | ||||
|     restrict: 'A', | ||||
|     link: function($scope, $element, $attr, ctrl) { | ||||
|       var getTemplate = function(templateName) { | ||||
|         var templateUrl = '/static/partials/' + templateName; | ||||
|         return $http.get(templateUrl, {cache: $templateCache}); | ||||
|       }; | ||||
| 
 | ||||
|       var result = $scope.$eval($attr.quayInclude); | ||||
|       if (!result) { | ||||
|         return; | ||||
|       } | ||||
| 
 | ||||
|       var scopeVals = { | ||||
|         'Features': Features, | ||||
|         'Config': Config | ||||
|       }; | ||||
| 
 | ||||
|       var templatePath = null; | ||||
|       for (var expr in result) { | ||||
|         if (!result.hasOwnProperty(expr)) { continue; } | ||||
| 
 | ||||
|         // Evaluate the expression with the entire features list added.
 | ||||
|         var value = $scope.$eval(expr, scopeVals); | ||||
|         if (value) { | ||||
|           templatePath = result[expr]; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       if (!templatePath) { | ||||
|         return; | ||||
|       } | ||||
|        | ||||
|       var promise = getTemplate(templatePath).success(function(html) { | ||||
|         $element.html(html); | ||||
|       }).then(function (response) { | ||||
|         $element.replaceWith($compile($element.html())($scope)); | ||||
|         if ($attr.onload) { | ||||
|           $scope.$eval($attr.onload); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('entityReference', function () { | ||||
|   var directiveDefinitionObject = { | ||||
|  | @ -1516,12 +1801,14 @@ quayApp.directive('signinForm', function () { | |||
|       'signInStarted': '&signInStarted', | ||||
|       'signedIn': '&signedIn' | ||||
|     }, | ||||
|     controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService) { | ||||
|     controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, CookieService, Features, Config) { | ||||
|       $scope.showGithub = function() { | ||||
|         if (!Features.GITHUB_LOGIN) { return; } | ||||
| 
 | ||||
|         $scope.markStarted(); | ||||
| 
 | ||||
|         var mixpanelDistinctIdClause = ''; | ||||
|         if (mixpanel.get_distinct_id !== undefined) { | ||||
|         if (Config.MIXPANEL_KEY && mixpanel.get_distinct_id !== undefined) { | ||||
|           $scope.mixpanelDistinctIdClause = "&state=" + encodeURIComponent(mixpanel.get_distinct_id()); | ||||
|         } | ||||
| 
 | ||||
|  | @ -1587,34 +1874,35 @@ quayApp.directive('signupForm', function () { | |||
|     scope: { | ||||
| 
 | ||||
|     }, | ||||
|     controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {       | ||||
|     controller: function($scope, $location, $timeout, ApiService, KeyService, UserService, Config, UIService) {       | ||||
|       $('.form-signup').popover(); | ||||
| 
 | ||||
|       angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) { | ||||
|         var mixpanelId = loadedMixpanel.get_distinct_id(); | ||||
|         $scope.github_state_clause = '&state=' + mixpanelId;     | ||||
|       }); | ||||
|       if (Config.MIXPANEL_KEY) { | ||||
|         angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) { | ||||
|           var mixpanelId = loadedMixpanel.get_distinct_id(); | ||||
|           $scope.github_state_clause = '&state=' + mixpanelId;     | ||||
|         }); | ||||
|       } | ||||
| 
 | ||||
|       $scope.githubClientId = KeyService.githubClientId; | ||||
| 
 | ||||
|       $scope.awaitingConfirmation = false; | ||||
|       $scope.registering = false; | ||||
|          | ||||
| 
 | ||||
|       $scope.register = function() { | ||||
|         $('.form-signup').popover('hide'); | ||||
|         UIService.hidePopover('#signupButton'); | ||||
|         $scope.registering = true; | ||||
| 
 | ||||
|         ApiService.createNewUser($scope.newUser).then(function() { | ||||
|           $scope.awaitingConfirmation = true; | ||||
|           $scope.registering  = false; | ||||
| 
 | ||||
|           mixpanel.alias($scope.newUser.username); | ||||
|           $scope.awaitingConfirmation = true; | ||||
|            | ||||
|           if (Config.MIXPANEL_KEY) { | ||||
|             mixpanel.alias($scope.newUser.username); | ||||
|           } | ||||
|         }, function(result) { | ||||
|           $scope.registering  = false; | ||||
|           $scope.registerError = result.data.message; | ||||
|           $timeout(function() { | ||||
|             $('.form-signup').popover('show'); | ||||
|           }); | ||||
|           UIService.showFormError('#signupButton', result); | ||||
|         }); | ||||
|       }; | ||||
|     } | ||||
|  | @ -1644,7 +1932,7 @@ quayApp.directive('plansTable', function () { | |||
| }); | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('dockerAuthDialog', function () { | ||||
| quayApp.directive('dockerAuthDialog', function (Config) { | ||||
|   var directiveDefinitionObject = { | ||||
|     priority: 0, | ||||
|     templateUrl: '/static/directives/docker-auth-dialog.html', | ||||
|  | @ -1665,11 +1953,10 @@ quayApp.directive('dockerAuthDialog', function () { | |||
| 
 | ||||
|       $scope.downloadCfg = function() { | ||||
|         var auth = $.base64.encode($scope.username + ":" + $scope.token); | ||||
|         config = { | ||||
|           "https://quay.io/v1/": { | ||||
|             "auth": auth, | ||||
|             "email": "" | ||||
|           } | ||||
|         config = {} | ||||
|         config[Config.getUrl('/v1/')] = { | ||||
|           "auth": auth, | ||||
|           "email": "" | ||||
|         }; | ||||
| 
 | ||||
|         var file = JSON.stringify(config, null, ' '); | ||||
|  | @ -2653,11 +2940,13 @@ quayApp.directive('entitySearch', function () { | |||
|       'isOrganization': '=isOrganization', | ||||
|       'isPersistent': '=isPersistent', | ||||
|       'currentEntity': '=currentEntity', | ||||
|       'clearNow': '=clearNow' | ||||
|       'clearNow': '=clearNow', | ||||
|       'filter': '=filter', | ||||
|     }, | ||||
|     controller: function($scope, $element, Restangular, UserService, ApiService) { | ||||
|       $scope.lazyLoading = true; | ||||
|       $scope.isAdmin = false; | ||||
|       $scope.currentEntityInternal = $scope.currentEntity; | ||||
| 
 | ||||
|       $scope.lazyLoad = function() { | ||||
|         if (!$scope.namespace || !$scope.lazyLoading) { return; } | ||||
|  | @ -2736,20 +3025,27 @@ quayApp.directive('entitySearch', function () { | |||
|           entity['is_org_member'] = true; | ||||
|         } | ||||
| 
 | ||||
|         $scope.setEntityInternal(entity); | ||||
|         $scope.setEntityInternal(entity, false); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.clearEntityInternal = function() { | ||||
|         $scope.currentEntityInternal = null; | ||||
|         $scope.currentEntity = null; | ||||
| 
 | ||||
|         if ($scope.entitySelected) { | ||||
|           $scope.entitySelected(null); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       $scope.setEntityInternal = function(entity) { | ||||
|         $(input).typeahead('val', $scope.isPersistent ? entity.name : ''); | ||||
|       $scope.setEntityInternal = function(entity, updateTypeahead) { | ||||
|         if (updateTypeahead) { | ||||
|           $(input).typeahead('val', $scope.isPersistent ? entity.name : ''); | ||||
|         } else { | ||||
|           $(input).val($scope.isPersistent ? entity.name : ''); | ||||
|         } | ||||
| 
 | ||||
|         if ($scope.isPersistent) { | ||||
|           $scope.currentEntityInternal = entity; | ||||
|           $scope.currentEntity = entity; | ||||
|         } | ||||
| 
 | ||||
|  | @ -2777,6 +3073,19 @@ quayApp.directive('entitySearch', function () { | |||
|             var datums = []; | ||||
|             for (var i = 0; i < data.results.length; ++i) { | ||||
|               var entity = data.results[i]; | ||||
|               if ($scope.filter) { | ||||
|                 var allowed = $scope.filter; | ||||
|                 var found = 'user'; | ||||
|                 if (entity.kind == 'user') { | ||||
|                   found = entity.is_robot ? 'robot' : 'user'; | ||||
|                 } else if (entity.kind == 'team') { | ||||
|                   found = 'team'; | ||||
|                 } | ||||
|                 if (allowed.indexOf(found)) { | ||||
|                   continue; | ||||
|                 } | ||||
|               } | ||||
| 
 | ||||
|               datums.push({ | ||||
|                 'value': entity.name, | ||||
|                 'tokens': [entity.name], | ||||
|  | @ -2849,7 +3158,7 @@ quayApp.directive('entitySearch', function () { | |||
| 
 | ||||
|       $(input).on('typeahead:selected', function(e, datum) { | ||||
|         $scope.$apply(function() { | ||||
|           $scope.setEntityInternal(datum.entity); | ||||
|           $scope.setEntityInternal(datum.entity, true); | ||||
|         }); | ||||
|       }); | ||||
| 
 | ||||
|  | @ -2861,6 +3170,16 @@ quayApp.directive('entitySearch', function () { | |||
|       $scope.$watch('inputTitle', function(title) { | ||||
|         input.setAttribute('placeholder', title); | ||||
|       }); | ||||
| 
 | ||||
|       $scope.$watch('currentEntity', function(entity) { | ||||
|         if ($scope.currentEntityInternal != entity) { | ||||
|           if (entity) { | ||||
|             $scope.setEntityInternal(entity, false); | ||||
|           } else { | ||||
|             $scope.clearEntityInternal(); | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|   return directiveDefinitionObject; | ||||
|  | @ -3072,7 +3391,7 @@ quayApp.directive('planManager', function () { | |||
|           } | ||||
| 
 | ||||
|           if (!$scope.chart) { | ||||
|             $scope.chart = new RepositoryUsageChart(); | ||||
|             $scope.chart = new UsageChart(); | ||||
|             $scope.chart.draw('repository-usage-chart'); | ||||
|           } | ||||
| 
 | ||||
|  | @ -3398,6 +3717,145 @@ quayApp.directive('dropdownSelectMenu', function () { | |||
| }); | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('setupTriggerDialog', function () { | ||||
|   var directiveDefinitionObject = { | ||||
|     templateUrl: '/static/directives/setup-trigger-dialog.html', | ||||
|     replace: false, | ||||
|     transclude: false, | ||||
|     restrict: 'C', | ||||
|     scope: { | ||||
|       'repository': '=repository', | ||||
|       'trigger': '=trigger', | ||||
|       'counter': '=counter', | ||||
|       'canceled': '&canceled', | ||||
|       'activated': '&activated' | ||||
|     }, | ||||
|     controller: function($scope, $element, ApiService, UserService) { | ||||
|       $scope.show = function() { | ||||
|         $scope.pullEntity = null; | ||||
|         $scope.publicPull = true; | ||||
|         $scope.showPullRequirements = false; | ||||
| 
 | ||||
|         $('#setupTriggerModal').modal({}); | ||||
|         $('#setupTriggerModal').on('hidden.bs.modal', function () { | ||||
|           $scope.$apply(function() { | ||||
|             $scope.cancelSetupTrigger(); | ||||
|           }); | ||||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.isNamespaceAdmin = function(namespace) { | ||||
|         return UserService.isNamespaceAdmin(namespace); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.cancelSetupTrigger = function() { | ||||
|         $scope.canceled({'trigger': $scope.trigger}); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.hide = function() { | ||||
|         $('#setupTriggerModal').modal('hide'); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.setPublicPull = function(value) { | ||||
|         $scope.publicPull = value; | ||||
|       }; | ||||
| 
 | ||||
|       $scope.checkAnalyze = function(isValid) { | ||||
|         if (!isValid) { | ||||
|           $scope.publicPull = true; | ||||
|           $scope.pullEntity = null;           | ||||
|           $scope.showPullRequirements = false; | ||||
|           $scope.checkingPullRequirements = false; | ||||
|           return; | ||||
|         } | ||||
|          | ||||
|         $scope.checkingPullRequirements = true; | ||||
|         $scope.showPullRequirements = true; | ||||
|         $scope.pullRequirements = null; | ||||
| 
 | ||||
|         var params = { | ||||
|           'repository': $scope.repository.namespace + '/' + $scope.repository.name, | ||||
|           'trigger_uuid': $scope.trigger.id | ||||
|         }; | ||||
| 
 | ||||
|         var data = { | ||||
|           'config': $scope.trigger.config | ||||
|         }; | ||||
| 
 | ||||
|         ApiService.analyzeBuildTrigger(data, params).then(function(resp) { | ||||
|           $scope.pullRequirements = resp; | ||||
| 
 | ||||
|           if (resp['status'] == 'publicbase') { | ||||
|             $scope.publicPull = true; | ||||
|             $scope.pullEntity = null; | ||||
|           } else if (resp['namespace']) { | ||||
|             $scope.publicPull = false; | ||||
| 
 | ||||
|             if (resp['robots'] && resp['robots'].length > 0) { | ||||
|               $scope.pullEntity = resp['robots'][0]; | ||||
|             } else { | ||||
|               $scope.pullEntity = null; | ||||
|             } | ||||
|           } | ||||
| 
 | ||||
|           $scope.checkingPullRequirements = false; | ||||
|         }, function(resp) { | ||||
|           $scope.pullRequirements = resp;           | ||||
|           $scope.checkingPullRequirements = false; | ||||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.activate = function() { | ||||
|         var params = { | ||||
|           'repository': $scope.repository.namespace + '/' + $scope.repository.name, | ||||
|           'trigger_uuid': $scope.trigger.id | ||||
|         }; | ||||
| 
 | ||||
|         var data = { | ||||
|           'config': $scope.trigger['config'] | ||||
|         }; | ||||
| 
 | ||||
|         if ($scope.pullEntity) { | ||||
|           data['pull_robot'] = $scope.pullEntity['name']; | ||||
|         } | ||||
| 
 | ||||
|         ApiService.activateBuildTrigger(data, params).then(function(resp) { | ||||
|           trigger['is_active'] = true; | ||||
|           trigger['pull_robot'] = resp['pull_robot']; | ||||
|           $scope.activated({'trigger': $scope.trigger}); | ||||
|         }, function(resp) { | ||||
|           $scope.hide(); | ||||
|           $scope.canceled({'trigger': $scope.trigger}); | ||||
| 
 | ||||
|           bootbox.dialog({ | ||||
|             "message": resp['data']['message'] || 'The build trigger setup could not be completed', | ||||
|             "title": "Could not activate build trigger", | ||||
|             "buttons": { | ||||
|               "close": { | ||||
|                 "label": "Close", | ||||
|                 "className": "btn-primary" | ||||
|               } | ||||
|             } | ||||
|           }); | ||||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|       var check = function() { | ||||
|         if ($scope.counter && $scope.trigger && $scope.repository) { | ||||
|           $scope.show(); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       $scope.$watch('trigger', check); | ||||
|       $scope.$watch('counter', check); | ||||
|       $scope.$watch('repository', check); | ||||
|     } | ||||
|   }; | ||||
|   return directiveDefinitionObject; | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| quayApp.directive('triggerSetupGithub', function () { | ||||
|   var directiveDefinitionObject = { | ||||
|     priority: 0, | ||||
|  | @ -3407,15 +3865,18 @@ quayApp.directive('triggerSetupGithub', function () { | |||
|     restrict: 'C', | ||||
|     scope: { | ||||
|       'repository': '=repository', | ||||
|       'trigger': '=trigger' | ||||
|       'trigger': '=trigger', | ||||
|       'analyze': '&analyze' | ||||
|     }, | ||||
|     controller: function($scope, $element, ApiService) { | ||||
|       $scope.analyzeCounter = 0; | ||||
|       $scope.setupReady = false; | ||||
|       $scope.loading = true; | ||||
|             | ||||
|       $scope.handleLocationInput = function(location) { | ||||
|         $scope.trigger['config']['subdir'] = location || ''; | ||||
|         $scope.isInvalidLocation = $scope.locations.indexOf(location) < 0; | ||||
|         $scope.analyze({'isValid': !$scope.isInvalidLocation}); | ||||
|       }; | ||||
| 
 | ||||
|       $scope.handleLocationSelected = function(datum) { | ||||
|  | @ -3426,6 +3887,7 @@ quayApp.directive('triggerSetupGithub', function () { | |||
|         $scope.currentLocation = location; | ||||
|         $scope.trigger['config']['subdir'] = location || ''; | ||||
|         $scope.isInvalidLocation = false; | ||||
|         $scope.analyze({'isValid': true}); | ||||
|       }; | ||||
|   | ||||
|       $scope.selectRepo = function(repo, org) { | ||||
|  | @ -3464,6 +3926,7 @@ quayApp.directive('triggerSetupGithub', function () { | |||
|               $scope.locations = null; | ||||
|               $scope.trigger.$ready = false; | ||||
|               $scope.isInvalidLocation = false; | ||||
|               $scope.analyze({'isValid': false}); | ||||
|               return; | ||||
|             } | ||||
| 
 | ||||
|  | @ -3476,12 +3939,14 @@ quayApp.directive('triggerSetupGithub', function () { | |||
|             } else { | ||||
|               $scope.currentLocation = null; | ||||
|               $scope.isInvalidLocation = resp['subdir'].indexOf('') < 0; | ||||
|               $scope.analyze({'isValid': !$scope.isInvalidLocation}); | ||||
|             } | ||||
|           }, function(resp) { | ||||
|             $scope.locationError = resp['message'] || 'Could not load Dockerfile locations'; | ||||
|             $scope.locations = null; | ||||
|             $scope.trigger.$ready = false; | ||||
|             $scope.isInvalidLocation = false; | ||||
|             $scope.analyze({'isValid': false}); | ||||
|           }); | ||||
|         } | ||||
|       }; | ||||
|  | @ -3526,7 +3991,14 @@ quayApp.directive('triggerSetupGithub', function () { | |||
|         }); | ||||
|       }; | ||||
| 
 | ||||
|       loadSources(); | ||||
|       var check = function() { | ||||
|         if ($scope.repository && $scope.trigger) { | ||||
|           loadSources(); | ||||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       $scope.$watch('repository', check); | ||||
|       $scope.$watch('trigger', check); | ||||
| 
 | ||||
|       $scope.$watch('currentRepo', function(repo) { | ||||
|         $scope.selectRepoInternal(repo); | ||||
|  | @ -3572,7 +4044,7 @@ quayApp.directive('dockerfileCommand', function () { | |||
|     scope: { | ||||
|       'command': '=command' | ||||
|     }, | ||||
|     controller: function($scope, $element, $sanitize) { | ||||
|     controller: function($scope, $element, $sanitize, Config) { | ||||
|       var registryHandlers = { | ||||
|         'quay.io': function(pieces) { | ||||
|           var rnamespace =  pieces[pieces.length - 2]; | ||||
|  | @ -3587,6 +4059,8 @@ quayApp.directive('dockerfileCommand', function () { | |||
|         } | ||||
|       }; | ||||
| 
 | ||||
|       registryHandlers[Config.getDomain()] = registryHandlers['quay.io']; | ||||
| 
 | ||||
|       var kindHandlers = { | ||||
|         'FROM': function(title) { | ||||
|           var pieces = title.split('/'); | ||||
|  | @ -4259,6 +4733,17 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   $rootScope.$watch('description', function(description) { | ||||
|     if (!description) { | ||||
|       description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.'; | ||||
|     } | ||||
|      | ||||
|     // Note: We set the content of the description tag manually here rather than using Angular binding
 | ||||
|     // because we need the <meta> tag to have a default description that is not of the form "{{ description }}",
 | ||||
|     // we read by tools that do not properly invoke the Angular code.
 | ||||
|     $('#descriptionTag').attr('content', description);     | ||||
|   }); | ||||
| 
 | ||||
|   $rootScope.$on('$routeUpdate', function(){ | ||||
|     if ($location.search()['tab']) { | ||||
|       changeTab($location.search()['tab']); | ||||
|  | @ -4275,7 +4760,7 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi | |||
|     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.description = ''; | ||||
|     } | ||||
| 
 | ||||
|     $rootScope.fixFooter = !!current.$$route.fixFooter; | ||||
|  |  | |||
|  | @ -48,14 +48,15 @@ function PlansCtrl($scope, $location, UserService, PlanService) { | |||
|   }; | ||||
| } | ||||
| 
 | ||||
| function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService) { | ||||
| function TutorialCtrl($scope, AngularTour, AngularTourSignals, UserService, Config) { | ||||
|   // Default to showing sudo on all commands if on linux.
 | ||||
|   var showSudo = navigator.appVersion.indexOf("Linux") != -1; | ||||
| 
 | ||||
|   $scope.tour = { | ||||
|     'title': 'Quay.io Tutorial', | ||||
|     'initialScope': { | ||||
|       'showSudo': showSudo | ||||
|       'showSudo': showSudo, | ||||
|       'domainName': Config.getDomain() | ||||
|     }, | ||||
|     'steps': [ | ||||
|       { | ||||
|  | @ -262,7 +263,7 @@ function RepoListCtrl($scope, $sanitize, Restangular, UserService, ApiService) { | |||
|   loadPublicRepos(); | ||||
| } | ||||
| 
 | ||||
| function LandingCtrl($scope, UserService, ApiService) { | ||||
| function LandingCtrl($scope, UserService, ApiService, Features, Config) { | ||||
|   $scope.namespace = null; | ||||
| 
 | ||||
|   $scope.$watch('namespace', function(namespace) { | ||||
|  | @ -303,10 +304,22 @@ function LandingCtrl($scope, UserService, ApiService) { | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   browserchrome.update(); | ||||
|   $scope.chromify = function() { | ||||
|     browserchrome.update(); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.getEnterpriseLogo = function() { | ||||
|     if (!Config.ENTERPRISE_LOGO_URL) { | ||||
|       return '/static/img/quay-logo.png'; | ||||
|     } | ||||
| 
 | ||||
|     return Config.ENTERPRISE_LOGO_URL; | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout) { | ||||
| function RepoCtrl($scope, $sanitize, Restangular, ImageMetadataService, ApiService, $routeParams, $rootScope, $location, $timeout, Config) { | ||||
|   $scope.Config = Config; | ||||
| 
 | ||||
|   var namespace = $routeParams.namespace; | ||||
|   var name = $routeParams.name; | ||||
| 
 | ||||
|  | @ -945,9 +958,13 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope | |||
| 
 | ||||
|     var data = { | ||||
|       'file_id': build['resource_key'], | ||||
|       'subdirectory': subdirectory | ||||
|       'subdirectory': subdirectory, | ||||
|     }; | ||||
| 
 | ||||
|     if (build['pull_robot']) { | ||||
|       data['pull_robot'] = build['pull_robot']['name']; | ||||
|     } | ||||
| 
 | ||||
|     var params = { | ||||
|       'repository': namespace + '/' + name | ||||
|     }; | ||||
|  | @ -1073,6 +1090,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope | |||
|       // Note: We use extend here rather than replacing as Angular is depending on the
 | ||||
|       // root build object to remain the same object.
 | ||||
|       $.extend(true, $scope.builds[$scope.currentBuildIndex], resp); | ||||
|       var currentBuild = $scope.builds[$scope.currentBuildIndex]; | ||||
|       checkPollTimer(); | ||||
| 
 | ||||
|       // Load the updated logs for the build.
 | ||||
|  | @ -1089,6 +1107,18 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope | |||
|         processLogs(resp['logs'], resp['start']); | ||||
|         $scope.logStartIndex = resp['total']; | ||||
|         $scope.polling = false; | ||||
| 
 | ||||
|         // If the build status is an error, open the last two log entries.
 | ||||
|         if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) { | ||||
|           var openLogEntries = function(entry) { | ||||
|             if (entry.logs) { | ||||
|               entry.logs.setVisible(true); | ||||
|             } | ||||
|           }; | ||||
| 
 | ||||
|           openLogEntries($scope.logEntries[$scope.logEntries.length - 2]); | ||||
|           openLogEntries($scope.logEntries[$scope.logEntries.length - 1]); | ||||
|         } | ||||
|       }, function() { | ||||
|         $scope.polling = false; | ||||
|       }); | ||||
|  | @ -1131,7 +1161,7 @@ function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope | |||
|   fetchRepository(); | ||||
| } | ||||
| 
 | ||||
| function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location) { | ||||
| function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams, $rootScope, $location, UserService, Config) { | ||||
|   var namespace = $routeParams.namespace; | ||||
|   var name = $routeParams.name; | ||||
| 
 | ||||
|  | @ -1144,15 +1174,17 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams | |||
|   $scope.githubRedirectUri = KeyService.githubRedirectUri; | ||||
|   $scope.githubClientId = KeyService.githubClientId; | ||||
| 
 | ||||
|   $scope.showTriggerSetupCounter = 0; | ||||
| 
 | ||||
|   $scope.getBadgeFormat = function(format, repo) { | ||||
|     if (!repo) { return; } | ||||
| 
 | ||||
|     var imageUrl = 'https://quay.io/repository/' + namespace + '/' + name + '/status'; | ||||
|     var imageUrl = Config.getUrl('/' + namespace + '/' + name + '/status'); | ||||
|     if (!$scope.repo.is_public) { | ||||
|       imageUrl += '?token=' + $scope.repo.status_token; | ||||
|     } | ||||
| 
 | ||||
|     var linkUrl = 'https://quay.io/repository/' + namespace + '/' + name; | ||||
|     var linkUrl = Config.getUrl('/' + namespace + '/' + name); | ||||
| 
 | ||||
|     switch (format) { | ||||
|       case 'svg': | ||||
|  | @ -1433,48 +1465,15 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams | |||
|   }; | ||||
| 
 | ||||
|   $scope.setupTrigger = function(trigger) { | ||||
|     $scope.triggerSetupReady = false; | ||||
|     $scope.currentSetupTrigger = trigger; | ||||
|     $('#setupTriggerModal').modal({}); | ||||
|     $('#setupTriggerModal').on('hidden.bs.modal', function () { | ||||
|       $scope.$apply(function() { | ||||
|         $scope.cancelSetupTrigger(); | ||||
|       }); | ||||
|     }); | ||||
|     $scope.showTriggerSetupCounter++; | ||||
|   }; | ||||
| 
 | ||||
|   $scope.finishSetupTrigger = function(trigger) { | ||||
|     $('#setupTriggerModal').modal('hide'); | ||||
|     $scope.currentSetupTrigger = null; | ||||
| 
 | ||||
|     var params = { | ||||
|       'repository': namespace + '/' + name, | ||||
|       'trigger_uuid': trigger.id | ||||
|     }; | ||||
| 
 | ||||
|     ApiService.activateBuildTrigger(trigger['config'], params).then(function(resp) { | ||||
|       trigger['is_active'] = true;       | ||||
|     }, function(resp) { | ||||
|       $scope.triggers.splice($scope.triggers.indexOf(trigger), 1); | ||||
|       bootbox.dialog({ | ||||
|         "message": resp['data']['message'] || 'The build trigger setup could not be completed', | ||||
|         "title": "Could not activate build trigger", | ||||
|         "buttons": { | ||||
|           "close": { | ||||
|             "label": "Close", | ||||
|             "className": "btn-primary" | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.cancelSetupTrigger = function() { | ||||
|     if (!$scope.currentSetupTrigger) { return; } | ||||
| 
 | ||||
|     $('#setupTriggerModal').modal('hide'); | ||||
|     $scope.deleteTrigger($scope.currentSetupTrigger); | ||||
|   $scope.cancelSetupTrigger = function(trigger) { | ||||
|     if ($scope.currentSetupTrigger != trigger) { return; } | ||||
| 
 | ||||
|     $scope.currentSetupTrigger = null; | ||||
|     $scope.deleteTrigger(trigger); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.startTrigger = function(trigger) { | ||||
|  | @ -1569,12 +1568,16 @@ function RepoAdminCtrl($scope, Restangular, ApiService, KeyService, $routeParams | |||
| } | ||||
| 
 | ||||
| function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, CookieService, KeyService, | ||||
|     $routeParams, $http) { | ||||
|                        $routeParams, $http, UIService, Features) { | ||||
|   $scope.Features = Features; | ||||
| 
 | ||||
|   if ($routeParams['migrate']) { | ||||
|     $('#migrateTab').tab('show') | ||||
|   } | ||||
| 
 | ||||
|   UserService.updateUserIn($scope, function(user) { | ||||
|     if (!Features.GITHUB_LOGIN) { return; } | ||||
| 
 | ||||
|     $scope.cuser = jQuery.extend({}, user); | ||||
| 
 | ||||
|     for (var i = 0; i < $scope.cuser.logins.length; i++) { | ||||
|  | @ -1602,8 +1605,6 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use | |||
|   $scope.githubClientId = KeyService.githubClientId; | ||||
|   $scope.authorizedApps = null; | ||||
| 
 | ||||
|   $('.form-change').popover(); | ||||
| 
 | ||||
|   $scope.logsShown = 0; | ||||
|   $scope.invoicesShown = 0; | ||||
|    | ||||
|  | @ -1652,13 +1653,15 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use | |||
|   }; | ||||
| 
 | ||||
|   $scope.showConvertForm = function() { | ||||
|     PlanService.getMatchingBusinessPlan(function(plan) { | ||||
|       $scope.org.plan = plan; | ||||
|     }); | ||||
|     if (Features.BILLING) { | ||||
|       PlanService.getMatchingBusinessPlan(function(plan) { | ||||
|         $scope.org.plan = plan; | ||||
|       }); | ||||
| 
 | ||||
|     PlanService.getPlans(function(plans) { | ||||
|       $scope.orgPlans = plans; | ||||
|     }); | ||||
|       PlanService.getPlans(function(plans) { | ||||
|         $scope.orgPlans = plans; | ||||
|       }); | ||||
|     } | ||||
|        | ||||
|     $scope.convertStep = 1; | ||||
|   }; | ||||
|  | @ -1673,7 +1676,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use | |||
|     var data = { | ||||
|       'adminUser': $scope.org.adminUser, | ||||
|       'adminPassword': $scope.org.adminPassword, | ||||
|       'plan': $scope.org.plan.stripeId | ||||
|       'plan': $scope.org.plan ? $scope.org.plan.stripeId : '' | ||||
|     }; | ||||
| 
 | ||||
|     ApiService.convertUserToOrganization(data).then(function(resp) { | ||||
|  | @ -1691,7 +1694,8 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use | |||
|   }; | ||||
| 
 | ||||
|   $scope.changeEmail = function() { | ||||
|     $('#changeEmailForm').popover('hide'); | ||||
|     UIService.hidePopover('#changeEmailForm'); | ||||
| 
 | ||||
|     $scope.updatingUser = true; | ||||
|     $scope.changeEmailSent = false; | ||||
| 
 | ||||
|  | @ -1706,16 +1710,13 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use | |||
|       $scope.changeEmailForm.$setPristine(); | ||||
|     }, function(result) { | ||||
|       $scope.updatingUser = false; | ||||
| 
 | ||||
|       $scope.changeEmailError = result.data.message; | ||||
|       $timeout(function() { | ||||
|         $('#changeEmailForm').popover('show'); | ||||
|       }); | ||||
|       UIService.showFormError('#changeEmailForm', result);       | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.changePassword = function() { | ||||
|     $('#changePasswordForm').popover('hide'); | ||||
|     UIService.hidePopover('#changePasswordForm'); | ||||
| 
 | ||||
|     $scope.updatingUser = true; | ||||
|     $scope.changePasswordSuccess = false; | ||||
| 
 | ||||
|  | @ -1733,11 +1734,7 @@ function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, Use | |||
|       UserService.load(); | ||||
|     }, function(result) { | ||||
|       $scope.updatingUser = false; | ||||
| 
 | ||||
|       $scope.changePasswordError = result.data.message; | ||||
|       $timeout(function() { | ||||
|         $('#changePasswordForm').popover('show'); | ||||
|       }); | ||||
|       UIService.showFormError('#changePasswordForm', result);       | ||||
|     }); | ||||
|   }; | ||||
| } | ||||
|  | @ -1874,7 +1871,7 @@ function V1Ctrl($scope, $location, UserService) { | |||
|   UserService.updateUserIn($scope); | ||||
| } | ||||
| 
 | ||||
| function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService) { | ||||
| function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService, KeyService, Features) { | ||||
|   UserService.updateUserIn($scope); | ||||
| 
 | ||||
|   $scope.githubRedirectUri = KeyService.githubRedirectUri; | ||||
|  | @ -1996,13 +1993,19 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService | |||
|   var checkPrivateAllowed = function() { | ||||
|     if (!$scope.repo || !$scope.repo.namespace) { return; } | ||||
| 
 | ||||
|     if (!Features.BILLING) { | ||||
|       $scope.checkingPlan = false; | ||||
|       $scope.planRequired = null; | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     $scope.checkingPlan = true; | ||||
| 
 | ||||
|     var isUserNamespace = $scope.isUserNamespace; | ||||
|     ApiService.getPrivateAllowed(isUserNamespace ? null : $scope.repo.namespace).then(function(resp) { | ||||
|       $scope.checkingPlan = false; | ||||
|   | ||||
|       if (resp['privateAllowed']) {         | ||||
|       if (resp['privateAllowed']) { | ||||
|         $scope.planRequired = null; | ||||
|         return; | ||||
|       } | ||||
|  | @ -2122,18 +2125,20 @@ function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) { | |||
|   loadOrganization(); | ||||
| } | ||||
| 
 | ||||
| function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService) { | ||||
| function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, UserService, PlanService, ApiService, Features, UIService) { | ||||
|   var orgname = $routeParams.orgname; | ||||
| 
 | ||||
|   // Load the list of plans.
 | ||||
|   PlanService.getPlans(function(plans) { | ||||
|     $scope.plans = plans; | ||||
|     $scope.plan_map = {}; | ||||
| 
 | ||||
|     for (var i = 0; i < plans.length; ++i) { | ||||
|       $scope.plan_map[plans[i].stripeId] = plans[i]; | ||||
|     } | ||||
|   }); | ||||
|   if (Features.BILLING) { | ||||
|     PlanService.getPlans(function(plans) { | ||||
|       $scope.plans = plans; | ||||
|       $scope.plan_map = {}; | ||||
|        | ||||
|       for (var i = 0; i < plans.length; ++i) { | ||||
|         $scope.plan_map[plans[i].stripeId] = plans[i]; | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   $scope.orgname = orgname; | ||||
|   $scope.membersLoading = true; | ||||
|  | @ -2161,10 +2166,12 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U | |||
|   }; | ||||
| 
 | ||||
|   $scope.$watch('organizationEmail', function(e) { | ||||
|     $('#changeEmailForm').popover('hide'); | ||||
|     UIService.hidePopover('#changeEmailForm'); | ||||
|   }); | ||||
| 
 | ||||
|   $scope.changeEmail = function() { | ||||
|     UIService.hidePopover('#changeEmailForm'); | ||||
| 
 | ||||
|     $scope.changingOrganization = true; | ||||
|     var params = { | ||||
|       'orgname': orgname | ||||
|  | @ -2180,10 +2187,7 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U | |||
|       $scope.organization = org; | ||||
|     }, function(result) { | ||||
|       $scope.changingOrganization = false; | ||||
|       $scope.changeEmailError = result.data.message; | ||||
|       $timeout(function() { | ||||
|         $('#changeEmailForm').popover('show'); | ||||
|       }); | ||||
|       UIService.showFormError('#changeEmailForm', result); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|  | @ -2316,30 +2320,39 @@ function OrgsCtrl($scope, UserService) { | |||
|   browserchrome.update(); | ||||
| } | ||||
| 
 | ||||
| function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService) { | ||||
| function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService, CookieService, Features) { | ||||
|   $scope.Features = Features; | ||||
|   $scope.holder = {}; | ||||
| 
 | ||||
|   UserService.updateUserIn($scope); | ||||
| 
 | ||||
|   var requested = $routeParams['plan']; | ||||
| 
 | ||||
|   // Load the list of plans.
 | ||||
|   PlanService.getPlans(function(plans) { | ||||
|     $scope.plans = plans; | ||||
|     $scope.currentPlan = null; | ||||
|     if (requested) { | ||||
|       PlanService.getPlan(requested, function(plan) { | ||||
|         $scope.currentPlan = plan; | ||||
|       }); | ||||
|     } | ||||
|   }); | ||||
|   if (Features.BILLING) { | ||||
|     // Load the list of plans.
 | ||||
|     PlanService.getPlans(function(plans) { | ||||
|       $scope.plans = plans; | ||||
|       $scope.currentPlan = null; | ||||
|       if (requested) { | ||||
|         PlanService.getPlan(requested, function(plan) { | ||||
|           $scope.currentPlan = plan; | ||||
|         }); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   $scope.signedIn = function() { | ||||
|     PlanService.handleNotedPlan(); | ||||
|     if (Features.BILLING) { | ||||
|       PlanService.handleNotedPlan(); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   $scope.signinStarted = function() { | ||||
|     PlanService.getMinimumPlan(1, true, function(plan) { | ||||
|       PlanService.notePlan(plan.stripeId); | ||||
|     }); | ||||
|     if (Features.BILLING) { | ||||
|       PlanService.getMinimumPlan(1, true, function(plan) { | ||||
|         PlanService.notePlan(plan.stripeId); | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
| 
 | ||||
|   $scope.setPlan = function(plan) { | ||||
|  | @ -2371,7 +2384,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan | |||
|       }; | ||||
| 
 | ||||
|       // If the selected plan is free, simply move to the org page.
 | ||||
|       if ($scope.currentPlan.price == 0) { | ||||
|       if (!Features.BILLING || $scope.currentPlan.price == 0) { | ||||
|         showOrg(); | ||||
|         return; | ||||
|       } | ||||
|  | @ -2564,4 +2577,135 @@ function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $tim | |||
|   // Load the organization and application info.
 | ||||
|   loadOrganization(); | ||||
|   loadApplicationInfo(); | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| function SuperUserAdminCtrl($scope, ApiService, Features, UserService) { | ||||
|   if (!Features.SUPER_USERS) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   // Monitor any user changes and place the current user into the scope.
 | ||||
|   UserService.updateUserIn($scope); | ||||
| 
 | ||||
|   $scope.loadUsers = function() { | ||||
|     if ($scope.users) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     $scope.loadUsersInternal(); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.loadUsersInternal = function() { | ||||
|     ApiService.listAllUsers().then(function(resp) { | ||||
|       $scope.users = resp['users']; | ||||
|     }, function(resp) { | ||||
|       $scope.users = []; | ||||
|       $scope.usersError = resp['data']['message'] || resp['data']['error_description']; | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.showChangePassword = function(user) { | ||||
|     $scope.userToChange = user; | ||||
|     $('#changePasswordModal').modal({}); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.showDeleteUser = function(user) { | ||||
|     if (user.username == UserService.currentUser().username) { | ||||
|       bootbox.dialog({ | ||||
|         "message": 'Cannot delete yourself!', | ||||
|         "title": "Cannot delete user", | ||||
|         "buttons": { | ||||
|           "close": { | ||||
|             "label": "Close", | ||||
|             "className": "btn-primary" | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     $scope.userToDelete = user; | ||||
|     $('#confirmDeleteUserModal').modal({}); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.changeUserPassword = function(user) { | ||||
|     $('#changePasswordModal').modal('hide'); | ||||
| 
 | ||||
|     var params = { | ||||
|       'username': user.username | ||||
|     }; | ||||
| 
 | ||||
|     var data = { | ||||
|       'password': user.password | ||||
|     }; | ||||
| 
 | ||||
|     ApiService.changeInstallUser(data, params).then(function(resp) { | ||||
|       $scope.loadUsersInternal(); | ||||
|     }, function(resp) { | ||||
|       bootbox.dialog({ | ||||
|         "message": resp.data ? resp.data.message : 'Could not change user', | ||||
|         "title": "Cannot change user", | ||||
|         "buttons": { | ||||
|           "close": { | ||||
|             "label": "Close", | ||||
|             "className": "btn-primary" | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   $scope.deleteUser = function(user) { | ||||
|     $('#confirmDeleteUserModal').modal('hide'); | ||||
| 
 | ||||
|     var params = { | ||||
|       'username': user.username | ||||
|     }; | ||||
| 
 | ||||
|     ApiService.deleteInstallUser(null, params).then(function(resp) { | ||||
|       $scope.loadUsersInternal(); | ||||
|     }, function(resp) { | ||||
|       bootbox.dialog({ | ||||
|         "message": resp.data ? resp.data.message : 'Could not delete user', | ||||
|         "title": "Cannot delete user", | ||||
|         "buttons": { | ||||
|           "close": { | ||||
|             "label": "Close", | ||||
|             "className": "btn-primary" | ||||
|           } | ||||
|         } | ||||
|       }); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   var seatUsageLoaded = function(usage) { | ||||
|     $scope.usageLoading = false; | ||||
| 
 | ||||
|     if (usage.count > usage.allowed) { | ||||
|       $scope.limit = 'over'; | ||||
|     } else if (usage.count == usage.allowed) { | ||||
|       $scope.limit = 'at'; | ||||
|     } else if (usage.count >= usage.allowed * 0.7) { | ||||
|       $scope.limit = 'near'; | ||||
|     } else { | ||||
|       $scope.limit = 'none'; | ||||
|     } | ||||
|      | ||||
|     if (!$scope.chart) { | ||||
|       $scope.chart = new UsageChart(); | ||||
|       $scope.chart.draw('seat-usage-chart'); | ||||
|     } | ||||
| 
 | ||||
|     $scope.chart.update(usage.count, usage.allowed); | ||||
|   }; | ||||
| 
 | ||||
|   var loadSeatUsage = function() { | ||||
|     $scope.usageLoading = true; | ||||
|     ApiService.getSeatCount().then(function(resp) { | ||||
|       seatUsageLoaded(resp); | ||||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   loadSeatUsage(); | ||||
| } | ||||
|  | @ -230,7 +230,17 @@ ImageHistoryTree.prototype.draw = function(container) { | |||
|       if (d.image.command && d.image.command.length) { | ||||
|         html += '<span class="command info-line"><i class="fa fa-terminal"></i>' + formatCommand(d.image) + '</span>'; | ||||
|       } | ||||
|       html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';            | ||||
|       html += '<span class="created info-line"><i class="fa fa-calendar"></i>' + formatTime(d.image.created) + '</span>';        | ||||
| 
 | ||||
|       var tags = d.tags || []; | ||||
|       html += '<span class="tooltip-tags tags">'; | ||||
|       for (var i = 0; i < tags.length; ++i) { | ||||
|         var tag = tags[i]; | ||||
|         var kind = 'default'; | ||||
|         html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '">' + tag + '</span>'; | ||||
|       } | ||||
|       html += '</span>'; | ||||
| 
 | ||||
|       return html; | ||||
|     }) | ||||
| 
 | ||||
|  | @ -338,6 +348,23 @@ ImageHistoryTree.prototype.changeImage_ = function(imageId) { | |||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Expands the given collapsed node in the tree. | ||||
|  */ | ||||
| ImageHistoryTree.prototype.expandCollapsed_ = function(imageNode) { | ||||
|   var index = imageNode.parent.children.indexOf(imageNode); | ||||
|   if (index < 0 || imageNode.encountered.length < 2) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   // Note: we start at 1 since the 0th encountered node is the parent.
 | ||||
|   imageNode.parent.children.splice(index, 1, imageNode.encountered[1]); | ||||
|   this.maxHeight_ = this.determineMaximumHeight_(this.root_); | ||||
|   this.update_(this.root_); | ||||
|   this.updateDimensions_(); | ||||
| }; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  * Builds the root node for the tree. | ||||
|  */ | ||||
|  | @ -640,7 +667,10 @@ ImageHistoryTree.prototype.update_ = function(source) { | |||
|     .attr("dy", ".35em") | ||||
|     .attr("text-anchor", function(d) { return d.children || d._children ? "end" : "start"; }) | ||||
|     .text(function(d) { return d.name; }) | ||||
|     .on("click", function(d) { if (d.image) { that.changeImage_(d.image.id); } }) | ||||
|     .on("click", function(d) { | ||||
|       if (d.image) { that.changeImage_(d.image.id); } | ||||
|       if (d.collapsed) { that.expandCollapsed_(d); } | ||||
|     }) | ||||
|     .on('mouseover', tip.show) | ||||
|     .on('mouseout', tip.hide) | ||||
|     .on("contextmenu", function(d, e) { | ||||
|  | @ -729,7 +759,7 @@ ImageHistoryTree.prototype.update_ = function(source) { | |||
|         if (tag == currentTag) { | ||||
|           kind = 'success'; | ||||
|         } | ||||
|         html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '"" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>'; | ||||
|         html += '<span class="label label-' + kind + ' tag" data-tag="' + tag + '" title="' + tag + '" style="max-width: ' + DEPTH_HEIGHT + 'px">' + tag + '</span>'; | ||||
|       } | ||||
|       html += '</div>'; | ||||
|       return html; | ||||
|  | @ -1350,7 +1380,7 @@ FileTree.prototype.getNodesHeight = function() { | |||
| /** | ||||
|  * Based off of http://bl.ocks.org/mbostock/1346410
 | ||||
|  */ | ||||
| function RepositoryUsageChart() { | ||||
| function UsageChart() { | ||||
|   this.total_ = null; | ||||
|   this.count_ = null; | ||||
|   this.drawn_ = false; | ||||
|  | @ -1360,7 +1390,7 @@ function RepositoryUsageChart() { | |||
| /** | ||||
|  * Updates the chart with the given count and total of number of repositories. | ||||
|  */ | ||||
| RepositoryUsageChart.prototype.update = function(count, total) { | ||||
| UsageChart.prototype.update = function(count, total) { | ||||
|   if (!this.g_) { return; } | ||||
|   this.total_ = total; | ||||
|   this.count_ = count; | ||||
|  | @ -1371,7 +1401,7 @@ RepositoryUsageChart.prototype.update = function(count, total) { | |||
| /** | ||||
|  * Conducts the actual draw or update (if applicable). | ||||
|  */ | ||||
| RepositoryUsageChart.prototype.drawInternal_ = function() { | ||||
| UsageChart.prototype.drawInternal_ = function() { | ||||
|   // If the total is null, then we have not yet set the proper counts.
 | ||||
|   if (this.total_ === null) { return; } | ||||
| 
 | ||||
|  | @ -1430,7 +1460,7 @@ RepositoryUsageChart.prototype.drawInternal_ = function() { | |||
| /** | ||||
|  * Draws the chart in the given container. | ||||
|  */ | ||||
| RepositoryUsageChart.prototype.draw = function(container) { | ||||
| UsageChart.prototype.draw = function(container) { | ||||
|   var cw = 200; | ||||
|   var ch = 200; | ||||
|   var radius = Math.min(cw, ch) / 2; | ||||
|  | @ -1707,7 +1737,7 @@ LogUsageChart.prototype.draw = function(container, logData, startDate, endDate) | |||
|       .duration(500) | ||||
|       .call(chart); | ||||
| 
 | ||||
|     nv.utils.windowResize(chart.update); | ||||
|     nv.utils.windoweResize(chart.update); | ||||
| 
 | ||||
|     chart.multibar.dispatch.on('elementClick', function(e) { that.handleElementClicked_(e); }); | ||||
|     chart.dispatch.on('stateChange', function(e) { that.handleStateChange_(e); }); | ||||
|  |  | |||
|  | @ -135,7 +135,7 @@ angular.module("angular-tour", []) | |||
|         }; | ||||
| 
 | ||||
|         var fireMixpanelEvent = function() { | ||||
|           if (!$scope.step || !mixpanel) { return; } | ||||
|           if (!$scope.step || !window['mixpanel']) { return; } | ||||
| 
 | ||||
|           var eventName = $scope.step['mixpanelEvent']; | ||||
|           if (eventName) { | ||||
|  |  | |||
		Reference in a new issue