diff --git a/data/model.py b/data/model.py index f95fe5a2a..f95abb8c5 100644 --- a/data/model.py +++ b/data/model.py @@ -459,12 +459,13 @@ def get_organization_team(orgname, teamname): return result[0] -def get_organization_members_with_teams(organization): +def get_organization_members_with_teams(organization, membername = None): joined = TeamMember.select().annotate(Team).annotate(User) query = joined.where(Team.organization == organization) + if membername: + query = query.where(User.username == membername) return query - def get_organization_team_members(teamid): joined = User.select().join(TeamMember).join(Team) query = joined.where(Team.id == teamid) @@ -1058,13 +1059,15 @@ def delete_webhook(namespace_name, repository_name, public_id): webhook.delete_instance() return webhook -def list_logs(user_or_organization_name, repository = None): - week_ago = datetime.today() - timedelta(7) # One week +def list_logs(user_or_organization_name, start_time, performer = None, repository = None): joined = LogEntry.select().join(User) if repository: joined = joined.where(LogEntry.repository == repository) - return joined.where(User.username == user_or_organization_name, LogEntry.datetime >= week_ago).order_by(LogEntry.datetime.desc()) + if performer: + joined = joined.where(LogEntry.performer == performer) + + return joined.where(User.username == user_or_organization_name, LogEntry.datetime >= start_time).order_by(LogEntry.datetime.desc()) def log_action(kind_name, user_or_organization_name, performer=None, repository=None, access_token=None, ip=None, metadata={}, timestamp=None): diff --git a/endpoints/api.py b/endpoints/api.py index 00e405d15..937b9b898 100644 --- a/endpoints/api.py +++ b/endpoints/api.py @@ -28,7 +28,7 @@ from auth.permissions import (ReadRepositoryPermission, from endpoints import registry from endpoints.web import common_login from util.cache import cache_control - +from datetime import datetime, timedelta store = app.config['STORAGE'] user_files = app.config['USERFILES'] @@ -444,6 +444,35 @@ def get_organization_members(orgname): abort(403) + +@app.route('/api/organization//members/', methods=['GET']) +@api_login_required +def get_organization_member(orgname, membername): + permission = AdministerOrganizationPermission(orgname) + if permission.can(): + try: + org = model.get_organization(orgname) + except model.InvalidOrganizationException: + abort(404) + + member_dict = None + member_teams = model.get_organization_members_with_teams(org, membername = membername) + for member in member_teams: + if not member_dict: + member_dict = {'username': member.user.username, + 'is_robot': member.user.robot, + 'teams': []} + + member_dict['teams'].append(member.team.name) + + if not member_dict: + abort(404) + + return jsonify({'member': member_dict}) + + abort(403) + + @app.route('/api/organization//private', methods=['GET']) @api_login_required def get_organization_private_allowed(orgname): @@ -1727,8 +1756,15 @@ def log_view(log): def org_logs_api(orgname): permission = AdministerOrganizationPermission(orgname) if permission.can(): - logs = model.list_logs(orgname) + performer_name = request.args.get('performer', None) + performer = None + if performer_name: + performer = model.get_user(performer_name) + + week_ago = datetime.today() - timedelta(7) # One week + logs = model.list_logs(orgname, week_ago, performer = performer) return jsonify({ + 'start_time': week_ago, 'logs': [log_view(log) for log in logs] }) diff --git a/static/css/quay.css b/static/css/quay.css index 21b3d3f1c..30b70e972 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -25,6 +25,7 @@ html, body { font-size: 22px; padding: 6px; cursor: pointer; + color: black; } i.toggle-icon:hover { diff --git a/static/directives/logs-view.html b/static/directives/logs-view.html index 4f0f88dea..104129543 100644 --- a/static/directives/logs-view.html +++ b/static/directives/logs-view.html @@ -4,10 +4,17 @@
- Usage Logs For the last seven days + + Usage Logs + + For the last seven days + + + +
diff --git a/static/js/app.js b/static/js/app.js index 8b80de5fb..11212cb1a 100644 --- a/static/js/app.js +++ b/static/js/app.js @@ -451,6 +451,7 @@ quayApp = angular.module('quay', ['ngRoute', 'restangular', 'angularMoment', 'an when('/organization/:orgname', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}). when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}). when('/organization/:orgname/teams/:teamname', {templateUrl: '/static/partials/team-view.html', controller: TeamViewCtrl}). + when('/organization/:orgname/logs/:membername', {templateUrl: '/static/partials/org-member-logs.html', controller: OrgMemberLogsCtrl}). when('/v1/', {title: 'Activation information', templateUrl: '/static/partials/v1-page.html', controller: V1Ctrl}). when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}). otherwise({redirectTo: '/'}); @@ -679,13 +680,15 @@ quayApp.directive('logsView', function () { 'organization': '=organization', 'user': '=user', 'visible': '=visible', - 'repository': '=repository' + 'repository': '=repository', + 'performer': '=performer' }, controller: function($scope, $element, $sce, Restangular) { $scope.loading = true; $scope.logs = null; $scope.kindsAllowed = null; $scope.chartVisible = true; + $scope.logsPath = ''; var logDescriptions = { 'account_change_plan': 'Change plan', @@ -781,9 +784,15 @@ quayApp.directive('logsView', function () { if ($scope.repository) { url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs'); } - + + if ($scope.performer) { + url += '?performer=' + encodeURIComponent($scope.performer.username); + } + var loadLogs = Restangular.one(url); loadLogs.customGET().then(function(resp) { + $scope.logsPath = '/api/' + url; + if (!$scope.chart) { $scope.chart = new LogUsageChart(logKinds); $($scope.chart).bind('filteringChanged', function(e) { @@ -848,6 +857,7 @@ quayApp.directive('logsView', function () { $scope.$watch('user', update); $scope.$watch('repository', update); $scope.$watch('visible', update); + $scope.$watch('performer', update); } }; diff --git a/static/js/controllers.js b/static/js/controllers.js index 85e9278a7..b3f8a5b90 100644 --- a/static/js/controllers.js +++ b/static/js/controllers.js @@ -1318,7 +1318,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan $scope.loading = false; }, true); - requested = $routeParams['plan']; + requested = $routeParams['plan']; // Load the list of plans. PlanService.getPlans(function(plans) { @@ -1380,4 +1380,52 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan }); }); }; +} + + +function OrgMemberLogsCtrl($scope, $routeParams, $rootScope, $timeout, Restangular) { + var orgname = $routeParams.orgname; + var membername = $routeParams.membername; + + $scope.orgname = orgname; + $scope.loading = true; + $scope.memberInfo = null; + $scope.ready = false; + + var checkReady = function() { + $scope.loading = !$scope.organization || !$scope.memberInfo; + if (!$scope.loading) { + $rootScope.title = 'Logs for ' + $scope.memberInfo.username + ' (' + $scope.orgname + ')'; + $rootScope.description = 'Shows all the actions of ' + $scope.memberInfo.username + + ' under organization ' + $scope.orgname; + $timeout(function() { + $scope.ready = true; + }); + } + }; + + var loadOrganization = function() { + var getOrganization = Restangular.one(getRestUrl('organization', orgname)) + getOrganization.get().then(function(resp) { + $scope.organization = resp; + checkReady(); + }, function() { + $scope.organization = null; + $scope.loading = false; + }); + }; + + var loadMemberInfo = function() { + var getMemberInfo = Restangular.one(getRestUrl('organization', orgname, 'members', membername)) + getMemberInfo.get().then(function(resp) { + $scope.memberInfo = resp.member; + checkReady(); + }, function() { + $scope.memberInfo = null; + $scope.loading = false; + }); + }; + + loadOrganization(); + loadMemberInfo(); } \ No newline at end of file diff --git a/static/partials/org-admin.html b/static/partials/org-admin.html index 45360ec76..7a8bcdb3e 100644 --- a/static/partials/org-admin.html +++ b/static/partials/org-admin.html @@ -121,8 +121,9 @@ User/Robot Account Teams + - + @@ -133,6 +134,11 @@ {{ team }} + + + + +
diff --git a/static/partials/org-member-logs.html b/static/partials/org-member-logs.html new file mode 100644 index 000000000..7750de305 --- /dev/null +++ b/static/partials/org-member-logs.html @@ -0,0 +1,16 @@ +
+ +
+ +
+ Organization not found +
+ +
+ Member not found +
+ +
+
+
+