Add full application management API, UI and test cases

This commit is contained in:
Joseph Schorr 2014-03-20 15:46:13 -04:00
parent a3eff7a2e8
commit f7c27f250b
16 changed files with 904 additions and 15 deletions

View file

@ -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;
}

View file

@ -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 }}

View 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>

View file

@ -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,

View file

@ -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();
}

View 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">&times;</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">&times;</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 -->

View file

@ -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>

View file

@ -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>