diff --git a/endpoints/api.py b/endpoints/api.py
index 07c3fd7f8..22300cb52 100644
--- a/endpoints/api.py
+++ b/endpoints/api.py
@@ -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
diff --git a/static/css/quay.css b/static/css/quay.css
index b7d63acc3..62e7cb7a8 100644
--- a/static/css/quay.css
+++ b/static/css/quay.css
@@ -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;
diff --git a/static/directives/docker-auth-dialog.html b/static/directives/docker-auth-dialog.html
new file mode 100644
index 000000000..dcb71a25b
--- /dev/null
+++ b/static/directives/docker-auth-dialog.html
@@ -0,0 +1,30 @@
+
+
-
Robot accounts allow for delegating access in multiple repositories to tokens
+
Robot accounts allow for delegating access in multiple repositories to role-based accounts that you manage
-
-
+
+
+
+
+ {{ shownRobot.name }}
diff --git a/static/js/app.js b/static/js/app.js
index 7561fd2e6..a2d30a5fb 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -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 = {
diff --git a/static/js/controllers.js b/static/js/controllers.js
index 2d6dd6ccc..c3edc3733 100644
--- a/static/js/controllers.js
+++ b/static/js/controllers.js
@@ -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;
}
diff --git a/static/partials/create-team-dialog.html b/static/partials/create-team-dialog.html
deleted file mode 100644
index 41a599189..000000000
--- a/static/partials/create-team-dialog.html
+++ /dev/null
@@ -1,3 +0,0 @@
-
diff --git a/static/partials/org-view.html b/static/partials/org-view.html
index c1011d74f..2d05e1426 100644
--- a/static/partials/org-view.html
+++ b/static/partials/org-view.html
@@ -9,7 +9,12 @@
diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html
index 34f378cb5..df7ff652c 100644
--- a/static/partials/repo-admin.html
+++ b/static/partials/repo-admin.html
@@ -34,16 +34,16 @@
-
User
and Team Access Permissions
+
Access Permissions
-
+
- User/Team |
+ User/Team/Robot Account |
Permissions |
|
@@ -52,7 +52,7 @@
-
+
{{name}}
|
@@ -69,9 +69,9 @@
|
-
-
- {{name}}
+
+
+ {{getPrefix(name)}}{{getShortenedName(name)}}
|
@@ -88,7 +88,7 @@
|
-
+ |
|
@@ -107,7 +107,7 @@
- Token Description |
+ Token Description |
Permissions |
|
@@ -133,10 +133,10 @@
-
+ |
|
-
+ |
|
@@ -238,6 +238,10 @@
+
+ {{ shownToken.friendlyName }}
+
@@ -257,36 +261,6 @@
-
-
-
-
-
-
-
The docker username is $token and the password is the token. You may use any value for email.
-
-
-
-
-
-
-
-
-
-