Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
8917812efa
8 changed files with 139 additions and 12 deletions
|
@ -459,12 +459,13 @@ def get_organization_team(orgname, teamname):
|
||||||
return result[0]
|
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)
|
joined = TeamMember.select().annotate(Team).annotate(User)
|
||||||
query = joined.where(Team.organization == organization)
|
query = joined.where(Team.organization == organization)
|
||||||
|
if membername:
|
||||||
|
query = query.where(User.username == membername)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
def get_organization_team_members(teamid):
|
def get_organization_team_members(teamid):
|
||||||
joined = User.select().join(TeamMember).join(Team)
|
joined = User.select().join(TeamMember).join(Team)
|
||||||
query = joined.where(Team.id == teamid)
|
query = joined.where(Team.id == teamid)
|
||||||
|
@ -1058,13 +1059,15 @@ def delete_webhook(namespace_name, repository_name, public_id):
|
||||||
webhook.delete_instance()
|
webhook.delete_instance()
|
||||||
return webhook
|
return webhook
|
||||||
|
|
||||||
def list_logs(user_or_organization_name, repository = None):
|
def list_logs(user_or_organization_name, start_time, performer = None, repository = None):
|
||||||
week_ago = datetime.today() - timedelta(7) # One week
|
|
||||||
joined = LogEntry.select().join(User)
|
joined = LogEntry.select().join(User)
|
||||||
if repository:
|
if repository:
|
||||||
joined = joined.where(LogEntry.repository == 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,
|
def log_action(kind_name, user_or_organization_name, performer=None, repository=None,
|
||||||
access_token=None, ip=None, metadata={}, timestamp=None):
|
access_token=None, ip=None, metadata={}, timestamp=None):
|
||||||
|
|
|
@ -28,7 +28,7 @@ from auth.permissions import (ReadRepositoryPermission,
|
||||||
from endpoints import registry
|
from endpoints import registry
|
||||||
from endpoints.web import common_login
|
from endpoints.web import common_login
|
||||||
from util.cache import cache_control
|
from util.cache import cache_control
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
store = app.config['STORAGE']
|
store = app.config['STORAGE']
|
||||||
user_files = app.config['USERFILES']
|
user_files = app.config['USERFILES']
|
||||||
|
@ -444,6 +444,35 @@ def get_organization_members(orgname):
|
||||||
|
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/organization/<orgname>/members/<membername>', 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/<orgname>/private', methods=['GET'])
|
@app.route('/api/organization/<orgname>/private', methods=['GET'])
|
||||||
@api_login_required
|
@api_login_required
|
||||||
def get_organization_private_allowed(orgname):
|
def get_organization_private_allowed(orgname):
|
||||||
|
@ -1727,8 +1756,15 @@ def log_view(log):
|
||||||
def org_logs_api(orgname):
|
def org_logs_api(orgname):
|
||||||
permission = AdministerOrganizationPermission(orgname)
|
permission = AdministerOrganizationPermission(orgname)
|
||||||
if permission.can():
|
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({
|
return jsonify({
|
||||||
|
'start_time': week_ago,
|
||||||
'logs': [log_view(log) for log in logs]
|
'logs': [log_view(log) for log in logs]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ html, body {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
i.toggle-icon:hover {
|
i.toggle-icon:hover {
|
||||||
|
|
|
@ -4,10 +4,17 @@
|
||||||
</div>
|
</div>
|
||||||
<div ng-show="!loading">
|
<div ng-show="!loading">
|
||||||
<div class="container header">
|
<div class="container header">
|
||||||
<span class="header-text">Usage Logs <span class="mini">For the last seven days</span></span>
|
<span class="header-text">
|
||||||
|
<span ng-show="!performer">Usage Logs</span>
|
||||||
|
<span class="entity-reference" name="performer.username" isrobot="performer.is_robot" ng-show="performer"></span>
|
||||||
|
<span class="mini">For the last seven days</span>
|
||||||
|
</span>
|
||||||
<span class="right">
|
<span class="right">
|
||||||
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
|
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
|
||||||
ng-click="toggleChart()" title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
ng-click="toggleChart()" title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||||
|
<a href="{{ logsPath }}" download="usage-log.json" target="_new">
|
||||||
|
<i class="fa fa-download toggle-icon" title="Download Logs" bs-tooltip="tooltip.title"></i>
|
||||||
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -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', {templateUrl: '/static/partials/org-view.html', controller: OrgViewCtrl}).
|
||||||
when('/organization/:orgname/admin', {templateUrl: '/static/partials/org-admin.html', controller: OrgAdminCtrl}).
|
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/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('/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}).
|
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
|
@ -679,13 +680,15 @@ quayApp.directive('logsView', function () {
|
||||||
'organization': '=organization',
|
'organization': '=organization',
|
||||||
'user': '=user',
|
'user': '=user',
|
||||||
'visible': '=visible',
|
'visible': '=visible',
|
||||||
'repository': '=repository'
|
'repository': '=repository',
|
||||||
|
'performer': '=performer'
|
||||||
},
|
},
|
||||||
controller: function($scope, $element, $sce, Restangular) {
|
controller: function($scope, $element, $sce, Restangular) {
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.logs = null;
|
$scope.logs = null;
|
||||||
$scope.kindsAllowed = null;
|
$scope.kindsAllowed = null;
|
||||||
$scope.chartVisible = true;
|
$scope.chartVisible = true;
|
||||||
|
$scope.logsPath = '';
|
||||||
|
|
||||||
var logDescriptions = {
|
var logDescriptions = {
|
||||||
'account_change_plan': 'Change plan',
|
'account_change_plan': 'Change plan',
|
||||||
|
@ -781,9 +784,15 @@ quayApp.directive('logsView', function () {
|
||||||
if ($scope.repository) {
|
if ($scope.repository) {
|
||||||
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
url = getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($scope.performer) {
|
||||||
|
url += '?performer=' + encodeURIComponent($scope.performer.username);
|
||||||
|
}
|
||||||
|
|
||||||
var loadLogs = Restangular.one(url);
|
var loadLogs = Restangular.one(url);
|
||||||
loadLogs.customGET().then(function(resp) {
|
loadLogs.customGET().then(function(resp) {
|
||||||
|
$scope.logsPath = '/api/' + url;
|
||||||
|
|
||||||
if (!$scope.chart) {
|
if (!$scope.chart) {
|
||||||
$scope.chart = new LogUsageChart(logKinds);
|
$scope.chart = new LogUsageChart(logKinds);
|
||||||
$($scope.chart).bind('filteringChanged', function(e) {
|
$($scope.chart).bind('filteringChanged', function(e) {
|
||||||
|
@ -848,6 +857,7 @@ quayApp.directive('logsView', function () {
|
||||||
$scope.$watch('user', update);
|
$scope.$watch('user', update);
|
||||||
$scope.$watch('repository', update);
|
$scope.$watch('repository', update);
|
||||||
$scope.$watch('visible', update);
|
$scope.$watch('visible', update);
|
||||||
|
$scope.$watch('performer', update);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1318,7 +1318,7 @@ function NewOrgCtrl($scope, $routeParams, $timeout, $location, UserService, Plan
|
||||||
$scope.loading = false;
|
$scope.loading = false;
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
requested = $routeParams['plan'];
|
requested = $routeParams['plan'];
|
||||||
|
|
||||||
// Load the list of plans.
|
// Load the list of plans.
|
||||||
PlanService.getPlans(function(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();
|
||||||
}
|
}
|
|
@ -121,8 +121,9 @@
|
||||||
<thead>
|
<thead>
|
||||||
<th>User/Robot Account</th>
|
<th>User/Robot Account</th>
|
||||||
<th>Teams</th>
|
<th>Teams</th>
|
||||||
|
<th></th>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tr ng-repeat="memberInfo in (membersFound | filter:search | limitTo:50)">
|
<tr ng-repeat="memberInfo in (membersFound | filter:search | limitTo:50)">
|
||||||
<td>
|
<td>
|
||||||
<span class="entity-reference" name="memberInfo.username" isrobot="memberInfo.is_robot"></span>
|
<span class="entity-reference" name="memberInfo.username" isrobot="memberInfo.is_robot"></span>
|
||||||
|
@ -133,6 +134,11 @@
|
||||||
<a href="/organization/{{ organization.name }}/teams/{{ team }}">{{ team }}</a>
|
<a href="/organization/{{ organization.name }}/teams/{{ team }}">{{ team }}</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="/organization/{{ organization.name }}/logs/{{ memberInfo.username }}" title="Member Usage Logs" bs-tooltip="tooltip.title">
|
||||||
|
<i class="fa fa-book"></i>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
16
static/partials/org-member-logs.html
Normal file
16
static/partials/org-member-logs.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<div class="org-member-logs container" ng-show="loading">
|
||||||
|
<i class="fa fa-spinner fa-spin fa-3x"></i>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-show="!loading && !organization">
|
||||||
|
Organization not found
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="container" ng-show="!loading && !memberInfo">
|
||||||
|
Member not found
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="org-member-logs container" ng-show="!loading && organization && memberInfo">
|
||||||
|
<div class="organization-header" organization="organization" clickable="true"></div>
|
||||||
|
<div class="logs-view" organization="organization" performer="memberInfo" visible="organization && memberInfo && ready"></div>
|
||||||
|
</div>
|
Reference in a new issue