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 @@
module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) {
'ngInject';
$scope.applications = $rootScope.applications;
$scope.notificationFilters = null;
$scope.notificationFiltersSupported = false;
$scope.expandAll = false;
$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.order = {
column: 'name',
descending: false
......
......@@ -74,6 +74,24 @@
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 {
......
......@@ -19,6 +19,9 @@ var yaml = require('js-yaml');
module.exports = function () {
return function (input) {
if (Object.keys(input).length === 0) {
return '';
}
return yaml.dump(input, {
skipInvalid: true,
sort: true,
......
......@@ -24,6 +24,7 @@ module.controller('applicationsCtrl', require('./controllers/applicationsCtrl.js
module.controller('applicationsHeaderCtrl', require('./controllers/applicationsHeaderCtrl.js'));
module.service('Application', require('./services/application.js'));
module.service('ApplicationGroups', require('./services/applicationGroups.js'));
module.service('Notification', require('./services/notification.js'));
module.service('NotificationFilters', require('./services/notificationFilters.js'));
module.service('ApplicationViews', require('./services/applicationViews.js'));
......@@ -36,6 +37,7 @@ module.component('sbaAccordion', require('./components/accordion.js'));
module.component('sbaAccordionGroup', require('./components/accordionGroup.js'));
module.component('sbaNotificationSettings', require('./components/notificationSettings.js'));
module.component('sbaPopover', require('./components/popover.js'));
module.component('sbaLimitedText', require('./components/limitedText.js'));
require('./css/module.css');
......@@ -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({
title: 'Applications',
state: 'applications-list',
order: -100
});
$rootScope.applications = [];
$rootScope.indexOfApplication = function (id) {
for (var i = 0; i < $rootScope.applications.length; i++) {
if ($rootScope.applications[i].id === id) {
return i;
}
}
return -1;
};
var applicationGroups = new ApplicationGroups();
$rootScope.applicationGroups = applicationGroups;
var refresh = function (application) {
application.info = {};
......@@ -93,7 +87,7 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
Application.query(function (applications) {
for (var i = 0; i < applications.length; 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) {
var event = JSON.parse(message.data);
Object.setPrototypeOf(event.application, Application.prototype);
var title = event.application.name;
var options = {
tag: event.application.id,
body: 'Instance ' + event.application.id + '\n' + event.application.healthUrl,
......@@ -112,31 +107,19 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
id: event.application.id
})
};
var title = event.application.name;
var index = $rootScope.indexOfApplication(event.application.id);
if (event.type === 'REGISTRATION') {
if (index === -1) {
$rootScope.applications.push(event.application);
}
applicationGroups.addApplication(event.application, false);
refresh(event.application);
title += ' instance registered.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'DEREGISTRATION') {
if (index > -1) {
$rootScope.applications.splice(index, 1);
}
applicationGroups.removeApplication(event.application);
title += ' instance removed.';
options.tag = event.application.id + '-REGISTRY';
} else if (event.type === 'STATUS_CHANGE') {
refresh(event.application);
if (index > -1) {
$rootScope.applications[index] = event.application;
} else {
$rootScope.applications.push(event.application);
}
applicationGroups.addApplication(event.application, true);
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');
......@@ -146,5 +129,4 @@ module.run(function ($rootScope, $state, Notification, Application, MainViews) {
$rootScope.$apply();
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">
<h3>Spring Boot applications</h3>
<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>
<table class="table table-hover">
<table class="table application-list">
<thead>
<tr>
<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 class="group-column" title="{{expandAll ? 'collapse' : 'expand'}} all" ng-click="toggleExpandAll()"><i class="fa" ng-class="{'fa-plus': !expandAll, 'fa-minus': expandAll}"></i></th>
<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><span class="sortable" ng-class="orderByCssClass('info.version')" ng-click="orderBy('info.version')">Version</span></th>
<th>Info</th>
<th colspan="3">Status</th>
<th>Status</th>
<th colspan="2"></th>
</tr>
</thead>
<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)">
<td>{{ application.name }} ({{ application.id }})
<br/><span class="muted">{{ application.serviceUrl || application.managementUrl || application.healthUrl }}</span></td>
<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 class="group-column" ng-click="group.collapsed = false"><i class="fa fa-plus"></i></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><pre class="pre-scrollable" style="resize: vertical; height: 62px;" ng-bind-html="application.info | yaml | linkify:60"></pre></td>
<td><span class="status-{{application.statusInfo.status}}" title="{{application.statusInfo.timestamp | date:'dd.MM.yyyy HH:mm:ss' }}">{{ application.statusInfo.status }}</span>
<td>
<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>
</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