Add proper messaging if an organization has gone over the repo limit. This change also moves plan information into the server
This commit is contained in:
parent
1f0b142535
commit
9fa77aaa48
6 changed files with 226 additions and 99 deletions
|
@ -183,7 +183,8 @@ def lookup_organization(name, username=None):
|
||||||
if name == 'testorg':
|
if name == 'testorg':
|
||||||
return dotdict({
|
return dotdict({
|
||||||
'username': 'testorg',
|
'username': 'testorg',
|
||||||
'email': 'testorg@quay.io'
|
'email': 'testorg@quay.io',
|
||||||
|
'stripe_id': ''
|
||||||
})
|
})
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
47
data/plans.py
Normal file
47
data/plans.py
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import json
|
||||||
|
|
||||||
|
USER_PLANS = [
|
||||||
|
{
|
||||||
|
'title': 'Open Source',
|
||||||
|
'price': 0,
|
||||||
|
'privateRepos': 0,
|
||||||
|
'stripeId': 'free',
|
||||||
|
'audience': 'Share with the world',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Micro',
|
||||||
|
'price': 700,
|
||||||
|
'privateRepos': 5,
|
||||||
|
'stripeId': 'micro',
|
||||||
|
'audience': 'For smaller teams',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Basic',
|
||||||
|
'price': 1200,
|
||||||
|
'privateRepos': 10,
|
||||||
|
'stripeId': 'small',
|
||||||
|
'audience': 'For your basic team',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'title': 'Medium',
|
||||||
|
'price': 2200,
|
||||||
|
'privateRepos': 20,
|
||||||
|
'stripeId': 'medium',
|
||||||
|
'audience': 'For medium-sized teams',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def getPlan(id):
|
||||||
|
""" Returns the plan with the given ID or None if none. """
|
||||||
|
for plan in USER_PLANS:
|
||||||
|
if plan['stripeId'] == id:
|
||||||
|
return plan
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def isPlanActive(stripe_subscription):
|
||||||
|
""" Returns whether the plan is active. """
|
||||||
|
# TODO: this.
|
||||||
|
return True
|
|
@ -16,6 +16,7 @@ import storage
|
||||||
from data import model
|
from data import model
|
||||||
from data.userfiles import UserRequestFiles
|
from data.userfiles import UserRequestFiles
|
||||||
from data.queue import dockerfile_build_queue
|
from data.queue import dockerfile_build_queue
|
||||||
|
from data.plans import USER_PLANS, getPlan, isPlanActive
|
||||||
from app import app
|
from app import app
|
||||||
from util.email import send_confirmation_email, send_recovery_email
|
from util.email import send_confirmation_email, send_recovery_email
|
||||||
from util.names import parse_repository_name
|
from util.names import parse_repository_name
|
||||||
|
@ -45,11 +46,13 @@ def api_login_required(f):
|
||||||
def handle_dme(ex):
|
def handle_dme(ex):
|
||||||
return make_response(ex.message, 400)
|
return make_response(ex.message, 400)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/')
|
@app.route('/api/')
|
||||||
def welcome():
|
def welcome():
|
||||||
return make_response('welcome', 200)
|
return make_response('welcome', 200)
|
||||||
|
|
||||||
|
@app.route('/api/plans/')
|
||||||
|
def plans_list():
|
||||||
|
return jsonify({ 'plans': USER_PLANS })
|
||||||
|
|
||||||
@app.route('/api/user/', methods=['GET'])
|
@app.route('/api/user/', methods=['GET'])
|
||||||
def get_logged_in_user():
|
def get_logged_in_user():
|
||||||
|
@ -216,6 +219,31 @@ def get_organization(orgname):
|
||||||
return jsonify(org_view(organization, teams))
|
return jsonify(org_view(organization, teams))
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||||
|
def get_organization_private_allowed(orgname):
|
||||||
|
if current_user.is_anonymous():
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
user = current_user.db_user()
|
||||||
|
organization = model.lookup_organization(orgname, username = user.username)
|
||||||
|
if not organization:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
private_repos = model.get_private_repo_count(organization.username)
|
||||||
|
if organization.stripe_id:
|
||||||
|
cus = stripe.Customer.retrieve(organization.stripe_id)
|
||||||
|
|
||||||
|
if cus.subscription and isPlanActive(cus.subscription):
|
||||||
|
repos_allowed = getPlan(cus.subscription.plan.id)
|
||||||
|
return jsonify({
|
||||||
|
'privateAllowed': (private_repos < repos_allowed)
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'privateAllowed': False
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository', methods=['POST'])
|
@app.route('/api/repository', methods=['POST'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def create_repo_api():
|
def create_repo_api():
|
||||||
|
@ -786,7 +814,7 @@ def get_subscription():
|
||||||
if user.stripe_id:
|
if user.stripe_id:
|
||||||
cus = stripe.Customer.retrieve(user.stripe_id)
|
cus = stripe.Customer.retrieve(user.stripe_id)
|
||||||
|
|
||||||
if cus.subscription:
|
if cus.subscription:
|
||||||
return jsonify(subscription_view(cus.subscription, private_repos))
|
return jsonify(subscription_view(cus.subscription, private_repos))
|
||||||
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
|
|
124
static/js/app.js
124
static/js/app.js
|
@ -10,6 +10,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
}
|
}
|
||||||
|
|
||||||
var userService = {}
|
var userService = {}
|
||||||
|
var currentSubscription = null;
|
||||||
|
|
||||||
userService.load = function() {
|
userService.load = function() {
|
||||||
var userFetch = Restangular.one('user/');
|
var userFetch = Restangular.one('user/');
|
||||||
|
@ -30,6 +31,20 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
userService.resetCurrentSubscription = function() {
|
||||||
|
currentSubscription = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
userService.getCurrentSubscription = function(callback, failure) {
|
||||||
|
if (currentSubscription) { callback(currentSubscription); }
|
||||||
|
|
||||||
|
var getSubscription = Restangular.one('user/plan');
|
||||||
|
getSubscription.get().then(function(sub) {
|
||||||
|
currentSubscription = sub;
|
||||||
|
callback(sub);
|
||||||
|
}, failure);
|
||||||
|
};
|
||||||
|
|
||||||
userService.currentUser = function() {
|
userService.currentUser = function() {
|
||||||
return userResponse;
|
return userResponse;
|
||||||
}
|
}
|
||||||
|
@ -55,62 +70,48 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
$provide.factory('PlanService', ['Restangular', 'KeyService', function(Restangular, KeyService) {
|
$provide.factory('PlanService', ['Restangular', 'KeyService', function(Restangular, KeyService) {
|
||||||
var plans = [
|
var plans = null;
|
||||||
{
|
|
||||||
title: 'Open Source',
|
|
||||||
price: 0,
|
|
||||||
privateRepos: 0,
|
|
||||||
stripeId: 'free',
|
|
||||||
audience: 'Share with the world',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Micro',
|
|
||||||
price: 700,
|
|
||||||
privateRepos: 5,
|
|
||||||
stripeId: 'micro',
|
|
||||||
audience: 'For smaller teams',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Basic',
|
|
||||||
price: 1200,
|
|
||||||
privateRepos: 10,
|
|
||||||
stripeId: 'small',
|
|
||||||
audience: 'For your basic team',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Medium',
|
|
||||||
price: 2200,
|
|
||||||
privateRepos: 20,
|
|
||||||
stripeId: 'medium',
|
|
||||||
audience: 'For medium-sized teams',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
var planDict = {};
|
var planDict = {};
|
||||||
var i;
|
|
||||||
for(i = 0; i < plans.length; i++) {
|
|
||||||
planDict[plans[i].stripeId] = plans[i];
|
|
||||||
}
|
|
||||||
|
|
||||||
var planService = {}
|
var planService = {}
|
||||||
|
|
||||||
planService.planList = function() {
|
planService.verifyLoaded = function(callback) {
|
||||||
return plans;
|
if (plans) {
|
||||||
};
|
callback(plans);
|
||||||
|
return;
|
||||||
planService.getPlan = function(planId) {
|
|
||||||
return planDict[planId];
|
|
||||||
};
|
|
||||||
|
|
||||||
planService.getMinimumPlan = function(privateCount) {
|
|
||||||
for (var i = 0; i < plans.length; i++) {
|
|
||||||
var plan = plans[i];
|
|
||||||
if (plan.privateRepos >= privateCount) {
|
|
||||||
return plan;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
var getPlans = Restangular.one('plans');
|
||||||
|
getPlans.get().then(function(data) {
|
||||||
|
for(var i = 0; i < data.plans.length; i++) {
|
||||||
|
planDict[data.plans[i].stripeId] = data.plans[i];
|
||||||
|
}
|
||||||
|
plans = data.plans;
|
||||||
|
callback(plans);
|
||||||
|
}, function() { callback([]); });
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.getPlanList = function(callback) {
|
||||||
|
planService.verifyLoaded(callback);
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.getPlan = function(planId, callback) {
|
||||||
|
planService.verifyLoaded(function() {
|
||||||
|
callback(planDict[planId]);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.getMinimumPlan = function(privateCount, callback) {
|
||||||
|
planService.verifyLoaded(function() {
|
||||||
|
for (var i = 0; i < plans.length; i++) {
|
||||||
|
var plan = plans[i];
|
||||||
|
if (plan.privateRepos >= privateCount) {
|
||||||
|
callback(plan);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.showSubscribeDialog = function($scope, planId, started, success, failed) {
|
planService.showSubscribeDialog = function($scope, planId, started, success, failed) {
|
||||||
|
@ -132,16 +133,17 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var planDetails = planService.getPlan(planId)
|
planService.getPlan(planId, function(planDetails) {
|
||||||
StripeCheckout.open({
|
StripeCheckout.open({
|
||||||
key: KeyService.stripePublishableKey,
|
key: KeyService.stripePublishableKey,
|
||||||
address: false, // TODO change to true
|
address: false,
|
||||||
amount: planDetails.price,
|
amount: planDetails.price,
|
||||||
currency: 'usd',
|
currency: 'usd',
|
||||||
name: 'Quay ' + planDetails.title + ' Subscription',
|
name: 'Quay ' + planDetails.title + ' Subscription',
|
||||||
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
description: 'Up to ' + planDetails.privateRepos + ' private repositories',
|
||||||
panelLabel: 'Subscribe',
|
panelLabel: 'Subscribe',
|
||||||
token: submitToken
|
token: submitToken
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -156,7 +156,11 @@ function SigninCtrl($scope, $location, $timeout, Restangular, KeyService, UserSe
|
||||||
};
|
};
|
||||||
|
|
||||||
function PlansCtrl($scope, UserService, PlanService) {
|
function PlansCtrl($scope, UserService, PlanService) {
|
||||||
$scope.plans = PlanService.planList();
|
// Load the list of plans.
|
||||||
|
PlanService.getPlanList(function(plans) {
|
||||||
|
$scope.plans = plans;
|
||||||
|
$scope.status = 'ready';
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.user = currentUser;
|
$scope.user = currentUser;
|
||||||
|
@ -169,8 +173,6 @@ function PlansCtrl($scope, UserService, PlanService) {
|
||||||
$('#signinModal').modal({});
|
$('#signinModal').modal({});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.status = 'ready';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function GuideCtrl($scope) {
|
function GuideCtrl($scope) {
|
||||||
|
@ -743,7 +745,10 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
||||||
$scope.plans = PlanService.planList();
|
// Load the list of plans.
|
||||||
|
PlanService.getPlanList(function(plans) {
|
||||||
|
$scope.plans = plans;
|
||||||
|
});
|
||||||
|
|
||||||
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.askForPassword = currentUser.askForPassword;
|
$scope.askForPassword = currentUser.askForPassword;
|
||||||
|
@ -751,28 +756,29 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
|
|
||||||
var subscribedToPlan = function(sub) {
|
var subscribedToPlan = function(sub) {
|
||||||
$scope.subscription = sub;
|
$scope.subscription = sub;
|
||||||
$scope.subscribedPlan = PlanService.getPlan(sub.plan);
|
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
||||||
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
$scope.subscribedPlan = subscribedPlan;
|
||||||
|
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
||||||
|
|
||||||
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
||||||
$scope.errorMessage = 'You are using more private repositories than your plan allows, please upgrate your subscription to avoid disruptions in your service.';
|
$scope.errorMessage = 'You are using more private repositories than your plan allows, please upgrate your subscription to avoid disruptions in your service.';
|
||||||
} else {
|
} else {
|
||||||
$scope.errorMessage = null;
|
$scope.errorMessage = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.planLoading = false;
|
$scope.planLoading = false;
|
||||||
$scope.planChanging = false;
|
$scope.planChanging = false;
|
||||||
|
|
||||||
mixpanel.people.set({
|
mixpanel.people.set({
|
||||||
'plan': sub.plan
|
'plan': sub.plan
|
||||||
});
|
});
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.planLoading = true;
|
$scope.planLoading = true;
|
||||||
var getSubscription = Restangular.one('user/plan');
|
UserService.getCurrentSubscription(subscribedToPlan, function() {
|
||||||
getSubscription.get().then(subscribedToPlan, function() {
|
|
||||||
// User has no subscription
|
// User has no subscription
|
||||||
$scope.planLoading = false;
|
$scope.planChanging = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.planChanging = false;
|
$scope.planChanging = false;
|
||||||
|
@ -782,6 +788,7 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
$scope.planChanging = true;
|
$scope.planChanging = true;
|
||||||
}, function(plan) {
|
}, function(plan) {
|
||||||
// Subscribed.
|
// Subscribed.
|
||||||
|
UserService.resetCurrentSubscription();
|
||||||
subscribedToPlan(plan);
|
subscribedToPlan(plan);
|
||||||
}, function() {
|
}, function() {
|
||||||
// Failure.
|
// Failure.
|
||||||
|
@ -798,6 +805,7 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
plan: planId,
|
plan: planId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
UserService.resetCurrentSubscription();
|
||||||
var changeSubscriptionRequest = Restangular.one('user/plan');
|
var changeSubscriptionRequest = Restangular.one('user/plan');
|
||||||
changeSubscriptionRequest.customPUT(subscriptionDetails).then(subscribedToPlan, function() {
|
changeSubscriptionRequest.customPUT(subscriptionDetails).then(subscribedToPlan, function() {
|
||||||
// Failure
|
// Failure
|
||||||
|
@ -813,9 +821,11 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
// Show the subscribe dialog if a plan was requested.
|
// Show the subscribe dialog if a plan was requested.
|
||||||
var requested = $routeParams['plan']
|
var requested = $routeParams['plan']
|
||||||
if (requested !== undefined && requested !== 'free') {
|
if (requested !== undefined && requested !== 'free') {
|
||||||
if (PlanService.getPlan(requested) !== undefined) {
|
PlanService.getPlan(requested, function(found) {
|
||||||
$scope.subscribe(requested);
|
if (found) {
|
||||||
}
|
$scope.subscribe(requested);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.updatingUser = false;
|
$scope.updatingUser = false;
|
||||||
|
@ -1039,11 +1049,21 @@ function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanSer
|
||||||
var subscribedToPlan = function(sub) {
|
var subscribedToPlan = function(sub) {
|
||||||
$scope.planChanging = false;
|
$scope.planChanging = false;
|
||||||
$scope.subscription = sub;
|
$scope.subscription = sub;
|
||||||
$scope.subscribedPlan = PlanService.getPlan(sub.plan);
|
|
||||||
$scope.planRequired = null;
|
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
||||||
if ($scope.subscription.usedPrivateRepos >= $scope.subscribedPlan.privateRepos) {
|
$scope.subscribedPlan = subscribedPlan;
|
||||||
$scope.planRequired = PlanService.getMinimumPlan($scope.subscription.usedPrivateRepos);
|
$scope.planRequired = null;
|
||||||
}
|
|
||||||
|
// Check to see if the current plan allows for an additional private repository to
|
||||||
|
// be created.
|
||||||
|
var privateAllowed = $scope.subscription.usedPrivateRepos < $scope.subscribedPlan.privateRepos;
|
||||||
|
if (!privateAllowed) {
|
||||||
|
// If not, find the minimum repository that does.
|
||||||
|
PlanService.getMinimumPlan($scope.subscription.usedPrivateRepos + 1, function(minimum) {
|
||||||
|
$scope.planRequired = minimum;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.editDescription = function() {
|
$scope.editDescription = function() {
|
||||||
|
@ -1109,6 +1129,7 @@ function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanSer
|
||||||
$scope.planChanging = true;
|
$scope.planChanging = true;
|
||||||
}, function(plan) {
|
}, function(plan) {
|
||||||
// Subscribed.
|
// Subscribed.
|
||||||
|
UserService.resetCurrentSubscription();
|
||||||
subscribedToPlan(plan);
|
subscribedToPlan(plan);
|
||||||
}, function() {
|
}, function() {
|
||||||
// Failure.
|
// Failure.
|
||||||
|
@ -1116,15 +1137,37 @@ function NewRepoCtrl($scope, $location, $http, UserService, Restangular, PlanSer
|
||||||
$scope.planChanging = false;
|
$scope.planChanging = false;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
|
||||||
|
// accordingly.
|
||||||
|
$scope.$watch('repo.namespace', function(namespace) {
|
||||||
|
// Note: Can initially be undefined.
|
||||||
|
if (!namespace) { return; }
|
||||||
|
|
||||||
|
var isUserNamespace = (namespace == $scope.user.username);
|
||||||
|
|
||||||
$scope.plans = PlanService.planList();
|
$scope.planRequired = null;
|
||||||
|
$scope.isUserNamespace = isUserNamespace;
|
||||||
|
|
||||||
// Load the user's subscription information in case they want to create a private
|
if (isUserNamespace) {
|
||||||
// repository.
|
// Load the user's subscription information in case they want to create a private
|
||||||
var getSubscription = Restangular.one('user/plan');
|
// repository.
|
||||||
getSubscription.get().then(subscribedToPlan, function() {
|
UserService.getCurrentSubscription(subscribedToPlan, function() {
|
||||||
// User has no subscription
|
PlanService.getMinimumPlan(1, function(minimum) { $scope.planRequired = minimum; });
|
||||||
$scope.planRequired = PlanService.getMinimumPlan(1);
|
});
|
||||||
|
} else {
|
||||||
|
$scope.planRequired = null;
|
||||||
|
|
||||||
|
var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private');
|
||||||
|
checkPrivateAllowed.get().then(function(resp) {
|
||||||
|
$scope.planRequired = resp.privateAllowed ? null : {};
|
||||||
|
}, function() {
|
||||||
|
$scope.planRequired = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-set to private repo.
|
||||||
|
$scope.repo.is_public = '0';
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -72,13 +72,19 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment -->
|
<!-- Payment -->
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired">
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
In order to make this repository private, you’ll need to upgrade your plan from <b>{{ subscribedPlan.title }}</b> to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
In order to make this repository private, you’ll need to upgrade your plan from <b>{{ subscribedPlan.title }}</b> to <b>{{ planRequired.title }}</b>. This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
|
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace">
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
This organization has reached its private repository limit. Please contact your administrator.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue