Commit db292d9c by Johannes Edmeier

Improve rendering performance for application list

* Number of concurrent requests for fetching the application info is limited to 15 requests * Fetching the application info is deferred by 50ms to let the browser render the view * Flattened application/group structure to save inner ng-repeat * ng-repeat for notification settings and view drop down is executed when the popup/dropdown is visible
parent b3e07520
...@@ -31,7 +31,11 @@ module.exports = { ...@@ -31,7 +31,11 @@ module.exports = {
var result = ApplicationViews.getApplicationViews(ctrl.application); var result = ApplicationViews.getApplicationViews(ctrl.application);
ctrl.primaryView = result.views[0]; ctrl.primaryView = result.views[0];
ctrl.secondaryViews = result.views.slice(1); ctrl.secondaryViews = result.views.slice(1);
ctrl.resolveViews = result.resolve; ctrl.viewsResolved = false;
ctrl.resolveViews = function () {
result.resolve();
ctrl.viewsResolved = true;
};
}; };
}, },
template: require('./btnDetailViews.tpl.html') template: require('./btnDetailViews.tpl.html')
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
ng-bind-html="$ctrl.primaryView.title"></a> ng-bind-html="$ctrl.primaryView.title"></a>
<button class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-show="$ctrl.secondaryViews" ng-click="$ctrl.resolveViews()"><i class="fa fa-caret-down"></i></button> <button class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-show="$ctrl.secondaryViews" ng-click="$ctrl.resolveViews()"><i class="fa fa-caret-down"></i></button>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li ng-repeat="view in $ctrl.secondaryViews track by view.href" ng-show="view.show == true"> <li ng-if="$ctrl.viewsResolved" ng-repeat="view in $ctrl.secondaryViews track by view.href" ng-show="view.show == true">
<a ng-href="{{view.href}}" target="{{view.target}}" ng-bind-html="view.title"></a> <a ng-href="{{view.href}}" target="{{view.target}}" ng-bind-html="view.title"></a>
</li> </li>
</ul> </ul>
......
<div class="popover fade bottom" ng-class="{'in' : $popover.isVisible()}" ng-style="{ display: $popover.isVisible() ? 'block' : 'none', top: $popover.offset.top+'px', left: $popover.offset.left + 'px' } "> <div ng-if="$popover.isVisible()" class="popover fade bottom" ng-class="{'in' : $popover.isVisible()}" ng-style="{ display: $popover.isVisible() ? 'block' : 'none', top: $popover.offset.top+'px', left: $popover.offset.left + 'px' } ">
<div class="arrow"></div> <div class="arrow"></div>
<h3 class="popover-title" ng-bind="$popover.title" ng-if="$popover.title"></h3> <h3 class="popover-title" ng-bind="$popover.title" ng-if="$popover.title"></h3>
<div class="popover-content" ng-transclude></div> <div class="popover-content" ng-transclude></div>
......
...@@ -15,20 +15,22 @@ ...@@ -15,20 +15,22 @@
*/ */
'use strict'; 'use strict';
var angular = require('angular');
module.exports = function ($rootScope, $scope, $state, NotificationFilters) { module.exports = function ($rootScope, $scope, $state, NotificationFilters) {
'ngInject'; 'ngInject';
$scope.notificationFilters = null; $scope.notificationFilters = null;
$scope.notificationFiltersSupported = false; $scope.notificationFiltersSupported = false;
$scope.expandAll = false; $scope.expandAll = false;
$scope.searchFilter = '';
$scope.remove = function (application) { $scope.remove = function (application) {
application.$remove(); application.$remove();
}; };
$scope.toggleExpandAll = function (value) { $scope.toggleExpandAll = function () {
$scope.expandAll = value || !$scope.expandAll; $scope.expandAll = !$scope.expandAll;
$rootScope.applicationGroups.groups.forEach(function (group) { angular.forEach($rootScope.applicationGroups.groups, function (group) {
group.collapsed = !$scope.expandAll && group.applications.length > 1; group.collapsed = !$scope.expandAll;
}); });
}; };
......
...@@ -19,7 +19,7 @@ var yaml = require('js-yaml'); ...@@ -19,7 +19,7 @@ var yaml = require('js-yaml');
module.exports = function () { module.exports = function () {
return function (input) { return function (input) {
if (typeof (input) === 'object' && Object.keys(input).length === 0) { if (typeof (input) === 'undefined' || (typeof (input) === 'object' && Object.keys(input).length === 0)) {
return ''; return '';
} }
......
...@@ -63,7 +63,7 @@ module.config(function ($stateProvider) { ...@@ -63,7 +63,7 @@ module.config(function ($stateProvider) {
}); });
}); });
module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews) { module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews, $timeout, $q) {
MainViews.register({ MainViews.register({
title: 'Applications', title: 'Applications',
state: 'applications-list', state: 'applications-list',
...@@ -73,14 +73,34 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -73,14 +73,34 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
var applicationGroups = new ApplicationGroups(); var applicationGroups = new ApplicationGroups();
$rootScope.applicationGroups = applicationGroups; $rootScope.applicationGroups = applicationGroups;
var refresh = function (group, application) { var refreshQueue = [];
var runningRefreshs = 0;
var queueRefresh = function (application) {
application.refreshing = true;
if (runningRefreshs++ >= 15) {
refreshQueue.push([application]);
} else {
refresh(application);
}
};
var refresh = function (application) {
doRefresh(application).finally(function () {
application.refreshing = false;
runningRefreshs--;
if (refreshQueue.length > 0) {
refresh(application);
}
});
};
var doRefresh = function (application) {
application.info = {}; application.info = {};
if (application.statusInfo.status === 'OFFLINE') { if (application.statusInfo.status === 'OFFLINE') {
return; return $q.reject();
} }
return application.getInfo().then(function (response) {
application.refreshing = true;
application.getInfo().then(function (response) {
var info = response.data; var info = response.data;
application.version = info.version; application.version = info.version;
delete info.version; delete info.version;
...@@ -88,22 +108,27 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -88,22 +108,27 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
application.version = info.build.version; application.version = info.build.version;
} }
if (application.version) { if (application.version) {
var group = application.group;
group.versionsCounter[application.version] = (group.versionsCounter[application.version] || 0) + 1; group.versionsCounter[application.version] = (group.versionsCounter[application.version] || 0) + 1;
var versions = Object.keys(group.versionsCounter); var versions = Object.keys(group.versionsCounter);
versions.sort(); versions.sort();
group.version = versions[0] + (versions.length > 1 ? ', ...' : ''); group.version = versions[0] + (versions.length > 1 ? ', ...' : '');
} }
application.info = info; application.info = info;
}).finally(function () {
application.refreshing = false;
}); });
}; };
Application.query(function (applications) { Application.query(function (applications) {
for (var i = 0; i < applications.length; i++) { applications.forEach(function (application) {
var group = applicationGroups.addApplication(applications[i]); applicationGroups.addApplication(application, true);
refresh(group, applications[i]); });
}
//Defer refresh to give the browser time to render
$timeout(function () {
applicationGroups.applications.forEach(function (application) {
queueRefresh(application);
});
}, 50);
}); });
if (typeof (EventSource) !== 'undefined') { if (typeof (EventSource) !== 'undefined') {
...@@ -129,17 +154,17 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -129,17 +154,17 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
}; };
if (event.type === 'REGISTRATION') { if (event.type === 'REGISTRATION') {
var group = applicationGroups.addApplication(event.application, false); applicationGroups.addApplication(event.application, false);
refresh(group, event.application); queueRefresh(event.application);
title += ' instance registered.'; title += ' instance registered.';
options.tag = event.application.id + '-REGISTRY'; options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'DEREGISTRATION') { } else if (event.type === 'DEREGISTRATION') {
applicationGroups.removeApplication(event.application); applicationGroups.removeApplication(event.application.id);
title += ' instance removed.'; title += ' instance removed.';
options.tag = event.application.id + '-REGISTRY'; options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'STATUS_CHANGE') { } else if (event.type === 'STATUS_CHANGE') {
var group2 = applicationGroups.addApplication(event.application, true); applicationGroups.addApplication(event.application, true);
refresh(group2, event.application); queueRefresh(event.application);
title += ' instance is ' + event.to.status; title += ' instance is ' + event.to.status;
options.tag = event.application.id + '-STATUS'; options.tag = event.application.id + '-STATUS';
options.icon = event.to.status !== 'UP' ? require('./img/error.png') : require('./img/ok.png'); options.icon = event.to.status !== 'UP' ? require('./img/error.png') : require('./img/ok.png');
......
...@@ -19,7 +19,8 @@ module.exports = function () { ...@@ -19,7 +19,8 @@ module.exports = function () {
'ngInject'; 'ngInject';
return function () { return function () {
this.groups = []; this.groups = {};
this.applications = [];
var getMaxStatus = function (statusCounter) { var getMaxStatus = function (statusCounter) {
var order = ['OFFLINE', 'DOWN', 'OUT_OF_SERVICE', 'UNKNOWN', 'UP']; var order = ['OFFLINE', 'DOWN', 'OUT_OF_SERVICE', 'UNKNOWN', 'UP'];
...@@ -31,62 +32,41 @@ module.exports = function () { ...@@ -31,62 +32,41 @@ module.exports = function () {
return 'UNKNOWN'; return 'UNKNOWN';
}; };
var findApplication = function (applications, application) { this.updateStatus = function (group) {
for (var i = 0; i < applications.length; i++) {
if (applications[i].id === application.id) {
return i;
}
}
return -1;
};
var findGroup = function (groups, name) {
for (var i = 0; i < groups.length; i++) {
if (groups[i].name === name) {
return i;
}
}
return -1;
};
var updateStatus = function (group) {
var statusCounter = {}; var statusCounter = {};
group.applications.forEach(function (application) { var applicationCount = 0;
this.applications.forEach(function (application) {
if (application.name === group.name) {
applicationCount++;
statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1; statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1;
}
}); });
group.applicationCount = applicationCount;
group.statusCounter = statusCounter; group.statusCounter = statusCounter;
group.status = getMaxStatus(statusCounter); group.status = getMaxStatus(statusCounter);
}; };
this.addApplication = function (application, overwrite) { this.addApplication = function (application, overwrite) {
var groupIdx = findGroup(this.groups, application.name); var idx = this.applications.findIndex(function (app) {
var group = groupIdx > -1 ? this.groups[groupIdx] : { applications: [], statusCounter: {}, versionsCounter: [], name: application.name, status: 'UNKNOWN' }; return app.id === application.id;
if (groupIdx === -1) { });
this.groups.push(group); this.groups[application.name] = this.groups[application.name] || { applicationCount: 0, statusCounter: {}, versionsCounter: [], name: application.name, status: 'UNKNOWN' };
} application.group = this.groups[application.name];
var index = findApplication(group.applications, application); if (idx < 0) {
if (index === -1) { this.applications.push(application);
group.applications.push(application);
} else if (overwrite) { } else if (overwrite) {
group.applications[index] = application; this.applications.splice(idx, 1, application);
} }
updateStatus(group); this.updateStatus(this.groups[application.name]);
return group;
}; };
this.removeApplication = function (application) { this.removeApplication = function (id) {
var groupIdx = findGroup(this.groups, application.name); var idx = this.applications.findIndex(function (application) {
if (groupIdx > -1) { return id === application.id;
var group = this.groups[groupIdx]; });
var index = findApplication(group.applications, application); var group = this.applications[idx].group;
if (index > -1) { this.applications.splice(idx, 1);
group.applications.splice(index, 1); this.updateStatus(group);
updateStatus(group);
}
if (group.length === 0) {
this.groups.splice(groupIdx, 1);
}
}
}; };
}; };
}; };
<div class="container-fluid"> <div class="container-fluid">
<h3>Spring Boot applications</h3> <h3>Spring Boot applications</h3>
<div> <div>
<input placeholder="Filter" class="input-xxlarge" type="search" ng-model="searchFilter" ng-keypress="toggleExpandAll(true)" <input placeholder="Filter" class="input-xxlarge" type="search" ng-model="searchFilter" />
/>
</div> </div>
<table class="table application-list"> <table class="table application-list">
<col style="width: 30px; "> <col style="width: 30px; ">
...@@ -23,20 +22,22 @@ ...@@ -23,20 +22,22 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat-start="group in applicationGroups.groups|orderBy:order.column:order.descending|orderBy:'status':false|filter:searchFilter track by group.name" <tr ng-repeat-start="application in filteredApps = (applicationGroups.applications|orderBy:order.column:order.descending|orderBy:'statusInfo.status':false|filter:searchFilter) track by application.id"
ng-init="group.collapsed = group.applications.length > 1 &amp;&amp; !expandAll" ng-show="group.collapsed"> ng-show="application.group.applicationCount > 1 &amp;&amp; (application.group.collapsed || application.group.collapsed == undefined) &amp;&amp; ($first || filteredApps[$index - 1].group.name != application.group.name) &amp;&amp; (searchFilter == '' || searchFilter == undefined)">
<td class="group-column" ng-click="group.collapsed = false"><i class="fa fa-plus"></i></td> <td class="group-column" ng-click="application.group.collapsed = false"><i class="fa fa-plus"></i></td>
<td ng-bind="group.name"></td> <td ng-bind="application.group.name"></td>
<td><span ng-bind="group.version"></span></td> <td><span ng-bind="application.group.version"></span></td>
<td></td> <td></td>
<td colspan="2"> <td colspan="2">
<span ng-repeat-start="(status, count) in group.statusCounter track by status" class="status-{{status}}" ng-bind="count + ' ' + status"></span> <span ng-repeat-start="(status, count) in application.group.statusCounter track by status" class="status-{{status}}" ng-bind="count + ' ' + status"></span>
<span ng-repeat-end ng-hide="$last"> / </span> <span ng-repeat-end ng-hide="$last"> / </span>
</td> </td>
</tr> </tr>
<tr ng-hide="group.collapsed" ng-repeat="application in filteredApps = (group.applications|orderBy:order.column:order.descending|orderBy:'statusInfo.status':false|filter:searchFilter) track by application.id" <tr ng-repeat-end ng-show="(!application.group.collapsed && application.group.collapsed != undefined ) || application.group.applicationCount == 1 || (searchFilter != '' && searchFilter != undefined)">
ng-repeat-end> <td class="group-column" ng-class="{'hidden': !($first || filteredApps[$index - 1].group.name != application.group.name) &amp;&amp; (searchFilter == '' || searchFilter == undefined) }"
<td class="group-column" ng-if="$first" rowspan="{{filteredApps.length}}" ng-click="group.collapsed = filteredApps.length > 1"><i ng-show="filteredApps.length > 1" class="fa fa-minus"></i></td> rowspan="{{ (searchFilter != '' &amp;&amp; searchFilter != undefined) ? 1 : application.group.applicationCount }}"
ng-click="application.group.collapsed = application.group.applicationCount > 1"><i ng-show="application.group.applicationCount > 1 &amp;&amp; (searchFilter == '' || searchFilter == undefined)"
class="fa fa-minus"></i></td>
<td>{{ application.name }} ({{ application.id }})<br/> <td>{{ application.name }} ({{ application.id }})<br/>
<span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td> <span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td>
<td>{{ application.version }}</td> <td>{{ application.version }}</td>
...@@ -48,7 +49,7 @@ ...@@ -48,7 +49,7 @@
<span ng-show="application.refreshing"><i class="fa fa-spinner fa-pulse fa-lg"></i></span> <span ng-show="application.refreshing"><i class="fa fa-spinner fa-pulse fa-lg"></i></span>
</td> </td>
<td> <td>
<sba-notification-settings ng-if="notificationFilters" application="application" filters="notificationFilters" refresh-callback="loadFilters()"></sba-notification-settings> <sba-notification-settings ng-show="notificationFilters" application="application" filters="notificationFilters" refresh-callback="loadFilters()"></sba-notification-settings>
<sba-btn-detail-views details-for="application"></sba-btn-detail-views> <sba-btn-detail-views details-for="application"></sba-btn-detail-views>
<div class="btn-group" title="remove"> <div class="btn-group" title="remove">
<a class="btn btn-danger" ng-click="remove(application)"><i class="fa fa-times"></i></a> <a class="btn btn-danger" ng-click="remove(application)"><i class="fa fa-times"></i></a>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment