Commit 34b2a071 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 6f3135c6
......@@ -31,7 +31,11 @@ module.exports = {
var result = ApplicationViews.getApplicationViews(ctrl.application);
ctrl.primaryView = result.views[0];
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')
......
......@@ -4,7 +4,7 @@
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>
<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>
</li>
</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 class="arrow"></div>
<h3 class="popover-title" ng-bind="$popover.title" ng-if="$popover.title"></h3>
<div class="popover-content" ng-transclude></div>
<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>
<h3 class="popover-title" ng-bind="$popover.title" ng-if="$popover.title"></h3>
<div class="popover-content" ng-transclude></div>
</div>
\ No newline at end of file
......@@ -15,20 +15,22 @@
*/
'use strict';
var angular = require('angular');
module.exports = function ($rootScope, $scope, $state, NotificationFilters) {
'ngInject';
$scope.notificationFilters = null;
$scope.notificationFiltersSupported = false;
$scope.expandAll = false;
$scope.searchFilter = '';
$scope.remove = function (application) {
application.$remove();
};
$scope.toggleExpandAll = function (value) {
$scope.expandAll = value || !$scope.expandAll;
$rootScope.applicationGroups.groups.forEach(function (group) {
group.collapsed = !$scope.expandAll && group.applications.length > 1;
$scope.toggleExpandAll = function () {
$scope.expandAll = !$scope.expandAll;
angular.forEach($rootScope.applicationGroups.groups, function (group) {
group.collapsed = !$scope.expandAll;
});
};
......
......@@ -19,7 +19,7 @@ var yaml = require('js-yaml');
module.exports = function () {
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 '';
}
......
......@@ -62,7 +62,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({
title: 'Applications',
state: 'applications-list',
......@@ -72,14 +72,34 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
var applicationGroups = new 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 = {};
if (application.statusInfo.status === 'OFFLINE') {
return;
return $q.reject();
}
application.refreshing = true;
application.getInfo().then(function (response) {
return application.getInfo().then(function (response) {
var info = response.data;
application.version = info.version;
delete info.version;
......@@ -87,22 +107,27 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
application.version = info.build.version;
}
if (application.version) {
var group = application.group;
group.versionsCounter[application.version] = (group.versionsCounter[application.version] || 0) + 1;
var versions = Object.keys(group.versionsCounter);
versions.sort();
group.version = versions[0] + (versions.length > 1 ? ', ...' : '');
}
application.info = info;
}).finally(function () {
application.refreshing = false;
});
};
Application.query(function (applications) {
for (var i = 0; i < applications.length; i++) {
var group = applicationGroups.addApplication(applications[i]);
refresh(group, applications[i]);
}
applications.forEach(function (application) {
applicationGroups.addApplication(application, true);
});
//Defer refresh to give the browser time to render
$timeout(function () {
applicationGroups.applications.forEach(function (application) {
queueRefresh(application);
});
}, 50);
});
if (typeof (EventSource) !== 'undefined') {
......@@ -128,17 +153,17 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
};
if (event.type === 'REGISTRATION') {
var group = applicationGroups.addApplication(event.application, false);
refresh(group, event.application);
applicationGroups.addApplication(event.application, false);
queueRefresh(event.application);
title += ' instance registered.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'DEREGISTRATION') {
applicationGroups.removeApplication(event.application);
applicationGroups.removeApplication(event.application.id);
title += ' instance removed.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'STATUS_CHANGE') {
var group2 = applicationGroups.addApplication(event.application, true);
refresh(group2, event.application);
applicationGroups.addApplication(event.application, true);
queueRefresh(event.application);
title += ' instance is ' + event.to.status;
options.tag = event.application.id + '-STATUS';
options.icon = event.to.status !== 'UP' ? require('./img/error.png') : require('./img/ok.png');
......
......@@ -19,7 +19,8 @@ module.exports = function () {
'ngInject';
return function () {
this.groups = [];
this.groups = {};
this.applications = [];
var getMaxStatus = function (statusCounter) {
var order = ['OFFLINE', 'DOWN', 'OUT_OF_SERVICE', 'UNKNOWN', 'UP'];
......@@ -31,62 +32,41 @@ module.exports = function () {
return 'UNKNOWN';
};
var findApplication = function (applications, application) {
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) {
this.updateStatus = function (group) {
var statusCounter = {};
group.applications.forEach(function (application) {
statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1;
var applicationCount = 0;
this.applications.forEach(function (application) {
if (application.name === group.name) {
applicationCount++;
statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1;
}
});
group.applicationCount = applicationCount;
group.statusCounter = statusCounter;
group.status = getMaxStatus(statusCounter);
};
this.addApplication = function (application, overwrite) {
var groupIdx = findGroup(this.groups, application.name);
var group = groupIdx > -1 ? this.groups[groupIdx] : { applications: [], statusCounter: {}, versionsCounter: [], name: application.name, status: 'UNKNOWN' };
if (groupIdx === -1) {
this.groups.push(group);
}
var index = findApplication(group.applications, application);
if (index === -1) {
group.applications.push(application);
var idx = this.applications.findIndex(function (app) {
return app.id === application.id;
});
this.groups[application.name] = this.groups[application.name] || { applicationCount: 0, statusCounter: {}, versionsCounter: [], name: application.name, status: 'UNKNOWN' };
application.group = this.groups[application.name];
if (idx < 0) {
this.applications.push(application);
} else if (overwrite) {
group.applications[index] = application;
this.applications.splice(idx, 1, application);
}
updateStatus(group);
return group;
this.updateStatus(this.groups[application.name]);
};
this.removeApplication = function (application) {
var groupIdx = findGroup(this.groups, application.name);
if (groupIdx > -1) {
var group = this.groups[groupIdx];
var index = findApplication(group.applications, application);
if (index > -1) {
group.applications.splice(index, 1);
updateStatus(group);
}
if (group.length === 0) {
this.groups.splice(groupIdx, 1);
}
}
this.removeApplication = function (id) {
var idx = this.applications.findIndex(function (application) {
return id === application.id;
});
var group = this.applications[idx].group;
this.applications.splice(idx, 1);
this.updateStatus(group);
};
};
};
<div class="container-fluid">
<h3>Spring Boot applications</h3>
<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>
<table class="table application-list">
<col style="width: 30px; ">
......@@ -23,20 +22,22 @@
</tr>
</thead>
<tbody>
<tr ng-repeat-start="group in applicationGroups.groups|orderBy:order.column:order.descending|orderBy:'status':false|filter:searchFilter track by group.name"
ng-init="group.collapsed = group.applications.length > 1 &amp;&amp; !expandAll" ng-show="group.collapsed">
<td class="group-column" ng-click="group.collapsed = false"><i class="fa fa-plus"></i></td>
<td ng-bind="group.name"></td>
<td><span ng-bind="group.version"></span></td>
<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-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="application.group.collapsed = false"><i class="fa fa-plus"></i></td>
<td ng-bind="application.group.name"></td>
<td><span ng-bind="application.group.version"></span></td>
<td></td>
<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>
</td>
</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"
ng-repeat-end>
<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>
<tr ng-repeat-end ng-show="(!application.group.collapsed && application.group.collapsed != undefined ) || application.group.applicationCount == 1 || (searchFilter != '' && searchFilter != undefined)">
<td class="group-column" ng-class="{'hidden': !($first || filteredApps[$index - 1].group.name != application.group.name) &amp;&amp; (searchFilter == '' || searchFilter == undefined) }"
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/>
<span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td>
<td>{{ application.version }}</td>
......@@ -48,7 +49,7 @@
<span ng-show="application.refreshing"><i class="fa fa-spinner fa-pulse fa-lg"></i></span>
</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>
<div class="btn-group" title="remove">
<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