Add full application management API, UI and test cases
This commit is contained in:
parent
a3eff7a2e8
commit
f7c27f250b
16 changed files with 904 additions and 15 deletions
|
@ -2801,7 +2801,7 @@ p.editable:hover i {
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.create-org .step-container .description {
|
||||
.form-group .description {
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
color: #888;
|
||||
|
@ -2809,7 +2809,7 @@ p.editable:hover i {
|
|||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.create-org .form-group input {
|
||||
.form-group.nested input {
|
||||
margin-top: 10px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
@ -3469,7 +3469,7 @@ pre.command:before {
|
|||
display: block !important;
|
||||
}
|
||||
|
||||
.auth-header img {
|
||||
.auth-header > img {
|
||||
float: left;
|
||||
margin-top: 8px;
|
||||
margin-right: 20px;
|
||||
|
@ -3560,3 +3560,14 @@ pre.command:before {
|
|||
.auth-container .button-bar button {
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.manage-application #oauth td {
|
||||
padding: 6px;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.manage-application .button-bar {
|
||||
margin-top: 10px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
<div class="application-info-element" style="padding-bottom: 18px">
|
||||
<div class="auth-header">
|
||||
<img src="//www.gravatar.com/avatar/{{ application.organization.gravatar }}?s=48&d=identicon">
|
||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar }}?s=48&d=identicon">
|
||||
<h2><a href="{{ application.url }}" target="_blank">{{ application.name }}</a></h2>
|
||||
<h4>
|
||||
{{ application.organization.name }}
|
||||
|
|
24
static/directives/application-manager.html
Normal file
24
static/directives/application-manager.html
Normal file
|
@ -0,0 +1,24 @@
|
|||
<div class="application-manager-element">
|
||||
<div class="quay-spinner" ng-show="loading"></div>
|
||||
|
||||
<div class="container" ng-show="!loading">
|
||||
<div class="side-controls">
|
||||
<span class="popup-input-button" placeholder="'Application Name'" submitted="createApplication(value)">
|
||||
<i class="fa fa-plus"></i> Create New Application
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<th>Application Name</th>
|
||||
<th>Application URI</th>
|
||||
</thead>
|
||||
|
||||
<tr ng-repeat="app in applications">
|
||||
<td><a href="/organization/{{ organization.name }}/application/{{ app.client_id }}">{{ app.name }}</a></td>
|
||||
<td><a href="{{ app.application_uri }}" ng-if="app.application_uri" target="_blank">{{ app.application_uri }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</div>
|
|
@ -237,7 +237,9 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
'robot': 'wrench',
|
||||
'tag': 'tag',
|
||||
'role': 'th-large',
|
||||
'original_role': 'th-large'
|
||||
'original_role': 'th-large',
|
||||
'application_name': 'cloud',
|
||||
'client_id': 'chain'
|
||||
};
|
||||
|
||||
var description = value_or_func;
|
||||
|
@ -1088,6 +1090,8 @@ quayApp = angular.module('quay', ['ngRoute', 'chieffancypants.loadingBar', 'angu
|
|||
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl, reloadOnSearch: false}).
|
||||
when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}).
|
||||
when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}).
|
||||
when('/organization/:orgname/application/:clientid', {templateUrl: '/static/partials/manage-application.html',
|
||||
controller: ManageApplicationCtrl, reloadOnSearch: false}).
|
||||
when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}).
|
||||
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
||||
otherwise({redirectTo: '/'});
|
||||
|
@ -1751,7 +1755,12 @@ quayApp.directive('logsView', function () {
|
|||
var triggerDescription = TriggerDescriptionBuilder.getDescription(
|
||||
metadata['service'], metadata['config']);
|
||||
return 'Delete build trigger - ' + triggerDescription;
|
||||
}
|
||||
},
|
||||
'create_application': 'Create application {application_name} with client ID {client_id}',
|
||||
'update_application': 'Update application to {application_name} for client ID {client_id}',
|
||||
'delete_application': 'Delete application {application_name} with client ID {client_id}',
|
||||
'reset_application_client_secret': 'Reset the Client Secret of application {application_name} ' +
|
||||
'with client ID {client_id}'
|
||||
};
|
||||
|
||||
var logKinds = {
|
||||
|
@ -1785,7 +1794,11 @@ quayApp.directive('logsView', function () {
|
|||
'modify_prototype_permission': 'Modify default permission',
|
||||
'delete_prototype_permission': 'Delete default permission',
|
||||
'setup_repo_trigger': 'Setup build trigger',
|
||||
'delete_repo_trigger': 'Delete build trigger'
|
||||
'delete_repo_trigger': 'Delete build trigger',
|
||||
'create_application': 'Create Application',
|
||||
'update_application': 'Update Application',
|
||||
'delete_application': 'Delete Application',
|
||||
'reset_application_client_secret': 'Reset Client Secret'
|
||||
};
|
||||
|
||||
var getDateString = function(date) {
|
||||
|
@ -1878,6 +1891,72 @@ quayApp.directive('logsView', function () {
|
|||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('applicationManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
templateUrl: '/static/directives/application-manager.html',
|
||||
replace: false,
|
||||
transclude: false,
|
||||
restrict: 'C',
|
||||
scope: {
|
||||
'organization': '=organization',
|
||||
'visible': '=visible'
|
||||
},
|
||||
controller: function($scope, $element, ApiService) {
|
||||
$scope.loading = false;
|
||||
|
||||
$scope.createApplication = function(appName) {
|
||||
if (!appName) { return; }
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
var data = {
|
||||
'name': appName
|
||||
};
|
||||
|
||||
ApiService.createOrganizationApplication(data, params).then(function(resp) {
|
||||
$scope.applications.push(resp);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp['message'] || 'The application could not be created',
|
||||
"title": "Cannot create application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
if (!$scope.organization || !$scope.visible) { return; }
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
var params = {
|
||||
'orgname': $scope.organization.name
|
||||
};
|
||||
|
||||
ApiService.getOrganizationApplications(null, params).then(function(resp) {
|
||||
$scope.loading = false;
|
||||
$scope.applications = resp['applications'];
|
||||
});
|
||||
};
|
||||
|
||||
$scope.$watch('organization', update);
|
||||
$scope.$watch('visible', update);
|
||||
}
|
||||
};
|
||||
return directiveDefinitionObject;
|
||||
});
|
||||
|
||||
|
||||
quayApp.directive('robotsManager', function () {
|
||||
var directiveDefinitionObject = {
|
||||
priority: 0,
|
||||
|
|
|
@ -2139,12 +2139,17 @@ function OrgAdminCtrl($rootScope, $scope, $timeout, Restangular, $routeParams, U
|
|||
$scope.invoiceLoading = true;
|
||||
$scope.logsShown = 0;
|
||||
$scope.invoicesShown = 0;
|
||||
$scope.applicationsShown = 0;
|
||||
$scope.changingOrganization = false;
|
||||
|
||||
$scope.loadLogs = function() {
|
||||
$scope.logsShown++;
|
||||
};
|
||||
|
||||
$scope.loadApplications = function() {
|
||||
$scope.applicationsShown++;
|
||||
};
|
||||
|
||||
$scope.loadInvoices = function() {
|
||||
$scope.invoicesShown++;
|
||||
};
|
||||
|
@ -2429,4 +2434,124 @@ function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangul
|
|||
// Load the org info and the member info.
|
||||
loadOrganization();
|
||||
loadMemberInfo();
|
||||
}
|
||||
|
||||
|
||||
function ManageApplicationCtrl($scope, $routeParams, $rootScope, $location, $timeout, ApiService) {
|
||||
var orgname = $routeParams.orgname;
|
||||
var clientId = $routeParams.clientid;
|
||||
|
||||
$scope.updating = false;
|
||||
|
||||
$scope.askResetClientSecret = function() {
|
||||
$('#resetSecretModal').modal({});
|
||||
};
|
||||
|
||||
$scope.askDelete = function() {
|
||||
$('#deleteAppModal').modal({});
|
||||
};
|
||||
|
||||
$scope.deleteApplication = function() {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'client_id': clientId
|
||||
};
|
||||
|
||||
$('#deleteAppModal').modal('hide');
|
||||
|
||||
ApiService.deleteOrganizationApplication(null, params).then(function(resp) {
|
||||
$timeout(function() {
|
||||
$location.path('/organization/' + orgname + '/admin');
|
||||
}, 500);
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not delete application',
|
||||
"title": "Cannot delete application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.updateApplication = function() {
|
||||
$scope.updating = true;
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'client_id': clientId
|
||||
};
|
||||
|
||||
ApiService.updateOrganizationApplication($scope.application, params).then(function(resp) {
|
||||
$scope.application = resp;
|
||||
$scope.updating = false;
|
||||
}, function(resp) {
|
||||
$scope.updating = false;
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not update application',
|
||||
"title": "Cannot update application",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.resetClientSecret = function() {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'client_id': clientId
|
||||
};
|
||||
|
||||
$('#resetSecretModal').modal('hide');
|
||||
|
||||
ApiService.resetOrganizationApplicationClientSecret(null, params).then(function(resp) {
|
||||
$scope.application = resp;
|
||||
}, function(resp) {
|
||||
bootbox.dialog({
|
||||
"message": resp.message || 'Could not reset client secret',
|
||||
"title": "Cannot reset client secret",
|
||||
"buttons": {
|
||||
"close": {
|
||||
"label": "Close",
|
||||
"className": "btn-primary"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
var loadOrganization = function() {
|
||||
$scope.orgResource = ApiService.getOrganizationAsResource({'orgname': orgname}).get(function(org) {
|
||||
$scope.organization = org;
|
||||
return org;
|
||||
});
|
||||
};
|
||||
|
||||
var loadApplicationInfo = function() {
|
||||
var params = {
|
||||
'orgname': orgname,
|
||||
'client_id': clientId
|
||||
};
|
||||
|
||||
$scope.appResource = ApiService.getOrganizationApplicationAsResource(params).get(function(resp) {
|
||||
$scope.application = resp;
|
||||
|
||||
$rootScope.title = 'Manage Application ' + $scope.application.name + ' (' + $scope.orgname + ')';
|
||||
$rootScope.description = 'Manage the details of application ' + $scope.application.name +
|
||||
' under organization ' + $scope.orgname;
|
||||
|
||||
return resp;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Load the organization and application info.
|
||||
loadOrganization();
|
||||
loadApplicationInfo();
|
||||
}
|
165
static/partials/manage-application.html
Normal file
165
static/partials/manage-application.html
Normal file
|
@ -0,0 +1,165 @@
|
|||
<div class="resource-view" resource="appResource" error-message="'Application not found'">
|
||||
</div>
|
||||
|
||||
<div ng-show="application">
|
||||
<div class="container manage-application">
|
||||
<!-- Header -->
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="auth-header">
|
||||
<img src="//www.gravatar.com/avatar/{{ application.gravatar_email | gravatar }}?s=48&d=identicon">
|
||||
<h2>{{ application.name || '(Untitled)' }}</h2>
|
||||
<h4>
|
||||
<img src="//www.gravatar.com/avatar/{{ organization.gravatar }}?s=24&d=identicon" style="vertical-align: middle; margin-right: 4px;">
|
||||
<span style="vertical-align: middle"><a href="/organization/{{ organization.name }}/admin">{{ organization.name }}</a></span>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" style="margin-top: 10px" ng-if="!application.redirect_uri">
|
||||
<div class="alert alert-warning">
|
||||
Warning: There is no OAuth Redirect setup for this application. Please enter it in the <strong>Settings</strong> tab.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="row" style="margin-top: 10px">
|
||||
<!-- Side tabs -->
|
||||
<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="#settings">Settings</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#oauth">OAuth Information</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#delete">Delete Application</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Content -->
|
||||
<div class="col-md-10">
|
||||
<div class="tab-content">
|
||||
<!-- Settings tab -->
|
||||
<div id="settings" class="tab-pane active">
|
||||
<form method="put" name="applicationForm" id="applicationForm" ng-submit="updateApplication()">
|
||||
<div class="form-group nested">
|
||||
<label for="fieldAppName">Application Name</label>
|
||||
<input type="text" class="form-control" id="fieldAppName" placeholder="Application Name" required ng-model="application.name">
|
||||
<div class="description">The name of the application that is displayed to users</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group nested">
|
||||
<label for="fieldAppURI">Homepage URL</label>
|
||||
<input type="url" class="form-control" id="fieldAppURI" placeholder="Homepage URL" required ng-model="application.application_uri">
|
||||
<div class="description">The URL to which the application will link in the authorization view</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group nested">
|
||||
<label for="fieldAppDescription">Description (optional)</label>
|
||||
<input type="text" class="form-control" id="fieldAppURI" placeholder="Description" ng-model="application.description">
|
||||
<div class="description">The user friendly description of the application</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group nested">
|
||||
<label for="fieldAppGravatar">Gravatar E-mail (optional)</label>
|
||||
<input type="email" class="form-control" id="fieldAppGravatar" placeholder="Gravatar E-mail" ng-model="application.gravatar_email">
|
||||
<div class="description">An e-mail address representing the <a href="http://en.gravatar.com/" target="_blank">Gravatar</a> for the application</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group nested" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #eee;">
|
||||
<label for="fieldAppRedirect">Redirect/Callback URL</label>
|
||||
<input type="url" class="form-control" id="fieldAppRedirect" placeholder="OAuth Redirect URL" ng-model="application.redirect_uri" required>
|
||||
<div class="description">The application's OAuth redirection/callback URL, invoked once access has been granted.</div>
|
||||
</div>
|
||||
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-success btn-large" type="submit" ng-disabled="applicationForm.$invalid || updating">
|
||||
Update Application
|
||||
</button>
|
||||
<span class="quay-spinner" ng-show="updating"></span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Delete tab -->
|
||||
<div id="delete" class="tab-pane">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<div style="text-align: center">
|
||||
<div class="alert alert-danger">Deleting an application <b>cannot be undone</b>. Any existing users of your application will <strong>break!</strong>. Here be dragons!</div>
|
||||
<button class="btn btn-danger" ng-click="askDelete()">Delete Application</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- OAuth tab -->
|
||||
<div id="oauth" class="tab-pane">
|
||||
<table style="margin-top: 20px;">
|
||||
<thead>
|
||||
<th style="width: 150px"></th>
|
||||
<th style="width: 250px"></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tr>
|
||||
<td>Client ID:</td>
|
||||
<td style="width: 250px">
|
||||
<div class="copy-box" hovering-message="true" value="application.client_id"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Client Secret: <i class="fa fa-lock fa-lg" title="Keep this secret!" bs-tooltip style="margin-left: 10px"></i></td>
|
||||
<td>
|
||||
{{ application.client_secret }}
|
||||
</td>
|
||||
<td style="padding-left: 10px">
|
||||
<button class="btn btn-primary" ng-click="askResetClientSecret()">Reset Client Secret</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="resetSecretModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Reset Client Secret?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="alert alert-info">
|
||||
Note that resetting the Client Secret for this application will <strong>not</strong> invalidate any user tokens.
|
||||
</div>
|
||||
<div>Are you sure you want to reset your Client Secret? Any existing users of this Secret <strong>will break!</strong></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="resetClientSecret()">Yes, I'm sure</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
<!-- Modal message dialog -->
|
||||
<div class="modal fade" id="deleteAppModal">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title">Delete Application?</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
Are you <b>absolutely, positively</b> sure you would like to delete this application? This <b>cannot be undone</b>.
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-danger" ng-click="deleteApplication()">Delete Application</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
|
@ -50,7 +50,7 @@
|
|||
<h3>Setup the new organization</h3>
|
||||
|
||||
<form method="post" name="newOrgForm" id="newOrgForm" ng-submit="createNewOrg()">
|
||||
<div class="form-group">
|
||||
<div class="form-group nested">
|
||||
<label for="orgName">Organization Name</label>
|
||||
<input id="orgName" name="orgName" type="text" class="form-control" placeholder="Organization Name"
|
||||
ng-model="org.name" required autofocus data-trigger="manual" data-content="{{ createError }}"
|
||||
|
@ -58,7 +58,7 @@
|
|||
<span class="description">This will also be the namespace for your repositories</span>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group nested">
|
||||
<label for="orgName">Organization Email</label>
|
||||
<input id="orgEmail" name="orgEmail" type="email" class="form-control" placeholder="Organization Email"
|
||||
ng-model="org.email" required>
|
||||
|
@ -66,7 +66,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Plans Table -->
|
||||
<div class="form-group plan-group">
|
||||
<div class="form-group nested plan-group">
|
||||
<strong>Choose your organization's plan</strong>
|
||||
<div class="plans-table" plans="plans" current-plan="currentPlan"></div>
|
||||
</div>
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#prototypes">Default Permissions</a></li>
|
||||
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#applications" ng-click="loadApplications()">Applications</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billingoptions">Billing</a></li>
|
||||
<li ng-show="hasPaidPlan"><a href="javascript:void(0)" data-toggle="tab" data-target="#billing" ng-click="loadInvoices()">Billing History</a></li>
|
||||
</ul>
|
||||
|
@ -60,6 +61,11 @@
|
|||
<div class="logs-view" organization="organization" visible="logsShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Applications tab -->
|
||||
<div id="applications" class="tab-pane">
|
||||
<div class="application-manager" organization="organization" visible="applicationsShown"></div>
|
||||
</div>
|
||||
|
||||
<!-- Billing Options tab -->
|
||||
<div id="billingoptions" class="tab-pane">
|
||||
<div class="billing-options" organization="organization"></div>
|
||||
|
|
Reference in a new issue