From 9197a20a77b3770cfac666750db0c4251037f97a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 10 Dec 2013 01:38:05 -0500 Subject: [PATCH 1/3] =?UTF-8?q?Add=20a=20dropdown=20next=20to=20the=20enti?= =?UTF-8?q?ty=20search=20which=20shows=20all=20the=20user=E2=80=99s=20team?= =?UTF-8?q?s=20and=20robot=20accounts,=20and=20lets=20them=20create=20new?= =?UTF-8?q?=20ones=20on=20the=20fly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- endpoints/api.py | 3 +- static/css/quay.css | 33 +++++ static/directives/entity-reference.html | 3 +- static/directives/entity-search.html | 40 ++++++- static/directives/robots-manager.html | 2 +- static/js/app.js | 152 ++++++++++++++++++++---- static/js/controllers.js | 30 ++--- static/partials/org-admin.html | 2 +- static/partials/org-view.html | 2 +- static/partials/repo-admin.html | 6 +- static/partials/team-view.html | 3 +- 11 files changed, 223 insertions(+), 53 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index 786393810..2cd7e208d 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -828,7 +828,8 @@ def get_repo_api(namespace, repository): 'can_admin': can_admin, 'is_public': is_public, 'is_building': len(active_builds) > 0, - 'is_organization': bool(organization) + 'is_organization': bool(organization), + 'is_org_admin': bool(organization) and AdministerOrganizationPermission(namespace).can() }) abort(404) # Not found diff --git a/static/css/quay.css b/static/css/quay.css index 3b13de3b8..ad2472baa 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -3,6 +3,39 @@ margin: 0; } +.entity-search-element input { + vertical-align: middle; +} + +.entity-search-element .twitter-typeahead { + vertical-align: middle; +} + +.entity-search-element .dropdown { + vertical-align: middle; + display: inline-block; + margin-top: 0px; +} + +.dropdown-menu i.fa { + margin-right: 6px; + position: relative; +} + +.dropdown-menu .new-action i.fa { + atext-shadow: 2px 2px 7px #5cb85c, 2px 2px 7px #5cb85c; +} + +.dropdown-menu .new-action i.fa:after { + content: "+"; + color: #5cb85c; + position: absolute; + left: -14px; + top: -2px; + font-weight: bold; + font-size: 115%; +} + #input-box { padding: 4px; font-size: 14px; diff --git a/static/directives/entity-reference.html b/static/directives/entity-reference.html index 9b82b457e..e1380800c 100644 --- a/static/directives/entity-reference.html +++ b/static/directives/entity-reference.html @@ -2,6 +2,7 @@ - {{team}} + {{team}} + {{team}} {{getPrefix(name)}}{{getShortenedName(name)}} diff --git a/static/directives/entity-search.html b/static/directives/entity-search.html index 37e2d1a3f..eddd44916 100644 --- a/static/directives/entity-search.html +++ b/static/directives/entity-search.html @@ -1 +1,39 @@ - + + + diff --git a/static/directives/robots-manager.html b/static/directives/robots-manager.html index ef787a3dd..c478fbb02 100644 --- a/static/directives/robots-manager.html +++ b/static/directives/robots-manager.html @@ -4,7 +4,7 @@
- Create Robot Account diff --git a/static/js/app.js b/static/js/app.js index cdc3ddabf..719bb9953 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1,3 +1,6 @@ +var TEAM_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; +var ROBOT_PATTERN = '^[a-zA-Z][a-zA-Z0-9]+$'; + function getFirstTextLine(commentString) { if (!commentString) { return ''; } @@ -31,6 +34,45 @@ function getFirstTextLine(commentString) { return ''; } +function createRobotAccount(Restangular, is_org, orgname, name, callback) { + var url = is_org ? getRestUrl('organization', orgname, 'robots', name) : + getRestUrl('user/robots', name); + var createRobot = Restangular.one(url); + createRobot.customPUT().then(callback, 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" + } + } + }); + }); +} + +function createOrganizationTeam(Restangular, orgname, teamname, callback) { + var createTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); + var data = { + 'name': teamname, + 'role': 'member' + }; + + createTeam.customPOST(data).then(callback, function() { + bootbox.dialog({ + "message": resp.data ? resp.data : 'The team could not be created', + "title": "Cannot create team", + "buttons": { + "close": { + "label": "Close", + "className": "btn-primary" + } + } + }); + }); +} + function getRestUrl(args) { var url = ''; for (var i = 0; i < arguments.length; ++i) { @@ -474,7 +516,8 @@ quayApp.directive('entityReference', function () { 'name': '=name', 'orgname': '=orgname', 'team': '=team', - 'isrobot': '=isrobot' + 'isrobot': '=isrobot', + 'isorgadmin': '=isorgadmin' }, controller: function($scope, $element) { $scope.getPrefix = function(name) { @@ -905,6 +948,7 @@ quayApp.directive('robotsManager', function () { 'user': '=user' }, controller: function($scope, $element, Restangular) { + $scope.ROBOT_PATTERN = ROBOT_PATTERN; $scope.robots = null; $scope.loading = false; $scope.shownRobot = null; @@ -928,23 +972,10 @@ quayApp.directive('robotsManager', function () { $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" - } - } - }); - }); + createRobotAccount(Restangular, !!$scope.organization, $scope.organization ? $scope.organization.name : '', name, + function(created) { + $scope.robots.push(created); + }); }; $scope.deleteRobot = function(info) { @@ -1218,14 +1249,89 @@ quayApp.directive('entitySearch', function () { 'namespace': '=namespace', 'inputTitle': '=inputTitle', 'entitySelected': '=entitySelected', - 'includeTeams': '=includeTeams' + 'includeTeams': '=includeTeams', + 'isOrganization': '=isOrganization' }, - controller: function($scope, $element) { + controller: function($scope, $element, Restangular) { + $scope.lazyLoading = true; + $scope.isAdmin = false; + + $scope.lazyLoad = function() { + if (!$scope.namespace || !$scope.lazyLoading) { return; } + + if ($scope.isOrganization && $scope.includeTeams) { + var url = getRestUrl('organization', $scope.namespace); + var getOrganization = Restangular.one(url); + getOrganization.customGET().then(function(resp) { + $scope.teams = resp.teams; + }); + } + + var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; + var getRobots = Restangular.one(url); + getRobots.customGET().then(function(resp) { + $scope.robots = resp.robots; + $scope.isAdmin = true; + $scope.lazyLoading = false; + }, function() { + $scope.isAdmin = false; + $scope.lazyLoading = false; + }); + }; + + $scope.createTeam = function() { + bootbox.prompt('Enter the name of the new team', function(teamname) { + if (!teamname) { return; } + + var regex = new RegExp(TEAM_PATTERN); + if (!regex.test(teamname)) { + bootbox.alert('Invalid team name'); + return; + } + + createOrganizationTeam(Restangular, $scope.namespace, teamname, function(created) { + $scope.setEntity(created.name, 'team', false); + $scope.teams[teamname] = created; + }); + }); + }; + + $scope.createRobot = function() { + bootbox.prompt('Enter the name of the new robot account', function(robotname) { + if (!robotname) { return; } + + var regex = new RegExp(ROBOT_PATTERN); + if (!regex.test(robotname)) { + bootbox.alert('Invalid robot account name'); + return; + } + + createRobotAccount(Restangular, $scope.isOrganization, $scope.namespace, robotname, function(created) { + $scope.setEntity(created.name, 'user', true); + $scope.robots.push(created); + }); + }); + }; + + $scope.setEntity = function(name, kind, is_robot) { + var entity = { + 'name': name, + 'kind': kind, + 'is_robot': is_robot + }; + + if ($scope.is_organization) { + entity['is_org_member'] = true; + } + + $scope.entitySelected(entity); + }; + if (!$scope.entitySelected) { return; } number++; - var input = $element[0].firstChild; + var input = $element[0].firstChild.firstChild; $scope.namespace = $scope.namespace || ''; $(input).typeahead({ name: 'entities' + number, @@ -1274,7 +1380,9 @@ quayApp.directive('entitySearch', function () { $(input).on('typeahead:selected', function(e, datum) { $(input).typeahead('setQuery', ''); - $scope.entitySelected(datum.entity); + $scope.$apply(function() { + $scope.entitySelected(datum.entity); + }); }); $scope.$watch('inputTitle', function(title) { diff --git a/static/js/controllers.js b/static/js/controllers.js index b3f8a5b90..00ffe5c85 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -449,11 +449,7 @@ function RepoAdminCtrl($scope, Restangular, $routeParams, $rootScope) { return; } - // Need the $scope.apply for both the permission stuff to change and for - // the XHR call to be made. - $scope.$apply(function() { - $scope.addRole(entity.name, 'read', entity.kind); - }); + $scope.addRole(entity.name, 'read', entity.kind); }; $scope.deleteRole = function(entityName, kind) { @@ -1051,6 +1047,7 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) { 'html': true }); + $scope.TEAM_PATTERN = TEAM_PATTERN; $rootScope.title = 'Loading...'; var orgname = $routeParams.orgname; @@ -1102,15 +1099,8 @@ function OrgViewCtrl($rootScope, $scope, Restangular, $routeParams) { return; } - var createTeam = Restangular.one(getRestUrl('organization', orgname, 'team', teamname)); - var data = { - 'name': teamname, - 'role': 'member' - }; - createTeam.customPOST(data).then(function(resp) { - $scope.organization.teams[teamname] = resp; - }, function() { - $('#cannotChangeTeamModal').modal({}); + createOrganizationTeam(Restangular, orgname, teamname, function(created) { + $scope.organization.teams[teamname] = created; }); }; @@ -1237,13 +1227,11 @@ function TeamViewCtrl($rootScope, $scope, Restangular, $routeParams) { $scope.addNewMember = function(member) { if ($scope.members[member.name]) { return; } - $scope.$apply(function() { - var addMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', member.name)); - addMember.customPOST().then(function(resp) { - $scope.members[member.name] = resp; - }, function() { - $('#cannotChangeMembersModal').modal({}); - }); + var addMember = Restangular.one(getRestUrl('organization', $scope.orgname, 'team', teamname, 'members', member.name)); + addMember.customPOST().then(function(resp) { + $scope.members[member.name] = resp; + }, function() { + $('#cannotChangeMembersModal').modal({}); }); }; diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 7a8bcdb3e..17bdf56df 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -126,7 +126,7 @@ - + diff --git a/static/partials/org-view.html b/static/partials/org-view.html index 2d05e1426..2a6deeb52 100644 --- a/static/partials/org-view.html +++ b/static/partials/org-view.html @@ -10,7 +10,7 @@
- Create Team diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index b48938b2f..9ca4e8686 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -58,7 +58,7 @@ - + @@ -74,7 +74,7 @@ - + @@ -92,7 +92,7 @@ - + diff --git a/static/partials/team-view.html b/static/partials/team-view.html index 9e97d21f5..1c52dfb35 100644 --- a/static/partials/team-view.html +++ b/static/partials/team-view.html @@ -32,7 +32,8 @@ - + From df1500b6d04e0496bd7952b30df01d9662acd035 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 10 Dec 2013 15:22:22 -0500 Subject: [PATCH 2/3] =?UTF-8?q?Switch=20to=20using=20the=20UserService?= =?UTF-8?q?=E2=80=99s=20cache=20of=20org=20information=20for=20whether=20a?= =?UTF-8?q?=20user=20is=20an=20admin=20of=20a=20namespace/org?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- endpoints/api.py | 3 +- static/directives/entity-reference.html | 4 +-- static/js/app.js | 44 ++++++++++++++++++------- static/partials/repo-admin.html | 4 +-- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index 2cd7e208d..786393810 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -828,8 +828,7 @@ def get_repo_api(namespace, repository): 'can_admin': can_admin, 'is_public': is_public, 'is_building': len(active_builds) > 0, - 'is_organization': bool(organization), - 'is_org_admin': bool(organization) and AdministerOrganizationPermission(namespace).can() + 'is_organization': bool(organization) }) abort(404) # Not found diff --git a/static/directives/entity-reference.html b/static/directives/entity-reference.html index e1380800c..509f4b03c 100644 --- a/static/directives/entity-reference.html +++ b/static/directives/entity-reference.html @@ -2,7 +2,7 @@ - {{team}} - {{team}} + {{team}} + {{team}} {{getPrefix(name)}}{{getShortenedName(name)}} diff --git a/static/js/app.js b/static/js/app.js index 719bb9953..4105e206b 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -137,6 +137,19 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an return null; }; + userService.isNamespaceAdmin = function(namespace) { + if (namespace == userResponse.username) { + return true; + } + + var org = userService.getOrganization(namespace); + if (!org) { + return false; + } + + return org.is_org_admin; + }; + userService.currentUser = function() { return userResponse; }; @@ -516,10 +529,13 @@ quayApp.directive('entityReference', function () { 'name': '=name', 'orgname': '=orgname', 'team': '=team', - 'isrobot': '=isrobot', - 'isorgadmin': '=isorgadmin' + 'isrobot': '=isrobot' }, - controller: function($scope, $element) { + controller: function($scope, $element, UserService) { + $scope.getIsAdmin = function(orgname) { + return UserService.isNamespaceAdmin(orgname); + }; + $scope.getPrefix = function(name) { if (!name) { return ''; } var plus = name.indexOf('+'); @@ -1252,7 +1268,7 @@ quayApp.directive('entitySearch', function () { 'includeTeams': '=includeTeams', 'isOrganization': '=isOrganization' }, - controller: function($scope, $element, Restangular) { + controller: function($scope, $element, Restangular, UserService) { $scope.lazyLoading = true; $scope.isAdmin = false; @@ -1267,16 +1283,20 @@ quayApp.directive('entitySearch', function () { }); } - var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; - var getRobots = Restangular.one(url); - getRobots.customGET().then(function(resp) { - $scope.robots = resp.robots; + if (UserService.isNamespaceAdmin($scope.namespace)) { $scope.isAdmin = true; + + var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; + var getRobots = Restangular.one(url); + getRobots.customGET().then(function(resp) { + $scope.robots = resp.robots; + $scope.lazyLoading = false; + }, function() { + $scope.lazyLoading = false; + }); + } else { $scope.lazyLoading = false; - }, function() { - $scope.isAdmin = false; - $scope.lazyLoading = false; - }); + } }; $scope.createTeam = function() { diff --git a/static/partials/repo-admin.html b/static/partials/repo-admin.html index 9ca4e8686..50adf5744 100644 --- a/static/partials/repo-admin.html +++ b/static/partials/repo-admin.html @@ -58,7 +58,7 @@ - + @@ -74,7 +74,7 @@ - + From 3302b58cc35b3b8492f74cbde175ecba93ee958a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Tue, 10 Dec 2013 15:49:34 -0500 Subject: [PATCH 3/3] =?UTF-8?q?Switch=20to=20using=20the=20UserService?= =?UTF-8?q?=E2=80=99s=20cache=20of=20org=20information=20for=20whether=20a?= =?UTF-8?q?=20user=20is=20an=20admin=20of=20a=20namespace/org=20(#2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- endpoints/api.py | 2 +- static/directives/entity-search.html | 5 +++-- static/js/app.js | 26 +++++++++++++------------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/endpoints/api.py b/endpoints/api.py index 786393810..ed85e6e35 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -1658,7 +1658,7 @@ def get_user_robots(): @app.route('/api/organization//robots', methods=['GET']) @api_login_required def get_org_robots(orgname): - permission = AdministerOrganizationPermission(orgname) + permission = OrganizationMemberPermission(orgname) if permission.can(): robots = model.list_entity_robots(orgname) return jsonify({ diff --git a/static/directives/entity-search.html b/static/directives/entity-search.html index eddd44916..fb4a6cf24 100644 --- a/static/directives/entity-search.html +++ b/static/directives/entity-search.html @@ -14,7 +14,7 @@ - +
  • @@ -22,7 +22,8 @@
  • - + +
  • Create team diff --git a/static/js/app.js b/static/js/app.js index 4105e206b..597bd5845 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -1275,6 +1275,8 @@ quayApp.directive('entitySearch', function () { $scope.lazyLoad = function() { if (!$scope.namespace || !$scope.lazyLoading) { return; } + $scope.isAdmin = UserService.isNamespaceAdmin($scope.namespace); + if ($scope.isOrganization && $scope.includeTeams) { var url = getRestUrl('organization', $scope.namespace); var getOrganization = Restangular.one(url); @@ -1283,23 +1285,19 @@ quayApp.directive('entitySearch', function () { }); } - if (UserService.isNamespaceAdmin($scope.namespace)) { - $scope.isAdmin = true; - - var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; - var getRobots = Restangular.one(url); - getRobots.customGET().then(function(resp) { - $scope.robots = resp.robots; - $scope.lazyLoading = false; - }, function() { - $scope.lazyLoading = false; - }); - } else { + var url = $scope.isOrganization ? getRestUrl('organization', $scope.namespace, 'robots') : 'user/robots'; + var getRobots = Restangular.one(url); + getRobots.customGET().then(function(resp) { + $scope.robots = resp.robots; $scope.lazyLoading = false; - } + }, function() { + $scope.lazyLoading = false; + }); }; $scope.createTeam = function() { + if (!$scope.isAdmin) { return; } + bootbox.prompt('Enter the name of the new team', function(teamname) { if (!teamname) { return; } @@ -1317,6 +1315,8 @@ quayApp.directive('entitySearch', function () { }; $scope.createRobot = function() { + if (!$scope.isAdmin) { return; } + bootbox.prompt('Enter the name of the new robot account', function(robotname) { if (!robotname) { return; }