Switch to using an aggregated logs query and infinite scrolling
This should allow users to work with large logs set. Fixes #294
This commit is contained in:
parent
572d6ba53c
commit
3d6c92901c
15 changed files with 270 additions and 99 deletions
|
@ -645,6 +645,9 @@ class LogEntry(BaseModel):
|
|||
indexes = (
|
||||
# create an index on repository and date
|
||||
(('repository', 'datetime'), False),
|
||||
|
||||
# create an index on repository, date and kind
|
||||
(('repository', 'datetime', 'kind'), False),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -10,8 +10,8 @@ up_mysql() {
|
|||
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=password -d mysql
|
||||
|
||||
# Sleep for 10s to get MySQL get started.
|
||||
echo 'Sleeping for 10...'
|
||||
sleep 10
|
||||
echo 'Sleeping for 20...'
|
||||
sleep 20
|
||||
|
||||
# Add the database to mysql.
|
||||
docker run --rm --link mysql:mysql mysql sh -c 'echo "create database genschema" | mysql -h"$MYSQL_PORT_3306_TCP_ADDR" -P"$MYSQL_PORT_3306_TCP_PORT" -uroot -ppassword'
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
"""Add LogEntry repo-datetime-kind index
|
||||
|
||||
Revision ID: 5232a5610a0a
|
||||
Revises: 437ee6269a9d
|
||||
Create Date: 2015-07-31 13:25:41.877733
|
||||
|
||||
"""
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '5232a5610a0a'
|
||||
down_revision = '437ee6269a9d'
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
def upgrade(tables):
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_index('logentry_repository_id_datetime_kind_id', 'logentry', ['repository_id', 'datetime', 'kind_id'], unique=False)
|
||||
### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade(tables):
|
||||
### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index('logentry_repository_id_datetime_kind_id', table_name='logentry')
|
||||
### end Alembic commands ###
|
|
@ -5,18 +5,13 @@ from datetime import datetime, timedelta, date
|
|||
|
||||
from data.database import LogEntry, LogEntryKind, User
|
||||
|
||||
|
||||
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
||||
Performer = User.alias()
|
||||
def _logs_query(selections, start_time, end_time, performer=None, repository=None, namespace=None):
|
||||
joined = (LogEntry
|
||||
.select(LogEntry, LogEntryKind, User, Performer)
|
||||
.join(User)
|
||||
.switch(LogEntry)
|
||||
.join(Performer, JOIN_LEFT_OUTER,
|
||||
on=(LogEntry.performer == Performer.id).alias('performer'))
|
||||
.select(*selections)
|
||||
.switch(LogEntry)
|
||||
.join(LogEntryKind)
|
||||
.switch(LogEntry))
|
||||
.switch(LogEntry)
|
||||
.where(LogEntry.datetime >= start_time, LogEntry.datetime < end_time))
|
||||
|
||||
if repository:
|
||||
joined = joined.where(LogEntry.repository == repository)
|
||||
|
@ -25,10 +20,32 @@ def list_logs(start_time, end_time, performer=None, repository=None, namespace=N
|
|||
joined = joined.where(LogEntry.performer == performer)
|
||||
|
||||
if namespace:
|
||||
joined = joined.where(User.username == namespace)
|
||||
joined = joined.join(User).where(User.username == namespace)
|
||||
|
||||
return list(joined.where(LogEntry.datetime >= start_time,
|
||||
LogEntry.datetime < end_time).order_by(LogEntry.datetime.desc()))
|
||||
return joined
|
||||
|
||||
|
||||
def get_aggregated_logs(start_time, end_time, performer=None, repository=None, namespace=None):
|
||||
selections = [LogEntryKind, fn.date(LogEntry.datetime, '%d'), fn.Count(LogEntry.id).alias('count')]
|
||||
query = _logs_query(selections, start_time, end_time, performer, repository, namespace)
|
||||
return query.group_by(fn.date(LogEntry.datetime, '%d'), LogEntryKind)
|
||||
|
||||
|
||||
def list_logs(start_time, end_time, performer=None, repository=None, namespace=None, page=None,
|
||||
count=None):
|
||||
|
||||
Performer = User.alias()
|
||||
selections = [LogEntry, LogEntryKind, Performer]
|
||||
|
||||
query = _logs_query(selections, start_time, end_time, performer, repository, namespace)
|
||||
query = (query.switch(LogEntry)
|
||||
.join(Performer, JOIN_LEFT_OUTER,
|
||||
on=(LogEntry.performer == Performer.id).alias('performer')))
|
||||
|
||||
if page and count:
|
||||
query = query.paginate(page, count)
|
||||
|
||||
return list(query.order_by(LogEntry.datetime.desc()))
|
||||
|
||||
|
||||
def log_action(kind_name, user_or_organization_name, performer=None, repository=None,
|
||||
|
|
|
@ -14,6 +14,7 @@ from data import model
|
|||
from auth import scopes
|
||||
from app import avatar
|
||||
|
||||
LOGS_PER_PAGE = 500
|
||||
|
||||
def log_view(log):
|
||||
view = {
|
||||
|
@ -33,8 +34,16 @@ def log_view(log):
|
|||
|
||||
return view
|
||||
|
||||
def aggregated_log_view(log):
|
||||
view = {
|
||||
'kind': log.kind.name,
|
||||
'count': log.count,
|
||||
'datetime': format_date(log.datetime)
|
||||
}
|
||||
|
||||
def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None):
|
||||
return view
|
||||
|
||||
def _validate_logs_arguments(start_time, end_time, performer_name):
|
||||
performer = None
|
||||
if performer_name:
|
||||
performer = model.user.get_user(performer_name)
|
||||
|
@ -58,12 +67,31 @@ def get_logs(start_time, end_time, performer_name=None, repository=None, namespa
|
|||
if not end_time:
|
||||
end_time = datetime.today()
|
||||
|
||||
return (start_time, end_time, performer)
|
||||
|
||||
|
||||
def get_logs(start_time, end_time, performer_name=None, repository=None, namespace=None, page=None):
|
||||
(start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name)
|
||||
page = page if page else 1
|
||||
logs = model.log.list_logs(start_time, end_time, performer=performer, repository=repository,
|
||||
namespace=namespace)
|
||||
namespace=namespace, page=page, count=LOGS_PER_PAGE + 1)
|
||||
|
||||
return {
|
||||
'start_time': format_date(start_time),
|
||||
'end_time': format_date(end_time),
|
||||
'logs': [log_view(log) for log in logs]
|
||||
'logs': [log_view(log) for log in logs[0:LOGS_PER_PAGE]],
|
||||
'page': page,
|
||||
'has_additional': len(logs) > LOGS_PER_PAGE,
|
||||
}
|
||||
|
||||
def get_aggregate_logs(start_time, end_time, performer_name=None, repository=None, namespace=None):
|
||||
(start_time, end_time, performer) = _validate_logs_arguments(start_time, end_time, performer_name)
|
||||
|
||||
aggregated_logs = model.log.get_aggregated_logs(start_time, end_time, performer=performer,
|
||||
repository=repository, namespace=namespace)
|
||||
|
||||
return {
|
||||
'aggregated': [aggregated_log_view(log) for log in aggregated_logs]
|
||||
}
|
||||
|
||||
|
||||
|
@ -76,6 +104,7 @@ class RepositoryLogs(RepositoryParamResource):
|
|||
@parse_args
|
||||
@query_param('starttime', 'Earliest time from which to get logs (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('page', 'The page number for the logs', type=int, default=1)
|
||||
def get(self, args, namespace, repository):
|
||||
""" List the logs for the specified repository. """
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
|
@ -84,7 +113,7 @@ class RepositoryLogs(RepositoryParamResource):
|
|||
|
||||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
return get_logs(start_time, end_time, repository=repo, namespace=namespace)
|
||||
return get_logs(start_time, end_time, repository=repo, page=args['page'])
|
||||
|
||||
|
||||
@resource('/v1/user/logs')
|
||||
|
@ -97,6 +126,7 @@ class UserLogs(ApiResource):
|
|||
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||
@query_param('page', 'The page number for the logs', type=int, default=1)
|
||||
def get(self, args):
|
||||
""" List the logs for the current user. """
|
||||
performer_name = args['performer']
|
||||
|
@ -104,7 +134,8 @@ class UserLogs(ApiResource):
|
|||
end_time = args['endtime']
|
||||
|
||||
user = get_authenticated_user()
|
||||
return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username)
|
||||
return get_logs(start_time, end_time, performer_name=performer_name, namespace=user.username,
|
||||
page=args['page'])
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/logs')
|
||||
|
@ -117,6 +148,7 @@ class OrgLogs(ApiResource):
|
|||
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||
@query_param('page', 'The page number for the logs', type=int, default=1)
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
def get(self, args, orgname):
|
||||
""" List the logs for the specified organization. """
|
||||
|
@ -126,6 +158,73 @@ class OrgLogs(ApiResource):
|
|||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
|
||||
return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name)
|
||||
return get_logs(start_time, end_time, namespace=orgname, performer_name=performer_name,
|
||||
page=args['page'])
|
||||
|
||||
raise Unauthorized()
|
||||
|
||||
|
||||
@resource('/v1/repository/<repopath:repository>/aggregatelogs')
|
||||
@path_param('repository', 'The full path of the repository. e.g. namespace/name')
|
||||
class RepositoryAggregateLogs(RepositoryParamResource):
|
||||
""" Resource for fetching aggregated logs for the specific repository. """
|
||||
@require_repo_admin
|
||||
@nickname('getAggregateRepoLogs')
|
||||
@parse_args
|
||||
@query_param('starttime', 'Earliest time from which to get logs (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs (%m/%d/%Y %Z)', type=str)
|
||||
def get(self, args, namespace, repository):
|
||||
""" Returns the aggregated logs for the specified repository. """
|
||||
repo = model.repository.get_repository(namespace, repository)
|
||||
if not repo:
|
||||
raise NotFound()
|
||||
|
||||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
return get_aggregate_logs(start_time, end_time, repository=repo)
|
||||
|
||||
|
||||
@resource('/v1/user/aggregatelogs')
|
||||
@internal_only
|
||||
class UserAggregateLogs(ApiResource):
|
||||
""" Resource for fetching aggregated logs for the current user. """
|
||||
@require_user_admin
|
||||
@nickname('getAggregateUserLogs')
|
||||
@parse_args
|
||||
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||
def get(self, args):
|
||||
""" Returns the aggregated logs for the current user. """
|
||||
performer_name = args['performer']
|
||||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
|
||||
user = get_authenticated_user()
|
||||
return get_aggregate_logs(start_time, end_time, performer_name=performer_name,
|
||||
namespace=user.username)
|
||||
|
||||
|
||||
@resource('/v1/organization/<orgname>/aggregatelogs')
|
||||
@path_param('orgname', 'The name of the organization')
|
||||
@related_user_resource(UserLogs)
|
||||
class OrgAggregateLogs(ApiResource):
|
||||
""" Resource for fetching aggregate logs for the entire organization. """
|
||||
@nickname('getAggregateOrgLogs')
|
||||
@parse_args
|
||||
@query_param('starttime', 'Earliest time from which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('endtime', 'Latest time to which to get logs. (%m/%d/%Y %Z)', type=str)
|
||||
@query_param('performer', 'Username for which to filter logs.', type=str)
|
||||
@require_scope(scopes.ORG_ADMIN)
|
||||
def get(self, args, orgname):
|
||||
""" Gets the aggregated logs for the specified organization. """
|
||||
permission = AdministerOrganizationPermission(orgname)
|
||||
if permission.can():
|
||||
performer_name = args['performer']
|
||||
start_time = args['starttime']
|
||||
end_time = args['endtime']
|
||||
|
||||
return get_aggregate_logs(start_time, end_time, namespace=orgname,
|
||||
performer_name=performer_name)
|
||||
|
||||
raise Unauthorized()
|
||||
|
|
|
@ -71,7 +71,7 @@
|
|||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
margin-top: 6px;
|
||||
margin-top: 7px;
|
||||
vertical-align: middle;
|
||||
float: left;
|
||||
}
|
||||
|
|
|
@ -14,29 +14,27 @@
|
|||
<span class="hidden-xs right">
|
||||
<i class="fa fa-bar-chart-o toggle-icon" ng-class="chartVisible ? 'active' : ''"
|
||||
ng-click="toggleChart()" data-title="Toggle Chart" bs-tooltip="tooltip.title"></i>
|
||||
<a href="{{ logsPath }}" download="usage-log.json" target="_new">
|
||||
<i class="fa fa-download toggle-icon" data-title="Download Logs" bs-tooltip="tooltip.title"></i>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div id="bar-chart" style="width: 800px; height: 500px;" ng-show="chartVisible">
|
||||
<svg style="width: 800px; height: 500px;"></svg>
|
||||
<div class="cor-loader" ng-if="chartLoading"></div>
|
||||
</div>
|
||||
|
||||
<div class="hidden-xs side-controls">
|
||||
<div class="result-count">
|
||||
Showing {{(logs | visibleLogFilter:kindsAllowed | filter:search | limitTo:150).length}} of
|
||||
{{(logs | visibleLogFilter:kindsAllowed | filter:search).length}} matching logs
|
||||
Showing {{(logs | visibleLogFilter:kindsAllowed | filter:search).length}} matching logs
|
||||
</div>
|
||||
<div class="filter-input">
|
||||
<input id="log-filter" class="form-control" placeholder="Filter Logs" type="text" ng-model="search.$">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-container">
|
||||
<div class="cor-loader" ng-show="loading"></div>
|
||||
<div class="table-container" infinite-scroll="nextPage()"
|
||||
infinite-scroll-disabled="loading || !hasAdditional"
|
||||
infinite-scroll-distance="2">
|
||||
<table class="cor-table">
|
||||
<thead>
|
||||
<td>Description</td>
|
||||
|
@ -44,14 +42,15 @@
|
|||
<td>User/Token/App</td>
|
||||
</thead>
|
||||
|
||||
<tr class="log" ng-repeat="log in (logs | visibleLogFilter:kindsAllowed | filter:search | limitTo:150)">
|
||||
<tr class="log" ng-repeat="log in (logs | visibleLogFilter:kindsAllowed | filter:search)"
|
||||
bindonce>
|
||||
<td>
|
||||
<span class="circle" style="{{ 'background: ' + getColor(log.kind) }}"></span>
|
||||
<span class="log-description" ng-bind-html="getDescription(log)"></span>
|
||||
<span class="circle" style="{{ 'background: ' + getColor(log.kind, chart) }}"></span>
|
||||
<span class="log-description" bo-html="getDescription(log)"></span>
|
||||
</td>
|
||||
<td>{{ log.datetime }}</td>
|
||||
<td><span bo-text="log.datetime"></span></td>
|
||||
<td>
|
||||
<span class="log-performer" ng-if="log.metadata.oauth_token_application">
|
||||
<span class="log-performer" bo-if="log.metadata.oauth_token_application">
|
||||
<div>
|
||||
<span class="application-reference"
|
||||
data-title="log.metadata.oauth_token_application"
|
||||
|
@ -62,19 +61,20 @@
|
|||
<span class="entity-reference" entity="log.performer" namespace="organization.name"></span>
|
||||
</div>
|
||||
</span>
|
||||
<span class="log-performer" ng-if="!log.metadata.oauth_token_application && log.performer">
|
||||
<span class="log-performer" bo-if="!log.metadata.oauth_token_application && log.performer">
|
||||
<span class="entity-reference" entity="log.performer" namespace="organization.name"></span>
|
||||
</span>
|
||||
<span class="log-performer" ng-if="!log.performer && log.metadata.token">
|
||||
<span class="log-performer" bo-if="!log.performer && log.metadata.token">
|
||||
<i class="fa fa-key"></i>
|
||||
<span>{{ log.metadata.token }}</span>
|
||||
<span bo-text="log.metadata.token"></span>
|
||||
</span>
|
||||
<span ng-if="!log.performer && !log.metadata.token">
|
||||
<span bo-if="!log.performer && !log.metadata.token">
|
||||
(anonymous)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="cor-loader" ng-show="loading"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -37,7 +37,7 @@ quayPages.constant('pages', {
|
|||
|
||||
quayDependencies = ['ngRoute', 'chieffancypants.loadingBar', 'cfp.hotkeys', 'angular-tour', 'restangular', 'angularMoment',
|
||||
'mgcrea.ngStrap', 'ngCookies', 'ngSanitize', 'angular-md5', 'pasvaz.bindonce', 'ansiToHtml', 'debounce',
|
||||
'core-ui', 'core-config-setup', 'quayPages'];
|
||||
'core-ui', 'core-config-setup', 'quayPages', 'infinite-scroll'];
|
||||
|
||||
if (window.__config && window.__config.MIXPANEL_KEY) {
|
||||
quayDependencies.push('angulartics');
|
||||
|
|
|
@ -17,7 +17,7 @@ angular.module('quay').directive('feedbackBar', function () {
|
|||
|
||||
$scope.$watch('feedback', function(feedback) {
|
||||
if (feedback) {
|
||||
$scope.formattedMessage = StringBuilderService.buildString(feedback.message, feedback.data || {}, 'span');
|
||||
$scope.formattedMessage = StringBuilderService.buildTrustedString(feedback.message, feedback.data || {}, 'span');
|
||||
$scope.viewCounter++;
|
||||
} else {
|
||||
$scope.viewCounter = 0;
|
||||
|
|
|
@ -22,7 +22,8 @@ angular.module('quay').directive('logsView', function () {
|
|||
$scope.logs = null;
|
||||
$scope.kindsAllowed = null;
|
||||
$scope.chartVisible = true;
|
||||
$scope.logsPath = '';
|
||||
$scope.chartLoading = true;
|
||||
$scope.currentPage = 1;
|
||||
|
||||
$scope.options = {};
|
||||
|
||||
|
@ -253,6 +254,29 @@ angular.module('quay').directive('logsView', function () {
|
|||
return new Date(date.getFullYear(), date.getMonth(), date.getDate() + days);
|
||||
};
|
||||
|
||||
var getUrl = function(suffix) {
|
||||
var url = UtilService.getRestUrl('user/' + suffix);
|
||||
if ($scope.organization) {
|
||||
url = UtilService.getRestUrl('organization', $scope.organization.name, suffix);
|
||||
}
|
||||
if ($scope.repository) {
|
||||
url = UtilService.getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, suffix);
|
||||
}
|
||||
|
||||
if ($scope.allLogs) {
|
||||
url = UtilService.getRestUrl('superuser', suffix)
|
||||
}
|
||||
|
||||
url += '?starttime=' + encodeURIComponent(getDateString($scope.options.logStartDate));
|
||||
url += '&endtime=' + encodeURIComponent(getDateString($scope.options.logEndDate));
|
||||
|
||||
if ($scope.performer) {
|
||||
url += '&performer=' + encodeURIComponent($scope.performer.name);
|
||||
}
|
||||
|
||||
return url;
|
||||
};
|
||||
|
||||
var update = function() {
|
||||
var hasValidUser = !!$scope.user;
|
||||
var hasValidOrg = !!$scope.organization;
|
||||
|
@ -269,44 +293,44 @@ angular.module('quay').directive('logsView', function () {
|
|||
$scope.options.logStartDate = twoWeeksAgo;
|
||||
}
|
||||
|
||||
$scope.chartLoading = true;
|
||||
|
||||
var aggregateUrl = getUrl('aggregatelogs')
|
||||
|
||||
var loadAggregate = Restangular.one(aggregateUrl);
|
||||
loadAggregate.customGET().then(function(resp) {
|
||||
$scope.chart = new LogUsageChart(logKinds);
|
||||
$($scope.chart).bind('filteringChanged', function(e) {
|
||||
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
|
||||
});
|
||||
|
||||
$scope.chart.draw('bar-chart', resp.aggregated, $scope.options.logStartDate,
|
||||
$scope.options.logEndDate);
|
||||
$scope.chartLoading = false;
|
||||
});
|
||||
|
||||
$scope.currentPage = 0;
|
||||
$scope.hasAdditional = true;
|
||||
$scope.loading = false;
|
||||
$scope.logs = [];
|
||||
$scope.nextPage();
|
||||
};
|
||||
|
||||
$scope.nextPage = function() {
|
||||
if ($scope.loading) { return; }
|
||||
|
||||
$scope.loading = true;
|
||||
|
||||
// Note: We construct the URLs here manually because we also use it for the download
|
||||
// path.
|
||||
var url = UtilService.getRestUrl('user/logs');
|
||||
if ($scope.organization) {
|
||||
url = UtilService.getRestUrl('organization', $scope.organization.name, 'logs');
|
||||
}
|
||||
if ($scope.repository) {
|
||||
url = UtilService.getRestUrl('repository', $scope.repository.namespace, $scope.repository.name, 'logs');
|
||||
}
|
||||
|
||||
if ($scope.allLogs) {
|
||||
url = UtilService.getRestUrl('superuser', 'logs')
|
||||
}
|
||||
|
||||
url += '?starttime=' + encodeURIComponent(getDateString($scope.options.logStartDate));
|
||||
url += '&endtime=' + encodeURIComponent(getDateString($scope.options.logEndDate));
|
||||
|
||||
if ($scope.performer) {
|
||||
url += '&performer=' + encodeURIComponent($scope.performer.name);
|
||||
}
|
||||
|
||||
var loadLogs = Restangular.one(url);
|
||||
var logsUrl = getUrl('logs') + '&page=' + ($scope.currentPage + 1);
|
||||
var loadLogs = Restangular.one(logsUrl);
|
||||
loadLogs.customGET().then(function(resp) {
|
||||
$scope.logsPath = '/api/v1/' + url;
|
||||
resp.logs.forEach(function(log) {
|
||||
$scope.logs.push(log);
|
||||
});
|
||||
|
||||
if (!$scope.chart) {
|
||||
$scope.chart = new LogUsageChart(logKinds);
|
||||
$($scope.chart).bind('filteringChanged', function(e) {
|
||||
$scope.$apply(function() { $scope.kindsAllowed = e.allowed; });
|
||||
});
|
||||
}
|
||||
|
||||
$scope.chart.draw('bar-chart', resp.logs, $scope.options.logStartDate, $scope.options.logEndDate);
|
||||
$scope.kindsAllowed = null;
|
||||
$scope.logs = resp.logs;
|
||||
$scope.loading = false;
|
||||
$scope.currentPage = resp.page;
|
||||
$scope.hasAdditional = resp.has_additional;
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -318,8 +342,9 @@ angular.module('quay').directive('logsView', function () {
|
|||
return allowed == null || allowed.hasOwnProperty(kind);
|
||||
};
|
||||
|
||||
$scope.getColor = function(kind) {
|
||||
return $scope.chart.getColor(kind);
|
||||
$scope.getColor = function(kind, chart) {
|
||||
if (!chart) { return ''; }
|
||||
return chart.getColor(kind);
|
||||
};
|
||||
|
||||
$scope.getDescription = function(log) {
|
||||
|
|
|
@ -1616,22 +1616,22 @@ UsageChart.prototype.draw = function(container) {
|
|||
function LogUsageChart(titleMap) {
|
||||
this.titleMap_ = titleMap;
|
||||
this.colorScale_ = d3.scale.category20();
|
||||
this.entryMap_ = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the D3-representation of the data.
|
||||
*/
|
||||
LogUsageChart.prototype.buildData_ = function(logs) {
|
||||
LogUsageChart.prototype.buildData_ = function(aggregatedLogs) {
|
||||
var parseDate = d3.time.format("%a, %d %b %Y %H:%M:%S %Z").parse
|
||||
|
||||
// Build entries for each kind of event that occurred, on each day. We have one
|
||||
// entry per {kind, day} pair.
|
||||
var map = {};
|
||||
var entries = [];
|
||||
for (var i = 0; i < logs.length; ++i) {
|
||||
var log = logs[i];
|
||||
var title = this.titleMap_[log.kind] || log.kind;
|
||||
var datetime = parseDate(log.datetime);
|
||||
for (var i = 0; i < aggregatedLogs.length; ++i) {
|
||||
var aggregated = aggregatedLogs[i];
|
||||
var title = this.titleMap_[aggregated.kind] || aggregated.kind;
|
||||
var datetime = parseDate(aggregated.datetime);
|
||||
var dateDay = datetime.getDate();
|
||||
if (dateDay < 10) {
|
||||
dateDay = '0' + dateDay;
|
||||
|
@ -1640,24 +1640,18 @@ LogUsageChart.prototype.buildData_ = function(logs) {
|
|||
var formatted = (datetime.getMonth() + 1) + '/' + dateDay;
|
||||
var adjusted = new Date(datetime.getFullYear(), datetime.getMonth(), datetime.getDate());
|
||||
var key = title + '_' + formatted;
|
||||
var found = map[key];
|
||||
if (!found) {
|
||||
found = {
|
||||
'kind': log.kind,
|
||||
'title': title,
|
||||
'adjusted': adjusted,
|
||||
'formatted': datetime.getDate(),
|
||||
'count': 0
|
||||
};
|
||||
var entry = {
|
||||
'kind': aggregated.kind,
|
||||
'title': title,
|
||||
'adjusted': adjusted,
|
||||
'formatted': datetime.getDate(),
|
||||
'count': aggregated.count
|
||||
};
|
||||
|
||||
map[key] = found;
|
||||
entries.push(found);
|
||||
}
|
||||
found['count']++;
|
||||
entries.push(entry);
|
||||
this.entryMap_[key] = entry;
|
||||
}
|
||||
|
||||
this.entries_ = map;
|
||||
|
||||
// Build the data itself. We create a single entry for each possible kind of data, and then add (x, y) pairs
|
||||
// for the number of times that kind of event occurred on a particular day.
|
||||
var dataArray = [];
|
||||
|
@ -1727,7 +1721,7 @@ LogUsageChart.prototype.renderTooltip_ = function(d, e) {
|
|||
}
|
||||
|
||||
var key = d + '_' + e;
|
||||
var entry = this.entries_[key];
|
||||
var entry = this.entryMap_[key];
|
||||
if (!entry) {
|
||||
entry = {'count': 0};
|
||||
}
|
||||
|
|
|
@ -174,7 +174,7 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
|
|||
if (!kindInfo) {
|
||||
return '(Unknown notification kind: ' + notification['kind'] + ')';
|
||||
}
|
||||
return StringBuilderService.buildString(kindInfo['message'], notification['metadata']);
|
||||
return StringBuilderService.buildTrustedString(kindInfo['message'], notification['metadata']);
|
||||
};
|
||||
|
||||
notificationService.getClass = function(notification) {
|
||||
|
|
|
@ -39,6 +39,10 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f
|
|||
return url;
|
||||
};
|
||||
|
||||
stringBuilderService.buildTrustedString = function(value_or_func, metadata, opt_codetag) {
|
||||
return $sce.trustAsHtml(stringBuilderService.buildString(value_or_func, metadata, opt_codetag));
|
||||
};
|
||||
|
||||
stringBuilderService.buildString = function(value_or_func, metadata, opt_codetag) {
|
||||
var fieldIcons = {
|
||||
'inviter': 'user',
|
||||
|
@ -113,7 +117,7 @@ angular.module('quay').factory('StringBuilderService', ['$sce', 'UtilService', f
|
|||
'<' + codeTag + ' title="' + safe + '">' + markedDown + '</' + codeTag + '>');
|
||||
}
|
||||
}
|
||||
return $sce.trustAsHtml(description.replace('\n', '<br>'));
|
||||
return description.replace('\n', '<br>');
|
||||
};
|
||||
|
||||
return stringBuilderService;
|
||||
|
|
|
@ -22,6 +22,7 @@ jquery.overscroll - MIT (https://github.com/azoff/overscroll/blob/master/mit.lic
|
|||
URI.js - MIT (https://github.com/medialize/URI.js)
|
||||
angular-hotkeys - MIT (https://github.com/chieffancypants/angular-hotkeys/blob/master/LICENSE)
|
||||
angular-debounce - MIT (https://github.com/shahata/angular-debounce/blob/master/LICENSE)
|
||||
infinite-scroll - MIT (https://github.com/sroze/ngInfiniteScroll/blob/master/LICENSE)
|
||||
|
||||
Issues:
|
||||
>>>>> jquery.spotlight - GPLv3 (https://github.com/jameshalsall/jQuery-Spotlight)
|
2
static/lib/ng-infinite-scroll.min.js
vendored
Normal file
2
static/lib/ng-infinite-scroll.min.js
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/* ng-infinite-scroll - v1.2.0 - 2015-02-14 */
|
||||
var mod;mod=angular.module("infinite-scroll",[]),mod.value("THROTTLE_MILLISECONDS",null),mod.directive("infiniteScroll",["$rootScope","$window","$interval","THROTTLE_MILLISECONDS",function(a,b,c,d){return{scope:{infiniteScroll:"&",infiniteScrollContainer:"=",infiniteScrollDistance:"=",infiniteScrollDisabled:"=",infiniteScrollUseDocumentBottom:"=",infiniteScrollListenForEvent:"@"},link:function(e,f,g){var h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y;return y=angular.element(b),t=null,u=null,i=null,j=null,q=!0,x=!1,w=null,p=function(a){return a=a[0]||a,isNaN(a.offsetHeight)?a.document.documentElement.clientHeight:a.offsetHeight},r=function(a){return a[0].getBoundingClientRect&&!a.css("none")?a[0].getBoundingClientRect().top+s(a):void 0},s=function(a){return a=a[0]||a,isNaN(window.pageYOffset)?a.document.documentElement.scrollTop:a.ownerDocument.defaultView.pageYOffset},o=function(){var b,c,d,g,h;return j===y?(b=p(j)+s(j[0].document.documentElement),d=r(f)+p(f)):(b=p(j),c=0,void 0!==r(j)&&(c=r(j)),d=r(f)-c+p(f)),x&&(d=p((f[0].ownerDocument||f[0].document).documentElement)),g=d-b,h=g<=p(j)*t+1,h?(i=!0,u?e.$$phase||a.$$phase?e.infiniteScroll():e.$apply(e.infiniteScroll):void 0):i=!1},v=function(a,b){var d,e,f;return f=null,e=0,d=function(){var b;return e=(new Date).getTime(),c.cancel(f),f=null,a.call(),b=null},function(){var g,h;return g=(new Date).getTime(),h=b-(g-e),0>=h?(clearTimeout(f),c.cancel(f),f=null,e=g,a.call()):f?void 0:f=c(d,h,1)}},null!=d&&(o=v(o,d)),e.$on("$destroy",function(){return j.unbind("scroll",o),null!=w?(w(),w=null):void 0}),m=function(a){return t=parseFloat(a)||0},e.$watch("infiniteScrollDistance",m),m(e.infiniteScrollDistance),l=function(a){return u=!a,u&&i?(i=!1,o()):void 0},e.$watch("infiniteScrollDisabled",l),l(e.infiniteScrollDisabled),n=function(a){return x=a},e.$watch("infiniteScrollUseDocumentBottom",n),n(e.infiniteScrollUseDocumentBottom),h=function(a){return null!=j&&j.unbind("scroll",o),j=a,null!=a?j.bind("scroll",o):void 0},h(y),e.infiniteScrollListenForEvent&&(w=a.$on(e.infiniteScrollListenForEvent,o)),k=function(a){if(null!=a&&0!==a.length){if(a instanceof HTMLElement?a=angular.element(a):"function"==typeof a.append?a=angular.element(a[a.length-1]):"string"==typeof a&&(a=angular.element(document.querySelector(a))),null!=a)return h(a);throw new Exception("invalid infinite-scroll-container attribute.")}},e.$watch("infiniteScrollContainer",k),k(e.infiniteScrollContainer||[]),null!=g.infiniteScrollParent&&h(angular.element(f.parent())),null!=g.infiniteScrollImmediateCheck&&(q=e.$eval(g.infiniteScrollImmediateCheck)),c(function(){return q?o():void 0},0,1)}}}]);
|
Reference in a new issue