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:
jakedt 2014-04-15 15:58:30 -04:00
commit 3f42d15335
132 changed files with 4266 additions and 1924 deletions

View file

@ -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;

View file

@ -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();
}

View file

@ -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); });

View file

@ -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) {