Merge pull request #294 from coreos-inc/logsload
Switch to using an aggregated logs query and infinite scrolling
This commit is contained in:
commit
8e6a0fbbee
15 changed files with 270 additions and 99 deletions
|
@ -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