From a6a225dd5f9c5c97cb2a026e4144caba75d156b4 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 6 Nov 2013 17:30:20 -0500 Subject: [PATCH] Check in all new plan manager directive and add a nice donut chart for the repository usage by the user/org --- static/css/quay.css | 35 ++++++ static/directives/plan-manager.html | 58 ++++++++++ static/js/app.js | 171 +++++++++++++++++++++++++--- static/js/controllers.js | 69 ----------- static/js/graphing.js | 126 ++++++++++++++++++++ static/partials/org-admin.html | 85 +------------- 6 files changed, 377 insertions(+), 167 deletions(-) create mode 100644 static/directives/plan-manager.html diff --git a/static/css/quay.css b/static/css/quay.css index 0e100e7f4..9bb9435a0 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1358,6 +1358,41 @@ p.editable:hover i { 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 path.warning-0 { + fill: #c09853; +} + +#repository-usage-chart path.error-0 { + fill: #b94a48; +} + +#repository-usage-chart path.warning-1 { + fill: #fcf8e3; +} + +#repository-usage-chart path.error-1 { + fill: #f2dede; +} + +.plan-manager-element .usage-caption { + display: inline-block; + color: #aaa; + font-size: 26px; + margin-left: 10px; +} + + /* Overrides for the markdown editor. */ .wmd-panel .btn-toolbar { diff --git a/static/directives/plan-manager.html b/static/directives/plan-manager.html new file mode 100644 index 000000000..125c02efe --- /dev/null +++ b/static/directives/plan-manager.html @@ -0,0 +1,58 @@ +
+ + + + +
+ You are using more private repositories than your plan allows, please + upgrade your subscription to avoid disruptions in your organization's service. +
+ +
+ 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. +
+ + +
+
+ Repository Usage +
+ + + + + + + + + + + + + + + + +
PlanPrivate RepositoriesPrice
{{ plan.title }}{{ plan.privateRepos }}
${{ plan.price / 100 }}
+
+
+ +
+
+ + +
+
+
+
diff --git a/static/js/app.js b/static/js/app.js index ef73280ee..b3bd37aac 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -48,7 +48,7 @@ function getMarkedDown(string) { // Start the application code itself. 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 = { verified: false, anonymous: true, @@ -85,9 +85,7 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', userService.getCurrentSubscription = function(callback, failure) { if (currentSubscription) { callback(currentSubscription); } - - var getSubscription = Restangular.one('user/plan'); - getSubscription.get().then(function(sub) { + PlanService.getSubscription(null, function(sub) { currentSubscription = sub; callback(sub); }, failure); @@ -171,23 +169,47 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics', }); }; - planService.showSubscribeDialog = function($scope, planId, orgname, started, success, failed) { - var submitToken = function(token) { - $scope.$apply(function() { - started(); - }); + planService.getSubscription = function(organization, success, failure) { + var url = planService.getSubscriptionUrl(organization); + var getSubscription = Restangular.one(url); + getSubscription.get().then(success, failure); + }; + planService.getSubscriptionUrl = function(orgname) { + return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan'; + }; + + planService.setSubscription = function(orgname, planId, success, failure, opt_token) { + var subscriptionDetails = { + plan: planId + }; + + if (opt_token) { + subscriptionDetails['token'] = opt_token.id; + } + + var url = planService.getSubscriptionUrl(orgname); + var createSubscriptionRequest = Restangular.one(url); + createSubscriptionRequest.customPUT(subscriptionDetails).then(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'); - var subscriptionDetails = { - token: token.id, - plan: planId, - }; - - var url = orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan'; - var createSubscriptionRequest = Restangular.one(url); $scope.$apply(function() { - createSubscriptionRequest.customPUT(subscriptionDetails).then(success, failed); + started(); + planService.setSubscription(orgname, planId, success, failure); }); }; @@ -485,6 +507,121 @@ 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' + }, + 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.overLimit = true; + } else if (sub.usedPrivateRepos >= $scope.subscribedPlan.privateRepos * 0.7) { + $scope.nearLimit = true; + } else { + $scope.overLimit = false; + $scope.nearLimit = false; + } + + 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; } + if (!$scope.user && !$scope.organization) { return; } + + PlanService.getSubscription($scope.organization, subscribedToPlan, function() { + // User/Organization has no subscription. + subscribedToPlan({ 'plan': getFreePlan() }); + }); + }; + + var loadPlans = function() { + PlanService.getPlans(function(plans) { + $scope.plans = plans[$scope.organization ? 'business' : 'user']; + update(); + }); + }; + + // Start the initial download. + loadPlans(); + update(); + + $scope.$watch('organization', function() { + update(); + }); + + $scope.$watch('user', function() { + update(); + }); + } + }; + return directiveDefinitionObject; +}); + + + quayApp.directive('namespaceSelector', function () { var directiveDefinitionObject = { priority: 0, diff --git a/static/js/controllers.js b/static/js/controllers.js index 309252f24..be9b3702f 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1191,75 +1191,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(); } diff --git a/static/js/graphing.js b/static/js/graphing.js index 5581dc3ce..274e72324 100644 --- a/static/js/graphing.js +++ b/static/js/graphing.js @@ -1130,4 +1130,130 @@ ImageFileChangeTree.prototype.toggle_ = function(d) { d.children = d._children; 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 getClass = function(i) { + if (total > 0 && (count / total) >= 0.7) { + return 'warning-' + i; + } + + if (count >= total) { + return 'error-' + i; + } + + return 'normal'; + }; + + 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 getClass(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 + this.path_.attr("class", function(d, i) { return getClass(i); }); + + // 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_(); }; \ No newline at end of file diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index d94ac4476..f95aa361f 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -13,94 +13,17 @@
-
- -
-
- You are using more private repositories than your plan allows, please - upgrade your subscription - to avoid disruptions in your organization's service. -
- -
- 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. -
- - - -
-
-
-
- Current Usage -
-
-
- {{ subscription.usedPrivateRepos }} of {{ subscribedPlan.privateRepos }} private repositories used -
-
-
-
-
-
-
-
-
-
- +
-
-
- You are using more private repositories than your plan allows, please - upgrade your subscription to avoid disruptions in your organization's service. -
- -
- 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. -
- - - - - - - - - - - - - - - - - -
PlanPrivate RepositoriesPrice
{{ plan.title }}{{ plan.privateRepos }}
${{ plan.price / 100 }}
-
-
- -
-
- - - -
-
-
+
+