Implement new design for user and org settings

Fixes #1376
This commit is contained in:
Joseph Schorr 2016-04-25 15:17:18 -04:00
parent d63ec8c6b0
commit fe735b8048
25 changed files with 784 additions and 614 deletions

View file

@ -866,6 +866,14 @@ a:focus {
margin-right: 6px;
}
.co-dialog .co-single-field-dialog {
padding: 10px;
}
.co-dialog .co-single-field-dialog input {
margin-top: 10px;
}
.co-step-bar .co-step-element {
cursor: default;
display: inline-block;
@ -1370,3 +1378,46 @@ a:focus {
float: none;
}
}
.co-list-table tr td:first-child {
font-weight: bold;
padding-right: 10px;
vertical-align: top;
width: 120px;
padding-left: 0px;
}
.co-list-table tr td {
padding: 10px;
font-size: 15px;
}
.co-list-table .help-text {
margin-top: 6px;
font-size: 14px;
color: #aaa;
}
.co-modify-link:after {
font-family: FontAwesome;
content: "\f054";
color: #ccc;
vertical-align: middle;
display: inline-block;
margin-left: 10px;
font-size: 10px;
line-height: 16px;
}
.co-option-table tr td:first-child {
padding-left: 16px;
padding-right: 16px;
padding-top: 0px;
vertical-align: top;
}
.co-option-table tr td:last-child {
padding-bottom: 10px;
}

View file

@ -0,0 +1,32 @@
.billing-management-panel-element .credit-card-image {
width: 33px;
margin-right: 10px;
}
.billing-management-panel-element .credit-card-number {
display: inline-block;
font-size: 16px;
margin-right: 10px;
}
.billing-management-panel-element .receipt-form {
padding-top: 20px;
padding-bottom: 12px;
}
.billing-management-panel-element .sub-usage {
margin-bottom: 10px;
font-size: 16px;
}
.billing-management-panel-element .sub-usage .fa {
margin-right: 4px;
}
.billing-management-panel-element .sub-usage .red {
color: #D64456;
}
.billing-management-panel-element .sub-usage .yellow {
color: #FCA657;
}

View file

@ -1,15 +0,0 @@
.billing-options .settings-option {
padding: 4px;
font-size: 18px;
margin-bottom: 10px;
}
.billing-options .settings-option label {
margin-left: 6px;
}
.billing-options .settings-option .settings-description {
font-size: 16px;
color: #888;
padding-left: 26px;
}

View file

@ -2,6 +2,10 @@
padding: 10px;
}
.convert-user-to-org .convert-form {
padding: 20px;
}
.convert-user-to-org .convert-form h3 {
margin-bottom: 20px;
}
@ -67,3 +71,7 @@
.convert-user-to-org .form-group-content .co-table {
margin: 0px;
}
.convert-user-to-org .co-option-table {
margin-top: 12px;
}

View file

@ -0,0 +1,3 @@
.billing-page .co-main-content-panel {
padding: 40px;
}

View file

@ -7,3 +7,8 @@
margin-bottom: 20px;
margin-top: 0px;
}
.org-view .settings-section {
margin-bottom: 50px;
}

View file

@ -8,27 +8,15 @@
}
.user-view h3 {
margin-bottom: 20px;
margin-top: 0px;
}
.user-view .section-description-header {
padding-left: 40px;
position: relative;
margin-bottom: 20px;
min-height: 50px;
}
.user-view .section-description-header:before {
font-family: FontAwesome;
content: "\f05a";
position: absolute;
top: -4px;
left: 6px;
font-size: 27px;
color: #888;
}
.user-view .user-settings-form .row {
padding: 10px;
margin: 0px;
@ -53,3 +41,6 @@
box-shadow: none;
}
.user-view .settings-section {
margin-bottom: 50px;
}

View file

@ -449,48 +449,6 @@ i.toggle-icon:hover {
visibility: hidden;
}
.billing-options-element .current-card {
font-size: 16px;
margin-bottom: 20px;
}
.billing-options-element .current-card .no-card-outline {
display: inline-block;
width: 73px;
height: 44px;
vertical-align: middle;
margin-right: 10px;
border: 1px dashed #aaa;
border-radius: 4px;
}
.billing-options-element .current-card .last4 {
color: #aaa;
}
.billing-options-element .current-card .last4 b {
color: black;
}
.billing-options-element .current-card .expires:before {
content: "Expires:";
color: #aaa;
font-size: 12px;
}
.billing-options-element .current-card .expires {
margin-left: 20px;
font-size: 12px;
}
.billing-options-element .current-card img {
margin-right: 10px;
vertical-align: middle;
}
.organization-header-element {
padding: 20px;
margin-bottom: 20px;
@ -4077,24 +4035,12 @@ i.rocket-icon {
}
.section-description-header {
padding-left: 40px;
position: relative;
margin-bottom: 20px;
margin-bottom: 10px;
min-height: 50px;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.section-description-header:before {
font-family: FontAwesome;
content: "\f05a";
position: absolute;
top: -4px;
left: 6px;
font-size: 27px;
color: #888;
}
.nvtooltip h3 {
margin: 0;
padding: 4px 14px;

View file

@ -0,0 +1,80 @@
<div class="billing-management-panel-element">
<div class="cor-loader-inline" ng-show="updating"></div>
<div ng-show="!updating">
<table class="co-list-table">
<tr>
<td>Current Plan:</td>
<td>
<div class="sub-usage" ng-if="subscription.usedPrivateRepos > currentPlan.privateRepos">
<i class="fa fa-exclamation-triangle red"></i> <strong>{{ subscription.usedPrivateRepos }}</strong> private repositories exceeds the amount allowed by your plan. Upgrade your plan to avoid service disruptions.
</div>
<div class="sub-usage" ng-if="subscription.usedPrivateRepos == currentPlan.privateRepos">
<i class="fa fa-exclamation-triangle yellow"></i> <strong>{{ subscription.usedPrivateRepos }}</strong> private repositories is the maximum allowed by your plan. Upgrade your plan to create more private repositories.
</div>
<a class="co-modify-link" ng-href="{{ getEntityPrefix() }}/billing">{{ currentPlan.privateRepos }} private repositories</a>
<div class="help-text">Up to {{ currentPlan.privateRepos }} private repositories, unlimited public repositories</div>
</td>
</tr>
<tr ng-show="currentCard">
<td>Credit Card:</td>
<td>
<img class="credit-card-image" ng-src="/static/img/creditcards/{{ getCreditImage(currentCard) }}">
<span class="credit-card-number">
&#8226;&#8226;&#8226;&#8226;&nbsp;
&#8226;&#8226;&#8226;&#8226;&nbsp;
&#8226;&#8226;&#8226;&#8226;&nbsp;
{{ currentCard.last4 }}
</span>
<a class="co-modify-link" ng-click="changeCreditCard()">Change card</a>
<div class="help-text">Expires {{ currentCard.exp_month }}/{{ currentCard.exp_year }}</div>
</td>
</tr>
<tr>
<td>Invoices:</td>
<td>
<a ng-href="{{ getEntityPrefix() }}/billing/invoices">View Invoices</a>
</td>
</tr>
<tr>
<td>Receipts:</td>
<td>
<a class="co-modify-link" ng-click="showChangeReceipts()" ng-show="!invoice_email">Do not email after successful charges</a>
<a class="co-modify-link" ng-click="showChangeReceipts()" ng-show="invoice_email">Email receipts to {{ invoice_email_address }}</a>
</td>
</tr>
</table>
</div>
<!-- Change receipts dialog -->
<div class="cor-confirm-dialog"
dialog-context="changeReceiptsInfo"
dialog-action="changeReceipts(info, callback)"
dialog-title="Receipts Settings"
dialog-action-title="Update Setting"
dialog-form="context.receiptform">
<form class="receipt-form" name="context.receiptform">
<table class="co-option-table">
<tr>
<td><input type="radio" id="emailReceiptNo" ng-model="changeReceiptsInfo.sendOption" ng-value="false"></td>
<td>
<label for="emailReceiptNo">Do not send email receipts</label>
<div class="help-text">Log into your account to view invoices</div>
</td>
</tr>
<tr>
<td><input type="radio" id="emailReceiptYes" ng-model="changeReceiptsInfo.sendOption" ng-value="true"></td>
<td>
<label for="emailReceiptYes">Send receipts via email</label>
<div class="help-text">
After every successful charge send an email to:
<div style="margin-top: 6px;"><input type="email" class="form-control" ng-model="changeReceiptsInfo.address" required></div>
</div>
</td>
</tr>
</table>
</form>
</div>
</div>

View file

@ -1,65 +0,0 @@
<div class="billing-options-element">
<!-- Credit Card -->
<div style="margin-bottom: 20px">
<div class="panel-title">
Credit Card
</div>
<div class="panel-body">
<div class="cor-loader-inline" ng-show="!currentCard || changingCard"></div>
<div class="current-card" ng-show="currentCard && !changingCard">
<div class="alert alert-warning" ng-if="currentCard.last4 && isExpiringSoon(currentCard)">
Your current credit card is expiring soon!
</div>
<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>
<span class="expires" ng-show="currentCard.last4">
{{ currentCard.exp_month }} / {{ currentCard.exp_year }}
</span>
<span class="not-found" ng-show="!currentCard.last4">No credit card found</span>
</div>
<button class="btn btn-primary" ng-show="currentCard && !changingCard" ng-click="changeCard()">
Change Credit Card
</button>
</div>
</div>
<!-- Options -->
<div style="margin-bottom: 20px">
<div class="panel-title">
Billing Receipts
<div class="cor-loader-inline" ng-show="working"></div>
</div>
<div class="panel-body">
<div class="settings-option">
<input id="invoiceEmail" type="checkbox" ng-model="invoice_email">
<label for="invoiceEmail">Send Receipt Emails</label>
<div class="settings-description">
If checked, a receipt email will be sent to <a ng-click="changeInvoiceEmailAddress()">{{ obj.invoice_email_address || obj.email }}</a> on every successful charge
</div>
</div>
</div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangecardModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot change credit card</h4>
</div>
<div class="modal-body">
Your credit card could not be changed
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -1,112 +1,108 @@
<div class="convert-user-to-org-element">
<!-- Step 0 -->
<div ng-show="convertStep == 0">
<div ng-show="user.organizations.length > 0">
Cannot convert this account into an organization, as it is a member of {{user.organizations.length}} other
organization{{user.organizations.length > 1 ? 's' : ''}}.
<br><br>
Please leave the following organizations first:
<ul class="org-list">
<li ng-repeat="org in user.organizations">
<span class="avatar" size="avatarSize || 16" data="org.avatar"></span>
<a href="/organization/{{ org.name }}">{{ org.name }}</a>
</li>
</ul>
</div>
<div class="co-dialog modal fade" id="convertAccountModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Change Account Type</h4>
</div>
<div class="modal-body">
<!-- Step 0 -->
<div ng-show="convertStep == 0">
<div ng-show="user.organizations.length > 0">
This account cannot be converted into an organization, as it is a member of {{user.organizations.length}} other
organization{{user.organizations.length > 1 ? 's' : ''}}.
<br><br>
Please leave the following organizations first:
<ul class="org-list">
<li ng-repeat="org in user.organizations">
<span class="avatar" size="avatarSize || 16" data="org.avatar"></span>
<a href="/organization/{{ org.name }}">{{ org.name }}</a>
</li>
</ul>
</div>
<div ng-show="user.organizations.length == 0">
<button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process <i class="fa fa-arrow-circle-right" aria-hidden="true"></i></button>
</div>
</div>
<!-- Step 1 -->
<div class="convert-form" ng-show="convertStep == 1">
Fill out the form below to convert your current user account into an organization. Your existing repositories will be maintained under the
namespace. All <strong>direct</strong> permissions delegated to {{ user.username }} will be deleted.
<form method="post" name="convertForm" id="convertForm" ng-submit="convertToOrg()">
<div class="form-group">
<label for="orgName">Organization Name</label>
<div class="form-group-content">
<div class="existing-data">
<span class="avatar" size="24" data="user.avatar"></span>
<span class="username">{{ user.username }}</span>
<div ng-show="user.organizations.length == 0">
<table class="co-option-table">
<tr>
<td><input type="radio" id="accountTypeI" ng-model="accountType" value="user"></td>
<td>
<label for="accountTypeI">Individual account (current)</label>
<div class="help-text">Single account with multiple repositories</div>
</td>
</tr>
<tr>
<td><input type="radio" id="accountTypeO" ng-model="accountType" value="organization"></td>
<td>
<label for="accountTypeO">Organization</label>
<div class="help-text">Multiple users and teams that share access and billing under a single namespace</div>
</td>
</tr>
</table>
</div>
</div>
<span class="description">This will continue to be the namespace for your repositories</span>
</div>
</div>
<div class="form-group">
<label for="orgName">Admin User</label>
<div class="form-group-content">
<input id="adminUsername" name="adminUsername" type="text" class="form-control" placeholder="Admin Username"
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 the account that will become an administrator of the organization.
Note that this account <b>must be a separate registered account</b> from the account that you are
trying to convert, and <b>must already exist</b>.
</span>
</div>
</div>
<!-- Step 1 -->
<div class="convert-form" ng-show="convertStep == 1">
Fill out the form below to convert your current user account into an organization. Your existing repositories will be maintained under the
namespace. All <strong>direct</strong> permissions delegated to {{ user.username }} will be deleted.
<!-- Plans Table -->
<div class="form-group plan-group" quay-require="['BILLING']">
<label>Organization Plan</label>
<div class="form-group-content">
<div class="plans-table" plans="orgPlans" current-plan="org.plan"></div>
<span class="description">The billing plan for the new organization. If private repositories are unneeded, select "Open Source".</span>
</div>
</div>
<form method="post" name="convertForm" id="convertForm" ng-submit="nextStep()">
<div class="form-group">
<label for="orgName">Organization Name</label>
<div class="form-group-content">
<div class="existing-data">
<span class="avatar" size="24" data="user.avatar"></span>
<span class="username">{{ user.username }}</span>
</div>
<span class="description">This will continue to be the namespace for your repositories</span>
</div>
</div>
<div class="button-bar">
<button class="btn btn-large btn-danger" type="submit"
ng-disabled="convertForm.$invalid || (Features.BILLING && !org.plan)"
analytics-on analytics-event="convert_to_organization">
Convert To Organization
</button>
</div>
</form>
</div>
<div class="form-group">
<label for="orgName">Admin User</label>
<div class="form-group-content">
<input id="adminUsername" name="adminUsername" type="text" class="form-control" placeholder="Admin Username"
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 the account that will become an administrator of the organization.
Note that this account <b>must be a separate registered account</b> from the account that you are
trying to convert, and <b>must already exist</b>.
</span>
</div>
</div>
</form>
</div>
<!-- Step 2 -->
<div class="convert-form" ng-show="convertStep == 2">
Please select the billing plan to use for the new organization. Select "Open Source" to create an organization without
private repositories.
<div class="plans-table" plans="orgPlans" current-plan="org.plan"></div>
</div>
<!-- Step 3 (conversion) -->
<div class="convert-form" ng-show="convertStep == 3">
<div class="cor-loader"></div>
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotconvertModal">
<div class="modal-dialog co-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Cannot convert account</h4>
</div>
<div class="modal-body">
Your account could not be converted. Please try again in a moment.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
<div class="modal-footer" ng-show="convertStep < 3">
<button class="btn btn-default" ng-show="convertStep == 0 && accountType == 'user'" data-dismiss="modal">Close</button>
<button class="btn btn-primary" ng-show="convertStep == 0 && accountType == 'organization'" ng-click="showConvertForm()">Convert Account</button>
<button class="btn btn-primary" ng-show="convertStep == 1" ng-disabled="convertForm.$invalid" ng-click="nextStep()">
<span ng-if="Features.BILLING">Choose billing</span>
<span ng-if="!Features.BILLING">Convert Account</span>
</button>
<button class="btn btn-primary" ng-show="convertStep == 2" ng-disabled="!org.plan" ng-click="performConversion()">
Convert Account
</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal co-modal fade" id="reallyconvertModal">
<div class="modal-dialog co-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Convert to organization?</h4>
</div>
<div class="modal-body">
<div class="co-alert co-alert-danger">You will not be able to login to this account once converted!</div>
<div>Are you <b>absolutely sure</b> you would like to convert this account to an organization? Once done, there is no going back.</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal" ng-click="reallyConvert()">Absolutely: Convert Now</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</div>

View file

@ -14,7 +14,7 @@
<span ng-transclude/>
</div>
<div class="modal-footer" ng-show="!working">
<button type="button" class="btn btn-primary" ng-click="performAction()">
<button type="button" class="btn btn-primary" ng-click="performAction()" ng-disabled="dialogForm && dialogForm.$invalid">
{{ dialogActionTitle }}
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>

View file

@ -121,9 +121,21 @@ quayApp.config(['$routeProvider', '$locationProvider', 'pages', function($routeP
// Organization View Application
.route('/organization/:orgname/application/:clientid', 'manage-application')
// View Organization Billing
.route('/organization/:orgname/billing', 'billing')
// View Organization Billing Invoices
.route('/organization/:orgname/billing/invoices', 'invoices')
// View User
.route('/user/:username', 'user-view')
// View User Billing
.route('/user/:username/billing', 'billing')
// View User Billing Invoices
.route('/user/:username/billing/invoices', 'invoices')
// Sign In
.route('/signin/', 'signin')

View file

@ -172,10 +172,12 @@ angular.module("core-ui", [])
scope: {
'dialogTitle': '@dialogTitle',
'dialogActionTitle': '@dialogActionTitle',
'dialogForm': '=dialogForm',
'dialogContext': '=dialogContext',
'dialogAction': '&dialogAction'
},
controller: function($rootScope, $scope, $element) {
$scope.working = false;
@ -195,6 +197,10 @@ angular.module("core-ui", [])
};
$scope.show = function() {
if ($scope.dialogForm) {
$scope.dialogForm.$setPristine();
}
$scope.working = false;
$element.find('.modal').modal({});
};

View file

@ -0,0 +1,140 @@
/**
* An element which displays the billing options for a user or an organization.
*/
angular.module('quay').directive('billingManagementPanel', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/billing-management-panel.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'organization': '=organization',
'isEnabled': '=isEnabled'
},
controller: function($scope, $element, PlanService, ApiService) {
$scope.currentCard = null;
$scope.subscription = null;
$scope.updating = true;
$scope.changeReceiptsInfo = null;
$scope.context = {};
var setSubscription = function(sub) {
$scope.subscription = sub;
// Load the plan info.
PlanService.getPlan(sub['plan'], function(plan) {
$scope.currentPlan = plan;
if (!sub.hasSubscription) {
$scope.updating = false;
return;
}
// Load credit card information.
PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) {
$scope.currentCard = card;
$scope.updating = false;
});
});
};
var update = function() {
if (!$scope.isEnabled || !($scope.user || $scope.organization)) {
return;
}
$scope.entity = $scope.user ? $scope.user : $scope.organization;
$scope.invoice_email = $scope.entity.invoice_email;
$scope.invoice_email_address = $scope.entity.invoice_email_address || $scope.entity.email;
$scope.updating = true;
// Load plan information.
PlanService.getSubscription($scope.organization, setSubscription, function() {
setSubscription({ 'plan': PlanService.getFreePlan() });
});
};
// Listen to plan changes.
PlanService.registerListener(this, function(plan) {
if (plan && plan.price > 0) {
update();
}
});
$scope.$on('$destroy', function() {
PlanService.unregisterListener(this);
});
$scope.$watch('isEnabled', update);
$scope.$watch('organization', update);
$scope.$watch('user', update);
$scope.getEntityPrefix = function() {
if ($scope.organization) {
return '/organization/' + $scope.organization.name;
} else {
return '/user/' + $scope.user.username;
}
};
$scope.changeCreditCard = function() {
var callbacks = {
'opened': function() { },
'closed': function() { },
'started': function() { },
'success': function(resp) {
$scope.currentCard = resp.card;
update();
},
'failure': function(resp) {
if (!PlanService.isCardError(resp)) {
bootbox.alert('Could not change credit card. Please try again later.');
}
}
};
PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks);
};
$scope.getCreditImage = function(creditInfo) {
if (!creditInfo || !creditInfo.type) { return 'credit.png'; }
var kind = creditInfo.type.toLowerCase() || 'credit';
var supported = {
'american express': 'amex',
'credit': 'credit',
'diners club': 'diners',
'discover': 'discover',
'jcb': 'jcb',
'mastercard': 'mastercard',
'visa': 'visa'
};
kind = supported[kind] || 'credit';
return kind + '.png';
};
$scope.changeReceipts = function(info, callback) {
$scope.entity['invoice_email'] = info['sendOption'] || false;
$scope.entity['invoice_email_address'] = info['address'] || $scope.entity.email;
var errorHandler = ApiService.errorDisplay('Could not change billing options', callback);
ApiService.changeDetails($scope.organization, $scope.entity).then(function(resp) {
callback(true);
update();
}, errorHandler);
};
$scope.showChangeReceipts = function() {
$scope.changeReceiptsInfo = {
'sendOption': $scope.invoice_email,
'address': $scope.invoice_email_address
};
};
}
};
return directiveDefinitionObject;
});

View file

@ -1,130 +0,0 @@
/**
* An element which displays the billing options for a user or an organization.
*/
angular.module('quay').directive('billingOptions', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/billing-options.html',
replace: false,
transclude: false,
restrict: 'C',
scope: {
'user': '=user',
'organization': '=organization'
},
controller: function($scope, $element, PlanService, ApiService) {
$scope.invoice_email = false;
$scope.currentCard = null;
// Listen to plan changes.
PlanService.registerListener(this, function(plan) {
if (plan && plan.price > 0) {
update();
}
});
$scope.$on('$destroy', function() {
PlanService.unregisterListener(this);
});
$scope.isExpiringSoon = function(cardInfo) {
var current = new Date();
var expires = new Date(cardInfo.exp_year, cardInfo.exp_month, 1);
var difference = expires - current;
return difference < (60 * 60 * 24 * 60 * 1000 /* 60 days */);
};
$scope.changeInvoiceEmailAddress = function() {
bootbox.prompt('Enter the email address for receiving receipts', function(email) {
// Copied from Angular.
var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-z0-9-]+(\.[a-z0-9-]+)*$/i;
if (!email || !EMAIL_REGEXP.test(email)) {
return;
}
$scope.obj['invoice_email_address'] = email;
var errorHandler = ApiService.errorDisplay('Could not change user details');
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
$scope.working = false;
}, errorHandler);
});
};
$scope.changeCard = function() {
var previousCard = $scope.currentCard;
$scope.changingCard = true;
var callbacks = {
'opened': function() { $scope.changingCard = true; },
'closed': function() { $scope.changingCard = false; },
'started': function() { $scope.currentCard = null; },
'success': function(resp) {
$scope.currentCard = resp.card;
$scope.changingCard = false;
},
'failure': function(resp) {
$scope.changingCard = false;
$scope.currentCard = previousCard;
if (!PlanService.isCardError(resp)) {
$('#cannotchangecardModal').modal({});
}
}
};
PlanService.changeCreditCard($scope, $scope.organization ? $scope.organization.name : null, callbacks);
};
$scope.getCreditImage = function(creditInfo) {
if (!creditInfo || !creditInfo.type) { return 'credit.png'; }
var kind = creditInfo.type.toLowerCase() || 'credit';
var supported = {
'american express': 'amex',
'credit': 'credit',
'diners club': 'diners',
'discover': 'discover',
'jcb': 'jcb',
'mastercard': 'mastercard',
'visa': 'visa'
};
kind = supported[kind] || 'credit';
return kind + '.png';
};
var update = function() {
if (!$scope.user && !$scope.organization) { return; }
$scope.obj = $scope.user ? $scope.user : $scope.organization;
$scope.invoice_email = $scope.obj.invoice_email;
// Load the credit card information.
PlanService.getCardInfo($scope.organization ? $scope.organization.name : null, function(card) {
$scope.currentCard = card;
});
};
var save = function() {
$scope.working = true;
var errorHandler = ApiService.errorDisplay('Could not change user details');
ApiService.changeDetails($scope.organization, $scope.obj).then(function(resp) {
$scope.working = false;
}, errorHandler);
};
var checkSave = function() {
if (!$scope.obj) { return; }
if ($scope.obj.invoice_email != $scope.invoice_email) {
$scope.obj.invoice_email = $scope.invoice_email;
save();
}
};
$scope.$watch('invoice_email', checkSave);
$scope.$watch('organization', update);
$scope.$watch('user', update);
}
};
return directiveDefinitionObject;
});

View file

@ -9,14 +9,29 @@ angular.module('quay').directive('convertUserToOrg', function () {
transclude: true,
restrict: 'C',
scope: {
'user': '=user'
'info': '=info'
},
controller: function($scope, $element, $location, Features, PlanService, Config, ApiService, CookieService, UserService) {
$scope.convertStep = 0;
$scope.org = {};
$scope.loading = false;
$scope.user = null;
$scope.Features = Features;
$scope.$watch('info', function(info) {
if (info && info.user) {
$scope.user = info.user;
$scope.accountType = 'user';
$scope.convertStep = 0;
$('#convertAccountModal').modal({});
}
});
$scope.showConvertForm = function() {
$scope.convertStep = 1;
};
$scope.nextStep = function() {
if (Features.BILLING) {
PlanService.getMatchingBusinessPlan(function(plan) {
$scope.org.plan = plan;
@ -25,22 +40,19 @@ angular.module('quay').directive('convertUserToOrg', function () {
PlanService.getPlans(function(plans) {
$scope.orgPlans = plans;
});
$scope.convertStep = 2;
} else {
$scope.performConversion();
}
$scope.convertStep = 1;
};
$scope.convertToOrg = function() {
$('#reallyconvertModal').modal({});
};
$scope.reallyConvert = function() {
$scope.performConversion = function() {
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
$scope.loading = true;
$scope.convertStep = 3;
var errorHandler = ApiService.errorDisplay(function() {
$scope.loading = false;
$('#convertAccountModal').modal('hide');
});
var data = {
@ -52,6 +64,7 @@ angular.module('quay').directive('convertUserToOrg', function () {
ApiService.convertUserToOrganization(data).then(function(resp) {
CookieService.putPermanent('quay.namespace', $scope.user.username);
UserService.load();
$('#convertAccountModal').modal('hide');
$location.path('/');
}, errorHandler);
};

View file

@ -0,0 +1,44 @@
(function() {
/**
* Billing plans page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('billing', 'billing.html', BillingCtrl, {
'title': 'Billing',
'description': 'Billing',
'newLayout': true
});
}]);
/**
* Billing invoices page.
*/
angular.module('quayPages').config(['pages', function(pages) {
pages.create('invoices', 'invoices.html', BillingCtrl, {
'title': 'Billing Invoices',
'description': 'Billing Invoices',
'newLayout': true
});
}]);
function BillingCtrl($scope, ApiService, $routeParams) {
$scope.orgname = $routeParams['orgname'];
$scope.username = $routeParams['username'];
var loadEntity = function() {
if ($scope.orgname) {
$scope.entityResource = ApiService.getOrganizationAsResource({'orgname': $scope.orgname}).get(function(org) {
$scope.organization = org;
});
} else {
$scope.entityResource = ApiService.getUserInformationAsResource({'username': $scope.username}).get(function(user) {
$scope.viewuser = user;
});
}
};
// Load the user or organization.
loadEntity();
}
}());

View file

@ -16,9 +16,10 @@
$scope.namespace = orgname;
$scope.showLogsCounter = 0;
$scope.showApplicationsCounter = 0;
$scope.showInvoicesCounter = 0;
$scope.showBillingCounter = 0;
$scope.showRobotsCounter = 0;
$scope.showTeamsCounter = 0;
$scope.changeEmailInfo = null;
$scope.orgScope = {
'changingOrganization': false,
@ -67,8 +68,8 @@
$scope.showTeamsCounter++;
};
$scope.showInvoices = function() {
$scope.showInvoicesCounter++;
$scope.showBilling = function() {
$scope.showBillingCounter = true;
};
$scope.showApplications = function() {
@ -79,25 +80,27 @@
$scope.showLogsCounter++;
};
$scope.changeEmail = function() {
UIService.hidePopover('#changeEmailForm input');
$scope.showChangeEmail = function() {
$scope.changeEmailInfo = {
'email': $scope.organization.email
};
};
$scope.orgScope.changingOrganization = true;
$scope.changeEmail = function(info, callback) {
var params = {
'orgname': orgname
};
var data = {
'email': $scope.orgScope.organizationEmail
var details = {
'email': $scope.changeEmailInfo.email
};
ApiService.changeOrganizationDetails(data, params).then(function(org) {
$scope.orgScope.changingOrganization = false;
$scope.organization = org;
}, function(result) {
$scope.orgScope.changingOrganization = false;
UIService.showFormError('#changeEmailForm input', result, 'right');
});
var errorDisplay = ApiService.errorDisplay('Could not change email address', callback);
ApiService.changeOrganizationDetails(details, params).then(function() {
$scope.organization.email = $scope.changeEmailInfo.email;
callback(true);
}, errorDisplay);
};
}
})();

View file

@ -13,11 +13,13 @@
function UserViewCtrl($scope, $routeParams, $timeout, ApiService, UserService, UIService, AvatarService, Config, ExternalLoginService) {
var username = $routeParams.username;
$scope.showInvoicesCounter = 0;
$scope.showAppsCounter = 0;
$scope.showRobotsCounter = 0;
$scope.changeEmailInfo = {};
$scope.changePasswordInfo = {};
$scope.showBillingCounter = 0;
$scope.showLogsCounter = 0;
$scope.changeEmailInfo = null;
$scope.changePasswordInfo = null;
$scope.hasSingleSignin = ExternalLoginService.hasSingleSignin();
$scope.context = {};
@ -54,37 +56,32 @@
$scope.showRobotsCounter++;
};
$scope.showInvoices = function() {
$scope.showInvoicesCounter++;
$scope.showLogs = function() {
$scope.showLogsCounter++;
};
$scope.showApplications = function() {
$scope.showAppsCounter++;
};
$scope.changePassword = function() {
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
$scope.showChangePassword = function() {
$scope.changePasswordInfo = {};
};
UIService.hidePopover('#changePasswordForm');
$scope.changePasswordInfo.state = 'changing';
$scope.changePassword = function(info, callback) {
if (Config.AUTHENTICATION_TYPE != 'Database') { return; }
var data = {
'password': $scope.changePasswordInfo.password
};
var errorDisplay = ApiService.errorDisplay('Could not change password', callback);
ApiService.changeUserDetails(data).then(function(resp) {
$scope.changePasswordInfo.state = 'changed';
// Reset the form
delete $scope.changePasswordInfo['password']
delete $scope.changePasswordInfo['repeatPassword']
// Reload the user.
UserService.load();
}, function(result) {
$scope.changePasswordInfo.state = 'change-error';
UIService.showFormError('#changePasswordForm', result);
});
callback(true);
}, errorDisplay);
};
$scope.generateClientToken = function() {
@ -102,21 +99,32 @@
UIService.showPasswordDialog('Enter your password to generate an encrypted version:', generateToken);
};
$scope.changeEmail = function() {
UIService.hidePopover('#changeEmailForm');
$scope.showChangeEmail = function() {
$scope.changeEmailInfo = {
'email': $scope.context.viewuser.email
};
};
$scope.changeEmail = function(info, callback) {
var details = {
'email': $scope.changeEmailInfo.email
};
$scope.changeEmailInfo.state = 'sending';
var errorDisplay = ApiService.errorDisplay('Could not change email address', callback);
ApiService.changeUserDetails(details).then(function() {
$scope.changeEmailInfo.state = 'sent';
delete $scope.changeEmailInfo['email'];
}, function(result) {
$scope.changeEmailInfo.state = 'send-error';
UIService.showFormError('#changeEmailForm', result);
});
callback(true);
}, errorDisplay);
};
$scope.showChangeAccount = function() {
$scope.convertAccountInfo = {
'user': $scope.context.viewuser
};
};
$scope.showBilling = function() {
$scope.showBillingCounter++;
};
}
})();

View file

@ -313,7 +313,6 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService'
}
message = UtilService.stringToHTML(message);
bootbox.dialog({
"message": message,
"title": defaultMessage || 'Request Failure',

View file

@ -0,0 +1,32 @@
<div class="billing-page page-content">
<div class="resource-view" resource="entityResource" error-message="'Could not load entity'">
<div class="cor-title" ng-if="organization">
<span class="cor-title-link">
<a class="back-link" href="/organization/{{ organization.name }}?tab=settings">
<span class="avatar" size="24" data="organization.avatar"></span>
{{ organization.name }}
</a>
</span>
<span class="cor-title-content">
Account Plan
</span>
</div>
<div class="cor-title" ng-if="viewuser">
<span class="cor-title-link">
<a class="back-link" href="/user/{{ viewuser.username }}?tab=settings">
<span class="avatar" size="24" data="viewuser.avatar"></span>
{{ viewuser.username }}
</a>
</span>
<span class="cor-title-content">
Account Plan
</span>
</div>
<div class="co-main-content-panel" style="min-height: 500px;">
<div class="plan-manager" organization="organization.name" has-subscription="hasSubscription" ng-if="organization"></div>
<div class="plan-manager" user="viewuser" has-subscription="hasSubscription" ng-if="!organization"></div>
</div>
</div>
</div>

View file

@ -0,0 +1,32 @@
<div class="billing-page page-content">
<div class="resource-view" resource="entityResource" error-message="'Could not load entity'">
<div class="cor-title" ng-if="organization">
<span class="cor-title-link">
<a class="back-link" href="/organization/{{ organization.name }}?tab=settings">
<span class="avatar" size="24" data="organization.avatar"></span>
{{ organization.name }}
</a>
</span>
<span class="cor-title-content">
Billing Invoices
</span>
</div>
<div class="cor-title" ng-if="viewuser">
<span class="cor-title-link">
<a class="back-link" href="/user/{{ viewuser.username }}?tab=settings">
<span class="avatar" size="24" data="viewuser.avatar"></span>
{{ viewuser.username }}
</a>
</span>
<span class="cor-title-content">
Billing Invoices
</span>
</div>
<div class="co-main-content-panel" style="min-height: 500px;">
<div class="billing-invoices" user="viewuser" makevisible="true" ng-if="!organization"></div>
<div class="billing-invoices" organization="organization" makevisible="true" ng-if="organization"></div>
</div>
</div>
</div>

View file

@ -35,14 +35,6 @@
<span class="cor-tab" tab-title="Default Permissions" tab-target="#default" ng-show="isAdmin">
<i class="fa ci-stamp"></i>
</span>
<span class="cor-tab" tab-title="Billing" tab-target="#usage"
quay-show="isAdmin && Features.BILLING">
<i class="fa fa-credit-card"></i>
</span>
<span class="cor-tab" tab-title="Billing Invoices" tab-target="#invoices"
tab-init="showInvoices()" quay-show="isAdmin && Features.BILLING">
<i class="fa ci-invoice"></i>
</span>
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs"
tab-init="showLogs()" ng-show="isAdmin">
<i class="fa fa-bar-chart"></i>
@ -52,7 +44,7 @@
<i class="fa ci-application"></i>
</span>
<span class="cor-tab" tab-title="Organization Settings" tab-target="#settings"
ng-show="isAdmin">
ng-show="isAdmin" tab-init="showBilling()">
<i class="fa fa-gears"></i>
</span>
</div> <!-- /cor-tabs -->
@ -95,45 +87,60 @@
makevisible="showApplicationsCounter"></div>
</div>
<!-- Plan and Usage -->
<div id="usage" class="tab-pane" quay-require="['BILLING']">
<h3>Plan Usage and Billing</h3>
<div ng-if="isAdmin">
<div class="plan-manager" organization="organization.name"
has-subscription="hasSubscription"></div>
<hr ng-show="hasSubscription">
<div class="billing-options" organization="organization" ng-show="hasSubscription"></div>
</div>
</div>
<!-- Billing Invoices -->
<div id="invoices" class="tab-pane" quay-require="['BILLING']">
<h3>Billing Invoices</h3>
<div class="billing-invoices" organization="organization"
makevisible="showInvoicesCounter"></div>
</div>
<!-- Settings -->
<div id="settings" class="tab-pane">
<div ng-if="isAdmin">
<h3>Organization Settings</h3>
<!-- Org Settings -->
<div class="settings-section">
<h3>Organization Settings</h3>
<table class="co-list-table">
<tr>
<td>Namespace:</td>
<td>
{{ organization.name }}
<div class="help-text">Organization names cannot currently be changed. Please <a href="/contact">contact support</a> to migrate accounts.</div>
</td>
</tr>
<tr>
<td>Avatar:</td>
<td>
<span class="avatar" size="48" data="organization.avatar"></span>
<div class="help-text" ng-if="Config.AVATAR_KIND == 'local'">Avatar is generated based off the organization's name.</div>
<div class="help-text" ng-if="Config.AVATAR_KIND == 'gravatar'">Avatar is served by <a href="http://gravatar.com" rel="nofollow" target="_blank">Gravatar</a> based on the {{ organization.email }} e-mail address.</div>
</td>
</tr>
<tr quay-show="Features.MAILING">
<td>Email Address:</td>
<td>
<a class="co-modify-link" ng-click="showChangeEmail()">{{ organization.email }}</a>
</td>
</tr>
</table>
</div>
<div class="panel" ng-show="!orgScope.changingOrganization">
<div class="panel-title">Organization's e-mail address</div>
<div class="panel-content" style="padding-left: 20px; margin-top: 10px;">
<form class="form-change" id="changeEmailForm" name="changeEmailForm" ng-submit="changeEmail()">
<input type="email" class="form-control" style="max-width: 500px;"
ng-model="orgScope.organizationEmail" required>
<button class="btn btn-primary" type="submit"
ng-disabled="changeEmailForm.$invalid || orgScope.organizationEmail == organization.email">
Save
</button>
</form>
</div>
<!-- Billing Information -->
<div class="settings-section">
<h3>Billing Information</h3>
<div class="billing-management-panel" organization="organization" is-enabled="showBillingCounter"></div>
</div>
</div>
</div>
</div> <!-- /cor-tab-content -->
</div>
</div>
<!-- Change email dialog -->
<div class="cor-confirm-dialog"
dialog-context="changeEmailInfo"
dialog-action="changeEmail(info, callback)"
dialog-title="Change E-mail Address"
dialog-action-title="Change Email"
dialog-form="context.emailform">
<form name="context.emailform" class="co-single-field-dialog">
Please enter a new email address.
<input type="email" class="form-control" placeholder="Your new e-mail address"
ng-model="changeEmailInfo.email" required>
</form>
</div>
</div>

View file

@ -28,24 +28,16 @@
<span class="cor-tab" tab-title="Robot Accounts" tab-init="showRobots()" tab-target="#robots">
<i class="fa ci-robot"></i>
</span>
<span class="cor-tab" tab-title="User Settings" tab-target="#settings">
<i class="fa fa-gears"></i>
</span>
<span class="cor-tab" tab-title="Billing" tab-target="#usage"
quay-show="Features.BILLING">
<i class="fa fa-credit-card"></i>
</span>
<span class="cor-tab" tab-title="Billing Invoices" tab-target="#invoices"
tab-init="showInvoices()" quay-show="Features.BILLING">
<i class="fa ci-invoice"></i>
</span>
<span class="cor-tab" tab-title="External Logins" tab-target="#external"
quay-show="!hasSingleSignin">
<span class="cor-tab" tab-title="External Logins And Applications" tab-target="#external"
tab-init="showApplications()">
<i class="fa fa-external-link-square"></i>
</span>
<span class="cor-tab" tab-title="Authorized Applications" tab-target="#applications"
tab-init="showApplications()">
<i class="fa ci-application"></i>
<span class="cor-tab" tab-title="Usage Logs" tab-target="#logs" tab-init="showLogs()"
quay-show="Features.USER_LOG_ACCESS">
<i class="fa fa-bar-chart"></i>
</span>
<span class="cor-tab" tab-title="User Settings" tab-target="#settings" tab-init="showBilling()">
<i class="fa fa-gears"></i>
</span>
</div> <!-- /cor-tabs -->
@ -60,149 +52,129 @@
<div class="robots-manager" user="viewuser" is-enabled="showRobotsCounter"></div>
</div>
<!-- External Logins -->
<!-- Usage Logs -->
<div id="logs" class="tab-pane">
<div class="logs-view" user="viewuser" makevisible="showLogsCounter"></div>
</div>
<!-- External Logins And Applications -->
<div id="external" class="tab-pane" quay-show="!hasSingleSignin">
<div class="external-logins-manager" user="viewuser"></div>
</div>
<!-- Applications -->
<div id="applications" class="tab-pane">
<div class="external-logins-manager" user="viewuser"
quay-show="!hasSingleSignin"></div>
<div style="margin: 50px" quay-show="!hasSingleSignin">
</div>
<div class="authorized-apps-manager" user="viewuser" is-enabled="showAppsCounter"></div>
</div>
<!-- Plan and Usage -->
<div id="usage" class="tab-pane" quay-require="['BILLING']">
<h3>Plan Usage and Billing</h3>
<div class="plan-manager" user="viewuser" has-subscription="hasSubscription"></div>
<hr ng-show="hasSubscription">
<div class="billing-options" user="viewuser" ng-show="hasSubscription"></div>
</div>
<!-- Billing Invoices -->
<div id="invoices" class="tab-pane" quay-require="['BILLING']">
<h3>Billing Invoices</h3>
<div class="billing-invoices" user="viewuser"
makevisible="showInvoicesCounter"></div>
</div>
<!-- Settings -->
<div id="settings" class="tab-pane">
<h3>User Settings</h3>
<!-- E-mail address -->
<div class="co-panel" quay-show="Features.MAILING">
<div class="co-panel-heading"><i class="fa fa-envelope-o"></i> E-mail Address</div>
<div class="panel-body" style="padding-top: 5px;">
<div class="co-alert co-alert-success" ng-show="changeEmailInfo.state == 'sent'">
An e-mail has been sent to {{ sentEmail }} to verify the change.
</div>
<div class="cor-loader" ng-show="changeEmailInfo.state == 'sending'"></div>
<div ng-show="changeEmailInfo.state != 'sending'">
<form class="form-change user-settings-form"
id="changeEmailForm" name="changeEmailForm"
ng-submit="changeEmail(); changeEmailForm.$setPristine()"
ng-show="!awaitingConfirmation && !registering">
<div class="row">
<table class="col-md-6">
<tr>
<td>Current E-mail Address:</td>
<td>{{ context.viewuser.email }}</td>
</tr>
<tr>
<td>New E-mail Address:</td>
<td>
<input type="email" class="form-control"
placeholder="Your new e-mail address"
ng-model="changeEmailInfo.email" required>
</td>
</tr>
</table>
</div>
<button class="btn btn-primary"
ng-disabled="changeEmailForm.$invalid || changeEmail.email == context.viewuser.email"
type="submit">
Change E-mail Address
</button>
</form>
</div>
<!-- Encrypted Password -->
<div class="settings-section">
<h3>Docker CLI Password</h3>
<div ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
The Docker CLI stores passwords entered on the command line in <strong>plaintext</strong>. It is therefore highly recommended to generate an an encrypted version of your password to use for <code>docker login</code>.
</div>
</div> <!-- /E-mail -->
<!-- Password -->
<div class="co-panel" style="margin-bottom: 0px">
<div class="co-panel-heading"><i class="fa fa-lock"></i> Password</div>
<div class="panel-body" style="padding-top: 5px;">
<div class="cor-loader" ng-show="changePasswordInfo.state == 'changing'"></div>
<!-- Encrypted Password -->
<div class="row" ng-show="changePasswordInfo.state !='changing'">
<div class="panel">
<div class="panel-title">Generate Encrypted Password</div>
<div class="panel-body">
<div class="co-alert co-alert-info" ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
Due to Docker storing passwords entered on the command line in <strong>plaintext</strong>, it is highly recommended to use the button below to generate an an encrypted version of your password.
</div>
<div class="co-alert co-alert-warning" ng-if="Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
This installation is set to <strong>require</strong> encrypted passwords when
using the Docker command line interface. To generate an encrypted password, click the button below.
</div>
<button class="btn btn-primary" ng-click="generateClientToken()">
<i class="fa fa-key" style="margin-right: 6px;"></i>Generate Encrypted Password
</button>
</div>
</div>
</div>
<!-- Change Password -->
<div class="row" quay-show="Config.AUTHENTICATION_TYPE == 'Database' && changePasswordInfo.state !='changing'">
<div class="panel">
<div class="panel-title">Change Password</div>
<span class="help-block" ng-show="changePasswordInfo.state == 'changed'">
Password changed successfully
</span>
<div class="panel-body">
<div class="co-alert co-alert-warning">Note: Changing your password will also invalidate any generated encrypted passwords.</div>
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword(); changePasswordForm.$setPristine()"
ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="changePasswordInfo.password" required
ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="changePasswordInfo.repeatPassword"
match="changePasswordInfo.password" required ng-pattern="/^.{8,}$/">
<button class="btn btn-danger" ng-disabled="changePasswordForm.$invalid" type="submit"
analytics-on analytics-event="change_pass">Change Password</button>
</form>
</div>
</div>
</div>
<div ng-if="Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
This installation is set to <strong>require</strong> encrypted passwords when
using the Docker command line interface.
</div>
</div> <!-- /Password -->
<!-- Convert -->
<div class="co-panel" quay-show="Config.AUTHENTICATION_TYPE == 'Database'">
<div class="co-panel-heading"><i class="fa fa-group"></i> Convert to organization</div>
<div class="panel-body" style="padding-top: 5px;">
<div class="convert-user-to-org" user="viewuser"></div>
</div>
<table class="co-list-table" style="margin-top: 10px;">
<tr>
<td>CLI Password:</td>
<td>
<a ng-click="generateClientToken()">Generate Encrypted Password</a>
</td>
</tr>
</table>
</div>
</div> <!-- /Convert -->
</div> <!-- /cor-tab-content -->
<!-- User Settings -->
<div class="settings-section">
<h3>User Settings</h3>
<table class="co-list-table">
<tr>
<td>Username:</td>
<td>
{{ context.viewuser.username }}
<div class="help-text">Usernames cannot currently be changed. Please <a href="/contact">contact support</a> to migrate accounts.</div>
</td>
</tr>
<tr>
<td>Avatar:</td>
<td>
<span class="avatar" size="48" data="context.viewuser.avatar"></span>
<div class="help-text" ng-if="Config.AVATAR_KIND == 'local'">Avatar is generated based off of your username.</div>
<div class="help-text" ng-if="Config.AVATAR_KIND == 'gravatar'">Avatar is served by <a href="http://gravatar.com" rel="nofollow" target="_blank">Gravatar</a> based on the {{ context.viewuser.email }} e-mail address.</div>
</td>
</tr>
<tr quay-show="Features.MAILING">
<td>Email Address:</td>
<td>
<a class="co-modify-link" ng-click="showChangeEmail()">{{ context.viewuser.email }}</a>
</td>
</tr>
<tr quay-show="Config.AUTHENTICATION_TYPE == 'Database'">
<td>Password:</td>
<td>
<a class="co-modify-link" ng-click="showChangePassword()">Change password</a>
</td>
</tr>
<tr quay-show="Config.AUTHENTICATION_TYPE == 'Database'">
<td>Account Type:</td>
<td>
<a class="co-modify-link" ng-click="showChangeAccount()">Individual account</a>
</td>
</tr>
</table>
</div>
<!-- Billing Information -->
<div class="settings-section">
<h3>Billing Information</h3>
<div class="billing-management-panel" user="context.viewuser" is-enabled="showBillingCounter"></div>
</div>
</div> <!-- /cor-tab-content -->
</div>
</div>
<!-- Change email dialog -->
<div class="cor-confirm-dialog"
dialog-context="changeEmailInfo"
dialog-action="changeEmail(info, callback)"
dialog-title="Change E-mail Address"
dialog-action-title="Change Email"
dialog-form="context.emailform">
<form name="context.emailform" class="co-single-field-dialog">
Please enter a new email address. A verification email will be sent before being applied.
<input type="email" class="form-control" placeholder="Your new e-mail address"
ng-model="changeEmailInfo.email" required>
</form>
</div>
<!-- Change password dialog -->
<div class="cor-confirm-dialog"
dialog-context="changePasswordInfo"
dialog-action="changePassword(info, callback)"
dialog-title="Change Password"
dialog-action-title="Change Password"
dialog-form="context.passwordform">
<form name="context.passwordform" class="co-single-field-dialog">
Enter a new password. Passwords must be at least 8 characters in length.
<input type="password" class="form-control" placeholder="Your new password" ng-model="changePasswordInfo.password" required
ng-pattern="/^.{8,}$/">
<input type="password" class="form-control" placeholder="Verify your new password" ng-model="changePasswordInfo.repeatPassword"
match="changePasswordInfo.password" required ng-pattern="/^.{8,}$/">
</form>
</div>
<!-- Convert account dialog -->
<div class="convert-user-to-org" info="convertAccountInfo"></div>
<!-- Modal message dialog -->
<div class="modal fade" id="clientTokenModal">
<div class="co-dialog modal fade" id="clientTokenModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">