Prevent change visibility of a repo in the UI when disallowed by billing plan
Fixes #486 - Extracts out the check plan logic and UI from the new repo page into its own directive (repo-count-checker) - Adds the new directive to the repo settings panel - Some additional UI improvements for the repo settings panel
This commit is contained in:
parent
fbfe7fdb54
commit
2739cf47ba
8 changed files with 173 additions and 109 deletions
|
@ -29,6 +29,18 @@
|
||||||
margin-top: -7px !important;
|
margin-top: -7px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.repo-panel-settings-element .repo-count-checker {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-settings-element .co-alert {
|
||||||
|
margin-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-panel-settings-element .panel-body {
|
||||||
|
border-bottom: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
.repo-panel-settings-element .delete-btn {
|
.repo-panel-settings-element .delete-btn {
|
||||||
float: none;
|
float: none;
|
||||||
|
|
26
static/css/directives/ui/repo-count-checker.css
Normal file
26
static/css/directives/ui/repo-count-checker.css
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
.repo-count-checker .btn {
|
||||||
|
margin-top: 0px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-count-checker .co-alert {
|
||||||
|
margin-bottom: 6px !important;
|
||||||
|
padding-right: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-count-checker .co-alert .btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.repo-count-checker .co-alert {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.repo-count-checker .co-alert .btn {
|
||||||
|
position: relative;
|
||||||
|
margin-top: 20px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
}
|
24
static/directives/repo-count-checker.html
Normal file
24
static/directives/repo-count-checker.html
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
<div class="repo-count-checker-element">
|
||||||
|
<div class="required-plan" ng-show="isEnabled && planRequired && planRequired.title">
|
||||||
|
<div class="co-alert co-alert-info">
|
||||||
|
In order to make this repository private under
|
||||||
|
<strong ng-if="isUserNamespace">your personal namespace</strong>
|
||||||
|
<strong ng-if="!isUserNamespace">organization <b>{{ repo.namespace }}</b></strong>, you will need to upgrade your plan to
|
||||||
|
<b style="border-bottom: 1px dotted black;" data-html="true"
|
||||||
|
data-title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
|
||||||
|
{{ planRequired.title }}
|
||||||
|
</b>.
|
||||||
|
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
||||||
|
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
||||||
|
</div>
|
||||||
|
<span ng-if="isUserNamespace && user.organizations.length == 1" style="margin-left: 6px; display: inline-block;">or did you mean to have this repository under the <b>{{ user.organizations[0].name }}</b> namespace?</span>
|
||||||
|
<div class="cor-loader-inline" ng-show="planChanging"></div>
|
||||||
|
</div>
|
||||||
|
<div class="cor-loader-inline" ng-show="isEnabled && checkingPlan"></div>
|
||||||
|
<div class="required-plan" ng-show="isEnabled && planRequired && !isUserNamespace && !planRequired.title">
|
||||||
|
<div class="co-alert co-alert-warning">
|
||||||
|
This organization has reached its private repository limit. Please contact your administrator.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -22,12 +22,10 @@
|
||||||
<div class="repository-events-table" repository="repository"
|
<div class="repository-events-table" repository="repository"
|
||||||
is-enabled="isEnabled"></div>
|
is-enabled="isEnabled"></div>
|
||||||
|
|
||||||
<!-- Other settings -->
|
<!-- Visibility settings -->
|
||||||
<div class="co-panel">
|
<div class="co-panel">
|
||||||
<div class="co-panel-heading"><i class="fa fa-gears"></i> Repository Settings</div>
|
<div class="co-panel-heading"><i class="fa fa-unlock-alt"></i> Repository Visibility</div>
|
||||||
|
|
||||||
<div class="cor-loader" ng-show="!repository"></div>
|
<div class="cor-loader" ng-show="!repository"></div>
|
||||||
|
|
||||||
<div ng-show="repository">
|
<div ng-show="repository">
|
||||||
<!-- Public/Private -->
|
<!-- Public/Private -->
|
||||||
<div class="panel-body panel-section lock-section" ng-if="!repository.is_public">
|
<div class="panel-body panel-section lock-section" ng-if="!repository.is_public">
|
||||||
|
@ -44,12 +42,23 @@
|
||||||
|
|
||||||
<div>This repository is currently <b>public</b> and is visible to all users, and may be pulled by all users.</div>
|
<div>This repository is currently <b>public</b> and is visible to all users, and may be pulled by all users.</div>
|
||||||
|
|
||||||
<button class="btn btn-default" ng-click="askChangeAccess('private')">
|
<button class="btn btn-default" ng-click="askChangeAccess('private')" ng-show="!planRequired">
|
||||||
<i class="fa fa-lock"></i>Make Private
|
<i class="fa fa-lock"></i>Make Private
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Payment -->
|
||||||
|
<div class="repo-count-checker" namespace="repository.namespace" plan-required="planRequired"
|
||||||
|
is-enabled="repository.is_public">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Repository -->
|
<!-- Delete repository -->
|
||||||
|
<div class="co-panel">
|
||||||
|
<div class="co-panel-heading"><i class="fa fa-trash"></i> Delete Repository</div>
|
||||||
|
<div class="cor-loader" ng-show="!repository"></div>
|
||||||
|
<div ng-show="repository">
|
||||||
<div class="panel-body panel-section">
|
<div class="panel-body panel-section">
|
||||||
<div class="co-alert co-alert-danger">
|
<div class="co-alert co-alert-danger">
|
||||||
<button class="btn btn-danger delete-btn" ng-click="askDelete()">
|
<button class="btn btn-danger delete-btn" ng-click="askDelete()">
|
||||||
|
@ -60,10 +69,16 @@
|
||||||
Deleting a repository <b>cannot be undone</b>. Here be dragons!
|
Deleting a repository <b>cannot be undone</b>. Here be dragons!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<!-- Build Status Badge -->
|
<!-- Build Status Badge -->
|
||||||
<div class="panel-body panel-section hidden-xs">
|
<div class="co-panel hidden-xs">
|
||||||
|
<div class="co-panel-heading"><i class="fa fa-tasks"></i> Build Status Badge</div>
|
||||||
|
<div class="cor-loader" ng-show="!repository"></div>
|
||||||
|
<div ng-show="repository">
|
||||||
|
<div class="panel-body panel-section">
|
||||||
<!-- Token Info Banner -->
|
<!-- Token Info Banner -->
|
||||||
<div class="co-alert co-alert-info" ng-if="!repository.is_public">
|
<div class="co-alert co-alert-info" ng-if="!repository.is_public">
|
||||||
Note: This badge contains a token so the badge can be seen by external users. The token does not grant any other access and is safe to share!
|
Note: This badge contains a token so the badge can be seen by external users. The token does not grant any other access and is safe to share!
|
||||||
|
|
81
static/js/directives/ui/repo-count-checker.js
Normal file
81
static/js/directives/ui/repo-count-checker.js
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
/**
|
||||||
|
* An element which displays a message when the maximum number of private repositories has been
|
||||||
|
* reached.
|
||||||
|
*/
|
||||||
|
angular.module('quay').directive('repoCountChecker', function () {
|
||||||
|
var directiveDefinitionObject = {
|
||||||
|
priority: 0,
|
||||||
|
templateUrl: '/static/directives/repo-count-checker.html',
|
||||||
|
replace: false,
|
||||||
|
transclude: true,
|
||||||
|
restrict: 'C',
|
||||||
|
scope: {
|
||||||
|
'namespace': '=namespace',
|
||||||
|
'planRequired': '=planRequired',
|
||||||
|
'isEnabled': '=isEnabled'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, ApiService, UserService, PlanService, Features) {
|
||||||
|
var refresh = function() {
|
||||||
|
$scope.planRequired = null;
|
||||||
|
|
||||||
|
if (!$scope.isEnabled || !$scope.namespace || !Features.BILLING) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.checkingPlan = true;
|
||||||
|
$scope.isUserNamespace = UserService.isUserNamespace($scope.namespace);
|
||||||
|
|
||||||
|
ApiService.getPrivateAllowed($scope.isUserNamespace ? null : $scope.namespace).then(function(resp) {
|
||||||
|
$scope.checkingPlan = false;
|
||||||
|
|
||||||
|
if (resp['privateAllowed']) {
|
||||||
|
$scope.planRequired = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp['privateCount'] == null) {
|
||||||
|
// Organization where we are not the admin.
|
||||||
|
$scope.planRequired = {};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, lookup the matching plan.
|
||||||
|
PlanService.getMinimumPlan(resp['privateCount'] + 1, !$scope.isUserNamespace, function(minimum) {
|
||||||
|
$scope.planRequired = minimum;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscribedToPlan = function(sub) {
|
||||||
|
$scope.planChanging = false;
|
||||||
|
$scope.subscription = sub;
|
||||||
|
|
||||||
|
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
||||||
|
$scope.subscribedPlan = subscribedPlan;
|
||||||
|
refresh();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.$watch('namespace', refresh);
|
||||||
|
$scope.$watch('isEnabled', refresh);
|
||||||
|
|
||||||
|
$scope.upgradePlan = function() {
|
||||||
|
var callbacks = {
|
||||||
|
'started': function() { $scope.planChanging = true; },
|
||||||
|
'opened': function() { $scope.planChanging = true; },
|
||||||
|
'closed': function() { $scope.planChanging = false; },
|
||||||
|
'success': subscribedToPlan,
|
||||||
|
'failure': function(resp) {
|
||||||
|
$('#couldnotsubscribeModal').modal();
|
||||||
|
$scope.planChanging = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var isUserNamespace = UserService.isUserNamespace($scope.namespace);
|
||||||
|
var namespace = isUserNamespace ? null : $scope.namespace;
|
||||||
|
PlanService.changePlan($scope, namespace, $scope.planRequired.stripeId, callbacks);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
|
@ -22,22 +22,6 @@
|
||||||
'initialize': ''
|
'initialize': ''
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch the namespace on the repo. If it changes, we update the plan and the public/private
|
|
||||||
// accordingly.
|
|
||||||
$scope.isUserNamespace = true;
|
|
||||||
$scope.$watch('repo.namespace', function(namespace) {
|
|
||||||
// Note: Can initially be undefined.
|
|
||||||
if (!namespace) { return; }
|
|
||||||
|
|
||||||
var isUserNamespace = (namespace == $scope.user.username);
|
|
||||||
|
|
||||||
$scope.planRequired = null;
|
|
||||||
$scope.isUserNamespace = isUserNamespace;
|
|
||||||
|
|
||||||
// Determine whether private repositories are allowed for the namespace.
|
|
||||||
checkPrivateAllowed();
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.changeNamespace = function(namespace) {
|
$scope.changeNamespace = function(namespace) {
|
||||||
$scope.repo.namespace = namespace;
|
$scope.repo.namespace = namespace;
|
||||||
};
|
};
|
||||||
|
@ -108,65 +92,5 @@
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.upgradePlan = function() {
|
|
||||||
var callbacks = {
|
|
||||||
'started': function() { $scope.planChanging = true; },
|
|
||||||
'opened': function() { $scope.planChanging = true; },
|
|
||||||
'closed': function() { $scope.planChanging = false; },
|
|
||||||
'success': subscribedToPlan,
|
|
||||||
'failure': function(resp) {
|
|
||||||
$('#couldnotsubscribeModal').modal();
|
|
||||||
$scope.planChanging = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var namespace = $scope.isUserNamespace ? null : $scope.repo.namespace;
|
|
||||||
PlanService.changePlan($scope, namespace, $scope.planRequired.stripeId, callbacks);
|
|
||||||
};
|
|
||||||
|
|
||||||
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']) {
|
|
||||||
$scope.planRequired = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (resp['privateCount'] == null) {
|
|
||||||
// Organization where we are not the admin.
|
|
||||||
$scope.planRequired = {};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, lookup the matching plan.
|
|
||||||
PlanService.getMinimumPlan(resp['privateCount'] + 1, !isUserNamespace, function(minimum) {
|
|
||||||
$scope.planRequired = minimum;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var subscribedToPlan = function(sub) {
|
|
||||||
$scope.planChanging = false;
|
|
||||||
$scope.subscription = sub;
|
|
||||||
|
|
||||||
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
|
||||||
$scope.subscribedPlan = subscribedPlan;
|
|
||||||
$scope.planRequired = null;
|
|
||||||
checkPrivateAllowed();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
})();
|
})();
|
|
@ -126,6 +126,10 @@ function(ApiService, CookieService, $rootScope, Config) {
|
||||||
return userResponse;
|
return userResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
userService.isUserNamespace = function(namespace) {
|
||||||
|
return namespace == userResponse.username;
|
||||||
|
};
|
||||||
|
|
||||||
// Update the user in the root scope.
|
// Update the user in the root scope.
|
||||||
userService.updateUserIn($rootScope);
|
userService.updateUserIn($rootScope);
|
||||||
|
|
||||||
|
|
|
@ -88,30 +88,8 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Payment -->
|
<!-- Payment -->
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && planRequired.title">
|
<div class="repo-count-checker" namespace="repo.namespace" plan-required="planRequired"
|
||||||
<div class="co-alert co-alert-warning">
|
is-enabled="repo.is_public == '0'">
|
||||||
In order to make this repository private under
|
|
||||||
<strong ng-if="isUserNamespace">your personal namespace</strong>
|
|
||||||
<strong ng-if="!isUserNamespace">organization <b>{{ repo.namespace }}</b></strong>, you will need to upgrade your plan to
|
|
||||||
<b style="border-bottom: 1px dotted black;" data-html="true"
|
|
||||||
data-title="{{ '<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories' }}" bs-tooltip>
|
|
||||||
{{ planRequired.title }}
|
|
||||||
</b>.
|
|
||||||
This will cost $<span>{{ planRequired.price / 100 }}</span>/month.
|
|
||||||
</div>
|
|
||||||
<a class="btn btn-primary" ng-click="upgradePlan()" ng-show="!planChanging">Upgrade now</a>
|
|
||||||
<span ng-if="isUserNamespace && user.organizations.length == 1" style="margin-left: 6px; display: inline-block;">or did you mean to create this repository
|
|
||||||
under <a href="javascript:void(0)" ng-click="changeNamespace(user.organizations[0].name)"><b>{{ user.organizations[0].name }}</b></a>?</span>
|
|
||||||
<div class="cor-loader-inline" ng-show="planChanging"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cor-loader-inline" ng-show="repo.is_public == '0' && checkingPlan"></div>
|
|
||||||
|
|
||||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && !isUserNamespace && !planRequired.title">
|
|
||||||
<div class="co-alert co-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