Commit 2d45fb4b by Johannes Edmeier

Group applications by name; redesign info column

After this commit the applications in the overview are grouped by application- name. In case the search box is used all groups get expanded automatically. Additionally the info column is now expandable via button and not a pre-section anymore. closes #218
parent dadf3c43
.ellipsis-expander {
padding: 0 2px 2px;
color: #333333;
vertical-align: middle;
background-color: #ddd;
border: 1px solid #e1e1e8;
border-radius: 4px;
cursor: pointer;
}
\ No newline at end of file
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
require('./limitedText.css');
module.exports = {
bindings: {
maxLines: '@maxLines',
content: '<bindHtml'
},
controller: function () {
'ngInject';
var ctrl = this;
ctrl.limited = '';
ctrl.collapsed = true;
var limit = function (text, limit) {
var lines = text.split('\n');
if (lines.length > limit) {
return [lines.slice(0, limit).join('\n'), lines.slice(limit).join('\n')];
}
return [text];
};
ctrl.$onChanges = function () {
ctrl.parts = limit(ctrl.content, ctrl.maxLines);
ctrl.isLimited = ctrl.parts.length > 1;
};
ctrl.toggle = function () {
ctrl.collapsed = !ctrl.collapsed;
ctrl.$onChanges();
};
},
template: '<div style="white-space: pre;"><ng-bind-html ng-bind-html="$ctrl.parts[0]"></ng-bind-html> <span class="ellipsis-expander" ng-show="$ctrl.isLimited" ng-click="$ctrl.toggle()">...</span><ng-bind-html ng-bind-html="$ctrl.collapsed ? \'\' : \'\n\' + $ctrl.parts[1]"></ng-bind-html></div>'
};
...@@ -17,15 +17,21 @@ ...@@ -17,15 +17,21 @@
module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) { module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) {
'ngInject'; 'ngInject';
$scope.applications = $rootScope.applications;
$scope.notificationFilters = null; $scope.notificationFilters = null;
$scope.notificationFiltersSupported = false; $scope.notificationFiltersSupported = false;
$scope.expandAll = false;
$scope.remove = function (application) { $scope.remove = function (application) {
application.$remove(); 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.order = { $scope.order = {
column: 'name', column: 'name',
descending: false descending: false
......
...@@ -74,6 +74,24 @@ ...@@ -74,6 +74,24 @@
text-decoration: none; text-decoration: none;
} }
.application-list {
background-color: #f9f9f9;
}
.table .group-column {
border-right: 1px solid #dddddd;
color: #999999;
vertical-align: middle;
text-align: center;
cursor: pointer;
}
.table .group-column:hover {
background-color: #c7c7c7;
color: #f9f9f9;
}
/* ---------- */ /* ---------- */
.sortable { .sortable {
......
...@@ -19,6 +19,9 @@ var yaml = require('js-yaml'); ...@@ -19,6 +19,9 @@ var yaml = require('js-yaml');
module.exports = function () { module.exports = function () {
return function (input) { return function (input) {
if (Object.keys(input).length === 0) {
return '';
}
return yaml.dump(input, { return yaml.dump(input, {
skipInvalid: true, skipInvalid: true,
sort: true, sort: true,
......
...@@ -24,6 +24,7 @@ module.controller('applicationsCtrl', require('./controllers/applicationsCtrl.js ...@@ -24,6 +24,7 @@ module.controller('applicationsCtrl', require('./controllers/applicationsCtrl.js
module.controller('applicationsHeaderCtrl', require('./controllers/applicationsHeaderCtrl.js')); module.controller('applicationsHeaderCtrl', require('./controllers/applicationsHeaderCtrl.js'));
module.service('Application', require('./services/application.js')); module.service('Application', require('./services/application.js'));
module.service('ApplicationGroups', require('./services/applicationGroups.js'));
module.service('Notification', require('./services/notification.js')); module.service('Notification', require('./services/notification.js'));
module.service('NotificationFilters', require('./services/notificationFilters.js')); module.service('NotificationFilters', require('./services/notificationFilters.js'));
module.service('ApplicationViews', require('./services/applicationViews.js')); module.service('ApplicationViews', require('./services/applicationViews.js'));
...@@ -36,6 +37,7 @@ module.component('sbaAccordion', require('./components/accordion.js')); ...@@ -36,6 +37,7 @@ module.component('sbaAccordion', require('./components/accordion.js'));
module.component('sbaAccordionGroup', require('./components/accordionGroup.js')); module.component('sbaAccordionGroup', require('./components/accordionGroup.js'));
module.component('sbaNotificationSettings', require('./components/notificationSettings.js')); module.component('sbaNotificationSettings', require('./components/notificationSettings.js'));
module.component('sbaPopover', require('./components/popover.js')); module.component('sbaPopover', require('./components/popover.js'));
module.component('sbaLimitedText', require('./components/limitedText.js'));
require('./css/module.css'); require('./css/module.css');
...@@ -59,23 +61,15 @@ module.config(function ($stateProvider) { ...@@ -59,23 +61,15 @@ module.config(function ($stateProvider) {
}); });
}); });
module.run(function ($rootScope, $state, Notification, Application, MainViews) { module.run(function ($rootScope, $state, Notification, Application, ApplicationGroups, MainViews) {
MainViews.register({ MainViews.register({
title: 'Applications', title: 'Applications',
state: 'applications-list', state: 'applications-list',
order: -100 order: -100
}); });
$rootScope.applications = []; var applicationGroups = new ApplicationGroups();
$rootScope.applicationGroups = applicationGroups;
$rootScope.indexOfApplication = function (id) {
for (var i = 0; i < $rootScope.applications.length; i++) {
if ($rootScope.applications[i].id === id) {
return i;
}
}
return -1;
};
var refresh = function (application) { var refresh = function (application) {
application.info = {}; application.info = {};
...@@ -93,7 +87,7 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) { ...@@ -93,7 +87,7 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
Application.query(function (applications) { Application.query(function (applications) {
for (var i = 0; i < applications.length; i++) { for (var i = 0; i < applications.length; i++) {
refresh(applications[i]); refresh(applications[i]);
$rootScope.applications.push(applications[i]); applicationGroups.addApplication(applications[i]);
} }
}); });
...@@ -103,6 +97,7 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) { ...@@ -103,6 +97,7 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
var event = JSON.parse(message.data); var event = JSON.parse(message.data);
Object.setPrototypeOf(event.application, Application.prototype); Object.setPrototypeOf(event.application, Application.prototype);
var title = event.application.name;
var options = { var options = {
tag: event.application.id, tag: event.application.id,
body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl, body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl,
...@@ -112,31 +107,19 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) { ...@@ -112,31 +107,19 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
id: event.application.id id: event.application.id
}) })
}; };
var title = event.application.name;
var index = $rootScope.indexOfApplication(event.application.id);
if (event.type === 'REGISTRATION') { if (event.type === 'REGISTRATION') {
if (index === -1) { applicationGroups.addApplication(event.application, false);
$rootScope.applications.push(event.application); refresh(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') {
if (index > -1) { applicationGroups.removeApplication(event.application);
$rootScope.applications.splice(index, 1);
}
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') {
refresh(event.application); refresh(event.application);
if (index > -1) { applicationGroups.addApplication(event.application, true);
$rootScope.applications[index] = event.application;
} else {
$rootScope.applications.push(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');
...@@ -146,5 +129,4 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) { ...@@ -146,5 +129,4 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
$rootScope.$apply(); $rootScope.$apply();
Notification.notify(title, options); Notification.notify(title, options);
}; };
}); });
/*
* Copyright 2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
module.exports = function () {
'ngInject';
return function () {
this.groups = [];
var getMaxStatus = function (statusCounter) {
var order = ['OFFLINE', 'DOWN', 'OUT_OF_SERVICE', 'UNKNOWN', 'UP'];
for (var i = 0; i < order.length; i++) {
if (statusCounter[order[i]] && statusCounter[order[i]] > 0) {
return order[i];
}
}
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) {
var statusCounter = {};
group.applications.forEach(function (application) {
statusCounter[application.statusInfo.status] = ++statusCounter[application.statusInfo.status] || 1;
});
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: {}, 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);
} else if (overwrite) {
group.applications[index] = application;
}
updateStatus(group);
};
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);
}
}
};
};
};
<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" /> <input placeholder="Filter" class="input-xxlarge" type="search" ng-model="searchFilter" ng-keypress="toggleExpandAll(true)" />
</div> </div>
<table class="table table-hover"> <table class="table application-list">
<thead> <thead>
<tr> <tr>
<th><span class="sortable" ng-class="orderByCssClass('name')" ng-click="orderBy('name')">Application</span> / <th class="group-column" title="{{expandAll ? 'collapse' : 'expand'}} all" ng-click="toggleExpandAll()"><i class="fa" ng-class="{'fa-plus': !expandAll, 'fa-minus': expandAll}"></i></th>
<span class="sortable" ng-class="orderByCssClass('healthUrl')" ng-click="orderBy('healthUrl')">URL</span> <th><span class="sortable" ng-class="orderByCssClass('name')" ng-click="orderBy('name')">Application</span> / <span class="sortable" ng-class="orderByCssClass('healthUrl')" ng-click="orderBy('healthUrl')">URL</span></th>
</th>
<th><span class="sortable" ng-class="orderByCssClass('info.version')" ng-click="orderBy('info.version')">Version</span></th> <th><span class="sortable" ng-class="orderByCssClass('info.version')" ng-click="orderBy('info.version')">Version</span></th>
<th>Info</th> <th>Info</th>
<th colspan="3">Status</th> <th>Status</th>
<th colspan="2"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-repeat="application in applications|orderBy:order.column:order.descending|orderBy:'statusInfo.status':false|filter:searchFilter track by application.id" ng-init="views = viewsForApplication(application)"> <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 && !expandAll" ng-show="group.collapsed">
<td>{{ application.name }} ({{ application.id }}) <td class="group-column" ng-click="group.collapsed = false"><i class="fa fa-plus"></i></td>
<br/><span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td> <td colspan="3" ng-bind="group.name"></td>
<td>
<span ng-repeat-start="(status, count) in group.statusCounter track by status" class="status-{{status}}" ng-bind="count + ' ' + status"></span><span ng-repeat-end ng-hide="$last"> / </span>
</td>
<th colspan="2"></th>
</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-init="views = viewsForApplication(application)" 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>
<td>{{ application.name }} ({{ application.id }})<br/>
<span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td>
<td>{{ application.version }}</td> <td>{{ application.version }}</td>
<td><pre class="pre-scrollable" style="resize: vertical; height: 62px;" ng-bind-html="application.info | yaml | linkify:60"></pre></td> <td>
<td><span class="status-{{application.statusInfo.status}}" title="{{application.statusInfo.timestamp | date:'dd.MM.yyyy HH:mm:ss' }}">{{ application.statusInfo.status }}</span> <sba-limited-text max-lines="3" bind-html="application.info | yaml | linkify:60"></sba-limited-text>
</td>
<td><span class="status-{{application.statusInfo.status}}" title="{{application.statusInfo.timestamp | date:'dd.MM.yyyy HH:mm:ss' }}" ng-bind="application.statusInfo.status"></span>
<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>
......
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