This repository has been archived on 2020-03-24. You can view files and clone it, but cannot push or open issues or pull requests.
quay/static/js/app.js

1627 lines
51 KiB
JavaScript
Raw Normal View History

function getFirstTextLine(commentString) {
if (!commentString) { return ''; }
var lines = commentString.split('\n');
var MARKDOWN_CHARS = {
'#': true,
'-': true,
'>': true,
'`': true
};
for (var i = 0; i < lines.length; ++i) {
// Skip code lines.
if (lines[i].indexOf(' ') == 0) {
continue;
}
// Skip empty lines.
if ($.trim(lines[i]).length == 0) {
continue;
}
// Skip control lines.
if (MARKDOWN_CHARS[$.trim(lines[i])[0]]) {
continue;
}
return getMarkedDown(lines[i]);
}
return '';
}
function getRestUrl(args) {
var url = '';
for (var i = 0; i < arguments.length; ++i) {
if (i > 0) {
url += '/';
}
url += encodeURI(arguments[i])
}
return url;
}
function getMarkedDown(string) {
return Markdown.getSanitizingConverter().makeHtml(string || '');
}
// Start the application code itself.
quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide) {
$provide.factory('UserService', ['Restangular', function(Restangular) {
var userResponse = {
verified: false,
anonymous: true,
username: null,
email: null,
askForPassword: false,
organizations: []
}
var userService = {}
userService.load = function(opt_callback) {
var userFetch = Restangular.one('user/');
userFetch.get().then(function(loadedUser) {
userResponse = loadedUser;
2013-10-03 19:46:22 +00:00
2013-10-03 20:16:10 +00:00
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()
})
2013-10-03 20:16:10 +00:00
}
if (opt_callback) {
opt_callback();
}
});
};
userService.getOrganization = function(name) {
if (!userResponse || !userResponse.organizations) { return null; }
for (var i = 0; i < userResponse.organizations.length; ++i) {
var org = userResponse.organizations[i];
if (org.name == name) {
return org;
}
}
return null;
};
userService.currentUser = function() {
return userResponse;
};
// Load the user the first time.
userService.load();
return userService;
}]);
$provide.factory('KeyService', ['$location', function($location) {
var keyService = {}
if ($location.host() === 'quay.io') {
keyService['stripePublishableKey'] = 'pk_live_P5wLU0vGdHnZGyKnXlFG4oiu';
keyService['githubClientId'] = '5a8c08b06c48d89d4d1e';
} else {
keyService['stripePublishableKey'] = 'pk_test_uEDHANKm9CHCvVa2DLcipGRh';
keyService['githubClientId'] = 'cfbc4aca88e5c1b40679';
}
return keyService;
}]);
$provide.factory('PlanService', ['Restangular', 'KeyService', 'UserService', function(Restangular, KeyService, UserService) {
var plans = null;
var planDict = {};
var planService = {};
var listeners = [];
planService.registerListener = function(obj, callback) {
listeners.push({'obj': obj, 'callback': callback});
};
planService.unregisterListener = function(obj) {
for (var i = 0; i < listeners.length; ++i) {
if (listeners[i].obj == obj) {
listeners.splice(i, 1);
break;
}
}
};
planService.handleCardError = function(resp) {
if (!planService.isCardError(resp)) { return; }
bootbox.dialog({
"message": resp.data.carderror,
"title": "Credit card issue",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
};
planService.isCardError = function(resp) {
return resp && resp.data && resp.data.carderror;
};
planService.verifyLoaded = function(callback) {
if (plans) {
callback(plans);
return;
}
var getPlans = Restangular.one('plans');
getPlans.get().then(function(data) {
2013-11-05 19:40:45 +00:00
var i = 0;
for(i = 0; i < data.user.length; i++) {
planDict[data.user[i].stripeId] = data.user[i];
}
2013-11-05 19:40:45 +00:00
for(i = 0; i < data.business.length; i++) {
planDict[data.business[i].stripeId] = data.business[i];
}
plans = data;
callback(plans);
}, function() { callback([]); });
};
planService.getMatchingBusinessPlan = function(callback) {
planService.getPlans(function() {
planService.getSubscription(null, function(sub) {
var plan = planDict[sub.plan];
if (!plan) {
planService.getMinimumPlan(0, true, callback);
return;
}
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
planService.getMinimumPlan(count, true, callback);
}, function() {
planService.getMinimumPlan(0, true, callback);
});
});
};
2013-11-05 19:40:45 +00:00
planService.getPlans = function(callback) {
planService.verifyLoaded(callback);
};
planService.getPlan = function(planId, callback) {
planService.verifyLoaded(function() {
if (planDict[planId]) {
callback(planDict[planId]);
}
});
};
2013-11-05 19:40:45 +00:00
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
planService.verifyLoaded(function() {
2013-11-05 19:40:45 +00:00
var planSource = plans.user;
if (isBusiness) {
planSource = plans.business;
}
for (var i = 0; i < planSource.length; i++) {
var plan = planSource[i];
if (plan.privateRepos >= privateCount) {
callback(plan);
return;
}
}
callback(null);
});
};
planService.getSubscription = function(orgname, success, failure) {
var url = planService.getSubscriptionUrl(orgname);
var getSubscription = Restangular.one(url);
getSubscription.get().then(success, failure);
};
planService.getSubscriptionUrl = function(orgname) {
return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
};
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
var subscriptionDetails = {
plan: planId
};
if (opt_token) {
subscriptionDetails['token'] = opt_token.id;
}
var url = planService.getSubscriptionUrl(orgname);
var createSubscriptionRequest = Restangular.one(url);
createSubscriptionRequest.customPUT(subscriptionDetails).then(function(resp) {
success(resp);
planService.getPlan(planId, function(plan) {
for (var i = 0; i < listeners.length; ++i) {
listeners[i]['callback'](plan);
}
});
}, failure);
};
planService.getCardInfo = function(orgname, callback) {
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
var getCard = Restangular.one(url);
getCard.customGET().then(function(resp) {
callback(resp.card);
}, function() {
callback({'is_valid': false});
});
};
planService.changePlan = function($scope, orgname, planId, callbacks) {
if (callbacks['started']) {
callbacks['started']();
}
planService.getPlan(planId, function(plan) {
planService.getCardInfo(orgname, function(cardInfo) {
if (plan.price > 0 && !cardInfo.last4) {
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
return;
}
planService.setSubscription(orgname, planId, callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
});
};
planService.changeCreditCard = function($scope, orgname, callbacks) {
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
var cardInfo = {
'token': token.id
};
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
var changeCardRequest = Restangular.one(url);
changeCardRequest.customPOST(cardInfo).then(callbacks['success'], function(resp) {
planService.handleCardError(resp);
callbacks['failure'](resp);
});
});
};
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
currency: 'usd',
name: 'Update credit card',
description: 'Enter your credit card number',
panelLabel: 'Update',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
};
planService.getEmail = function(orgname) {
var email = null;
if (UserService.currentUser()) {
email = UserService.currentUser().email;
if (orgname) {
org = UserService.getOrganization(orgname);
if (org) {
emaiil = org.email;
}
}
}
return email;
};
planService.showSubscribeDialog = function($scope, orgname, planId, callbacks) {
if (callbacks['opening']) {
callbacks['opening']();
}
var submitted = false;
var submitToken = function(token) {
if (submitted) { return; }
submitted = true;
mixpanel.track('plan_subscribe');
$scope.$apply(function() {
if (callbacks['started']) {
callbacks['started']();
}
2013-11-15 20:31:05 +00:00
planService.setSubscription(orgname, planId, callbacks['success'], callbacks['failure'], token);
});
};
planService.getPlan(planId, function(planDetails) {
var email = planService.getEmail(orgname);
StripeCheckout.open({
key: KeyService.stripePublishableKey,
address: false,
email: email,
amount: planDetails.price,
currency: 'usd',
name: 'Quay ' + planDetails.title + ' Subscription',
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
panelLabel: 'Subscribe',
token: submitToken,
image: 'static/img/quay-icon-stripe.png',
opened: function() { $scope.$apply(function() { callbacks['opened']() }); },
closed: function() { $scope.$apply(function() { callbacks['closed']() }); }
});
});
};
return planService;
}]);
}).
directive('match', function($parse) {
return {
require: 'ngModel',
link: function(scope, elem, attrs, ctrl) {
scope.$watch(function() {
return $parse(attrs.match)(scope) === ctrl.$modelValue;
}, function(currentValue) {
ctrl.$setValidity('mismatch', currentValue);
});
}
};
}).
directive('onresize', function ($window, $parse) {
return function (scope, element, attr) {
var fn = $parse(attr.onresize);
var notifyResized = function() {
scope.$apply(function () {
fn(scope);
});
};
angular.element($window).on('resize', null, notifyResized);
scope.$on('$destroy', function() {
angular.element($window).off('resize', null, notifyResized);
});
};
}).
2013-10-03 19:46:22 +00:00
config(['$routeProvider', '$locationProvider', '$analyticsProvider',
function($routeProvider, $locationProvider, $analyticsProvider) {
2013-10-03 20:16:10 +00:00
$analyticsProvider.virtualPageviews(true);
2013-10-10 23:06:04 +00:00
$locationProvider.html5Mode(true);
// WARNING WARNING WARNING
// If you add a route here, you must add a corresponding route in thr endpoints/web.py
// index rule to make sure that deep links directly deep into the app continue to work.
// WARNING WARNING WARNING
2013-10-03 20:16:10 +00:00
$routeProvider.
when('/repository/:namespace/:name', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
fixFooter: true, reloadOnSearch: false}).
when('/repository/:namespace/:name/tag/:tag', {templateUrl: '/static/partials/view-repo.html', controller: RepoCtrl,
fixFooter: true}).
when('/repository/:namespace/:name/image/:image', {templateUrl: '/static/partials/image-view.html', controller: ImageViewCtrl}).
2013-10-03 20:16:10 +00:00
when('/repository/:namespace/:name/admin', {templateUrl: '/static/partials/repo-admin.html', controller:RepoAdminCtrl}).
when('/repository/', {title: 'Repositories', description: 'Public and private docker repositories list',
templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
when('/user/', {title: 'Account Settings', description:'Account settings for Quay.io', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
when('/guide/', {title: 'Guide', description:'Guide to using private docker repositories on Quay.io', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
when('/plans/', {title: 'Plans and Pricing', description: 'Plans and pricing for private docker repositories on Quay.io',
templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
when('/security/', {title: 'Security', description: 'Security features used when transmitting and storing data',
templateUrl: '/static/partials/security.html', controller: SecurityCtrl}).
when('/signin/', {title: 'Sign In', description: 'Sign into Quay.io', templateUrl: '/static/partials/signin.html', controller: SigninCtrl}).
when('/new/', {title: 'Create new repository', description: 'Create a new public or private docker repository, optionally constructing from a dockerfile',
templateUrl: '/static/partials/new-repo.html', controller: NewRepoCtrl}).
when('/organizations/', {title: 'Organizations', description: 'Private docker repository hosting for businesses and organizations',
templateUrl: '/static/partials/organizations.html', controller: OrgsCtrl}).
when('/organizations/new/', {title: 'New Organization', description: 'Create a new organization on Quay.io',
templateUrl: '/static/partials/new-organization.html', controller: NewOrgCtrl}).
when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}).
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
2013-10-09 22:33:25 +00:00
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
2013-10-03 20:16:10 +00:00
otherwise({redirectTo: '/'});
}]).
2013-09-24 22:21:14 +00:00
config(function(RestangularProvider) {
RestangularProvider.setBaseUrl('/api/');
2013-09-26 21:59:20 +00:00
});
quayApp.directive('entityReference', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/entity-reference.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'name': '=name',
'orgname': '=orgname',
'team': '=team',
'isrobot': '=isrobot'
},
controller: function($scope, $element) {
$scope.getPrefix = function(name) {
if (!name) { return ''; }
var plus = name.indexOf('+');
return name.substr(0, plus + 1);
};
$scope.getShortenedName = function(name) {
if (!name) { return ''; }
var plus = name.indexOf('+');
return name.substr(plus + 1);
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('markdownView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/markdown-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'content': '=content',
'firstLineOnly': '=firstLineOnly'
},
controller: function($scope, $element, $sce) {
$scope.getMarkedDown = function(content, firstLineOnly) {
if (firstLineOnly) {
content = getFirstTextLine(content);
}
return $sce.trustAsHtml(getMarkedDown(content));
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('repoCircle', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-circle.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'repo': '=repo'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('signinForm', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/signin-form.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'redirectUrl': '=redirectUrl'
},
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) {
$scope.githubClientId = KeyService.githubClientId;
var appendMixpanelId = function() {
if (mixpanel.get_distinct_id !== undefined) {
$scope.mixpanelDistinctIdClause = "&state=" + mixpanel.get_distinct_id();
} else {
// Mixpanel not yet loaded, try again later
$timeout(appendMixpanelId, 200);
}
};
appendMixpanelId();
$scope.signin = function() {
var signinPost = Restangular.one('signin');
signinPost.customPOST($scope.user).then(function() {
$scope.needsEmailVerification = false;
$scope.invalidCredentials = false;
// Redirect to the specified page or the landing page
UserService.load();
$location.path($scope.redirectUrl ? $scope.redirectUrl : '/');
}, function(result) {
$scope.needsEmailVerification = result.data.needsEmailVerification;
$scope.invalidCredentials = result.data.invalidCredentials;
});
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('plansTable', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/plans-table.html',
replace: false,
2013-11-23 01:14:44 +00:00
transclude: false,
restrict: 'C',
scope: {
'plans': '=plans',
'currentPlan': '=currentPlan'
},
controller: function($scope, $element) {
$scope.setPlan = function(plan) {
$scope.currentPlan = plan;
};
}
};
return directiveDefinitionObject;
});
2013-11-23 01:14:44 +00:00
quayApp.directive('dockerAuthDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/docker-auth-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'username': '=username',
'token': '=token',
'shown': '=shown',
'counter': '=counter'
},
controller: function($scope, $element, Restangular) {
$scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){}
return false;
};
$scope.downloadCfg = function() {
var auth = $.base64.encode($scope.username + ":" + $scope.token);
config = {
"https://quay.io/v1/": {
"auth": auth,
"email": ""
}
};
var file = JSON.stringify(config, null, ' ');
var blob = new Blob([file]);
saveAs(blob, '.dockercfg');
};
var show = function(r) {
if (!$scope.shown || !$scope.username || !$scope.token) {
$('#dockerauthmodal').modal('hide');
return;
}
$('#copyClipboard').clipboardCopy();
$('#dockerauthmodal').modal({});
};
$scope.$watch('counter', show);
$scope.$watch('shown', show);
$scope.$watch('username', show);
$scope.$watch('token', show);
}
};
return directiveDefinitionObject;
});
2013-11-22 23:20:51 +00:00
quayApp.filter('visibleLogFilter', function () {
return function (logs, allowed) {
if (!allowed) {
return logs;
}
var filtered = [];
angular.forEach(logs, function (log) {
if (allowed[log.kind]) {
filtered.push(log);
}
});
return filtered;
};
});
quayApp.directive('logsView', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/logs-view.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user',
'visible': '=visible'
},
controller: function($scope, $element, $sce, Restangular) {
$scope.loading = true;
$scope.logs = null;
$scope.kindsAllowed = null;
$scope.chartVisible = true;
var logDescriptions = {
'account_change_plan': 'Change plan',
'account_change_cc': 'Update credit card',
'account_change_password': 'Change password',
'account_convert': 'Convert account to organization',
'create_robot': 'Create Robot Account: {robot}',
'delete_robot': 'Delete Robot Account: {robot}',
'create_repo': 'Create Repository: {repo}',
'push_repo': 'Push to repository: {repo}',
'pull_repo': function(metadata) {
if (metadata.token) {
return 'Pull repository {repo} via token {token}';
} else if (metadata.username) {
return 'Pull repository {repo} by {username}';
} else {
return 'Public pull of repository {repo} by {_ip}';
}
},
'delete_repo': 'Delete repository: {repo}',
'change_repo_permission': 'Change permission for user {username} in repository {repo} to {role}',
'delete_repo_permission': 'Remove permission for user {username} from repository {repo}',
'change_repo_visibility': 'Change visibility for repository {repo} to {visibility}',
'add_repo_accesstoken': 'Create access token {token} in repository {repo}',
'delete_repo_accesstoken': 'Delete access token {token} in repository {repo}',
'add_repo_webhook': 'Add webhook in repository {repo}',
'delete_repo_webhook': 'Delete webhook in repository {repo}',
'set_repo_description': 'Change description for repository {repo}: {description}',
'build_dockerfile': 'Build image from Dockerfile for repository {repo}',
'org_create_team': 'Create team: {team}',
'org_delete_team': 'Delete team: {team}',
'org_add_team_member': 'Add member {member} to team {team}',
'org_remove_team_member': 'Remove member {member} from team {team}',
'org_set_team_description': 'Change description of team {team}: {description}',
'org_set_team_role': 'Change permission of team {team} to {role}'
};
var logKinds = {
'account_change_plan': 'Change plan',
'account_change_cc': 'Update credit card',
'account_change_password': 'Change password',
'account_convert': 'Convert account to organization',
'create_robot': 'Create Robot Account',
'delete_robot': 'Delete Robot Account',
'create_repo': 'Create Repository',
'push_repo': 'Push to repository',
'pull_repo': 'Pull repository',
'delete_repo': 'Delete repository',
'change_repo_permission': 'Change repository permission',
'delete_repo_permission': 'Remove user permission from repository',
'change_repo_visibility': 'Change repository visibility',
'add_repo_accesstoken': 'Create access token',
'delete_repo_accesstoken': 'Delete access token',
'add_repo_webhook': 'Add webhook',
'delete_repo_webhook': 'Delete webhook',
'set_repo_description': 'Change repository description',
'build_dockerfile': 'Build image from Dockerfile',
'org_create_team': 'Create team',
'org_delete_team': 'Delete team',
'org_add_team_member': 'Add team member',
'org_remove_team_member': 'Remove team member',
'org_set_team_description': 'Change team description',
'org_set_team_role': 'Change team permission'
};
var update = function() {
if (!$scope.visible || (!$scope.organization && !$scope.user)) {
return;
}
2013-11-27 21:56:07 +00:00
$scope.loading = true;
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'logs') :
getRestUrl('user/logs');
var loadLogs = Restangular.one(url);
loadLogs.customGET().then(function(resp) {
if (!$scope.chart) {
2013-11-27 21:56:07 +00:00
$scope.chart = new LogUsageChart(logKinds);
$($scope.chart).bind('filteringChanged', function(e) {
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
});
}
2013-11-27 21:56:07 +00:00
$scope.chart.draw('bar-chart', resp.logs);
$scope.kindsAllowed = null;
$scope.logs = resp.logs;
$scope.loading = false;
});
};
$scope.toggleChart = function() {
$scope.chartVisible = !$scope.chartVisible;
};
$scope.isVisible = function(allowed, kind) {
return allowed == null || allowed.hasOwnProperty(kind);
};
$scope.getColor = function(kind) {
return $scope.chart.getColor(kind);
};
$scope.getDescription = function(log) {
var description = logDescriptions[log.kind] || logTitles[log.kind] || log.kind;
log.metadata['_ip'] = log.ip;
for (var key in log.metadata) {
if (log.metadata.hasOwnProperty(key)) {
var markedDown = getMarkedDown(log.metadata[key].toString());
markedDown = markedDown.substr('<p>'.length, markedDown.length - '<p></p>'.length);
description = description.replace('{' + key + '}', '<code>' + markedDown + '</code>');
}
}
return $sce.trustAsHtml(description);
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
$scope.$watch('visible', update);
}
};
return directiveDefinitionObject;
});
2013-11-22 23:20:51 +00:00
quayApp.directive('robotsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/robots-manager.html',
replace: false,
2013-11-23 01:14:44 +00:00
transclude: false,
2013-11-22 23:20:51 +00:00
restrict: 'C',
scope: {
'organization': '=organization',
'user': '=user'
},
controller: function($scope, $element, Restangular) {
$scope.robots = null;
$scope.loading = false;
2013-11-23 01:14:44 +00:00
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
$scope.showRobot = function(info) {
$scope.shownRobot = info;
$scope.showRobotCounter++;
};
2013-11-22 23:20:51 +00:00
$scope.getShortenedName = function(name) {
var plus = name.indexOf('+');
return name.substr(plus + 1);
};
$scope.getPrefix = function(name) {
var plus = name.indexOf('+');
return name.substr(0, plus);
};
2013-11-23 01:14:44 +00:00
$scope.createRobot = function(name) {
if (!name) { return; }
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', name) :
getRestUrl('user/robots', name);
var createRobot = Restangular.one(url);
createRobot.customPUT().then(function(resp) {
$scope.robots.push(resp);
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'The robot account could not be created',
"title": "Cannot create robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
2013-11-22 23:20:51 +00:00
$scope.deleteRobot = function(info) {
var shortName = $scope.getShortenedName(info.name);
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', shortName) :
getRestUrl('user/robots', shortName);
var deleteRobot = Restangular.one(url);
deleteRobot.customDELETE().then(function(resp) {
for (var i = 0; i < $scope.robots.length; ++i) {
if ($scope.robots[i].name == info.name) {
2013-11-23 01:14:44 +00:00
$scope.robots.splice(i, 1);
2013-11-22 23:20:51 +00:00
return;
}
}
}, function() {
2013-11-23 01:14:44 +00:00
bootbox.dialog({
"message": 'The selected robot account could not be deleted',
"title": "Cannot delete robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
2013-11-22 23:20:51 +00:00
});
};
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
if ($scope.loading) { return; }
$scope.loading = true;
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots') : 'user/robots';
var getRobots = Restangular.one(url);
getRobots.customGET($scope.obj).then(function(resp) {
$scope.robots = resp.robots;
$scope.loading = false;
});
};
$scope.$watch('organization', update);
$scope.$watch('user', update);
}
};
return directiveDefinitionObject;
});
2013-11-23 01:14:44 +00:00
quayApp.directive('popupInputButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/popup-input-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'placeholder': '=placeholder',
'pattern': '=pattern',
'submitted': '&submitted'
},
controller: function($scope, $element) {
$scope.popupShown = function() {
setTimeout(function() {
var box = $('#input-box');
box[0].value = '';
box.focus();
}, 10);
};
$scope.getRegexp = function(pattern) {
if (!pattern) {
pattern = '.*';
}
return new RegExp(pattern);
};
$scope.inputSubmit = function() {
var box = $('#input-box');
if (box.hasClass('ng-invalid')) { return; }
var entered = box[0].value;
if (!entered) {
return;
}
if ($scope.submitted) {
$scope.submitted({'value': entered});
}
};
}
};
return directiveDefinitionObject;
});
2013-11-22 23:20:51 +00:00
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/organization-header.html',
replace: false,
2013-11-05 21:15:04 +00:00
transclude: true,
restrict: 'C',
scope: {
'organization': '=organization',
'teamName': '=teamName'
},
controller: function($scope, $element) {
}
};
return directiveDefinitionObject;
});
quayApp.directive('markdownInput', function () {
var counter = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/markdown-input.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'content': '=content',
'canWrite': '=canWrite',
'contentChanged': '=contentChanged',
'fieldTitle': '=fieldTitle'
},
controller: function($scope, $element) {
var elm = $element[0];
$scope.id = (counter++);
$scope.editContent = function() {
if (!$scope.canWrite) { return; }
if (!$scope.markdownDescriptionEditor) {
var converter = Markdown.getSanitizingConverter();
var editor = new Markdown.Editor(converter, '-description-' + $scope.id);
editor.run();
$scope.markdownDescriptionEditor = editor;
}
$('#wmd-input-description-' + $scope.id)[0].value = $scope.content;
$(elm).find('.modal').modal({});
};
$scope.saveContent = function() {
$scope.content = $('#wmd-input-description-' + $scope.id)[0].value;
$(elm).find('.modal').modal('hide');
if ($scope.contentChanged) {
$scope.contentChanged($scope.content);
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('repoSearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/repo-search.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, Restangular) {
var searchToken = 0;
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
++searchToken;
}, true);
var element = $($element[0].childNodes[0]);
element.typeahead({
name: 'repositories',
remote: {
url: '/api/find/repository?query=%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
url += '&cb=' + searchToken;
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.repositories.length; ++i) {
var repo = data.repositories[i];
datums.push({
'value': repo.name,
'tokens': [repo.name, repo.namespace],
'repo': repo
});
}
return datums;
}
},
template: function (datum) {
template = '<div class="repo-mini-listing">';
template += '<i class="fa fa-hdd fa-lg"></i>'
template += '<span class="name">' + datum.repo.namespace +'/' + datum.repo.name + '</span>'
if (datum.repo.description) {
template += '<span class="description">' + getFirstTextLine(datum.repo.description) + '</span>'
}
template += '</div>'
return template;
}
});
element.on('typeahead:selected', function (e, datum) {
element.typeahead('setQuery', '');
document.location = '/repository/' + datum.repo.namespace + '/' + datum.repo.name;
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('headerBar', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/header-bar.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
},
controller: function($scope, $element, $location, UserService, Restangular) {
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
$scope.user = currentUser;
}, true);
$scope.signout = function() {
var signoutPost = Restangular.one('signout');
signoutPost.customPOST().then(function() {
UserService.load();
$location.path('/');
});
};
$scope.appLinkTarget = function() {
if ($("div[ng-view]").length === 0) {
return "_self";
}
return "";
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('entitySearch', function () {
var number = 0;
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/entity-search.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
2013-11-20 23:23:59 +00:00
'namespace': '=namespace',
'inputTitle': '=inputTitle',
2013-11-20 23:23:59 +00:00
'entitySelected': '=entitySelected',
'includeTeams': '=includeTeams'
},
controller: function($scope, $element) {
if (!$scope.entitySelected) { return; }
number++;
var input = $element[0].firstChild;
2013-11-20 23:23:59 +00:00
$scope.namespace = $scope.namespace || '';
$(input).typeahead({
name: 'entities' + number,
remote: {
url: '/api/entities/%QUERY',
replace: function (url, uriEncodedQuery) {
url = url.replace('%QUERY', uriEncodedQuery);
2013-11-20 23:23:59 +00:00
url += '?namespace=' + encodeURIComponent($scope.namespace);
if ($scope.includeTeams) {
url += '&includeTeams=true'
}
return url;
},
filter: function(data) {
var datums = [];
for (var i = 0; i < data.results.length; ++i) {
var entity = data.results[i];
datums.push({
'value': entity.name,
'tokens': [entity.name],
'entity': entity
});
}
return datums;
}
},
template: function (datum) {
template = '<div class="entity-mini-listing">';
if (datum.entity.kind == 'user' && !datum.entity.is_robot) {
template += '<i class="fa fa-user fa-lg"></i>';
} else if (datum.entity.kind == 'user' && datum.entity.is_robot) {
template += '<i class="fa fa-wrench fa-lg"></i>';
} else if (datum.entity.kind == 'team') {
template += '<i class="fa fa-group fa-lg"></i>';
}
template += '<span class="name">' + datum.value + '</span>';
2013-11-20 23:23:59 +00:00
if (datum.entity.is_org_member !== undefined && !datum.entity.is_org_member && datum.kind == 'user') {
template += '<div class="alert-warning warning">This user is outside your organization</div>';
}
template += '</div>';
return template;
},
});
$(input).on('typeahead:selected', function(e, datum) {
$(input).typeahead('setQuery', '');
$scope.entitySelected(datum.entity);
});
$scope.$watch('inputTitle', function(title) {
input.setAttribute('placeholder', title);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('roleGroup', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/role-group.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'roles': '=roles',
'currentRole': '=currentRole',
'roleChanged': '&roleChanged'
},
controller: function($scope, $element) {
$scope.setRole = function(role) {
if ($scope.currentRole == role) { return; }
if ($scope.roleChanged) {
$scope.roleChanged({'role': role});
} else {
$scope.currentRole = role;
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('billingOptions', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/billing-options.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'organization': '=organization'
},
controller: function($scope, $element, PlanService, Restangular) {
$scope.invoice_email = false;
$scope.currentCard = null;
// Listen to plan changes.
PlanService.registerListener(this, function(plan) {
if (plan && plan.price > 0) {
update();
}
});
$scope.$on('$destroy', function() {
PlanService.unregisterListener(this);
});
$scope.changeCard = function() {
var previousCard = $scope.currentCard;
$scope.changingCard = true;
var callbacks = {
'opened': function() { $scope.changingCard = true; },
'closed': function() { $scope.changingCard = false; },
'started': function() { $scope.currentCard = null; },
'success': function(resp) {
$scope.currentCard = resp.card;
$scope.changingCard = false;
},
'failure': function(resp) {
$scope.changingCard = false;
$scope.currentCard = previousCard;
if (!PlanService.isCardError(resp)) {
$('#cannotchangecardModal').modal({});
}
}
};
PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks);
};
$scope.getCreditImage = function(creditInfo) {
2013-11-15 23:34:54 +00:00
if (!creditInfo || !creditInfo.type) { return 'credit.png'; }
var kind = creditInfo.type.toLowerCase() || 'credit';
var supported = {
'american express': 'amex',
'credit': 'credit',
'diners club': 'diners',
'discover': 'discover',
'jcb': 'jcb',
'mastercard': 'mastercard',
'visa': 'visa'
};
kind = supported[kind] || 'credit';
return kind + '.png';
};
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
$scope.obj = $scope.user ? $scope.user : $scope.organization;
$scope.invoice_email = $scope.obj.invoice_email;
// Load the credit card information.
PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) {
$scope.currentCard = card;
});
};
var save = function() {
$scope.working = true;
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name) : 'user/';
var conductSave = Restangular.one(url);
conductSave.customPUT($scope.obj).then(function(resp) {
$scope.working = false;
});
};
var checkSave = function() {
if (!$scope.obj) { return; }
if ($scope.obj.invoice_email != $scope.invoice_email) {
$scope.obj.invoice_email = $scope.invoice_email;
save();
}
};
$scope.$watch('invoice_email', checkSave);
$scope.$watch('organization', update);
$scope.$watch('user', update);
}
};
return directiveDefinitionObject;
});
quayApp.directive('planManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/plan-manager.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'organization': '=organization',
'readyForPlan': '&readyForPlan',
'planChanged': '&planChanged'
},
controller: function($scope, $element, PlanService, Restangular) {
var hasSubscription = false;
$scope.getActiveSubClass = function() {
return 'active';
};
$scope.changeSubscription = function(planId) {
if ($scope.planChanging) { return; }
var callbacks = {
'opening': function() { $scope.planChanging = true; },
'started': function() { $scope.planChanging = true; },
'opened': function() { $scope.planChanging = true; },
'closed': function() { $scope.planChanging = false; },
'success': subscribedToPlan,
'failure': function(resp) {
$scope.planChanging = false;
}
};
PlanService.changePlan($scope, $scope.organization, planId, callbacks);
};
$scope.cancelSubscription = function() {
$scope.changeSubscription(getFreePlan());
};
var subscribedToPlan = function(sub) {
$scope.subscription = sub;
if (sub.plan != getFreePlan()) {
hasSubscription = true;
}
PlanService.getPlan(sub.plan, function(subscribedPlan) {
$scope.subscribedPlan = subscribedPlan;
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
if ($scope.planChanged) {
$scope.planChanged({ 'plan': subscribedPlan });
}
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
$scope.limit = 'over';
} else if (sub.usedPrivateRepos == $scope.subscribedPlan.privateRepos) {
$scope.limit = 'at';
} else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) {
$scope.limit = 'near';
} else {
$scope.limit = 'none';
}
if (!$scope.chart) {
$scope.chart = new RepositoryUsageChart();
$scope.chart.draw('repository-usage-chart');
}
$scope.chart.update(sub.usedPrivateRepos || 0, $scope.subscribedPlan.privateRepos || 0);
$scope.planChanging = false;
$scope.planLoading = false;
});
};
var getFreePlan = function() {
for (var i = 0; i < $scope.plans.length; ++i) {
if ($scope.plans[i].price == 0) {
return $scope.plans[i].stripeId;
}
}
return 'free';
};
var update = function() {
$scope.planLoading = true;
if (!$scope.plans) { return; }
PlanService.getSubscription($scope.organization, subscribedToPlan, function() {
// User/Organization has no subscription.
subscribedToPlan({ 'plan': getFreePlan() });
});
};
var loadPlans = function() {
if ($scope.plans || $scope.loadingPlans) { return; }
if (!$scope.user && !$scope.organization) { return; }
$scope.loadingPlans = true;
PlanService.getPlans(function(plans) {
$scope.plans = plans[$scope.organization ? 'business' : 'user'];
update();
if ($scope.readyForPlan) {
var planRequested = $scope.readyForPlan();
if (planRequested && planRequested != getFreePlan()) {
$scope.changeSubscription(planRequested);
}
}
});
};
// Start the initial download.
$scope.planLoading = true;
loadPlans();
2013-11-07 00:19:21 +00:00
$scope.$watch('organization', loadPlans);
$scope.$watch('user', loadPlans);
}
};
return directiveDefinitionObject;
});
quayApp.directive('namespaceSelector', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/namespace-selector.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'namespace': '=namespace',
'requireCreate': '=requireCreate'
},
controller: function($scope, $element, $routeParams, $cookieStore) {
$scope.namespaces = {};
$scope.initialize = function(user) {
var namespaces = {};
namespaces[user.username] = user;
if (user.organizations) {
for (var i = 0; i < user.organizations.length; ++i) {
namespaces[user.organizations[i].name] = user.organizations[i];
}
}
var initialNamespace = $routeParams['namespace'] || $cookieStore.get('quay.currentnamespace') || $scope.user.username;
$scope.namespaces = namespaces;
$scope.setNamespace($scope.namespaces[initialNamespace]);
};
$scope.setNamespace = function(namespaceObj) {
if (!namespaceObj) {
namespaceObj = $scope.namespaces[$scope.user.username];
}
if ($scope.requireCreate && !namespaceObj.can_create_repo) {
namespaceObj = $scope.namespaces[$scope.user.username];
}
var newNamespace = namespaceObj.name || namespaceObj.username;
$scope.namespaceObj = namespaceObj;
$scope.namespace = newNamespace;
$cookieStore.put('quay.currentnamespace', newNamespace);
};
$scope.$watch('user', function(user) {
$scope.user = user;
$scope.initialize(user);
});
}
};
return directiveDefinitionObject;
});
quayApp.directive('buildStatus', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/build-status.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'build': '=build'
},
controller: function($scope, $element) {
$scope.getBuildProgress = function(buildInfo) {
switch (buildInfo.status) {
case 'building':
return (buildInfo.current_command / buildInfo.total_commands) * 100;
break;
case 'pushing':
var imagePercentDecimal = (buildInfo.image_completion_percent / 100);
return ((buildInfo.current_image + imagePercentDecimal) / buildInfo.total_images) * 100;
break;
case 'complete':
return 100;
break;
case 'initializing':
case 'starting':
case 'waiting':
return 0;
break;
}
return -1;
};
$scope.getBuildMessage = function(buildInfo) {
switch (buildInfo.status) {
case 'initializing':
return 'Starting Dockerfile build';
break;
case 'starting':
case 'waiting':
case 'building':
return 'Building image from Dockerfile';
break;
case 'pushing':
return 'Pushing image built from Dockerfile';
break;
case 'complete':
return 'Dockerfile build completed and pushed';
break;
case 'error':
return 'Dockerfile build failed: ' + buildInfo.message;
break;
}
};
}
};
return directiveDefinitionObject;
});
2013-11-05 22:20:43 +00:00
// Note: ngBlur is not yet in Angular stable, so we add it manaully here.
quayApp.directive('ngBlur', function() {
return function( scope, elem, attrs ) {
elem.bind('blur', function() {
scope.$apply(attrs.ngBlur);
});
};
});
quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', '$http',
function($location, $rootScope, Restangular, UserService, $http) {
Restangular.setErrorInterceptor(function(response) {
if (response.status == 401) {
$('#sessionexpiredModal').modal({});
return false;
}
return true;
});
2013-09-26 23:07:25 +00:00
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
if (current.$$route.title) {
$rootScope.title = current.$$route.title;
}
if (current.$$route.description) {
$rootScope.description = current.$$route.description;
} else {
$rootScope.description = 'Hosted private docker repositories. Includes full user management and history. Free for public repositories.';
}
$rootScope.fixFooter = !!current.$$route.fixFooter;
2013-09-26 23:07:25 +00:00
});
var initallyChecked = false;
window.__isLoading = function() {
if (!initallyChecked) {
initallyChecked = true;
return true;
}
return $http.pendingRequests.length > 0;
};
}]);