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:
Joseph Schorr 2015-07-31 13:38:02 -04:00
parent 572d6ba53c
commit 3d6c92901c
15 changed files with 270 additions and 99 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -71,7 +71,7 @@
height: 12px;
border-radius: 50%;
margin-right: 6px;
margin-top: 6px;
margin-top: 7px;
vertical-align: middle;
float: left;
}

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
View 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)}}}]);