Get robots UI working

This commit is contained in:
Joseph Schorr 2013-11-22 20:14:44 -05:00
parent 43f2dd80a0
commit 12eb932da1
11 changed files with 309 additions and 143 deletions

View file

@ -297,7 +297,7 @@ def get_matching_entities(prefix):
}
if user.is_org_member is not None:
user_json['is_org_member'] = user.is_org_member
user_json['is_org_member'] = user.is_robot or user.is_org_member
return user_json
@ -924,8 +924,8 @@ def wrap_role_view_user(role_json, user):
return role_json
def wrap_role_view_org(role_json, org_member):
role_json['is_org_member'] = org_member
def wrap_role_view_org(role_json, user, org_members):
role_json['is_org_member'] = user.robot or user.username in org_members
return role_json
@ -1034,22 +1034,30 @@ def list_repo_team_permissions(namespace, repository):
def list_repo_user_permissions(namespace, repository):
permission = AdministerRepositoryPermission(namespace, repository)
if permission.can():
# Determine how to wrap the permissions
role_view_func = role_view
# Lookup the organization (if any).
org = None
try:
model.get_organization(namespace) # Will raise an error if not org
org_members = model.get_organization_member_set(namespace)
def wrapped_role_view(repo_perm):
unwrapped = wrap_role_view_user(role_view(repo_perm), repo_perm.user)
return wrap_role_view_org(unwrapped,
repo_perm.user.username in org_members)
role_view_func = wrapped_role_view
org = model.get_organization(namespace) # Will raise an error if not org
except model.InvalidOrganizationException:
# This repository isn't under an org
pass
# Determine how to wrap the role(s).
def wrapped_role_view(repo_perm):
return wrap_role_view_user(role_view(repo_perm), repo_perm.user)
role_view_func = wrapped_role_view
if org:
org_members = model.get_organization_member_set(namespace)
current_func = role_view_func
def wrapped_role_org_view(repo_perm):
return wrap_role_view_org(current_func(repo_perm), repo_perm.user, org_members)
role_view_func = wrapped_role_org_view
# Load and return the permissions.
repo_perms = model.get_all_repo_users(namespace, repository)
return jsonify({
'permissions': {perm.user.username: role_view_func(perm)
@ -1074,8 +1082,7 @@ def get_user_permissions(namespace, repository, username):
try:
model.get_organization(namespace)
org_members = model.get_organization_member_set(namespace)
perm_view = wrap_role_view_org(perm_view,
perm.user.username in org_members)
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
except model.InvalidOrganizationException:
# This repository is not part of an organization
pass
@ -1119,8 +1126,7 @@ def change_user_permissions(namespace, repository, username):
try:
model.get_organization(namespace)
org_members = model.get_organization_member_set(namespace)
perm_view = wrap_role_view_org(perm_view,
perm.user.username in org_members)
perm_view = wrap_role_view_org(perm_view, perm.user, org_members)
except model.InvalidOrganizationException:
# This repository is not part of an organization
pass

View file

@ -3,6 +3,11 @@
margin: 0;
}
#input-box {
padding: 4px;
font-size: 14px;
}
html, body {
height: 100%;
}
@ -16,6 +21,45 @@ html, body {
border-bottom: 1px dashed #aaa;
}
.docker-auth-dialog .token-dialog-body .well {
margin-bottom: 0px;
}
.docker-auth-dialog .token-view {
background: transparent;
display: block;
border: 0px transparent;
font-size: 12px;
width: 100%;
}
.docker-auth-dialog .download-cfg {
float: left;
padding-top: 6px;
font-size: 16px;
}
.docker-auth-dialog .download-cfg .fa-download {
margin-right: 10px;
font-size: 25px;
vertical-align: middle;
}
#copyClipboard {
cursor: pointer;
}
#copyClipboard.zeroclipboard-is-hover {
background: #428bca;
color: white;
}
#clipboardCopied.hovering {
position: absolute;
right: 0px;
top: 40px;
}
.content-container {
padding-bottom: 70px;
}
@ -44,8 +88,9 @@ html, body {
margin-bottom: 20px;
}
.robots-manager-element .robot {
.robots-manager-element .robot a {
font-size: 16px;
cursor: pointer;
}
.robots-manager-element .robot .prefix {
@ -970,21 +1015,6 @@ p.editable:hover i {
width: 300px;
}
.repo #copyClipboard {
cursor: pointer;
}
.repo #copyClipboard.zeroclipboard-is-hover {
background: #428bca;
color: white;
}
.repo #clipboardCopied.hovering {
position: absolute;
right: 0px;
top: 40px;
}
.repo-image-view .id-container {
display: inline-block;
margin-top: 10px;
@ -1027,7 +1057,7 @@ p.editable:hover i {
margin-top: 28px;
}
.repo #clipboardCopied {
#clipboardCopied {
font-size: 0.8em;
display: inline-block;
margin-right: 10px;
@ -1038,7 +1068,7 @@ p.editable:hover i {
border-radius: 4px;
}
.repo #clipboardCopied.animated {
#clipboardCopied.animated {
-webkit-animation: fadeOut 4s ease-in-out 0s 1 forwards;
-moz-animation: fadeOut 4s ease-in-out 0s 1 forwards;
-ms-animation: fadeOut 4s ease-in-out 0s 1 forwards;
@ -1125,21 +1155,17 @@ p.editable:hover i {
width: 300px;
}
.repo-admin .token-dialog-body .well {
margin-bottom: 0px;
}
.repo-admin .token-view {
background: transparent;
display: block;
border: 0px transparent;
font-size: 12px;
width: 100%;
}
.repo-admin .panel {
display: inline-block;
width: 620px;
width: 720px;
}
.repo-admin .prefix {
color: #aaa;
}
.repo-admin .admin-search {
padding-top: 20px;
}
.repo-admin .user i.fa-user {
@ -1147,6 +1173,11 @@ p.editable:hover i {
margin-right: 7px;
}
.repo-admin .user i.fa-wrench {
margin-left: 1px;
margin-right: 5px;
}
.repo-admin .team i.fa-group {
margin-right: 4px;
}
@ -1278,18 +1309,6 @@ p.editable:hover i {
white-space: nowrap;
}
.repo .download-cfg {
float: left;
padding-top: 6px;
font-size: 16px;
}
.repo .download-cfg .icon-download {
margin-right: 10px;
font-size: 25px;
vertical-align: middle;
}
.navbar-nav > li > .user-dropdown {
padding-top: 9px;
padding-bottom: 9px;

View file

@ -0,0 +1,30 @@
<!-- Modal message dialog -->
<div class="modal fade" id="dockerauthmodal">
<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">
<span ng-transclude></span>
</h4>
</div>
<div class="modal-body token-dialog-body">
<div class="alert alert-info">The docker <u>username</u> is <b>{{ username }}</b> and the <u>password</u> is the token below. You may use any value for email.</div>
<div class="well well-sm">
<input id="token-view" class="token-view" type="text" value="{{ token }}" onClick="this.select();" readonly>
</div>
</div>
<div class="modal-footer">
<span class="download-cfg" ng-show="isDownloadSupported()">
<i class="fa fa-download"></i>
<a href="javascript:void(0)" ng-click="downloadCfg(shownRobot)">Download .dockercfg file</a>
</span>
<div id="clipboardCopied" style="display: none">
Copied to clipboard
</div>
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->

View file

@ -0,0 +1,5 @@
<button class="btn btn-success" data-trigger="click" bs-popover="'static/directives/popup-input-dialog.html'"
data-placement="bottom" ng-click="popupShown()">
<span ng-transclude></span>
</button>

View file

@ -0,0 +1,4 @@
<form name="popupinput" ng-submit="inputSubmit(); hide()" novalidate>
<input id="input-box" type="text form-control" placeholder="{{ placeholder }}" ng-blur="hide()"
ng-pattern="getRegexp(pattern)" ng-model="inputValue" ng-trim="false" ng-minlength="2" required>
</form>

View file

@ -1,10 +1,13 @@
<div class="robots-manager-element">
<i class="fa fa-spinner fa-spin fa-3x" ng-show="loading"></i>
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to tokens</div>
<div class="alert alert-info">Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage</div>
<div class="container" ng-show="!loading">
<div class="side-controls">
<button class="btn btn-success"><i class="fa fa-wrench"></i> Create Robot Account</button>
<div class="side-controls">
<span class="popup-input-button" pattern="'^[a-zA-Z][a-zA-Z0-9]+$'" placeholder="'Robot Account Name'"
submitted="createRobot(value)">
<i class="fa fa-wrench"></i> Create Robot Account
</span>
</div>
<table class="table">
@ -16,7 +19,9 @@
<tr ng-repeat="robotInfo in robots">
<td class="robot">
<i class="fa fa-wrench"></i>
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }}
<a ng-click="showRobot(robotInfo)">
<span class="prefix">{{ getPrefix(robotInfo.name) }}+</span>{{ getShortenedName(robotInfo.name) }}
</a>
</td>
<td>
<span class="delete-ui" tabindex="0">
@ -26,6 +31,10 @@
</td>
</tr>
</table>
</div>
<div class="docker-auth-dialog" username="shownRobot.name" token="shownRobot.token"
shown="!!shownRobot" counter="showRobotCounter">
<i class="fa fa-wrench"></i> {{ shownRobot.name }}
</div>
</div>

View file

@ -550,7 +550,7 @@ quayApp.directive('plansTable', function () {
priority: 0,
templateUrl: '/static/directives/plans-table.html',
replace: false,
transclude: true,
transclude: false,
restrict: 'C',
scope: {
'plans': '=plans',
@ -566,13 +566,65 @@ quayApp.directive('plansTable', function () {
});
quayApp.directive('dockerAuthDialog', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/docker-auth-dialog.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'username': '=username',
'token': '=token',
'shown': '=shown',
'counter': '=counter'
},
controller: function($scope, $element, Restangular) {
$scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){}
return false;
};
$scope.downloadCfg = function() {
var auth = $.base64.encode($scope.username + ":" + $scope.token);
config = {
"https://quay.io/v1/": {
"auth": auth,
"email": ""
}
};
var file = JSON.stringify(config, null, ' ');
var blob = new Blob([file]);
saveAs(blob, '.dockercfg');
};
var show = function(r) {
if (!$scope.shown || !$scope.username || !$scope.token) {
$('#dockerauthmodal').modal('hide');
return;
}
$('#copyClipboard').clipboardCopy();
$('#dockerauthmodal').modal({});
};
$scope.$watch('counter', show);
$scope.$watch('shown', show);
$scope.$watch('username', show);
$scope.$watch('token', show);
}
};
return directiveDefinitionObject;
});
quayApp.directive('robotsManager', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/robots-manager.html',
replace: false,
transclude: true,
transclude: false,
restrict: 'C',
scope: {
'organization': '=organization',
@ -581,6 +633,13 @@ quayApp.directive('robotsManager', function () {
controller: function($scope, $element, Restangular) {
$scope.robots = null;
$scope.loading = false;
$scope.shownRobot = null;
$scope.showRobotCounter = 0;
$scope.showRobot = function(info) {
$scope.shownRobot = info;
$scope.showRobotCounter++;
};
$scope.getShortenedName = function(name) {
var plus = name.indexOf('+');
@ -592,6 +651,28 @@ quayApp.directive('robotsManager', function () {
return name.substr(0, plus);
};
$scope.createRobot = function(name) {
if (!name) { return; }
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', name) :
getRestUrl('user/robots', name);
var createRobot = Restangular.one(url);
createRobot.customPUT().then(function(resp) {
$scope.robots.push(resp);
}, function(resp) {
bootbox.dialog({
"message": resp.data ? resp.data : 'The robot account could not be created',
"title": "Cannot create robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
$scope.deleteRobot = function(info) {
var shortName = $scope.getShortenedName(info.name);
var url = $scope.organization ? getRestUrl('organization', $scope.organization.name, 'robots', shortName) :
@ -601,12 +682,21 @@ quayApp.directive('robotsManager', function () {
deleteRobot.customDELETE().then(function(resp) {
for (var i = 0; i < $scope.robots.length; ++i) {
if ($scope.robots[i].name == info.name) {
$scope.robots.slice(i, 1);
$scope.robots.splice(i, 1);
return;
}
}
}, function() {
bootbox.dialog({
"message": 'The selected robot account could not be deleted',
"title": "Cannot delete robot account",
"buttons": {
"close": {
"label": "Close",
"className": "btn-primary"
}
}
});
});
};
@ -631,6 +721,52 @@ quayApp.directive('robotsManager', function () {
});
quayApp.directive('popupInputButton', function () {
var directiveDefinitionObject = {
priority: 0,
templateUrl: '/static/directives/popup-input-button.html',
replace: false,
transclude: true,
restrict: 'C',
scope: {
'placeholder': '=placeholder',
'pattern': '=pattern',
'submitted': '&submitted'
},
controller: function($scope, $element) {
$scope.popupShown = function() {
setTimeout(function() {
var box = $('#input-box');
box[0].value = '';
box.focus();
}, 10);
};
$scope.getRegexp = function(pattern) {
if (!pattern) {
pattern = '.*';
}
return new RegExp(pattern);
};
$scope.inputSubmit = function() {
var box = $('#input-box');
if (box.hasClass('ng-invalid')) { return; }
var entered = box[0].value;
if (!entered) {
return;
}
if ($scope.submitted) {
$scope.submitted({'value': entered});
}
};
}
};
return directiveDefinitionObject;
});
quayApp.directive('organizationHeader', function () {
var directiveDefinitionObject = {

View file

@ -422,30 +422,19 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
'html': true
});
$('#copyClipboard').clipboardCopy();
var namespace = $routeParams.namespace;
var name = $routeParams.name;
$scope.permissions = {'team': [], 'user': []};
$scope.isDownloadSupported = function() {
try { return !!new Blob(); } catch(e){}
return false;
$scope.getPrefix = function(name) {
var plus = name.indexOf('+');
return name.substr(0, plus + 1);
};
$scope.downloadCfg = function(token) {
var auth = $.base64.encode("$token:" + token.code);
config = {
"https://quay.io/v1/": {
"auth": auth,
"email": ""
}
};
var file = JSON.stringify(config, null, ' ');
var blob = new Blob([file]);
saveAs(blob, '.dockercfg');
$scope.getShortenedName = function(name) {
var plus = name.indexOf('+');
return name.substr(plus + 1);
};
$scope.grantRole = function() {
@ -554,9 +543,11 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) {
});
};
$scope.shownTokenCounter = 0;
$scope.showToken = function(tokenCode) {
$scope.shownToken = $scope.tokens[tokenCode];
$('#tokenmodal').modal({});
$scope.shownTokenCounter++;
};
$scope.askChangeAccess = function(newAccess) {
@ -1103,17 +1094,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) {
});
};
$scope.createTeamShown = function() {
setTimeout(function() {
$('#create-team-box').focus();
}, 10);
};
$scope.createTeam = function() {
var box = $('#create-team-box');
if (box.hasClass('ng-invalid')) { return; }
var teamname = box[0].value.toLowerCase();
$scope.createTeam = function(teamname) {
if (!teamname) {
return;
}

View file

@ -1,3 +0,0 @@
<form name="newteamform" ng-submit="createTeam(); hide()" novalidate>
<input id="create-team-box" type="text form-control" placeholder="Team Name" ng-blur="hide()" ng-pattern="/^[a-zA-Z][a-zA-Z0-9]+$/" ng-model="newTeamName" ng-trim="false" ng-minlength="2" required>
</form>

View file

@ -9,7 +9,12 @@
<div class="org-view container" ng-show="!loading && organization">
<div class="organization-header" organization="organization">
<div class="header-buttons" ng-show="organization.is_admin">
<button class="btn btn-success" data-trigger="click" bs-popover="'static/partials/create-team-dialog.html'" data-placement="bottom" ng-click="createTeamShown()"><i class="fa fa-group"></i> Create Team</button>
<span class="popup-input-button" pattern="'^[a-zA-Z][a-zA-Z0-9]+$'" placeholder="'Team Name'"
submitted="createTeam(value)">
<i class="fa fa-group"></i> Create Team
</span>
<a class="btn btn-default" href="/organization/{{ organization.name }}/admin"><i class="fa fa-gear"></i> Settings</a>
</div>
</div>

View file

@ -34,16 +34,16 @@
<div id="permissions" class="tab-pane active">
<!-- User Access Permissions -->
<div class="panel panel-default">
<div class="panel-heading">User <span ng-show="repo.is_organization">and Team</span> Access Permissions
<div class="panel-heading">Access Permissions
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users or teams to read, write or administer this repository"></i>
<i class="info-icon fa fa-info-circle" data-placement="left" data-content="Allow any number of users, robot accounts or teams to read, write or administer this repository"></i>
</div>
<div class="panel-body">
<table class="permissions">
<thead>
<tr>
<td>User<span ng-show="repo.is_organization">/Team</span></td>
<td style="min-width: 400px;">User<span ng-show="repo.is_organization">/Team</span>/Robot Account</td>
<td>Permissions</td>
<td style="width: 95px;"></td>
</tr>
@ -52,7 +52,7 @@
<!-- Team Permissions -->
<tr ng-repeat="(name, permission) in permissions['team']">
<td class="team entity">
<i class="fa fa-group"></i>
<i class="fa fa-group" title="Team" bs-tooltip="tooltip.title"></i>
<span><a href="/organization/{{ repo.namespace }}/teams/{{ name }}">{{name}}</a></span>
</td>
<td class="user-permissions">
@ -69,9 +69,9 @@
<!-- User Permissions -->
<tr ng-repeat="(name, permission) in permissions['user']">
<td class="{{ 'user entity ' + (permission.is_org_member ? '' : 'outside') }}">
<i class="fa fa-user" ng-show="!permission.is_robot"></i>
<i class="fa fa-wrench" ng-show="permission.is_robot"></i>
<span>{{name}}</span>
<i class="fa fa-user" ng-show="!permission.is_robot" title="User" bs-tooltip="tooltip.title"></i>
<i class="fa fa-wrench" ng-show="permission.is_robot" title="Robot Account" bs-tooltip="tooltip.title"></i>
<span class="prefix">{{getPrefix(name)}}</span><span>{{getShortenedName(name)}}</span>
<i class="fa fa-exclamation-triangle" ng-show="permission.is_org_member === false" data-trigger="hover" bs-popover="{'content': 'This user is not a member of the organization'}"></i>
</td>
<td class="user-permissions">
@ -88,7 +88,7 @@
</tr>
<tr>
<td colspan="2">
<td colspan="2" class="admin-search">
<span class="entity-search" namespace="repo.namespace" include-teams="true" input-title="'Add a ' + (repo.is_organization ? 'team or ' : '') + 'user...'" entity-selected="addNewPermission"></span>
</td>
</tr>
@ -107,7 +107,7 @@
<table class="permissions">
<thead>
<tr>
<td>Token Description</td>
<td style="min-width: 400px;">Token Description</td>
<td>Permissions</td>
<td></td>
</tr>
@ -133,10 +133,10 @@
</tr>
<tr>
<td>
<td class="admin-search">
<input type="text" class="form-control" placeholder="New token description" ng-model="newToken.friendlyName"required>
</td>
<td>
<td class="admin-search">
<button type="submit" ng-disabled="createTokenForm.$invalid" class="btn btn-sm btn-default">Create</button>
</td>
</tr>
@ -238,6 +238,10 @@
</div>
</div>
<div class="docker-auth-dialog" username="shownToken.friendlyName" token="shownToken.code"
shown="!!shownToken" counter="shownTokenCounter">
<i class="fa fa-key"></i> {{ shownToken.friendlyName }}
</div>
<!-- Modal message dialog -->
<div class="modal fade" id="cannotchangeModal">
@ -257,36 +261,6 @@
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="tokenmodal">
<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"><i class="fa fa-key"></i> {{ shownToken.friendlyName }}</h4>
</div>
<div class="modal-body token-dialog-body">
<div class="alert alert-info">The docker <u>username</u> is <b>$token</b> and the <u>password</u> is the token. You may use any value for email.</div>
<div class="well well-sm">
<input id="token-view" class="token-view" type="text" value="{{ shownToken.code }}" onClick="this.select();" readonly>
</div>
</div>
<div class="modal-footer">
<span class="download-cfg" ng-show="isDownloadSupported()">
<i class="icon-download"></i>
<a href="javascript:void(0)" ng-click="downloadCfg(shownToken)">Download .dockercfg file</a>
</span>
<div id="clipboardCopied" style="display: none">
Copied to clipboard
</div>
<button id="copyClipboard" type="button" class="btn btn-primary" data-clipboard-target="token-view">Copy to clipboard</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="makepublicModal">
<div class="modal-dialog">