Merge branch 'orgs' of ssh://bitbucket.org/yackob03/quay into orgs
This commit is contained in:
commit
2b0f3a9ba7
12 changed files with 625 additions and 348 deletions
|
@ -348,6 +348,12 @@ def get_organization_team(orgname, teamname):
|
||||||
return result[0]
|
return result[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_organization_members_with_teams(organization):
|
||||||
|
joined = TeamMember.select().annotate(Team).annotate(User)
|
||||||
|
query = joined.where(Team.organization == organization)
|
||||||
|
return query
|
||||||
|
|
||||||
|
|
||||||
def get_organization_team_members(teamid):
|
def get_organization_team_members(teamid):
|
||||||
joined = User.select().join(TeamMember).join(Team)
|
joined = User.select().join(TeamMember).join(Team)
|
||||||
query = joined.where(Team.id == teamid)
|
query = joined.where(Team.id == teamid)
|
||||||
|
|
|
@ -300,6 +300,31 @@ def get_organization(orgname):
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
@app.route('/api/organization/<orgname>/members', methods=['GET'])
|
||||||
|
@api_login_required
|
||||||
|
def get_organization_members(orgname):
|
||||||
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
|
if permission.can():
|
||||||
|
try:
|
||||||
|
org = model.get_organization(orgname)
|
||||||
|
except model.InvalidOrganizationException:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Loop to create the members dictionary. Note that the members collection
|
||||||
|
# will return an entry for *every team* a member is on, so we will have
|
||||||
|
# duplicate keys (which is why we pre-build the dictionary).
|
||||||
|
members_dict = {}
|
||||||
|
members = model.get_organization_members_with_teams(org)
|
||||||
|
for member in members:
|
||||||
|
if not member.user.username in members_dict:
|
||||||
|
members_dict[member.user.username] = {'username': member.user.username, 'teams': []}
|
||||||
|
|
||||||
|
members_dict[member.user.username]['teams'].append(member.team.name)
|
||||||
|
|
||||||
|
return jsonify({'members': members_dict})
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_organization_private_allowed(orgname):
|
def get_organization_private_allowed(orgname):
|
||||||
|
|
|
@ -1358,6 +1358,49 @@ p.editable:hover i {
|
||||||
stroke-width: 1.5px;
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart .count-text {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart.limit-at path.arc-0 {
|
||||||
|
fill: #c09853;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart.limit-over path.arc-0 {
|
||||||
|
fill: #b94a48;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart.limit-near path.arc-0 {
|
||||||
|
fill: #468847;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart.limit-over path.arc-1 {
|
||||||
|
fill: #fcf8e3;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart.limit-at path.arc-1 {
|
||||||
|
fill: #f2dede;
|
||||||
|
}
|
||||||
|
|
||||||
|
#repository-usage-chart.limit-near path.arc-1 {
|
||||||
|
fill: #dff0d8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-manager-element .usage-caption {
|
||||||
|
display: inline-block;
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 26px;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Overrides for the markdown editor. */
|
/* Overrides for the markdown editor. */
|
||||||
|
|
||||||
.wmd-panel .btn-toolbar {
|
.wmd-panel .btn-toolbar {
|
||||||
|
@ -1471,7 +1514,6 @@ p.editable:hover i {
|
||||||
100% { background-color: rgba(92, 184, 92, 0.36); }
|
100% { background-color: rgba(92, 184, 92, 0.36); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.org-view .team-title {
|
.org-view .team-title {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
|
@ -1490,22 +1532,49 @@ p.editable:hover i {
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .plans-table thead td {
|
.org-admin .team-link {
|
||||||
|
display: inline-block;
|
||||||
|
text-transform: capitalize;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin #members table td {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin #members table i {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin #members .side-controls {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin #members .result-count {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.org-admin #members .filter-input {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.plan-manager-element .plans-table thead td {
|
||||||
color: #aaa;
|
color: #aaa;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .plans-table td {
|
.plan-manager-element .plans-table td {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .plans-table td.controls {
|
.plan-manager-element .plans-table td.controls {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-admin .plans-table .plan-price {
|
.plan-manager-element .plans-table .plan-price {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
margin-bottom: 0px;
|
margin-bottom: 0px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
<span class="namespace-selector-dropdown">
|
<span class="namespace-selector-dropdown">
|
||||||
<span ng-show="user.organizations.length == 0">{{user.username}}</span>
|
<span ng-show="user.organizations.length == 0">
|
||||||
|
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon" />
|
||||||
|
<span class="namespace-name">{{user.username}}</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<div class="btn-group" ng-show="user.organizations.length > 0">
|
<div class="btn-group" ng-show="user.organizations.length > 0">
|
||||||
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
|
||||||
|
|
62
static/directives/plan-manager.html
Normal file
62
static/directives/plan-manager.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<div class="plan-manager-element">
|
||||||
|
<!-- Loading/Changing -->
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planLoading"></i>
|
||||||
|
|
||||||
|
<!-- Alerts -->
|
||||||
|
<div class="alert alert-danger" ng-show="limit == 'over' && !planLoading">
|
||||||
|
You are using more private repositories than your plan allows. Please
|
||||||
|
upgrade your subscription to avoid disruptions in your <span ng-show="organization">organization's</span> service.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-warning" ng-show="limit == 'at' && !planLoading">
|
||||||
|
You are at your current plan's number of allowed private repositories. Please upgrade your subscription to avoid future disruptions in your <span ng-show="organization">organization's</span> service.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="alert alert-success" ng-show="limit == 'near' && !planLoading">
|
||||||
|
You are nearing the number of allowed private repositories. It might be time to think about
|
||||||
|
upgrading your subscription to avoid future disruptions in your <span ng-show="organization">organization's</span> service.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart -->
|
||||||
|
<div>
|
||||||
|
<div id="repository-usage-chart" class="limit-{{limit}}"></div>
|
||||||
|
<span class="usage-caption" ng-show="chart">Repository Usage</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Plans Table -->
|
||||||
|
<table class="table table-hover plans-table" ng-show="!planLoading">
|
||||||
|
<thead>
|
||||||
|
<td>Plan</td>
|
||||||
|
<td>Private Repositories</td>
|
||||||
|
<td style="min-width: 64px">Price</td>
|
||||||
|
<td></td>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr ng-repeat="plan in plans" ng-class="(subscribedPlan.stripeId === plan.stripeId) ? getActiveSubClass() : ''">
|
||||||
|
<td>{{ plan.title }}</td>
|
||||||
|
<td>{{ plan.privateRepos }}</td>
|
||||||
|
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
|
||||||
|
<td class="controls">
|
||||||
|
<div ng-switch='plan.stripeId'>
|
||||||
|
<div ng-switch-when='bus-free'>
|
||||||
|
<button class="btn button-hidden">Hidden!</button>
|
||||||
|
</div>
|
||||||
|
<div ng-switch-default>
|
||||||
|
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"
|
||||||
|
ng-class="subscribedPlan.price == 0 ? 'btn-primary' : 'btn-default'"
|
||||||
|
ng-click="changeSubscription(plan.stripeId)">
|
||||||
|
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
|
||||||
|
<span ng-show="!planChanging && subscribedPlan.price != 0">Change</span>
|
||||||
|
<span ng-show="!planChanging && subscribedPlan.price == 0">Subscribe</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId && plan.price > 0"
|
||||||
|
ng-click="cancelSubscription()">
|
||||||
|
<i class="fa fa-spinner fa-spin" ng-show="planChanging"></i>
|
||||||
|
<span ng-show="!planChanging">Cancel</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
180
static/js/app.js
180
static/js/app.js
|
@ -48,7 +48,7 @@ function getMarkedDown(string) {
|
||||||
|
|
||||||
// Start the application code itself.
|
// Start the application code itself.
|
||||||
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives'], function($provide) {
|
quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', 'angulartics.mixpanel', '$strap.directives'], function($provide) {
|
||||||
$provide.factory('UserService', ['Restangular', function(Restangular) {
|
$provide.factory('UserService', ['Restangular', 'PlanService', function(Restangular, PlanService) {
|
||||||
var userResponse = {
|
var userResponse = {
|
||||||
verified: false,
|
verified: false,
|
||||||
anonymous: true,
|
anonymous: true,
|
||||||
|
@ -85,9 +85,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
|
|
||||||
userService.getCurrentSubscription = function(callback, failure) {
|
userService.getCurrentSubscription = function(callback, failure) {
|
||||||
if (currentSubscription) { callback(currentSubscription); }
|
if (currentSubscription) { callback(currentSubscription); }
|
||||||
|
PlanService.getSubscription(null, function(sub) {
|
||||||
var getSubscription = Restangular.one('user/plan');
|
|
||||||
getSubscription.get().then(function(sub) {
|
|
||||||
currentSubscription = sub;
|
currentSubscription = sub;
|
||||||
callback(sub);
|
callback(sub);
|
||||||
}, failure);
|
}, failure);
|
||||||
|
@ -148,7 +146,9 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
|
|
||||||
planService.getPlan = function(planId, callback) {
|
planService.getPlan = function(planId, callback) {
|
||||||
planService.verifyLoaded(function() {
|
planService.verifyLoaded(function() {
|
||||||
callback(planDict[planId]);
|
if (planDict[planId]) {
|
||||||
|
callback(planDict[planId]);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -171,23 +171,47 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
planService.showSubscribeDialog = function($scope, planId, orgname, started, success, failed) {
|
planService.getSubscription = function(organization, success, failure) {
|
||||||
var submitToken = function(token) {
|
var url = planService.getSubscriptionUrl(organization);
|
||||||
$scope.$apply(function() {
|
var getSubscription = Restangular.one(url);
|
||||||
started();
|
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(success, failure);
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.changePlan = function($scope, orgname, planId, hasExistingSubscription, started, success, failure) {
|
||||||
|
if (!hasExistingSubscription) {
|
||||||
|
planService.showSubscribeDialog($scope, orgname, planId, started, success, failure);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
started();
|
||||||
|
planService.setSubscription(orgname, planId, success, failure);
|
||||||
|
};
|
||||||
|
|
||||||
|
planService.showSubscribeDialog = function($scope, orgname, planId, started, success, failure) {
|
||||||
|
var submitToken = function(token) {
|
||||||
mixpanel.track('plan_subscribe');
|
mixpanel.track('plan_subscribe');
|
||||||
|
|
||||||
var subscriptionDetails = {
|
|
||||||
token: token.id,
|
|
||||||
plan: planId,
|
|
||||||
};
|
|
||||||
|
|
||||||
var url = orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
|
|
||||||
var createSubscriptionRequest = Restangular.one(url);
|
|
||||||
$scope.$apply(function() {
|
$scope.$apply(function() {
|
||||||
createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failed);
|
started();
|
||||||
|
planService.setSubscription(orgname, planId, success, failure);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -486,6 +510,126 @@ quayApp.directive('roleGroup', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
controller: function($scope, $element, PlanService, Restangular) {
|
||||||
|
var hasSubscription = false;
|
||||||
|
|
||||||
|
$scope.getActiveSubClass = function() {
|
||||||
|
return 'active';
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.changeSubscription = function(planId) {
|
||||||
|
if ($scope.planChanging) { return; }
|
||||||
|
|
||||||
|
PlanService.changePlan($scope, $scope.organization, planId, hasSubscription, function() {
|
||||||
|
// Started.
|
||||||
|
$scope.planChanging = true;
|
||||||
|
}, function(sub) {
|
||||||
|
// Success.
|
||||||
|
subscribedToPlan(sub);
|
||||||
|
}, function() {
|
||||||
|
// Failure.
|
||||||
|
$scope.planChanging = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$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 (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) { return; }
|
||||||
|
if (!$scope.user && !$scope.organization) { return; }
|
||||||
|
|
||||||
|
PlanService.getPlans(function(plans) {
|
||||||
|
$scope.plans = plans[$scope.organization ? 'business' : 'user'];
|
||||||
|
update();
|
||||||
|
|
||||||
|
if ($scope.readyForPlan) {
|
||||||
|
var planRequested = $scope.readyForPlan();
|
||||||
|
if (planRequested) {
|
||||||
|
$scope.changeSubscription(planRequested);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start the initial download.
|
||||||
|
$scope.planLoading = true;
|
||||||
|
loadPlans();
|
||||||
|
|
||||||
|
$scope.$watch('organization', loadPlans);
|
||||||
|
$scope.$watch('user', loadPlans);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return directiveDefinitionObject;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
quayApp.directive('namespaceSelector', function () {
|
quayApp.directive('namespaceSelector', function () {
|
||||||
var directiveDefinitionObject = {
|
var directiveDefinitionObject = {
|
||||||
priority: 0,
|
priority: 0,
|
||||||
|
|
|
@ -143,38 +143,61 @@ function GuideCtrl($scope) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function RepoListCtrl($scope, Restangular, UserService) {
|
function RepoListCtrl($scope, Restangular, UserService) {
|
||||||
|
$scope.namespace = null;
|
||||||
|
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.user = currentUser;
|
$scope.user = currentUser;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
$scope.$watch('namespace', function(namespace) {
|
||||||
|
loadMyRepos(namespace);
|
||||||
|
});
|
||||||
|
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.public_repositories = null;
|
$scope.public_repositories = null;
|
||||||
$scope.private_repositories = null;
|
$scope.user_repositories = null;
|
||||||
|
|
||||||
// Load the list of personal repositories.
|
var loadMyRepos = function(namespace) {
|
||||||
var repositoryPrivateFetch = Restangular.all('repository/');
|
if (!$scope.user || $scope.user.anonymous || !namespace) {
|
||||||
repositoryPrivateFetch.getList({'public': false, 'sort': true}).then(function(resp) {
|
return;
|
||||||
$scope.private_repositories = resp.repositories;
|
}
|
||||||
$scope.loading = !($scope.public_repositories && $scope.private_repositories);
|
|
||||||
});
|
$scope.loadingmyrepos = true;
|
||||||
|
|
||||||
|
// Load the list of repositories.
|
||||||
|
var params = {
|
||||||
|
'limit': 10,
|
||||||
|
'public': true,
|
||||||
|
'sort': true,
|
||||||
|
'namespace': namespace
|
||||||
|
};
|
||||||
|
|
||||||
|
var repositoryFetch = Restangular.all('repository/');
|
||||||
|
repositoryFetch.getList(params).then(function(resp) {
|
||||||
|
$scope.user_repositories = resp.repositories;
|
||||||
|
$scope.loading = !($scope.public_repositories && $scope.user_repositories);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Load the list of public repositories.
|
// Load the list of public repositories.
|
||||||
var options = {'public': true, 'private': false, 'sort': true, 'limit': 10};
|
var options = {'public': true, 'private': false, 'sort': true, 'limit': 10};
|
||||||
var repositoryPublicFetch = Restangular.all('repository/');
|
var repositoryPublicFetch = Restangular.all('repository/');
|
||||||
repositoryPublicFetch.getList(options).then(function(resp) {
|
repositoryPublicFetch.getList(options).then(function(resp) {
|
||||||
$scope.public_repositories = resp.repositories;
|
$scope.public_repositories = resp.repositories;
|
||||||
$scope.loading = !($scope.public_repositories && $scope.private_repositories);
|
$scope.loading = !($scope.public_repositories && $scope.user_repositories);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyService) {
|
function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyService) {
|
||||||
$('.form-signup').popover();
|
$('.form-signup').popover();
|
||||||
|
|
||||||
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.namespace = null;
|
||||||
if (!currentUser.anonymous) {
|
|
||||||
$scope.loadMyRepos();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
$scope.$watch('namespace', function(namespace) {
|
||||||
|
loadMyRepos(namespace);
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.$watch( function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.user = currentUser;
|
$scope.user = currentUser;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
|
@ -207,18 +230,23 @@ function LandingCtrl($scope, $timeout, $location, Restangular, UserService, KeyS
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.loadMyRepos = function() {
|
var loadMyRepos = function(namespace) {
|
||||||
$scope.loadingmyrepos = true;
|
if (!$scope.user || $scope.user.anonymous || !namespace) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Load the list of repositories.
|
$scope.loadingmyrepos = true;
|
||||||
var params = {
|
|
||||||
'limit': 5,
|
|
||||||
'public': false,
|
|
||||||
'sort': true
|
|
||||||
};
|
|
||||||
|
|
||||||
var repositoryFetch = Restangular.all('repository/');
|
// Load the list of repositories.
|
||||||
repositoryFetch.getList(params).then(function(resp) {
|
var params = {
|
||||||
|
'limit': 4,
|
||||||
|
'public': true,
|
||||||
|
'sort': true,
|
||||||
|
'namespace': namespace
|
||||||
|
};
|
||||||
|
|
||||||
|
var repositoryFetch = Restangular.all('repository/');
|
||||||
|
repositoryFetch.getList(params).then(function(resp) {
|
||||||
$scope.myrepos = resp.repositories;
|
$scope.myrepos = resp.repositories;
|
||||||
$scope.loadingmyrepos = false;
|
$scope.loadingmyrepos = false;
|
||||||
});
|
});
|
||||||
|
@ -667,91 +695,23 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
||||||
// Load the list of plans.
|
|
||||||
PlanService.getPlans(function(plans) {
|
|
||||||
$scope.plans = plans.user;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
$scope.$watch(function () { return UserService.currentUser(); }, function (currentUser) {
|
||||||
$scope.askForPassword = currentUser.askForPassword;
|
$scope.askForPassword = currentUser.askForPassword;
|
||||||
|
if (!currentUser.anonymous) {
|
||||||
|
$scope.user = currentUser;
|
||||||
|
}
|
||||||
|
$scope.loading = false;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
var subscribedToPlan = function(sub) {
|
$scope.readyForPlan = function() {
|
||||||
$scope.subscription = sub;
|
// Show the subscribe dialog if a plan was requested.
|
||||||
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
return $routeParams['plan'];
|
||||||
$scope.subscribedPlan = subscribedPlan;
|
|
||||||
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
|
||||||
|
|
||||||
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
|
||||||
$scope.errorMessage = 'You are using more private repositories than your plan allows, please upgrade your subscription to avoid disruptions in your service.';
|
|
||||||
} else {
|
|
||||||
$scope.errorMessage = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.planLoading = false;
|
|
||||||
$scope.planChanging = false;
|
|
||||||
|
|
||||||
mixpanel.people.set({
|
|
||||||
'plan': sub.plan
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.planLoading = true;
|
$scope.loading = true;
|
||||||
UserService.getCurrentSubscription(subscribedToPlan, function() {
|
|
||||||
// User has no subscription
|
|
||||||
$scope.planChanging = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.planChanging = false;
|
|
||||||
$scope.subscribe = function(planId) {
|
|
||||||
PlanService.showSubscribeDialog($scope, planId, null, function() {
|
|
||||||
// Subscribing.
|
|
||||||
$scope.planChanging = true;
|
|
||||||
}, function(plan) {
|
|
||||||
// Subscribed.
|
|
||||||
UserService.resetCurrentSubscription();
|
|
||||||
subscribedToPlan(plan);
|
|
||||||
}, function() {
|
|
||||||
// Failure.
|
|
||||||
$scope.errorMessage = 'Unable to subscribe.';
|
|
||||||
$scope.planChanging = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.changeSubscription = function(planId) {
|
|
||||||
$scope.planChanging = true;
|
|
||||||
$scope.errorMessage = undefined;
|
|
||||||
|
|
||||||
var subscriptionDetails = {
|
|
||||||
plan: planId,
|
|
||||||
};
|
|
||||||
|
|
||||||
UserService.resetCurrentSubscription();
|
|
||||||
var changeSubscriptionRequest = Restangular.one('user/plan');
|
|
||||||
changeSubscriptionRequest.customPUT(subscriptionDetails).then(subscribedToPlan, function() {
|
|
||||||
// Failure
|
|
||||||
$scope.errorMessage = 'Unable to change subscription.';
|
|
||||||
$scope.planChanging = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.cancelSubscription = function() {
|
|
||||||
$scope.changeSubscription('free');
|
|
||||||
};
|
|
||||||
|
|
||||||
// Show the subscribe dialog if a plan was requested.
|
|
||||||
var requested = $routeParams['plan']
|
|
||||||
if (requested !== undefined && requested !== 'free') {
|
|
||||||
PlanService.getPlan(requested, function(found) {
|
|
||||||
if (found) {
|
|
||||||
$scope.subscribe(requested);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.updatingUser = false;
|
$scope.updatingUser = false;
|
||||||
$scope.changePasswordSuccess = false;
|
$scope.changePasswordSuccess = false;
|
||||||
|
|
||||||
$('.form-change-pw').popover();
|
$('.form-change-pw').popover();
|
||||||
|
|
||||||
$scope.changePassword = function() {
|
$scope.changePassword = function() {
|
||||||
|
@ -768,6 +728,7 @@ function UserAdminCtrl($scope, $timeout, Restangular, PlanService, UserService,
|
||||||
$scope.user.repeatPassword = '';
|
$scope.user.repeatPassword = '';
|
||||||
$scope.changePasswordForm.$setPristine();
|
$scope.changePasswordForm.$setPristine();
|
||||||
|
|
||||||
|
// Reload the user.
|
||||||
UserService.load();
|
UserService.load();
|
||||||
}, function(result) {
|
}, function(result) {
|
||||||
$scope.updatingUser = false;
|
$scope.updatingUser = false;
|
||||||
|
@ -1177,6 +1138,28 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
||||||
|
|
||||||
var orgname = $routeParams.orgname;
|
var orgname = $routeParams.orgname;
|
||||||
|
|
||||||
|
$scope.orgname = orgname;
|
||||||
|
$scope.membersLoading = true;
|
||||||
|
$scope.membersFound = null;
|
||||||
|
|
||||||
|
$scope.loadMembers = function() {
|
||||||
|
if ($scope.membersFound) { return; }
|
||||||
|
$scope.membersLoading = true;
|
||||||
|
|
||||||
|
var getMembers = Restangular.one(getRestUrl('organization', orgname, 'members'));
|
||||||
|
getMembers.get().then(function(resp) {
|
||||||
|
var membersArray = [];
|
||||||
|
for (var key in resp.members) {
|
||||||
|
if (resp.members.hasOwnProperty(key)) {
|
||||||
|
membersArray.push(resp.members[key]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$scope.membersFound = membersArray;
|
||||||
|
$scope.membersLoading = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var loadOrganization = function() {
|
var loadOrganization = function() {
|
||||||
var getOrganization = Restangular.one(getRestUrl('organization', orgname));
|
var getOrganization = Restangular.one(getRestUrl('organization', orgname));
|
||||||
getOrganization.get().then(function(resp) {
|
getOrganization.get().then(function(resp) {
|
||||||
|
@ -1191,75 +1174,6 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
var subscribedToPlan = function(sub) {
|
|
||||||
$scope.planChanging = false;
|
|
||||||
$scope.subscription = sub;
|
|
||||||
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
|
||||||
$scope.subscribedPlan = subscribedPlan;
|
|
||||||
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
|
||||||
|
|
||||||
if (sub.usedPrivateRepos > $scope.subscribedPlan.privateRepos) {
|
|
||||||
$scope.overLimit = true;
|
|
||||||
} else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) {
|
|
||||||
$scope.nearLimit = true;
|
|
||||||
} else {
|
|
||||||
$scope.overLimit = false;
|
|
||||||
$scope.nearLimit = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$scope.planLoading = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var loadSubscription = function() {
|
|
||||||
$scope.planLoading = true;
|
|
||||||
|
|
||||||
var getSubscription = Restangular.one(getRestUrl('organization', orgname, 'plan'));
|
|
||||||
getSubscription.get().then(subscribedToPlan, function() {
|
|
||||||
// Organization has no subscription.
|
|
||||||
subscribedToPlan({'plan': 'bus-free'});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.getActiveSubClass = function() {
|
|
||||||
if ($scope.overLimit) { return 'danger'; }
|
|
||||||
if ($scope.nearLimit) { return 'warning'; }
|
|
||||||
return 'success';
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.subscribe = function(planId) {
|
|
||||||
$scope.planChanging = true;
|
|
||||||
PlanService.showSubscribeDialog($scope, planId, orgname, function() {
|
|
||||||
// Subscribing.
|
|
||||||
}, function(plan) {
|
|
||||||
// Subscribed.
|
|
||||||
subscribedToPlan(plan);
|
|
||||||
}, function() {
|
|
||||||
// Failure.
|
|
||||||
$scope.planChanging = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.changeSubscription = function(planId) {
|
|
||||||
$scope.planChanging = true;
|
|
||||||
$scope.errorMessage = undefined;
|
|
||||||
|
|
||||||
var subscriptionDetails = {
|
|
||||||
plan: planId,
|
|
||||||
};
|
|
||||||
|
|
||||||
var changeSubscriptionRequest = Restangular.one(getRestUrl('organization', orgname, 'plan'));
|
|
||||||
changeSubscriptionRequest.customPUT(subscriptionDetails).then(subscribedToPlan, function() {
|
|
||||||
// Failure
|
|
||||||
$scope.planChanging = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.cancelSubscription = function() {
|
|
||||||
$scope.changeSubscription('bus-free');
|
|
||||||
};
|
|
||||||
|
|
||||||
loadSubscription();
|
|
||||||
loadOrganization();
|
loadOrganization();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1131,3 +1131,115 @@ ImageFileChangeTree.prototype.toggle_ = function(d) {
|
||||||
d._children = null;
|
d._children = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Based off of http://bl.ocks.org/mbostock/1346410
|
||||||
|
*/
|
||||||
|
function RepositoryUsageChart() {
|
||||||
|
this.total_ = null;
|
||||||
|
this.count_ = null;
|
||||||
|
this.drawn_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the chart with the given count and total of number of repositories.
|
||||||
|
*/
|
||||||
|
RepositoryUsageChart.prototype.update = function(count, total) {
|
||||||
|
if (!this.g_) { return; }
|
||||||
|
this.total_ = total;
|
||||||
|
this.count_ = count;
|
||||||
|
this.drawInternal_();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Conducts the actual draw or update (if applicable).
|
||||||
|
*/
|
||||||
|
RepositoryUsageChart.prototype.drawInternal_ = function() {
|
||||||
|
// If the total is null, then we have not yet set the proper counts.
|
||||||
|
if (this.total_ === null) { return; }
|
||||||
|
|
||||||
|
var duration = 750;
|
||||||
|
|
||||||
|
var arc = this.arc_;
|
||||||
|
var pie = this.pie_;
|
||||||
|
var arcTween = this.arcTween_;
|
||||||
|
|
||||||
|
var color = d3.scale.category20();
|
||||||
|
var count = this.count_;
|
||||||
|
var total = this.total_;
|
||||||
|
|
||||||
|
var data = [count, Math.max(0, total - count)];
|
||||||
|
|
||||||
|
var arcTween = function(a) {
|
||||||
|
var i = d3.interpolate(this._current, a);
|
||||||
|
this._current = i(0);
|
||||||
|
return function(t) {
|
||||||
|
return arc(i(t));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.drawn_) {
|
||||||
|
var text = this.g_.append("svg:text")
|
||||||
|
.attr("dy", 10)
|
||||||
|
.attr("dx", 0)
|
||||||
|
.attr('dominant-baseline', 'auto')
|
||||||
|
.attr('text-anchor', 'middle')
|
||||||
|
.attr('class', 'count-text')
|
||||||
|
.text(this.count_ + ' / ' + this.total_);
|
||||||
|
|
||||||
|
var path = this.g_.datum(data).selectAll("path")
|
||||||
|
.data(pie)
|
||||||
|
.enter().append("path")
|
||||||
|
.attr("fill", function(d, i) { return color(i); })
|
||||||
|
.attr("class", function(d, i) { return 'arc-' + i; })
|
||||||
|
.attr("d", arc)
|
||||||
|
.each(function(d) { this._current = d; }); // store the initial angles
|
||||||
|
|
||||||
|
this.path_ = path;
|
||||||
|
this.text_ = text;
|
||||||
|
} else {
|
||||||
|
pie.value(function(d, i) { return data[i]; }); // change the value function
|
||||||
|
this.path_ = this.path_.data(pie); // compute the new angles
|
||||||
|
this.path_.transition().duration(duration).attrTween("d", arcTween); // redraw the arcs
|
||||||
|
|
||||||
|
// Update the text.
|
||||||
|
this.text_.text(this.count_ + ' / ' + this.total_);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawn_ = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the chart in the given container.
|
||||||
|
*/
|
||||||
|
RepositoryUsageChart.prototype.draw = function(container) {
|
||||||
|
var cw = document.getElementById(container).clientWidth;
|
||||||
|
var ch = document.getElementById(container).clientHeight;
|
||||||
|
var radius = Math.min(cw, ch) / 2;
|
||||||
|
|
||||||
|
var pie = d3.layout.pie().sort(null);
|
||||||
|
|
||||||
|
var arc = d3.svg.arc()
|
||||||
|
.innerRadius(radius - 50)
|
||||||
|
.outerRadius(radius - 25);
|
||||||
|
|
||||||
|
var svg = d3.select("#" + container).append("svg:svg")
|
||||||
|
.attr("width", cw)
|
||||||
|
.attr("height", ch);
|
||||||
|
|
||||||
|
var g = svg.append("g")
|
||||||
|
.attr("transform", "translate(" + cw / 2 + "," + ch / 2 + ")");
|
||||||
|
|
||||||
|
this.svg_ = svg;
|
||||||
|
this.g_ = g;
|
||||||
|
this.pie_ = pie;
|
||||||
|
this.arc_ = arc;
|
||||||
|
this.width_ = cw;
|
||||||
|
this.drawInternal_();
|
||||||
|
};
|
|
@ -12,8 +12,9 @@
|
||||||
<div ng-show="loadingmyrepos">
|
<div ng-show="loadingmyrepos">
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
</div>
|
</div>
|
||||||
|
<span class="namespace-selector" user="user" namespace="namespace" ng-show="!loadingmyrepos && user.organizations"></span>
|
||||||
<div ng-show="!loadingmyrepos && myrepos.length > 0">
|
<div ng-show="!loadingmyrepos && myrepos.length > 0">
|
||||||
<h2>Your Top Repositories</h2>
|
<h2>Top Repositories</h2>
|
||||||
<div class="repo-listing" ng-repeat="repository in myrepos">
|
<div class="repo-listing" ng-repeat="repository in myrepos">
|
||||||
<span class="repo-circle no-background" repo="repository"></span>
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
|
@ -21,13 +22,14 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!loadingmyrepos && myrepos.length == 0">
|
<div ng-show="!loadingmyrepos && myrepos.length == 0">
|
||||||
<div class="sub-message">
|
<div class="sub-message" style="margin-top: 20px">
|
||||||
You don't have any <b>private</b> repositories yet!
|
<span ng-show="namespace != user.username">This organization doesn't</span>
|
||||||
|
<span ng-show="namespace == user.username">You don't</span>
|
||||||
|
have any repositories yet!
|
||||||
|
|
||||||
<div class="options">
|
<div class="options">
|
||||||
<div class="option"><a href="/guide">Learn how to create a repository</a></div>
|
<a class="btn btn-primary" href="/repository/">Browse all repositories</a>
|
||||||
<div class="or"><span>or</span></div>
|
<a class="btn btn-success" href="/new/">Create a new repository</a>
|
||||||
<div class="option"><a href="/repository">Browse the public repositories</a></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -13,99 +13,53 @@
|
||||||
<!-- Side tabs -->
|
<!-- Side tabs -->
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<ul class="nav nav-pills nav-stacked">
|
<ul class="nav nav-pills nav-stacked">
|
||||||
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#usage">Current Usage</a></li>
|
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan/Billing</a></li>
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
|
||||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members">Members</a></li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
<div class="col-md-10">
|
<div class="col-md-10">
|
||||||
<div class="tab-content">
|
<div class="tab-content">
|
||||||
<!-- Usage tab -->
|
|
||||||
<div id="usage" class="tab-pane active">
|
|
||||||
<div class="alert alert-danger" ng-show="overLimit">
|
|
||||||
You are using more private repositories than your plan allows, please
|
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#plan">upgrade your subscription</a>
|
|
||||||
to avoid disruptions in your organization's service.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" ng-show="nearLimit">
|
|
||||||
You are nearing the number of allowed private repositories. It might be time to think about
|
|
||||||
<a href="javascript:void(0)" data-toggle="tab" data-target="#plan">upgrading your subscription</a>
|
|
||||||
to avoid future disruptions in your organization's service.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Subscription -->
|
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planLoading"></i>
|
|
||||||
<div class="row" ng-show="!planLoading && subscription">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
Current Usage
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="used-description">
|
|
||||||
<b>{{ subscription.usedPrivateRepos }}</b> of <b>{{ subscribedPlan.privateRepos }}</b> private repositories used
|
|
||||||
</div>
|
|
||||||
<div class="progress">
|
|
||||||
<div ng-class="'progress-bar ' + (planUsagePercent > 90 ? 'progress-bar-danger' : '')" role="progressbar" aria-valuenow="{{ subscription.usedPrivateRepos }}" aria-valuemin="0" aria-valuemax="{{ subscribedPlan.privateRepos }}" style="width: {{ planUsagePercent }}%;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Plans tab -->
|
<!-- Plans tab -->
|
||||||
<div id="plan" class="tab-pane">
|
<div id="plan" class="tab-pane active">
|
||||||
<div class="alert alert-danger" ng-show="overLimit">
|
<div class="plan-manager" organization="orgname"></div>
|
||||||
You are using more private repositories than your plan allows, please
|
|
||||||
upgrade your subscription to avoid disruptions in your organization's service.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="alert alert-warning" ng-show="nearLimit">
|
|
||||||
You are nearing the number of allowed private repositories. It might be time to think about
|
|
||||||
upgrading your subscription to avoid future disruptions in your organization's service.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<i class="fa fa-spinner fa-spin fa-3x" ng-show="planChanging"></i>
|
|
||||||
|
|
||||||
<table class="table table-hover plans-table" ng-show="!planChanging">
|
|
||||||
<thead>
|
|
||||||
<td>Plan</td>
|
|
||||||
<td>Private Repositories</td>
|
|
||||||
<td style="min-width: 64px">Price</td>
|
|
||||||
<td></td>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tr ng-repeat="plan in plans" ng-class="(subscription.plan === plan.stripeId) ? getActiveSubClass() : ''">
|
|
||||||
<td>{{ plan.title }}</td>
|
|
||||||
<td>{{ plan.privateRepos }}</td>
|
|
||||||
<td><div class="plan-price">${{ plan.price / 100 }}</div></td>
|
|
||||||
<td class="controls">
|
|
||||||
<div ng-switch='plan.stripeId'>
|
|
||||||
<div ng-switch-when='bus-free'>
|
|
||||||
<button class="btn button-hidden">Hidden!</button>
|
|
||||||
</div>
|
|
||||||
<div ng-switch-default>
|
|
||||||
<button class="btn btn-primary" ng-show="subscription.plan === 'bus-free'" ng-click="subscribe(plan.stripeId)">Subscribe</button>
|
|
||||||
<button class="btn btn-default" ng-hide="subscription.plan === 'bus-free' || subscription.plan === plan.stripeId"
|
|
||||||
ng-click="changeSubscription(plan.stripeId)">
|
|
||||||
Change
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId" ng-click="cancelSubscription()">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Members tab -->
|
<!-- Members tab -->
|
||||||
<div id="members" class="tab-pane">
|
<div id="members" class="tab-pane">
|
||||||
members
|
<i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i>
|
||||||
|
|
||||||
|
<div ng-show="!membersLoading">
|
||||||
|
<div class="side-controls">
|
||||||
|
<div class="result-count">
|
||||||
|
Showing {{(membersFound | filter:search | limitTo:50).length}} of {{(membersFound | filter:search).length}} matching members
|
||||||
|
</div>
|
||||||
|
<div class="filter-input">
|
||||||
|
<input id="member-filter" class="form-control" placeholder="Filter Members" type="text" ng-model="search.$">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table class="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Teams</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tr ng-repeat="memberInfo in (membersFound | filter:search | limitTo:50)">
|
||||||
|
<td>
|
||||||
|
<i class="fa fa-user"></i>
|
||||||
|
{{ memberInfo.username }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="team-link" ng-repeat="team in memberInfo.teams">
|
||||||
|
<i class="fa fa-group"></i>
|
||||||
|
<a href="/organization/{{ organization.name }}/teams/{{ team }}">{{ team }}</a>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -11,18 +11,22 @@
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<h3>Your Repositories</h3>
|
<span class="namespace-selector" user="user" namespace="namespace" ng-show="user.organizations"></span>
|
||||||
<div ng-show="private_repositories.length > 0">
|
<h3 ng-show="namespace == user.username">Your Repositories</h3>
|
||||||
<div class="repo-listing" ng-repeat="repository in private_repositories">
|
<h3 ng-show="namespace != user.username">Repositories</h3>
|
||||||
|
|
||||||
|
<div ng-show="user_repositories.length > 0">
|
||||||
|
<div class="repo-listing" ng-repeat="repository in user_repositories">
|
||||||
<span class="repo-circle no-background" repo="repository"></span>
|
<span class="repo-circle no-background" repo="repository"></span>
|
||||||
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
<a ng-href="/repository/{{repository.namespace}}/{{ repository.name }}">{{repository.namespace}}/{{repository.name}}</a>
|
||||||
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
<div class="description markdown-view" content="repository.description" first-line-only="true"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-show="private_repositories.length == 0" style="padding:20px;">
|
<div ng-show="user_repositories.length == 0" style="padding:20px;">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<h4>You don't have any repositories yet!</h4>
|
<h4 ng-show="namespace == user.username">You don't have any repositories yet!</h4>
|
||||||
|
<h4 ng-show="namespace != user.username">This organization does not have any repositories yet!</h4>
|
||||||
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
|
<a href="/guide"><b>Click here</b> to learn how to create a repository</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,80 +1,62 @@
|
||||||
<div class="container user-admin">
|
<div class="loading" ng-show="loading">
|
||||||
<div class="loading" ng-show="planLoading || planChanging">
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="row" ng-show="errorMessage">
|
<div class="loading" ng-show="!loading && !user">
|
||||||
<div class="col-md-12">
|
No matching user found
|
||||||
<div class="alert alert-danger">{{ errorMessage }}</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="user-admin container" ng-show="!loading && user">
|
||||||
|
<div class="row">
|
||||||
|
<div class="organization-header-element">
|
||||||
|
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=24&d=identicon">
|
||||||
|
<span class="organization-name">
|
||||||
|
{{ user.username }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row" ng-show="askForPassword">
|
<div class="row" ng-show="askForPassword">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<div class="alert alert-warning">Your account does not currently have a password. You will need to create a password before you will be able to <strong>push</strong> or <strong>pull</strong> repositories.</div>
|
<div class="alert alert-warning">Your account does not currently have a password. You will need to create a password before you will be able to <strong>push</strong> or <strong>pull</strong> repositories.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row" ng-hide="planLoading">
|
|
||||||
<div class="col-md-3" ng-repeat='plan in plans'>
|
|
||||||
<div class="panel" ng-class="{'panel-success': subscription.plan == plan.stripeId, 'panel-default': subscription.plan != plan.stripeId}">
|
|
||||||
<div class="panel-heading">
|
|
||||||
{{ plan.title }}
|
|
||||||
<span class="pull-right" ng-show="subscription.plan == plan.stripeId">
|
|
||||||
<i class="fa fa-ok"></i>
|
|
||||||
Subscribed
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body panel-plan">
|
|
||||||
<div class="plan-price">${{ plan.price / 100 }}</div>
|
|
||||||
<div class="plan-description"><b>{{ plan.privateRepos }}</b> Private Repositories</div>
|
|
||||||
<div ng-switch='plan.stripeId'>
|
|
||||||
<div ng-switch-when='free'>
|
|
||||||
<button class="btn button-hidden">Hidden!</button>
|
|
||||||
</div>
|
|
||||||
<div ng-switch-default>
|
|
||||||
<button class="btn btn-primary" ng-show="subscription.plan === 'free'" ng-click="subscribe(plan.stripeId)">Subscribe</button>
|
|
||||||
<button class="btn btn-default" ng-hide="subscription.plan === 'free' || subscription.plan === plan.stripeId" ng-click="changeSubscription(plan.stripeId)">Change</button>
|
|
||||||
<button class="btn btn-danger" ng-show="subscription.plan === plan.stripeId" ng-click="cancelSubscription()">Cancel</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row" ng-show="subscription">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
Plan Usage
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="used-description">
|
|
||||||
<b>{{ subscription.usedPrivateRepos }}</b> of <b>{{ subscribedPlan.privateRepos }}</b> private repositories used
|
|
||||||
</div>
|
|
||||||
<div class="progress">
|
|
||||||
<div ng-class="'progress-bar ' + (planUsagePercent > 90 ? 'progress-bar-danger' : '')" role="progressbar" aria-valuenow="{{ subscription.usedPrivateRepos }}" aria-valuemin="0" aria-valuemax="{{ subscribedPlan.privateRepos }}" style="width: {{ planUsagePercent }}%;">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="loading" ng-show="updatingUser">
|
<!-- Side tabs -->
|
||||||
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
<div class="col-md-2">
|
||||||
|
<ul class="nav nav-pills nav-stacked">
|
||||||
|
<li class="active"><a href="javascript:void(0)" data-toggle="tab" data-target="#plan">Plan and Usage</a></li>
|
||||||
|
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Set Password</a></li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
|
||||||
<div class="panel panel-default">
|
<!-- Content -->
|
||||||
<div class="panel-heading">
|
<div class="col-md-10">
|
||||||
Change Password
|
<div class="tab-content">
|
||||||
|
<!-- Plans tab -->
|
||||||
|
<div id="plan" class="tab-pane active">
|
||||||
|
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
|
||||||
<form class="form-change-pw" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual" data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
<!-- Change password tab -->
|
||||||
|
<div id="password" class="tab-pane">
|
||||||
|
<div class="loading" ng-show="updatingUser">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="form-change-pw" name="changePasswordForm" ng-submit="changePassword()" data-trigger="manual"
|
||||||
|
data-content="{{ changePasswordError }}" data-placement="right" ng-show="!awaitingConfirmation && !registering">
|
||||||
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required>
|
<input type="password" class="form-control" placeholder="Your new password" ng-model="user.password" required>
|
||||||
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword" match="user.password" required>
|
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="user.repeatPassword"
|
||||||
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit" analytics-on analytics-event="register">Change Password</button>
|
match="user.password" required>
|
||||||
|
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
|
||||||
|
analytics-on analytics-event="register">Change Password</button>
|
||||||
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Reference in a new issue