Merge branch 'master' into fix_build
This commit is contained in:
commit
96fdae4f0d
23 changed files with 1323 additions and 692 deletions
|
@ -3,6 +3,12 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 410px) {
|
||||
.olrk-normal {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.resource-view-element {
|
||||
position: relative;
|
||||
}
|
||||
|
@ -104,10 +110,6 @@ html, body {
|
|||
word-wrap: normal !important;
|
||||
}
|
||||
|
||||
.code-info {
|
||||
border-bottom: 1px dashed #aaa;
|
||||
}
|
||||
|
||||
.toggle-icon {
|
||||
font-size: 22px;
|
||||
padding: 6px;
|
||||
|
@ -662,49 +664,165 @@ i.toggle-icon:hover {
|
|||
|
||||
.plans-list .plan {
|
||||
vertical-align: top;
|
||||
|
||||
padding: 10px;
|
||||
border: 1px solid #eee;
|
||||
border-top: 4px solid #94C9F7;
|
||||
font-size: 1.4em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.plans-list .plan.small {
|
||||
border: 1px solid #ddd;
|
||||
border-top: 4px solid #428bca;
|
||||
margin-top: 0px;
|
||||
font-size: 1.6em;
|
||||
}
|
||||
|
||||
.plans-list .plan.business-plan {
|
||||
border: 1px solid #eee;
|
||||
border-top: 4px solid #94F794;
|
||||
}
|
||||
|
||||
.plans-list .plan.bus-small {
|
||||
border: 1px solid #ddd;
|
||||
border-top: 4px solid #47A447;
|
||||
margin-top: 0px;
|
||||
font-size: 1.6em;
|
||||
border-top: 6px solid #46ac39;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.plans-list .plan.bus-small .plan-box {
|
||||
background: black !important;
|
||||
}
|
||||
|
||||
.plans-list .plan:last-child {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.plans-list .plan .plan-box {
|
||||
background: #444;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.plans-list .plan .plan-title {
|
||||
text-transform: uppercase;
|
||||
padding-top: 25px;
|
||||
padding-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.visible-sm-inline {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.hidden-sm-inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
@media (max-width: 991px) and (min-width: 768px) {
|
||||
.visible-sm-inline {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.hidden-sm-inline {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.plans-list .plan-box .description {
|
||||
color: white;
|
||||
margin-top: 6px;
|
||||
font-size: 12px !important;
|
||||
}
|
||||
|
||||
.plans-list .plan button {
|
||||
margin-top: 6px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.plans-list .plan.bus-small button {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.plans-list .features-bar {
|
||||
padding-top: 248px;
|
||||
}
|
||||
|
||||
.plans-list .features-bar .feature .count {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.plans-list .features-bar .feature {
|
||||
height: 43px;
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.context-tooltip {
|
||||
border-bottom: 1px dotted black;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.plans-list .features-bar .feature i {
|
||||
margin-left: 16px;
|
||||
float: right;
|
||||
width: 16px;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.plans-list .plan .features {
|
||||
padding: 6px;
|
||||
background: #eee;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.plans-list .plan .feature {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.plans-list .plan .feature:after {
|
||||
content: "";
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.plans-list .plan .visible-xs .feature {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.plans-list .plan .visible-xs .feature:after {
|
||||
float: left;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.plans-list .plan .feature.notpresent {
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.plans-list .plan .feature.present:after {
|
||||
background: #428bca;
|
||||
}
|
||||
|
||||
.plans-list .plan.business-plan .feature.present:after {
|
||||
background: #46ac39;
|
||||
}
|
||||
|
||||
.plans-list .plan .count, .plans-list .features-bar .count {
|
||||
background: white;
|
||||
border-bottom: 0px;
|
||||
text-align: center !important;
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.plans-list .plan .count b, .plans-list .features-bar .count b {
|
||||
font-size: 1.5em;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.plans-list .plan .feature:last-child {
|
||||
border-bottom: 0px;
|
||||
}
|
||||
|
||||
.plans-list .plan-price {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
margin-bottom: 10px;
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
font-size: 1.8em;
|
||||
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -732,7 +850,8 @@ i.toggle-icon:hover {
|
|||
.plans-list .plan .description {
|
||||
font-size: 1em;
|
||||
font-size: 16px;
|
||||
margin-bottom: 10px;
|
||||
height: 34px;
|
||||
|
||||
}
|
||||
|
||||
.plans-list .plan .smaller {
|
||||
|
@ -1926,28 +2045,28 @@ p.editable:hover i {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.org-admin .invoice-title {
|
||||
.billing-invoices-element .invoice-title {
|
||||
padding: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.org-admin .invoice-status .success {
|
||||
.billing-invoices-element .invoice-status .success {
|
||||
color: green;
|
||||
}
|
||||
|
||||
.org-admin .invoice-status .pending {
|
||||
.billing-invoices-element .invoice-status .pending {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
.org-admin .invoice-status .danger {
|
||||
.billing-invoices-element .invoice-status .danger {
|
||||
color: red;
|
||||
}
|
||||
|
||||
.org-admin .invoice-amount:before {
|
||||
.billing-invoices-element .invoice-amount:before {
|
||||
content: '$';
|
||||
}
|
||||
|
||||
.org-admin .invoice-details {
|
||||
.billing-invoices-element .invoice-details {
|
||||
margin-left: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
|
@ -1956,21 +2075,21 @@ p.editable:hover i {
|
|||
border-left: 2px solid #eee !important;
|
||||
}
|
||||
|
||||
.org-admin .invoice-details td {
|
||||
.billing-invoices-element .invoice-details td {
|
||||
border: 0px solid transparent !important;
|
||||
}
|
||||
|
||||
.org-admin .invoice-details dl {
|
||||
.billing-invoices-element .invoice-details dl {
|
||||
margin: 0px;
|
||||
}
|
||||
|
||||
.org-admin .invoice-details dd {
|
||||
.billing-invoices-element .invoice-details dd {
|
||||
margin-left: 10px;
|
||||
padding: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.org-admin .invoice-title:hover {
|
||||
.billing-invoices-element .invoice-title:hover {
|
||||
color: steelblue;
|
||||
}
|
||||
|
||||
|
@ -2096,6 +2215,14 @@ p.editable:hover i {
|
|||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.plan-manager-element .plans-list-table .deprecated-plan {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.plan-manager-element .plans-list-table .deprecated-plan-label {
|
||||
font-size: 0.7em;
|
||||
}
|
||||
|
||||
.plans-table-element table {
|
||||
margin: 20px;
|
||||
border: 1px solid #eee;
|
||||
|
|
53
static/directives/billing-invoices.html
Normal file
53
static/directives/billing-invoices.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
<div class="billing-invoices-element">
|
||||
<div ng-show="loading">
|
||||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!loading && !invoices">
|
||||
No invoices have been created
|
||||
</div>
|
||||
|
||||
<div ng-show="!loading && invoices">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Billing Date/Time</th>
|
||||
<th>Amount Due</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody class="invoice" ng-repeat="invoice in invoices">
|
||||
<tr class="invoice-title">
|
||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
||||
<td>
|
||||
<span class="invoice-status">
|
||||
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
|
||||
<span class="danger" ng-show="!invoice.paid && invoice.attempted && invoice.closed">Payment failed</span>
|
||||
<span class="danger" ng-show="!invoice.paid && invoice.attempted && !invoice.closed">Payment failed - Will retry soon</span>
|
||||
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
||||
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
|
||||
<td colspan="3">
|
||||
<dl class="dl-normal">
|
||||
<dt>Billing Period</dt>
|
||||
<dd>
|
||||
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
|
||||
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -7,7 +7,7 @@
|
|||
<div class="panel-body">
|
||||
<div class="quay-spinner" ng-show="!currentCard || changingCard"></div>
|
||||
<div class="current-card" ng-show="currentCard && !changingCard">
|
||||
<img src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4">
|
||||
<img ng-src="{{ '/static/img/creditcards/' + getCreditImage(currentCard) }}" ng-show="currentCard.last4">
|
||||
<span class="no-card-outline" ng-show="!currentCard.last4"></span>
|
||||
|
||||
<span class="last4" ng-show="currentCard.last4">****-****-****-<b>{{ currentCard.last4 }}</b></span>
|
||||
|
|
|
@ -38,7 +38,10 @@
|
|||
<a href="javascript:void(0)" class="dropdown-toggle user-dropdown" data-toggle="dropdown">
|
||||
<img src="//www.gravatar.com/avatar/{{ user.gravatar }}?s=32&d=identicon" />
|
||||
{{ user.username }}
|
||||
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan">
|
||||
<span class="badge user-notification notification-animated" ng-show="user.askForPassword || overPlan"
|
||||
bs-tooltip="(user.askForPassword ? 'A password is needed for this account<br>' : '') + (overPlan ? 'You are using more private repositories than your plan allows' : '')"
|
||||
data-placement="left"
|
||||
data-container="body">
|
||||
{{ (user.askForPassword ? 1 : 0) + (overPlan ? 1 : 0) }}
|
||||
</span>
|
||||
<b class="caret"></b>
|
||||
|
|
|
@ -32,14 +32,23 @@
|
|||
<td></td>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="plan in plans" ng-class="(subscribedPlan.stripeId === plan.stripeId) ? getActiveSubClass() : ''">
|
||||
<td>{{ plan.title }}</td>
|
||||
<tr ng-repeat="plan in plans" ng-show="isPlanVisible(plan, subscribedPlan)"
|
||||
ng-class="{'active':(subscribedPlan.stripeId === plan.stripeId), 'deprecated-plan':plan.deprecated}">
|
||||
<td>
|
||||
{{ plan.title }}
|
||||
<div class="deprecated-plan-label" ng-show="plan.deprecated">
|
||||
<span class="context-tooltip" title="This plan has been discontinued. As a valued early adopter, you may continue to stay on this plan indefinitely." bs-tooltip="tooltip.title" data-placement="right">Discontinued Plan</span>
|
||||
</div>
|
||||
</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 ng-switch='plan.deprecated'>
|
||||
<div ng-switch-when='true'>
|
||||
<button class="btn btn-danger" ng-click="cancelSubscription()">
|
||||
<span class="quay-spinner" ng-show="planChanging"></span>
|
||||
<span ng-show="!planChanging">Cancel</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ng-switch-default>
|
||||
<button class="btn" ng-show="subscribedPlan.stripeId !== plan.stripeId"
|
||||
|
|
531
static/js/app.js
531
static/js/app.js
|
@ -1,6 +1,18 @@
|
|||
var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
||||
var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$';
|
||||
|
||||
function getRestUrl(args) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
if (i > 0) {
|
||||
url += '/';
|
||||
}
|
||||
url += encodeURI(arguments[i])
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
function getFirstTextLine(commentString) {
|
||||
if (!commentString) { return ''; }
|
||||
|
||||
|
@ -34,11 +46,8 @@ function getFirstTextLine(commentString) {
|
|||
return '';
|
||||
}
|
||||
|
||||
function createRobotAccount(Restangular, is_org, orgname, name, callback) {
|
||||
var url = is_org ? getRestUrl('organization', orgname, 'robots', name) :
|
||||
getRestUrl('user/robots', name);
|
||||
var createRobot = Restangular.one(url);
|
||||
createRobot.customPUT().then(callback, function(resp) {
|
||||
function createRobotAccount(ApiService, is_org, orgname, name, callback) {
|
||||
ApiService.createRobot(is_org ? orgname : null, null, {'robot_shortname': name}).then(callback, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The robot account could not be created',
|
||||
"title": "Cannot create robot account",
|
||||
|
@ -52,14 +61,18 @@ function createRobotAccount(Restangular, is_org, orgname, name, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function createOrganizationTeam(Restangular, orgname, teamname, callback) {
|
||||
var createTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname));
|
||||
function createOrganizationTeam(ApiService, orgname, teamname, callback) {
|
||||
var data = {
|
||||
'name': teamname,
|
||||
'role': 'member'
|
||||
};
|
||||
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
createTeam.customPOST(data).then(callback, function() {
|
||||
ApiService.updateOrganizationTeam(data, params).then(callback, function() {
|
||||
bootbox.dialog({
|
||||
"message": resp.data ? resp.data : 'The team could not be created',
|
||||
"title": "Cannot create team",
|
||||
|
@ -73,17 +86,6 @@ function createOrganizationTeam(Restangular, orgname, teamname, callback) {
|
|||
});
|
||||
}
|
||||
|
||||
function getRestUrl(args) {
|
||||
var url = '';
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
if (i > 0) {
|
||||
url += '/';
|
||||
}
|
||||
url += encodeURI(arguments[i])
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
function getMarkedDown(string) {
|
||||
return Markdown.getSanitizingConverter().makeHtml(string || '');
|
||||
}
|
||||
|
@ -91,6 +93,137 @@ function getMarkedDown(string) {
|
|||
// Start the application code itself.
|
||||
quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'restangular', 'angularMoment', 'angulartics', /*'angulartics.google.analytics',*/ 'angulartics.mixpanel', '$strap.directives', 'ngCookies'], function($provide, cfpLoadingBarProvider) {
|
||||
cfpLoadingBarProvider.includeSpinner = false;
|
||||
|
||||
$provide.factory('ApiService', ['Restangular', function(Restangular) {
|
||||
var apiService = {};
|
||||
|
||||
var getResource = function(path) {
|
||||
var resource = {};
|
||||
resource.url = path;
|
||||
resource.withOptions = function(options) {
|
||||
this.options = options;
|
||||
return this;
|
||||
};
|
||||
|
||||
resource.get = function(processor, opt_errorHandler) {
|
||||
var options = this.options;
|
||||
var performer = Restangular.one(this.url);
|
||||
|
||||
var result = {
|
||||
'loading': true,
|
||||
'value': null,
|
||||
'hasError': false
|
||||
};
|
||||
|
||||
performer.get(options).then(function(resp) {
|
||||
result.value = processor(resp);
|
||||
result.loading = false;
|
||||
}, function(resp) {
|
||||
result.hasError = true;
|
||||
result.loading = false;
|
||||
if (opt_errorHandler) {
|
||||
opt_errorHandler(resp);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
return resource;
|
||||
};
|
||||
|
||||
var formatMethodName = function(endpointName) {
|
||||
var formatted = '';
|
||||
for (var i = 0; i < endpointName.length; ++i) {
|
||||
var c = endpointName[i];
|
||||
if (c == '_') {
|
||||
c = endpointName[i + 1].toUpperCase();
|
||||
i++;
|
||||
}
|
||||
|
||||
formatted += c;
|
||||
}
|
||||
|
||||
return formatted;
|
||||
};
|
||||
|
||||
var buildUrl = function(path, parameters) {
|
||||
// We already have /api/ on the URLs, so remove them from the paths.
|
||||
path = path.substr('/api/'.length, path.length);
|
||||
|
||||
var url = '';
|
||||
for (var i = 0; i < path.length; ++i) {
|
||||
var c = path[i];
|
||||
if (c == '<') {
|
||||
var end = path.indexOf('>', i);
|
||||
var varName = path.substr(i + 1, end - i - 1);
|
||||
var colon = varName.indexOf(':');
|
||||
var isPathVar = false;
|
||||
if (colon > 0) {
|
||||
isPathVar = true;
|
||||
varName = varName.substr(colon + 1);
|
||||
}
|
||||
|
||||
if (!parameters[varName]) {
|
||||
throw new Error('Missing parameter: ' + varName);
|
||||
}
|
||||
|
||||
url += isPathVar ? parameters[varName] : encodeURI(parameters[varName]);
|
||||
i = end;
|
||||
continue;
|
||||
}
|
||||
|
||||
url += c;
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var getGenericMethodName = function(userMethodName) {
|
||||
return formatMethodName(userMethodName.replace('_user', ''));
|
||||
};
|
||||
|
||||
var buildMethodsForEndpoint = function(endpoint) {
|
||||
var method = endpoint.methods[0].toLowerCase();
|
||||
var methodName = formatMethodName(endpoint['name']);
|
||||
apiService[methodName] = function(opt_options, opt_parameters) {
|
||||
return Restangular.one(buildUrl(endpoint['path'], opt_parameters))['custom' + method.toUpperCase()](opt_options);
|
||||
};
|
||||
|
||||
if (method == 'get') {
|
||||
apiService[methodName + 'AsResource'] = function(opt_parameters) {
|
||||
return getResource(buildUrl(endpoint['path'], opt_parameters));
|
||||
};
|
||||
}
|
||||
|
||||
if (endpoint['user_method']) {
|
||||
apiService[getGenericMethodName(endpoint['user_method'])] = function(orgname, opt_options, opt_parameters) {
|
||||
if (orgname) {
|
||||
if (orgname.name) {
|
||||
orgname = orgname.name;
|
||||
}
|
||||
|
||||
var params = jQuery.extend({'orgname' : orgname}, opt_parameters || {});
|
||||
return apiService[methodName](opt_options, params);
|
||||
} else {
|
||||
return apiService[formatMethodName(endpoint['user_method'])](opt_options, opt_parameters);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Construct the methods for each API endpoint.
|
||||
if (!window.__endpoints) {
|
||||
return apiService;
|
||||
}
|
||||
|
||||
for (var i = 0; i < window.__endpoints.length; ++i) {
|
||||
var endpoint = window.__endpoints[i];
|
||||
buildMethodsForEndpoint(endpoint);
|
||||
}
|
||||
|
||||
return apiService;
|
||||
}]);
|
||||
|
||||
$provide.factory('CookieService', ['$cookies', '$cookieStore', function($cookies, $cookieStore) {
|
||||
var cookieService = {};
|
||||
|
@ -113,7 +246,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
return cookieService;
|
||||
}]);
|
||||
|
||||
$provide.factory('UserService', ['Restangular', 'CookieService', function(Restangular, CookieService) {
|
||||
$provide.factory('UserService', ['ApiService', 'CookieService', function(ApiService, CookieService) {
|
||||
var userResponse = {
|
||||
verified: false,
|
||||
anonymous: true,
|
||||
|
@ -139,8 +272,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
};
|
||||
|
||||
userService.load = function(opt_callback) {
|
||||
var userFetch = Restangular.one('user/');
|
||||
userFetch.get().then(function(loadedUser) {
|
||||
ApiService.getLoggedInUser().then(function(loadedUser) {
|
||||
userResponse = loadedUser;
|
||||
|
||||
if (!userResponse.anonymous) {
|
||||
|
@ -198,48 +330,6 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
return userService;
|
||||
}]);
|
||||
|
||||
|
||||
$provide.factory('ApiService', ['Restangular', function(Restangular) {
|
||||
var apiService = {}
|
||||
apiService.at = function(locationPieces) {
|
||||
var location = getRestUrl.apply(this, arguments);
|
||||
var info = {
|
||||
'url': location,
|
||||
'caller': Restangular.one(location),
|
||||
'withOptions': function(options) {
|
||||
info.options = options;
|
||||
return info;
|
||||
},
|
||||
'get': function(processor, opt_errorHandler) {
|
||||
var options = info.options;
|
||||
var caller = info.caller;
|
||||
var result = {
|
||||
'loading': true,
|
||||
'value': null,
|
||||
'hasError': false
|
||||
};
|
||||
|
||||
caller.get(options).then(function(resp) {
|
||||
result.value = processor(resp);
|
||||
result.loading = false;
|
||||
}, function(resp) {
|
||||
result.hasError = true;
|
||||
result.loading = false;
|
||||
if (opt_errorHandler) {
|
||||
opt_errorHandler(resp);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
return info;
|
||||
};
|
||||
|
||||
return apiService;
|
||||
}]);
|
||||
|
||||
$provide.factory('KeyService', ['$location', function($location) {
|
||||
var keyService = {}
|
||||
|
||||
|
@ -254,13 +344,17 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
return keyService;
|
||||
}]);
|
||||
|
||||
$provide.factory('PlanService', ['Restangular', 'KeyService', 'UserService', 'CookieService',
|
||||
function(Restangular, KeyService, UserService, CookieService) {
|
||||
$provide.factory('PlanService', ['KeyService', 'UserService', 'CookieService', 'ApiService',
|
||||
function(KeyService, UserService, CookieService, ApiService) {
|
||||
var plans = null;
|
||||
var planDict = {};
|
||||
var planService = {};
|
||||
var listeners = [];
|
||||
|
||||
planService.getFreePlan = function() {
|
||||
return 'free';
|
||||
};
|
||||
|
||||
planService.registerListener = function(obj, callback) {
|
||||
listeners.push({'obj': obj, 'callback': callback});
|
||||
};
|
||||
|
@ -278,6 +372,27 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
CookieService.putSession('quay.notedplan', planId);
|
||||
};
|
||||
|
||||
planService.isOrgCompatible = function(plan) {
|
||||
return plan['stripeId'] == planService.getFreePlan() || plan['bus_features'];
|
||||
};
|
||||
|
||||
planService.getMatchingBusinessPlan = function(callback) {
|
||||
planService.getPlans(function() {
|
||||
planService.getSubscription(null, function(sub) {
|
||||
var plan = planDict[sub.plan];
|
||||
if (!plan) {
|
||||
planService.getMinimumPlan(0, true, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
|
||||
planService.getMinimumPlan(count, true, callback);
|
||||
}, function() {
|
||||
planService.getMinimumPlan(0, true, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
planService.handleNotedPlan = function() {
|
||||
var planId = planService.getAndResetNotedPlan();
|
||||
if (!planId) { return; }
|
||||
|
@ -288,13 +403,11 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
}
|
||||
|
||||
planService.getPlan(planId, function(plan) {
|
||||
planService.isBusinessPlan(planId, function(bus) {
|
||||
if (bus) {
|
||||
document.location = '/organizations/new/?plan=' + planId;
|
||||
} else {
|
||||
document.location = '/user?plan=' + planId;
|
||||
}
|
||||
});
|
||||
if (planService.isOrgCompatible(plan)) {
|
||||
document.location = '/organizations/new/?plan=' + planId;
|
||||
} else {
|
||||
document.location = '/user?plan=' + planId;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
@ -330,72 +443,53 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
return;
|
||||
}
|
||||
|
||||
var getPlans = Restangular.one('plans');
|
||||
getPlans.get().then(function(data) {
|
||||
ApiService.listPlans().then(function(data) {
|
||||
var i = 0;
|
||||
for(i = 0; i < data.user.length; i++) {
|
||||
planDict[data.user[i].stripeId] = data.user[i];
|
||||
for(i = 0; i < data.plans.length; i++) {
|
||||
planDict[data.plans[i].stripeId] = data.plans[i];
|
||||
}
|
||||
for(i = 0; i < data.business.length; i++) {
|
||||
planDict[data.business[i].stripeId] = data.business[i];
|
||||
}
|
||||
plans = data;
|
||||
plans = data.plans;
|
||||
callback(plans);
|
||||
}, function() { callback([]); });
|
||||
};
|
||||
|
||||
planService.getMatchingBusinessPlan = function(callback) {
|
||||
planService.getPlans(function() {
|
||||
planService.getSubscription(null, function(sub) {
|
||||
var plan = planDict[sub.plan];
|
||||
if (!plan) {
|
||||
planService.getMinimumPlan(0, true, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Math.max(sub.usedPrivateRepos, plan.privateRepos);
|
||||
planService.getMinimumPlan(count, true, callback);
|
||||
}, function() {
|
||||
planService.getMinimumPlan(0, true, callback);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
planService.isBusinessPlan = function(planId, callback) {
|
||||
planService.getPlans = function(callback, opt_includePersonal) {
|
||||
planService.verifyLoaded(function() {
|
||||
planSource = plans.business;
|
||||
for (var i = 0; i < planSource.length; i++) {
|
||||
var plan = planSource[i];
|
||||
if (plan.stripeId == planId) {
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
var filtered = [];
|
||||
for (var i = 0; i < plans.length; ++i) {
|
||||
var plan = plans[i];
|
||||
if (plan['deprecated']) { continue; }
|
||||
if (!opt_includePersonal && !planService.isOrgCompatible(plan)) { continue; }
|
||||
filtered.push(plan);
|
||||
}
|
||||
callback(false);
|
||||
callback(filtered);
|
||||
});
|
||||
};
|
||||
|
||||
planService.getPlans = function(callback) {
|
||||
planService.verifyLoaded(callback);
|
||||
};
|
||||
|
||||
planService.getPlan = function(planId, callback) {
|
||||
planService.getPlanIncludingDeprecated(planId, function(plan) {
|
||||
if (!plan['deprecated']) {
|
||||
callback(plan);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
planService.getPlanIncludingDeprecated = function(planId, callback) {
|
||||
planService.verifyLoaded(function() {
|
||||
if (planDict[planId]) {
|
||||
if (planDict[planId]) {
|
||||
callback(planDict[planId]);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
planService.getMinimumPlan = function(privateCount, isBusiness, callback) {
|
||||
planService.verifyLoaded(function() {
|
||||
var planSource = plans.user;
|
||||
if (isBusiness) {
|
||||
planSource = plans.business;
|
||||
}
|
||||
planService.getPlans(function(plans) {
|
||||
for (var i = 0; i < plans.length; i++) {
|
||||
var plan = plans[i];
|
||||
if (isBusiness && !planService.isOrgCompatible(plan)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (var i = 0; i < planSource.length; i++) {
|
||||
var plan = planSource[i];
|
||||
if (plan.privateRepos >= privateCount) {
|
||||
callback(plan);
|
||||
return;
|
||||
|
@ -407,13 +501,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
};
|
||||
|
||||
planService.getSubscription = function(orgname, success, failure) {
|
||||
var url = planService.getSubscriptionUrl(orgname);
|
||||
var getSubscription = Restangular.one(url);
|
||||
getSubscription.get().then(success, failure);
|
||||
};
|
||||
|
||||
planService.getSubscriptionUrl = function(orgname) {
|
||||
return orgname ? getRestUrl('organization', orgname, 'plan') : 'user/plan';
|
||||
ApiService.getSubscription(orgname).then(success, failure);
|
||||
};
|
||||
|
||||
planService.setSubscription = function(orgname, planId, success, failure, opt_token) {
|
||||
|
@ -425,9 +513,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
subscriptionDetails['token'] = opt_token.id;
|
||||
}
|
||||
|
||||
var url = planService.getSubscriptionUrl(orgname);
|
||||
var createSubscriptionRequest = Restangular.one(url);
|
||||
createSubscriptionRequest.customPUT(subscriptionDetails).then(function(resp) {
|
||||
ApiService.updateSubscription(orgname, subscriptionDetails).then(function(resp) {
|
||||
success(resp);
|
||||
planService.getPlan(planId, function(plan) {
|
||||
for (var i = 0; i < listeners.length; ++i) {
|
||||
|
@ -438,9 +524,7 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
};
|
||||
|
||||
planService.getCardInfo = function(orgname, callback) {
|
||||
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
|
||||
var getCard = Restangular.one(url);
|
||||
getCard.customGET().then(function(resp) {
|
||||
ApiService.getCard(orgname).then(function(resp) {
|
||||
callback(resp.card);
|
||||
}, function() {
|
||||
callback({'is_valid': false});
|
||||
|
@ -453,6 +537,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
}
|
||||
|
||||
planService.getPlan(planId, function(plan) {
|
||||
if (orgname && !planService.isOrgCompatible(plan)) { return; }
|
||||
|
||||
planService.getCardInfo(orgname, function(cardInfo) {
|
||||
if (plan.price > 0 && !cardInfo.last4) {
|
||||
planService.showSubscribeDialog($scope, orgname, planId, callbacks);
|
||||
|
@ -485,12 +571,10 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'rest
|
|||
'token': token.id
|
||||
};
|
||||
|
||||
var url = orgname ? getRestUrl('organization', orgname, 'card') : 'user/card';
|
||||
var changeCardRequest = Restangular.one(url);
|
||||
changeCardRequest.customPOST(cardInfo).then(callbacks['success'], function(resp) {
|
||||
planService.handleCardError(resp);
|
||||
callbacks['failure'](resp);
|
||||
});
|
||||
ApiService.setCard(orgname, cardInfo).then(callbacks['success'], function(resp) {
|
||||
planService.handleCardError(resp);
|
||||
callbacks['failure'](resp);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -732,10 +816,9 @@ quayApp.directive('userSetup', function () {
|
|||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) {
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||
$scope.sendRecovery = function() {
|
||||
var signinPost = Restangular.one('recovery');
|
||||
signinPost.customPOST($scope.recovery).then(function() {
|
||||
ApiService.requestRecoveryEmail($scope.recovery).then(function() {
|
||||
$scope.invalidEmail = false;
|
||||
$scope.sent = true;
|
||||
}, function(result) {
|
||||
|
@ -765,7 +848,7 @@ quayApp.directive('signinForm', function () {
|
|||
'signInStarted': '&signInStarted',
|
||||
'signedIn': '&signedIn'
|
||||
},
|
||||
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) {
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||
$scope.showGithub = function() {
|
||||
$scope.markStarted();
|
||||
|
||||
|
@ -792,8 +875,7 @@ quayApp.directive('signinForm', function () {
|
|||
$scope.signin = function() {
|
||||
$scope.markStarted();
|
||||
|
||||
var signinPost = Restangular.one('signin');
|
||||
signinPost.customPOST($scope.user).then(function() {
|
||||
ApiService.signinUser($scope.user).then(function() {
|
||||
$scope.needsEmailVerification = false;
|
||||
$scope.invalidCredentials = false;
|
||||
|
||||
|
@ -833,7 +915,7 @@ quayApp.directive('signupForm', function () {
|
|||
scope: {
|
||||
|
||||
},
|
||||
controller: function($scope, $location, $timeout, Restangular, KeyService, UserService) {
|
||||
controller: function($scope, $location, $timeout, ApiService, KeyService, UserService) {
|
||||
$('.form-signup').popover();
|
||||
|
||||
angulartics.waitForVendorApi(mixpanel, 500, function(loadedMixpanel) {
|
||||
|
@ -850,8 +932,7 @@ quayApp.directive('signupForm', function () {
|
|||
$('.form-signup').popover('hide');
|
||||
$scope.registering = true;
|
||||
|
||||
var newUserPost = Restangular.one('user/');
|
||||
newUserPost.customPOST($scope.newUser).then(function() {
|
||||
ApiService.createNewUser($scope.newUser).then(function() {
|
||||
$scope.awaitingConfirmation = true;
|
||||
$scope.registering = false;
|
||||
|
||||
|
@ -904,7 +985,7 @@ quayApp.directive('dockerAuthDialog', function () {
|
|||
'shown': '=shown',
|
||||
'counter': '=counter'
|
||||
},
|
||||
controller: function($scope, $element, Restangular) {
|
||||
controller: function($scope, $element) {
|
||||
$scope.isDownloadSupported = function() {
|
||||
try { return !!new Blob(); } catch(e){}
|
||||
return false;
|
||||
|
@ -962,6 +1043,53 @@ quayApp.filter('visibleLogFilter', function () {
|
|||
});
|
||||
|
||||
|
||||
quayApp.directive('billingInvoices', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/billing-invoices.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'user': '=user',
|
||||
'visible': '=visible'
|
||||
},
|
||||
controller: function($scope, $element, $sce, ApiService) {
|
||||
$scope.loading = false;
|
||||
$scope.invoiceExpanded = {};
|
||||
|
||||
$scope.toggleInvoice = function(id) {
|
||||
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
||||
var hasValidOrg = !!$scope.organization;
|
||||
var isValid = hasValidUser || hasValidOrg;
|
||||
|
||||
if (!$scope.visible || !isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
ApiService.listInvoices($scope.organization).then(function(resp) {
|
||||
$scope.invoices = resp.invoices;
|
||||
$scope.loading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('user', update);
|
||||
$scope.$watch('visible', update);
|
||||
}
|
||||
};
|
||||
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('logsView', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
@ -976,7 +1104,7 @@ quayApp.directive('logsView', function () {
|
|||
'repository': '=repository',
|
||||
'performer': '=performer'
|
||||
},
|
||||
controller: function($scope, $element, $sce, Restangular) {
|
||||
controller: function($scope, $element, $sce, Restangular, ApiService) {
|
||||
$scope.loading = true;
|
||||
$scope.logs = null;
|
||||
$scope.kindsAllowed = null;
|
||||
|
@ -1092,6 +1220,8 @@ quayApp.directive('logsView', function () {
|
|||
|
||||
$scope.loading = true;
|
||||
|
||||
// Note: We construct the URLs here manually because we also use it for the download
|
||||
// path.
|
||||
var url = getRestUrl('user/logs');
|
||||
if ($scope.organization) {
|
||||
url = getRestUrl('organization', $scope.organization.name, 'logs');
|
||||
|
@ -1195,7 +1325,7 @@ quayApp.directive('robotsManager', function () {
|
|||
'organization': '=organization',
|
||||
'user': '=user'
|
||||
},
|
||||
controller: function($scope, $element, Restangular) {
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.ROBOT_PATTERN = ROBOT_PATTERN;
|
||||
$scope.robots = null;
|
||||
$scope.loading = false;
|
||||
|
@ -1220,7 +1350,7 @@ quayApp.directive('robotsManager', function () {
|
|||
$scope.createRobot = function(name) {
|
||||
if (!name) { return; }
|
||||
|
||||
createRobotAccount(Restangular, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
|
||||
createRobotAccount(ApiService, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name,
|
||||
function(created) {
|
||||
$scope.robots.push(created);
|
||||
});
|
||||
|
@ -1228,11 +1358,7 @@ quayApp.directive('robotsManager', function () {
|
|||
|
||||
$scope.deleteRobot = function(info) {
|
||||
var shortName = $scope.getShortenedName(info.name);
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', shortName) :
|
||||
getRestUrl('user/robots', shortName);
|
||||
|
||||
var deleteRobot = Restangular.one(url);
|
||||
deleteRobot.customDELETE().then(function(resp) {
|
||||
ApiService.deleteRobot($scope.organization, null, {'robot_shortname': shortName}).then(function(resp) {
|
||||
for (var i = 0; i < $scope.robots.length; ++i) {
|
||||
if ($scope.robots[i].name == info.name) {
|
||||
$scope.robots.splice(i, 1);
|
||||
|
@ -1258,9 +1384,7 @@ quayApp.directive('robotsManager', function () {
|
|||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots') : 'user/robots';
|
||||
var getRobots = Restangular.one(url);
|
||||
getRobots.customGET($scope.obj).then(function(resp) {
|
||||
ApiService.getRobots($scope.organization).then(function(resp) {
|
||||
$scope.robots = resp.robots;
|
||||
$scope.loading = false;
|
||||
});
|
||||
|
@ -1493,7 +1617,7 @@ quayApp.directive('headerBar', function () {
|
|||
restrict: 'C',
|
||||
scope: {
|
||||
},
|
||||
controller: function($scope, $element, $location, UserService, PlanService, Restangular) {
|
||||
controller: function($scope, $element, $location, UserService, PlanService, ApiService) {
|
||||
$scope.overPlan = false;
|
||||
|
||||
var checkOverPlan = function() {
|
||||
|
@ -1502,8 +1626,7 @@ quayApp.directive('headerBar', function () {
|
|||
return;
|
||||
}
|
||||
|
||||
var checkPrivate = Restangular.one('user/private');
|
||||
checkPrivate.customGET().then(function(resp) {
|
||||
ApiService.getUserPrivateCount().then(function(resp) {
|
||||
$scope.overPlan = resp.privateCount > resp.reposAllowed;
|
||||
});
|
||||
};
|
||||
|
@ -1515,10 +1638,9 @@ quayApp.directive('headerBar', function () {
|
|||
PlanService.registerListener(this, checkOverPlan);
|
||||
|
||||
$scope.signout = function() {
|
||||
var signoutPost = Restangular.one('signout');
|
||||
signoutPost.customPOST().then(function() {
|
||||
UserService.load();
|
||||
$location.path('/');
|
||||
ApiService.logout().then(function() {
|
||||
UserService.load();
|
||||
$location.path('/');
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1549,7 +1671,7 @@ quayApp.directive('entitySearch', function () {
|
|||
'includeTeams': '=includeTeams',
|
||||
'isOrganization': '=isOrganization'
|
||||
},
|
||||
controller: function($scope, $element, Restangular, UserService) {
|
||||
controller: function($scope, $element, Restangular, UserService, ApiService) {
|
||||
$scope.lazyLoading = true;
|
||||
$scope.isAdmin = false;
|
||||
|
||||
|
@ -1559,16 +1681,12 @@ quayApp.directive('entitySearch', function () {
|
|||
$scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace);
|
||||
|
||||
if ($scope.isOrganization && $scope.includeTeams) {
|
||||
var url = getRestUrl('organization', $scope.namespace);
|
||||
var getOrganization = Restangular.one(url);
|
||||
getOrganization.customGET().then(function(resp) {
|
||||
ApiService.getOrganization(null, {'orgname': $scope.namespace}).then(function(resp) {
|
||||
$scope.teams = resp.teams;
|
||||
});
|
||||
}
|
||||
|
||||
var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots';
|
||||
var getRobots = Restangular.one(url);
|
||||
getRobots.customGET().then(function(resp) {
|
||||
ApiService.getRobots($scope.isOrganization ? $scope.namespace : null).then(function(resp) {
|
||||
$scope.robots = resp.robots;
|
||||
$scope.lazyLoading = false;
|
||||
}, function() {
|
||||
|
@ -1588,7 +1706,7 @@ quayApp.directive('entitySearch', function () {
|
|||
return;
|
||||
}
|
||||
|
||||
createOrganizationTeam(Restangular, $scope.namespace, teamname, function(created) {
|
||||
createOrganizationTeam(ApiService, $scope.namespace, teamname, function(created) {
|
||||
$scope.setEntity(created.name, 'team', false);
|
||||
$scope.teams[teamname] = created;
|
||||
});
|
||||
|
@ -1607,7 +1725,7 @@ quayApp.directive('entitySearch', function () {
|
|||
return;
|
||||
}
|
||||
|
||||
createRobotAccount(Restangular, $scope.isOrganization, $scope.namespace, robotname, function(created) {
|
||||
createRobotAccount(ApiService, $scope.isOrganization, $scope.namespace, robotname, function(created) {
|
||||
$scope.setEntity(created.name, 'user', true);
|
||||
$scope.robots.push(created);
|
||||
});
|
||||
|
@ -1733,7 +1851,7 @@ quayApp.directive('billingOptions', function () {
|
|||
'user': '=user',
|
||||
'organization': '=organization'
|
||||
},
|
||||
controller: function($scope, $element, PlanService, Restangular) {
|
||||
controller: function($scope, $element, PlanService, ApiService) {
|
||||
$scope.invoice_email = false;
|
||||
$scope.currentCard = null;
|
||||
|
||||
|
@ -1803,9 +1921,7 @@ quayApp.directive('billingOptions', function () {
|
|||
|
||||
var save = function() {
|
||||
$scope.working = true;
|
||||
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name) : 'user/';
|
||||
var conductSave = Restangular.one(url);
|
||||
conductSave.customPUT($scope.obj).then(function(resp) {
|
||||
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
|
||||
$scope.working = false;
|
||||
});
|
||||
};
|
||||
|
@ -1840,11 +1956,19 @@ quayApp.directive('planManager', function () {
|
|||
'readyForPlan': '&readyForPlan',
|
||||
'planChanged': '&planChanged'
|
||||
},
|
||||
controller: function($scope, $element, PlanService, Restangular) {
|
||||
controller: function($scope, $element, PlanService, ApiService) {
|
||||
var hasSubscription = false;
|
||||
|
||||
$scope.getActiveSubClass = function() {
|
||||
return 'active';
|
||||
$scope.isPlanVisible = function(plan, subscribedPlan) {
|
||||
if (plan['deprecated']) {
|
||||
return plan == subscribedPlan;
|
||||
}
|
||||
|
||||
if ($scope.organization && !PlanService.isOrgCompatible(plan)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
$scope.changeSubscription = function(planId) {
|
||||
|
@ -1865,17 +1989,17 @@ quayApp.directive('planManager', function () {
|
|||
};
|
||||
|
||||
$scope.cancelSubscription = function() {
|
||||
$scope.changeSubscription(getFreePlan());
|
||||
$scope.changeSubscription(PlanService.getFreePlan());
|
||||
};
|
||||
|
||||
var subscribedToPlan = function(sub) {
|
||||
$scope.subscription = sub;
|
||||
|
||||
if (sub.plan != getFreePlan()) {
|
||||
if (sub.plan != PlanService.getFreePlan()) {
|
||||
hasSubscription = true;
|
||||
}
|
||||
|
||||
PlanService.getPlan(sub.plan, function(subscribedPlan) {
|
||||
PlanService.getPlanIncludingDeprecated(sub.plan, function(subscribedPlan) {
|
||||
$scope.subscribedPlan = subscribedPlan;
|
||||
$scope.planUsagePercent = sub.usedPrivateRepos * 100 / $scope.subscribedPlan.privateRepos;
|
||||
|
||||
|
@ -1905,22 +2029,13 @@ quayApp.directive('planManager', function () {
|
|||
});
|
||||
};
|
||||
|
||||
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() });
|
||||
subscribedToPlan({ 'plan': PlanService.getFreePlan() });
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1929,13 +2044,13 @@ quayApp.directive('planManager', function () {
|
|||
if (!$scope.user && !$scope.organization) { return; }
|
||||
|
||||
$scope.loadingPlans = true;
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans[$scope.organization ? 'business' : 'user'];
|
||||
PlanService.verifyLoaded(function(plans) {
|
||||
$scope.plans = plans;
|
||||
update();
|
||||
|
||||
if ($scope.readyForPlan) {
|
||||
var planRequested = $scope.readyForPlan();
|
||||
if (planRequested && planRequested != getFreePlan()) {
|
||||
if (planRequested && planRequested != PlanService.getFreePlan()) {
|
||||
$scope.changeSubscription(planRequested);
|
||||
}
|
||||
}
|
||||
|
@ -2101,15 +2216,27 @@ quayApp.run(['$location', '$rootScope', 'Restangular', 'UserService', 'PlanServi
|
|||
// Check if we need to redirect based on a previously chosen plan.
|
||||
PlanService.handleNotedPlan();
|
||||
|
||||
var changeTab = function(activeTab) {
|
||||
var changeTab = function(activeTab, opt_timeout) {
|
||||
var checkCount = 0;
|
||||
|
||||
$timeout(function() {
|
||||
if (checkCount > 5) { return; }
|
||||
checkCount++;
|
||||
|
||||
$('a[data-toggle="tab"]').each(function(index) {
|
||||
var tabName = this.getAttribute('data-target').substr(1);
|
||||
if (tabName == activeTab) {
|
||||
this.click();
|
||||
if (tabName != activeTab) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.clientWidth == 0) {
|
||||
changeTab(activeTab, 500);
|
||||
return;
|
||||
}
|
||||
|
||||
this.click();
|
||||
});
|
||||
});
|
||||
}, opt_timeout);
|
||||
};
|
||||
|
||||
var resetDefaultTab = function() {
|
||||
|
|
13
static/js/bootstrap.js
vendored
Normal file
13
static/js/bootstrap.js
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
$.ajax({
|
||||
type: 'GET',
|
||||
async: false,
|
||||
url: '/api/discovery',
|
||||
success: function(data) {
|
||||
window.__endpoints = data.endpoints;
|
||||
},
|
||||
error: function() {
|
||||
setTimeout(function() {
|
||||
$('#couldnotloadModal').modal({});
|
||||
}, 250);
|
||||
}
|
||||
});
|
|
@ -22,7 +22,7 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
|
|||
// Load the list of plans.
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans;
|
||||
});
|
||||
}, /* include the personal plan */ true);
|
||||
|
||||
// Monitor any user changes and place the current user into the scope.
|
||||
UserService.updateUserIn($scope);
|
||||
|
@ -33,19 +33,10 @@ function PlansCtrl($scope, $location, UserService, PlanService) {
|
|||
};
|
||||
|
||||
$scope.buyNow = function(plan) {
|
||||
PlanService.notePlan(plan);
|
||||
if ($scope.user && !$scope.user.anonymous) {
|
||||
document.location = '/user?plan=' + plan;
|
||||
PlanService.handleNotedPlan();
|
||||
} else {
|
||||
PlanService.notePlan(plan);
|
||||
$('#signinModal').modal({});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.createOrg = function(plan) {
|
||||
if ($scope.user && !$scope.user.anonymous) {
|
||||
document.location = '/organizations/new/?plan=' + plan;
|
||||
} else {
|
||||
PlanService.notePlan(plan);
|
||||
$('#signinModal').modal({});
|
||||
}
|
||||
};
|
||||
|
@ -76,14 +67,15 @@ function RepoListCtrl($scope, Restangular, UserService, ApiService) {
|
|||
}
|
||||
|
||||
var options = {'public': false, 'sort': true, 'namespace': namespace};
|
||||
$scope.user_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
|
||||
|
||||
$scope.user_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories;
|
||||
});
|
||||
};
|
||||
|
||||
var loadPublicRepos = function() {
|
||||
var options = {'public': true, 'private': false, 'sort': true, 'limit': 10};
|
||||
$scope.public_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
|
||||
$scope.public_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories;
|
||||
});
|
||||
};
|
||||
|
@ -127,7 +119,7 @@ function LandingCtrl($scope, UserService, ApiService) {
|
|||
}
|
||||
|
||||
var options = {'limit': 4, 'public': false, 'sort': true, 'namespace': namespace };
|
||||
$scope.my_repositories = ApiService.at('repository').withOptions(options).get(function(resp) {
|
||||
$scope.my_repositories = ApiService.listReposAsResource().withOptions(options).get(function(resp) {
|
||||
return resp.repositories;
|
||||
});
|
||||
};
|
||||
|
@ -178,7 +170,8 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
|||
};
|
||||
|
||||
$scope.loadImageChanges = function(image) {
|
||||
$scope.currentImageChangeResource = ApiService.at('repository', namespace, name, 'image', image.id, 'changes').get(function(ci) {
|
||||
var params = {'repository': namespace + '/' + name, 'image_id': image.id};
|
||||
$scope.currentImageChangeResource = ApiService.getImageChangesAsResource(params).get(function(ci) {
|
||||
$scope.currentImageChanges = ci;
|
||||
});
|
||||
};
|
||||
|
@ -249,8 +242,9 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
|||
};
|
||||
|
||||
var fetchRepository = function() {
|
||||
var params = {'repository': namespace + '/' + name};
|
||||
$rootScope.title = 'Loading Repository...';
|
||||
$scope.repository = ApiService.at('repository', namespace, name).get(function(repo) {
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
// Set the repository object.
|
||||
$scope.repo = repo;
|
||||
|
||||
|
@ -292,6 +286,7 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
|||
};
|
||||
|
||||
var getBuildInfo = function(repo) {
|
||||
// Note: We use restangular manually here because we need to turn off the loading bar.
|
||||
var buildInfo = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
|
||||
buildInfo.withHttpConfig({
|
||||
'ignoreLoadingBar': true
|
||||
|
@ -321,7 +316,8 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
|||
};
|
||||
|
||||
var listImages = function() {
|
||||
$scope.imageHistory = ApiService.at('repository', namespace, name, 'image').get(function(resp) {
|
||||
var params = {'repository': namespace + '/' + name};
|
||||
$scope.imageHistory = ApiService.listRepositoryImagesAsResource(params).get(function(resp) {
|
||||
// Dispose of any existing tree.
|
||||
if ($scope.tree) {
|
||||
$scope.tree.dispose();
|
||||
|
@ -361,11 +357,6 @@ function RepoCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $lo
|
|||
}
|
||||
|
||||
function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope) {
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
|
||||
|
@ -452,8 +443,8 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
'friendlyName': $scope.newToken.friendlyName
|
||||
};
|
||||
|
||||
var permissionPost = Restangular.one('repository/' + namespace + '/' + name + '/tokens/');
|
||||
permissionPost.customPOST(friendlyName).then(function(newToken) {
|
||||
var params = {'repository': namespace + '/' + name};
|
||||
ApiService.createToken(friendlyName, params).then(function(newToken) {
|
||||
$scope.newToken.friendlyName = '';
|
||||
$scope.createTokenForm.$setPristine();
|
||||
$scope.tokens[newToken.code] = newToken;
|
||||
|
@ -461,8 +452,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
$scope.deleteToken = function(tokenCode) {
|
||||
var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode);
|
||||
deleteAction.customDELETE().then(function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'code': tokenCode
|
||||
};
|
||||
|
||||
ApiService.deleteToken(null, params).then(function() {
|
||||
delete $scope.tokens[tokenCode];
|
||||
});
|
||||
};
|
||||
|
@ -472,8 +467,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
'role': newAccess
|
||||
};
|
||||
|
||||
var deleteAction = Restangular.one('repository/' + namespace + '/' + name + '/tokens/' + tokenCode);
|
||||
deleteAction.customPUT(role).then(function(updated) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'code': tokenCode
|
||||
};
|
||||
|
||||
ApiService.changeToken(role, params).then(function(updated) {
|
||||
$scope.tokens[updated.code] = updated;
|
||||
});
|
||||
};
|
||||
|
@ -495,8 +494,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
var visibility = {
|
||||
'visibility': newAccess
|
||||
};
|
||||
var visibilityPost = Restangular.one('repository/' + namespace + '/' + name + '/changevisibility');
|
||||
visibilityPost.customPOST(visibility).then(function() {
|
||||
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.changeRepoVisibility(visibility, params).then(function() {
|
||||
$scope.repo.is_public = newAccess == 'public';
|
||||
}, function() {
|
||||
$('#cannotchangeModal').modal({});
|
||||
|
@ -510,8 +513,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
$scope.deleteRepo = function() {
|
||||
$('#confirmdeleteModal').modal('hide');
|
||||
|
||||
var deleteAction = Restangular.one('repository/' + namespace + '/' + name);
|
||||
deleteAction.customDELETE().then(function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.deleteRepository(null, params).then(function() {
|
||||
$scope.repo = null;
|
||||
|
||||
setTimeout(function() {
|
||||
|
@ -523,8 +529,12 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
$scope.loadWebhooks = function() {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
$scope.newWebhook = {};
|
||||
$scope.webhooksResource = ApiService.at('repository', namespace, name, 'webhook').get(function(resp) {
|
||||
$scope.webhooksResource = ApiService.listWebhooksAsResource(params).get(function(resp) {
|
||||
$scope.webhooks = resp.webhooks;
|
||||
return $scope.webhooks;
|
||||
});
|
||||
|
@ -535,8 +545,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
return;
|
||||
}
|
||||
|
||||
var newWebhook = Restangular.one('repository/' + namespace + '/' + name + '/webhook/');
|
||||
newWebhook.customPOST($scope.newWebhook).then(function(resp) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.createWebhook($scope.newWebhook, params).then(function(resp) {
|
||||
$scope.webhooks.push(resp);
|
||||
$scope.newWebhook.url = '';
|
||||
$scope.createWebhookForm.$setPristine();
|
||||
|
@ -544,15 +557,22 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
$scope.deleteWebhook = function(webhook) {
|
||||
var deleteWebhookReq = Restangular.one('repository/' + namespace + '/' + name + '/webhook/' + webhook.public_id);
|
||||
deleteWebhookReq.customDELETE().then(function(resp) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'public_id': webhook.public_id
|
||||
};
|
||||
|
||||
ApiService.deleteWebhook(null, params).then(function(resp) {
|
||||
$scope.webhooks.splice($scope.webhooks.indexOf(webhook), 1);
|
||||
});
|
||||
};
|
||||
|
||||
var fetchTokens = function() {
|
||||
var tokensFetch = Restangular.one('repository/' + namespace + '/' + name + '/tokens/');
|
||||
tokensFetch.get().then(function(resp) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
ApiService.listRepoTokens(null, params).then(function(resp) {
|
||||
$scope.tokens = resp.tokens;
|
||||
}, function() {
|
||||
$scope.tokens = null;
|
||||
|
@ -569,7 +589,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
};
|
||||
|
||||
var fetchRepository = function() {
|
||||
$scope.repository = ApiService.at('repository', namespace, name).get(function(repo) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name
|
||||
};
|
||||
|
||||
$scope.repository = ApiService.getRepoAsResource(params).get(function(repo) {
|
||||
$scope.repo = repo;
|
||||
|
||||
$rootScope.title = 'Settings - ' + namespace + '/' + name;
|
||||
|
@ -581,6 +605,11 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
fetchPermissions('team');
|
||||
fetchTokens();
|
||||
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
|
||||
return $scope.repo;
|
||||
});
|
||||
};
|
||||
|
@ -589,13 +618,14 @@ function RepoAdminCtrl($scope, Restangular, ApiService, $routeParams, $rootScope
|
|||
fetchRepository();
|
||||
}
|
||||
|
||||
function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, UserService, KeyService, $routeParams) {
|
||||
function UserAdminCtrl($scope, $timeout, $location, ApiService, PlanService, UserService, KeyService, $routeParams) {
|
||||
if ($routeParams['migrate']) {
|
||||
$('#migrateTab').tab('show')
|
||||
}
|
||||
|
||||
UserService.updateUserIn($scope, function(user) {
|
||||
$scope.askForPassword = user.askForPassword;
|
||||
$scope.cuser = jQuery.extend({}, user);
|
||||
});
|
||||
|
||||
$scope.readyForPlan = function() {
|
||||
|
@ -611,8 +641,22 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
|
|||
|
||||
$('.form-change-pw').popover();
|
||||
|
||||
$scope.logsShown = 0;
|
||||
$scope.invoicesShown = 0;
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
if (!$scope.hasPaidBusinessPlan) { return; }
|
||||
$scope.logsShown++;
|
||||
};
|
||||
|
||||
$scope.loadInvoices = function() {
|
||||
if (!$scope.hasPaidBusinessPlan) { return; }
|
||||
$scope.invoicesShown++;
|
||||
};
|
||||
|
||||
$scope.planChanged = function(plan) {
|
||||
$scope.hasPaidPlan = plan && plan.price > 0;
|
||||
$scope.hasPaidBusinessPlan = PlanService.isOrgCompatible(plan) && plan.price > 0;
|
||||
};
|
||||
|
||||
$scope.showConvertForm = function() {
|
||||
|
@ -621,7 +665,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
|
|||
});
|
||||
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.orgPlans = plans.business;
|
||||
$scope.orgPlans = plans;
|
||||
});
|
||||
|
||||
$scope.convertStep = 1;
|
||||
|
@ -640,8 +684,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
|
|||
'plan': $scope.org.plan.stripeId
|
||||
};
|
||||
|
||||
var convertAccount = Restangular.one('user/convert');
|
||||
convertAccount.customPOST(data).then(function(resp) {
|
||||
ApiService.convertUserToOrganization(data).then(function(resp) {
|
||||
UserService.load();
|
||||
$location.path('/');
|
||||
}, function(resp) {
|
||||
|
@ -658,14 +701,14 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
|
|||
$('.form-change-pw').popover('hide');
|
||||
$scope.updatingUser = true;
|
||||
$scope.changePasswordSuccess = false;
|
||||
var changePasswordPost = Restangular.one('user/');
|
||||
changePasswordPost.customPUT($scope.user).then(function() {
|
||||
|
||||
ApiService.changeUserDetails($scope.cuser).then(function() {
|
||||
$scope.updatingUser = false;
|
||||
$scope.changePasswordSuccess = true;
|
||||
|
||||
// Reset the form
|
||||
$scope.user.password = '';
|
||||
$scope.user.repeatPassword = '';
|
||||
$scope.cuser.password = '';
|
||||
$scope.cuser.repeatPassword = '';
|
||||
$scope.changePasswordForm.$setPristine();
|
||||
|
||||
// Reload the user.
|
||||
|
@ -681,7 +724,7 @@ function UserAdminCtrl($scope, $timeout, $location, Restangular, PlanService, Us
|
|||
};
|
||||
}
|
||||
|
||||
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, Restangular) {
|
||||
function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService) {
|
||||
var namespace = $routeParams.namespace;
|
||||
var name = $routeParams.name;
|
||||
var imageid = $routeParams.image;
|
||||
|
@ -737,7 +780,12 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, R
|
|||
};
|
||||
|
||||
var fetchImage = function() {
|
||||
$scope.image = ApiService.at('repository', namespace, name, 'image', imageid).get(function(image) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'image_id': imageid
|
||||
};
|
||||
|
||||
$scope.image = ApiService.getImageAsResource(params).get(function(image) {
|
||||
$scope.repo = {
|
||||
'name': name,
|
||||
'namespace': namespace
|
||||
|
@ -757,8 +805,12 @@ function ImageViewCtrl($scope, $routeParams, $rootScope, $timeout, ApiService, R
|
|||
};
|
||||
|
||||
var fetchChanges = function() {
|
||||
var changesFetch = Restangular.one('repository/' + namespace + '/' + name + '/image/' + imageid + '/changes');
|
||||
changesFetch.get().then(function(changes) {
|
||||
var params = {
|
||||
'repository': namespace + '/' + name,
|
||||
'image_id': imageid
|
||||
};
|
||||
|
||||
ApiService.getImageChanges(null, params).then(function(changes) {
|
||||
var combinedChanges = [];
|
||||
var addCombinedChanges = function(c, kind) {
|
||||
for (var i = 0; i < c.length; ++i) {
|
||||
|
@ -786,7 +838,7 @@ function V1Ctrl($scope, $location, UserService) {
|
|||
UserService.updateUserIn($scope);
|
||||
}
|
||||
|
||||
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangular, PlanService) {
|
||||
function NewRepoCtrl($scope, $location, $http, $timeout, UserService, ApiService, PlanService) {
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
$scope.repo = {
|
||||
|
@ -817,8 +869,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
|||
if (isUserNamespace) {
|
||||
// Load the user's subscription information in case they want to create a private
|
||||
// repository.
|
||||
var checkPrivateAllowed = Restangular.one('user/private');
|
||||
checkPrivateAllowed.get().then(function(resp) {
|
||||
ApiService.getUserPrivateCount().then(function(resp) {
|
||||
if (resp.privateCount + 1 > resp.reposAllowed) {
|
||||
PlanService.getMinimumPlan(resp.privateCount + 1, false, function(minimum) {
|
||||
$scope.planRequired = minimum;
|
||||
|
@ -831,8 +882,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
|||
$scope.checkingPlan = false;
|
||||
});
|
||||
} else {
|
||||
var checkPrivateAllowed = Restangular.one('organization/' + namespace + '/private');
|
||||
checkPrivateAllowed.get().then(function(resp) {
|
||||
ApiService.getOrganizationPrivateAllowed(null, {'orgname': namespace}).then(function(resp) {
|
||||
$scope.planRequired = resp.privateAllowed ? null : {};
|
||||
$scope.checkingPlan = false;
|
||||
}, function() {
|
||||
|
@ -863,8 +913,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
|||
'description': repo.description
|
||||
};
|
||||
|
||||
var createPost = Restangular.one('repository');
|
||||
createPost.customPOST(data).then(function(created) {
|
||||
ApiService.createRepo(data).then(function(created) {
|
||||
$scope.creating = false;
|
||||
$scope.created = created;
|
||||
|
||||
|
@ -907,9 +956,12 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
|||
'file_id': fileId
|
||||
};
|
||||
|
||||
var startBuildCall = Restangular.one('repository/' + repo.namespace + '/' + repo.name + '/build/');
|
||||
startBuildCall.customPOST(data).then(function(resp) {
|
||||
$location.path('/repository/' + repo.namespace + '/' + repo.name);
|
||||
var params = {
|
||||
'repository': repo.namespace + '/' + repo.name
|
||||
};
|
||||
|
||||
ApiService.requestRepoBuild(data, params).then(function(resp) {
|
||||
$location.path('/repository/' + params.repository);
|
||||
}, function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
});
|
||||
|
@ -958,8 +1010,7 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
|||
'mimeType': mimeType
|
||||
};
|
||||
|
||||
var getUploadUrl = Restangular.one('filedrop/');
|
||||
getUploadUrl.customPOST(data).then(function(resp) {
|
||||
var getUploadUrl = ApiService.getFiledropUrl(data).then(function(resp) {
|
||||
conductUpload(repo, file, resp.url, resp.file_id, mimeType);
|
||||
}, function() {
|
||||
$('#couldnotbuildModal').modal();
|
||||
|
@ -987,14 +1038,9 @@ function NewRepoCtrl($scope, $location, $http, $timeout, UserService, Restangula
|
|||
};
|
||||
}
|
||||
|
||||
function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
|
||||
function OrgViewCtrl($rootScope, $scope, ApiService, $routeParams) {
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
|
||||
$scope.TEAM_PATTERN = TEAM_PATTERN;
|
||||
$rootScope.title = 'Loading...';
|
||||
|
||||
|
@ -1008,10 +1054,14 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
var previousRole = $scope.organization.teams[teamname].role;
|
||||
$scope.organization.teams[teamname].role = role;
|
||||
|
||||
var updateTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname));
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
var data = $scope.organization.teams[teamname];
|
||||
|
||||
updateTeam.customPUT(data).then(function(resp) {
|
||||
ApiService.updateOrganizationTeam(data, params).then(function(resp) {
|
||||
}, function(resp) {
|
||||
$scope.organization.teams[teamname].role = previousRole;
|
||||
$scope.roleError = resp.data || '';
|
||||
|
@ -1032,7 +1082,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
return;
|
||||
}
|
||||
|
||||
createOrganizationTeam(Restangular, orgname, teamname, function(created) {
|
||||
createOrganizationTeam(ApiService, orgname, teamname, function(created) {
|
||||
$scope.organization.teams[teamname] = created;
|
||||
});
|
||||
};
|
||||
|
@ -1047,8 +1097,12 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
if (!$scope.currentDeleteTeam) { return; }
|
||||
|
||||
var teamname = $scope.currentDeleteTeam;
|
||||
var deleteAction = Restangular.one(getRestUrl('organization', orgname, 'team', teamname));
|
||||
deleteAction.customDELETE().then(function() {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
ApiService.deleteOrganizationTeam(null, params).then(function() {
|
||||
delete $scope.organization.teams[teamname];
|
||||
$scope.currentDeleteTeam = null;
|
||||
}, function() {
|
||||
|
@ -1058,10 +1112,15 @@ function OrgViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
$rootScope.title = orgname;
|
||||
$rootScope.description = 'Viewing organization ' + orgname;
|
||||
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1074,17 +1133,12 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
|||
|
||||
// Load the list of plans.
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans.business;
|
||||
$scope.plans = plans;
|
||||
$scope.plan_map = {};
|
||||
|
||||
var addPlans = function(plans) {
|
||||
for (var i = 0; i < plans.length; ++i) {
|
||||
$scope.plan_map[plans[i].stripeId] = plans[i];
|
||||
}
|
||||
};
|
||||
|
||||
addPlans(plans.user);
|
||||
addPlans(plans.business);
|
||||
for (var i = 0; i < plans.length; ++i) {
|
||||
$scope.plan_map[plans[i].stripeId] = plans[i];
|
||||
}
|
||||
});
|
||||
|
||||
$scope.orgname = orgname;
|
||||
|
@ -1092,37 +1146,29 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
|||
$scope.membersFound = null;
|
||||
$scope.invoiceLoading = true;
|
||||
$scope.logsShown = 0;
|
||||
$scope.invoicesShown = 0;
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsShown++;
|
||||
};
|
||||
|
||||
$scope.loadInvoices = function() {
|
||||
$scope.invoicesShown++;
|
||||
};
|
||||
|
||||
$scope.planChanged = function(plan) {
|
||||
$scope.hasPaidPlan = plan && plan.price > 0;
|
||||
};
|
||||
|
||||
$scope.loadInvoices = function() {
|
||||
if ($scope.invoices) { return; }
|
||||
$scope.invoiceLoading = true;
|
||||
|
||||
var getInvoices = Restangular.one(getRestUrl('organization', orgname, 'invoices'));
|
||||
getInvoices.get().then(function(resp) {
|
||||
$scope.invoiceExpanded = {};
|
||||
$scope.invoices = resp.invoices;
|
||||
$scope.invoiceLoading = false;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.toggleInvoice = function(id) {
|
||||
$scope.invoiceExpanded[id] = !$scope.invoiceExpanded[id];
|
||||
};
|
||||
|
||||
$scope.loadMembers = function() {
|
||||
if ($scope.membersFound) { return; }
|
||||
$scope.membersLoading = true;
|
||||
|
||||
var getMembers = Restangular.one(getRestUrl('organization', orgname, 'members'));
|
||||
getMembers.get().then(function(resp) {
|
||||
|
||||
var params = {
|
||||
'orgname': orgname
|
||||
};
|
||||
|
||||
ApiService.getOrganizationMembers(null, params).then(function(resp) {
|
||||
var membersArray = [];
|
||||
for (var key in resp.members) {
|
||||
if (resp.members.hasOwnProperty(key)) {
|
||||
|
@ -1136,7 +1182,7 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
|||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
if (org && org.is_admin) {
|
||||
$scope.organization = org;
|
||||
$rootScope.title = orgname + ' (Admin)';
|
||||
|
@ -1150,11 +1196,6 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService
|
|||
}
|
||||
|
||||
function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams) {
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
|
||||
var teamname = $routeParams.teamname;
|
||||
var orgname = $routeParams.orgname;
|
||||
|
||||
|
@ -1166,8 +1207,13 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
$scope.addNewMember = function(member) {
|
||||
if ($scope.members[member.name]) { return; }
|
||||
|
||||
var addMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', member.name));
|
||||
addMember.customPOST().then(function(resp) {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname,
|
||||
'membername': member.name
|
||||
};
|
||||
|
||||
ApiService.updateOrganizationTeamMember(null, params).then(function(resp) {
|
||||
$scope.members[member.name] = resp;
|
||||
}, function() {
|
||||
$('#cannotChangeMembersModal').modal({});
|
||||
|
@ -1175,8 +1221,13 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
};
|
||||
|
||||
$scope.removeMember = function(username) {
|
||||
var removeMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', username));
|
||||
removeMember.customDELETE().then(function(resp) {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname,
|
||||
'membername': username
|
||||
};
|
||||
|
||||
ApiService.deleteOrganizationTeamMember(null, params).then(function(resp) {
|
||||
delete $scope.members[username];
|
||||
}, function() {
|
||||
$('#cannotChangeMembersModal').modal({});
|
||||
|
@ -1186,16 +1237,20 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
$scope.updateForDescription = function(content) {
|
||||
$scope.organization.teams[teamname].description = content;
|
||||
|
||||
var updateTeam = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname));
|
||||
var data = $scope.organization.teams[teamname];
|
||||
updateTeam.customPUT(data).then(function(resp) {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
var teaminfo = $scope.organization.teams[teamname];
|
||||
ApiService.updateOrganizationTeam(teaminfo, params).then(function(resp) {
|
||||
}, function() {
|
||||
$('#cannotChangeTeamModal').modal({});
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
$scope.team = $scope.organization.teams[teamname];
|
||||
$rootScope.title = teamname + ' (' + $scope.orgname + ')';
|
||||
|
@ -1206,11 +1261,22 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
};
|
||||
|
||||
var loadMembers = function() {
|
||||
$scope.membersResource = ApiService.at('organization', $scope.orgname, 'team', teamname, 'members').get(function(resp) {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'teamname': teamname
|
||||
};
|
||||
|
||||
$scope.membersResource = ApiService.getOrganizationTeamMembersAsResource(params).get(function(resp) {
|
||||
$scope.members = resp.members;
|
||||
$scope.canEditMembers = resp.can_edit;
|
||||
|
||||
$('.info-icon').popover({
|
||||
'trigger': 'hover',
|
||||
'html': true
|
||||
});
|
||||
|
||||
return resp.members;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Load the organization.
|
||||
|
@ -1219,18 +1285,17 @@ function TeamViewCtrl($rootScope, $scope, Restangular, ApiService, $routeParams)
|
|||
|
||||
function OrgsCtrl($scope, UserService) {
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
browserchrome.update();
|
||||
}
|
||||
|
||||
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, Restangular) {
|
||||
function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, PlanService, ApiService) {
|
||||
UserService.updateUserIn($scope);
|
||||
|
||||
var requested = $routeParams['plan'];
|
||||
|
||||
// Load the list of plans.
|
||||
PlanService.getPlans(function(plans) {
|
||||
$scope.plans = plans.business;
|
||||
$scope.plans = plans;
|
||||
$scope.currentPlan = null;
|
||||
if (requested) {
|
||||
PlanService.getPlan(requested, function(plan) {
|
||||
|
@ -1263,8 +1328,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
|||
'email': org.email
|
||||
};
|
||||
|
||||
var createPost = Restangular.one('organization/');
|
||||
createPost.customPOST(data).then(function(created) {
|
||||
ApiService.createOrganization(data).then(function(created) {
|
||||
$scope.created = created;
|
||||
|
||||
// Reset the organizations list.
|
||||
|
@ -1311,14 +1375,19 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul
|
|||
$scope.ready = false;
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.at('organization', orgname).get(function(org) {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
return org;
|
||||
});
|
||||
};
|
||||
|
||||
var loadMemberInfo = function() {
|
||||
$scope.memberResource = ApiService.at('organization', $scope.orgname, 'members', membername).get(function(resp) {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'membername': membername
|
||||
};
|
||||
|
||||
$scope.memberResource = ApiService.getOrganizationMemberAsResource(params).get(function(resp) {
|
||||
$scope.memberInfo = resp.member;
|
||||
|
||||
$rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')';
|
||||
|
|
|
@ -101,16 +101,16 @@ Email: my@email.com</pre>
|
|||
as an HTTP <b>POST</b> to the specified URL, with a JSON body describing the push:<br><br>
|
||||
<pre>
|
||||
{
|
||||
<span class="code-info" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
|
||||
<span class="code-info" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
|
||||
<span class="code-info" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
|
||||
<span class="code-info" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
|
||||
<span class="code-info" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
|
||||
<span class="context-tooltip" title="The number of images pushed" bs-tooltip="tooltip.title">"pushed_image_count"</span>: 2,
|
||||
<span class="context-tooltip" title="The name of the repository (without its namespace)" bs-tooltip="tooltip.title">"name"</span>: "ubuntu",
|
||||
<span class="context-tooltip" title="The full name of the repository" bs-tooltip="tooltip.title">"repository"</span>:"devtable/ubuntu",
|
||||
<span class="context-tooltip" title="The URL at which the repository can be pulled by Docker" bs-tooltip="tooltip.title">"docker_url"</span>: "quay.io/devtable/ubuntu",
|
||||
<span class="context-tooltip" title="Map of updated tag names to their latest image IDs" bs-tooltip="tooltip.title">"updated_tags"</span>: {
|
||||
"latest": "b750fe79269d2ec9a3c593ef05b4332b1d1a02a62b4accb2c21d589ff2f5f2dc"
|
||||
},
|
||||
<span class="code-info" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
|
||||
<span class="code-info" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
|
||||
<span class="code-info" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
|
||||
<span class="context-tooltip" title="The namespace of the repository" bs-tooltip="tooltip.title">"namespace"</span>: "devtable",
|
||||
<span class="context-tooltip" title="Whether the repository is public or private" bs-tooltip="tooltip.title">"visibility"</span>: "private",
|
||||
<span class="context-tooltip" title="The Quay URL for the repository" bs-tooltip="tooltip.title">"homepage"</span>: "https://quay.io/repository/devtable/ubuntu"
|
||||
}
|
||||
</pre>
|
||||
</div>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div ng-show="user.anonymous">
|
||||
<h1>Secure hosting for <b>private</b> Docker<a class="disclaimer-link" href="/disclaimer" target="_self">*</a> repositories</h1>
|
||||
<h3>Use the Docker images <b>your team</b> needs with the safety of <b>private</b> repositories</h3>
|
||||
<div class="sellcall"><a href="/plans/">Private repository plans starting at $7/mo</a></div>
|
||||
<div class="sellcall"><a href="/plans/">Private repository plans starting at $12/mo</a></div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!user.anonymous">
|
||||
|
|
|
@ -72,7 +72,8 @@
|
|||
</div>
|
||||
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan">
|
||||
<button class="btn btn-large btn-success" type="submit" ng-disabled="newOrgForm.$invalid || !currentPlan"
|
||||
analytics-on analytics-event="create_organization">
|
||||
Create Organization
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -74,7 +74,11 @@
|
|||
<!-- Payment -->
|
||||
<div class="required-plan" ng-show="repo.is_public == '0' && planRequired && isUserNamespace">
|
||||
<div class="alert alert-warning">
|
||||
In order to make this repository private, you’ll need to upgrade your plan 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 to
|
||||
<b style="border-bottom: 1px dotted black;" bs-tooltip="'<b>' + planRequired.title + '</b><br>' + planRequired.privateRepos + ' private repositories'">
|
||||
{{ 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>
|
||||
<div class="quay-spinner" ng-show="planChanging"></div>
|
||||
|
|
|
@ -40,60 +40,7 @@
|
|||
|
||||
<!-- Billing History tab -->
|
||||
<div id="billing" class="tab-pane">
|
||||
<div ng-show="invoiceLoading">
|
||||
<div class="quay-spinner"></div>
|
||||
</div>
|
||||
|
||||
<div ng-show="!invoiceLoading && !invoices">
|
||||
No invoices have been created
|
||||
</div>
|
||||
|
||||
<div ng-show="!invoiceLoading && invoices">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Billing Date/Time</th>
|
||||
<th>Amount Due</th>
|
||||
<th>Status</th>
|
||||
<th></th>
|
||||
</thead>
|
||||
|
||||
<tbody class="invoice" ng-repeat="invoice in invoices">
|
||||
<tr class="invoice-title">
|
||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-datetime">{{ invoice.date * 1000 | date:'medium' }}</span></td>
|
||||
<td ng-click="toggleInvoice(invoice.id)"><span class="invoice-amount">{{ invoice.amount_due / 100 }}</span></td>
|
||||
<td>
|
||||
<span class="invoice-status">
|
||||
<span class="success" ng-show="invoice.paid">Paid - Thank you!</span>
|
||||
<span class="danger" ng-show="!invoice.paid && invoice.attempted && invoice.closed">Payment failed</span>
|
||||
<span class="danger" ng-show="!invoice.paid && invoice.attempted && !invoice.closed">Payment failed - Will retry soon</span>
|
||||
<span class="pending" ng-show="!invoice.paid && !invoice.attempted">Payment pending</span>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<a ng-show="invoice.paid" href="/receipt?id={{ invoice.id }}" download="receipt.pdf" target="_new">
|
||||
<i class="fa fa-download" title="Download Receipt" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr ng-class="invoiceExpanded[invoice.id] ? 'in' : 'out'" class="invoice-details panel-collapse collapse">
|
||||
<td colspan="3">
|
||||
<dl class="dl-normal">
|
||||
<dt>Billing Period</dt>
|
||||
<dd>
|
||||
<span>{{ invoice.period_start * 1000 | date:'mediumDate' }}</span> -
|
||||
<span>{{ invoice.period_end * 1000 | date:'mediumDate' }}</span>
|
||||
</dd>
|
||||
<dt>Plan</dt>
|
||||
<dd>
|
||||
<span>{{ invoice.plan ? plan_map[invoice.plan].title : '(N/A)' }}</span>
|
||||
</dd>
|
||||
</dl>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="billing-invoices" organization="organization" visible="invoicesShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Members tab -->
|
||||
|
|
|
@ -1,44 +1,107 @@
|
|||
<div class="container plans content-container">
|
||||
<div class="callout">
|
||||
Plans & Pricing
|
||||
</div>
|
||||
|
||||
<div class="all-plans">
|
||||
All plans include <span class="feature">unlimited public repositories</span> and <span class="feature">unlimited sharing</span>. All paid plans have a <span class="feature">14-day free trial</span>.
|
||||
</div>
|
||||
|
||||
<div class="row plans-list">
|
||||
<div class="col-xs-0 col-lg-1"></div>
|
||||
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.user">
|
||||
<div class="plan" ng-class="plan.stripeId">
|
||||
<div class="plan-title">{{ plan.title }}</div>
|
||||
<div class="plan-price">${{ plan.price/100 }}</div>
|
||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||
<div class="description">{{ plan.audience }}</div>
|
||||
<div class="smaller">SSL secured connections</div>
|
||||
<button class="btn btn-primary btn-block" ng-click="buyNow(plan.stripeId)">Sign Up Now</button>
|
||||
</div>
|
||||
<div class="col-sm-2">
|
||||
<div class="features-bar hidden-xs">
|
||||
<div class="visible-lg" style="height: 50px"></div>
|
||||
<div class="visible-md visible-sm" style="height: 70px"></div>
|
||||
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="All plans have unlimited public repositories">
|
||||
<span class="hidden-sm-inline">Public Repositories</span>
|
||||
<span class="visible-sm-inline">Public Repos</span>
|
||||
</span>
|
||||
<i class="fa fa-hdd visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="SSL encryption is enabled end-to-end for all operations">
|
||||
SSL Encryption
|
||||
</span>
|
||||
<i class="fa fa-lock visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Allows users or organizations to grant permissions in multiple repositories to the same non-login-capable account">
|
||||
Robot accounts
|
||||
</span>
|
||||
<i class="fa fa-wrench visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Repository images can be built directly from Dockerfiles">
|
||||
Dockerfile Build
|
||||
</span>
|
||||
<i class="fa fa-upload visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Grant subsets of users in an organization their own permissions, either on a global basis or a per-repository basis">
|
||||
Teams
|
||||
</span>
|
||||
<i class="fa fa-group visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Every action take within an organization is logged in detail, with the ability to visualize logs and download them">
|
||||
Logging
|
||||
</span>
|
||||
<i class="fa fa-bar-chart-o visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="Administrators can view and download the full invoice history for their organization">
|
||||
Invoice History
|
||||
</span>
|
||||
<i class="fa fa-calendar visible-lg"></i>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<span class="context-tooltip" bs-tooltip="tooltip.title" data-container="body" data-placement="right"
|
||||
title="All plans have a 14-day free trial">
|
||||
<span class="hidden-sm-inline">14-Day Free Trial</span>
|
||||
<span class="visible-sm-inline">14-Day Trial</span>
|
||||
</span>
|
||||
<i class="fa fa-clock-o visible-lg"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-2 plan-container" ng-repeat="plan in plans" ng-show="plan.price > 0 && !plan.deprecated">
|
||||
<div class="plan" ng-class="plan.stripeId + ' ' + (plan.bus_features ? 'business-plan' : '')">
|
||||
<div class="plan-box">
|
||||
<div class="plan-title">{{ plan.title }}</div>
|
||||
<div class="plan-price">${{ plan.price/100 }}</div>
|
||||
|
||||
<div class="callout">
|
||||
Business Plan Pricing
|
||||
</div>
|
||||
<div class="description">{{ plan.audience }}</div>
|
||||
</div>
|
||||
|
||||
<div class="all-plans">
|
||||
All business plans include all of the personal plan features, plus: <span class="business-feature">organizations</span> and <span class="business-feature">teams</span> with <span class="business-feature">delegated access</span> to the organization. All business plans have a <span class="business-feature">14-day free trial</span>.
|
||||
</div>
|
||||
<div class="features hidden-xs">
|
||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature present"></div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : ''"></div>
|
||||
<div class="feature present"></div>
|
||||
</div>
|
||||
|
||||
<div class="features visible-xs">
|
||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||
<div class="feature present">Unlimited Public Repositories</div>
|
||||
<div class="feature present">SSL Encryption</div>
|
||||
<div class="feature present">Robot accounts</div>
|
||||
<div class="feature present">Dockerfile Build</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Teams</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Logging</div>
|
||||
<div class="feature" ng-class="plan.bus_features ? 'present' : 'notpresent'">Invoice History</div>
|
||||
<div class="feature present">14-Day Free Trial</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-block" ng-class="plan.bus_features ? 'btn-success' : 'btn-primary'"
|
||||
ng-click="buyNow(plan.stripeId)">Start <span class="hidden-sm-inline">Free</span> Trial</button>
|
||||
|
||||
<div class="row plans-list">
|
||||
<div class="col-xs-0 col-lg-1"></div>
|
||||
<div class="col-lg-2 col-xs-4 plan-container" ng-repeat="plan in plans.business">
|
||||
<div class="plan business-plan" ng-class="plan.stripeId">
|
||||
<div class="plan-title">{{ plan.title }}</div>
|
||||
<div class="plan-price">${{ plan.price/100 }}</div>
|
||||
<div class="count"><b>{{ plan.privateRepos }}</b> private repositories</div>
|
||||
<div class="description">{{ plan.audience }}</div>
|
||||
<div class="smaller">SSL secured connections</div>
|
||||
<button class="btn btn-success btn-block" ng-click="createOrg(plan.stripeId)">Sign Up Now</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -27,16 +27,23 @@
|
|||
<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 ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing">Billing Options</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing Options</a></li>
|
||||
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Set Password</a></li>
|
||||
<li ng-show="hasPaidBusinessPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#logs" ng-click="loadLogs()">Usage Logs</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#migrate" id="migrateTab">Convert to Organization</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="col-md-10">
|
||||
<div class="tab-content">
|
||||
<div class="tab-content">
|
||||
<!-- Logs tab -->
|
||||
<div id="logs" class="tab-pane">
|
||||
<div class="logs-view" user="user" visible="logsShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Plans tab -->
|
||||
<div id="plan" class="tab-pane active">
|
||||
<div class="plan-manager" user="user.username" ready-for-plan="readyForPlan()" plan-changed="planChanged(plan)"></div>
|
||||
|
@ -53,9 +60,9 @@
|
|||
<div ng-show="!updatingUser">
|
||||
<form class="form-change-pw col-md-6" 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="Verify your new password" ng-model="user.repeatPassword"
|
||||
match="user.password" required>
|
||||
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required>
|
||||
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="cuser.repeatPassword"
|
||||
match="cuser.password" required>
|
||||
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
|
||||
analytics-on analytics-event="change_pass">Change Password</button>
|
||||
</form>
|
||||
|
@ -69,10 +76,15 @@
|
|||
</div>
|
||||
|
||||
<!-- Billing options tab -->
|
||||
<div id="billing" class="tab-pane">
|
||||
<div id="billingoptions" class="tab-pane">
|
||||
<div class="billing-options" user="user"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing History tab -->
|
||||
<div id="billing" class="tab-pane">
|
||||
<div class="billing-invoices" user="user" visible="invoicesShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Convert to organization tab -->
|
||||
<div id="migrate" class="tab-pane">
|
||||
<!-- Step 0 -->
|
||||
|
@ -86,11 +98,11 @@
|
|||
</div>
|
||||
|
||||
<div class="panel-body" ng-show="user.organizations.length == 0">
|
||||
<div class="alert alert-danger">
|
||||
Converting a user account into an organization <b>cannot be undone</b>.<br> Here be many fire-breathing dragons!
|
||||
<div class="alert alert-warning">
|
||||
Note: Converting a user account into an organization <b>cannot be undone</b>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-danger" ng-click="showConvertForm()">Start conversion process</button>
|
||||
<button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -113,7 +125,7 @@
|
|||
ng-model="org.adminUser" required autofocus>
|
||||
<input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password"
|
||||
ng-model="org.adminPassword" required>
|
||||
<span class="description">The username and password for an <b>existing account</b> that will become administrator of the organization</span>
|
||||
<span class="description">The username and password for the account that will become administrator of the organization</span>
|
||||
</div>
|
||||
|
||||
<!-- Plans Table -->
|
||||
|
@ -123,7 +135,8 @@
|
|||
</div>
|
||||
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan">
|
||||
<button class="btn btn-large btn-danger" type="submit" ng-disabled="convertForm.$invalid || !org.plan"
|
||||
analytics-on analytics-event="convert_to_organization">
|
||||
Convert To Organization
|
||||
</button>
|
||||
</div>
|
||||
|
|
Reference in a new issue