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