diff --git a/data/model.py b/data/model.py index 7dbf578a5..94c71e67e 100644 --- a/data/model.py +++ b/data/model.py @@ -348,6 +348,12 @@ def get_organization_team(orgname, teamname): return result[0] +def get_organization_members_with_teams(organization): + joined = TeamMember.select().annotate(Team).annotate(User) + query = joined.where(Team.organization == organization) + return query + + def get_organization_team_members(teamid): joined = User.select().join(TeamMember).join(Team) query = joined.where(Team.id == teamid) diff --git a/endpoints/api.py b/endpoints/api.py index 36a32053f..5a760137e 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -307,6 +307,31 @@ def get_organization(orgname): abort(403) +@app.route('/api/organization/<orgname>/members', methods=['GET']) +@api_login_required +def get_organization_members(orgname): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + abort(404) + + # Loop to create the members dictionary. Note that the members collection + # will return an entry for *every team* a member is on, so we will have + # duplicate keys (which is why we pre-build the dictionary). + members_dict = {} + members = model.get_organization_members_with_teams(org) + for member in members: + if not member.user.username in members_dict: + members_dict[member.user.username] = {'username': member.user.username, 'teams': []} + + members_dict[member.user.username]['teams'].append(member.team.name) + + return jsonify({'members': members_dict}) + + abort(403) + @app.route('/api/organization/<orgname>/private', methods=['GET']) @api_login_required def get_organization_private_allowed(orgname): diff --git a/static/css/quay.css b/static/css/quay.css index c51a6c320..0fead2172 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -1514,7 +1514,6 @@ p.editable:hover i { 100% { background-color: rgba(92, 184, 92, 0.36); } } - .org-view .team-title { font-size: 20px; text-transform: capitalize; @@ -1533,6 +1532,33 @@ p.editable:hover i { padding: 6px; } +.org-admin .team-link { + display: inline-block; + text-transform: capitalize; + margin-right: 20px; +} + +.org-admin #members table td { + font-size: 16px; +} + +.org-admin #members table i { + margin-right: 4px; +} + +.org-admin #members .side-controls { + float: right; +} + +.org-admin #members .result-count { + display: inline-block; + margin-right: 10px; +} + +.org-admin #members .filter-input { + display: inline-block; +} + .plan-manager-element .plans-table thead td { color: #aaa; font-weight: bold; diff --git a/static/js/controllers.js b/static/js/controllers.js index ada86a872..87b6bd220 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1109,7 +1109,28 @@ function OrgAdminCtrl($rootScope, $scope, Restangular, $routeParams, UserService }); var orgname = $routeParams.orgname; + $scope.orgname = orgname; + $scope.membersLoading = true; + $scope.membersFound = null; + + $scope.loadMembers = function() { + if ($scope.membersFound) { return; } + $scope.membersLoading = true; + + var getMembers = Restangular.one(getRestUrl('organization', orgname, 'members')); + getMembers.get().then(function(resp) { + var membersArray = []; + for (var key in resp.members) { + if (resp.members.hasOwnProperty(key)) { + membersArray.push(resp.members[key]); + } + } + + $scope.membersFound = membersArray; + $scope.membersLoading = false; + }); + }; var loadOrganization = function() { var getOrganization = Restangular.one(getRestUrl('organization', orgname)); diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 4f1a77210..c9811e82a 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -14,7 +14,7 @@ <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="#plan">Plan and Usage</a></li> - <li><a href="javascript:void(0)" data-toggle="tab" data-target="#members">Members</a></li> + <li><a href="javascript:void(0)" data-toggle="tab" data-target="#members" ng-click="loadMembers()">Members</a></li> </ul> </div> @@ -28,7 +28,38 @@ <!-- Members tab --> <div id="members" class="tab-pane"> - members + <i class="fa fa-spinner fa-spin fa-3x" ng-show="membersLoading"></i> + + <div ng-show="!membersLoading"> + <div class="side-controls"> + <div class="result-count"> + Showing {{(membersFound | filter:search | limitTo:50).length}} of {{(membersFound | filter:search).length}} matching members + </div> + <div class="filter-input"> + <input id="member-filter" class="form-control" placeholder="Filter Members" type="text" ng-model="search.$"> + </div> + </div> + + <table class="table table-striped"> + <thead> + <th>User</th> + <th>Teams</th> + </thead> + + <tr ng-repeat="memberInfo in (membersFound | filter:search | limitTo:50)"> + <td> + <i class="fa fa-user"></i> + {{ memberInfo.username }} + </td> + <td> + <span class="team-link" ng-repeat="team in memberInfo.teams"> + <i class="fa fa-group"></i> + <a href="/organization/{{ organization.name }}/teams/{{ team }}">{{ team }}</a> + </span> + </td> + </tr> + </table> + </div> </div> </div> </div>