Commit 32e1d9ba by Johannes Edmeier

Resolve dynamic views lazily

When the list contains a lots of applications many requests are made for dynamic views (e.g. heapdump, flyway, logfile, ...). With this commit the availability check for these views is deferred until the user opens toggles the dropdown. This should lead to far fewer requests. fixes #346 Additionally the url list is not anymore overlapped then having a lot of tabs for the detail views. fixes #347
parent 8d69cc6b
......@@ -15,80 +15,76 @@
*/
(function (sbaModules, angular) {
'use strict';
var module = angular.module('sba-applications-activiti', ['sba-applications']);
sbaModules.push(module.name);
'use strict';
var module = angular.module('sba-applications-activiti', ['sba-applications']);
sbaModules.push(module.name);
module.controller('activitiCtrl', ['$scope', '$http', 'application', function ($scope, $http, application) {
$scope.application = application;
$http.get('api/applications/' + application.id + '/activiti').then(function (response) {
var activiti = response.data;
$scope.summary = [];
$scope.summary.push({
key: 'Completed Task Count Today',
value: activiti.completedTaskCountToday
});
$scope.summary.push({
key: 'Process Definition Count',
value: activiti.processDefinitionCount
});
$scope.summary.push({
key: 'Cached Process Definition Count',
value: activiti.cachedProcessDefinitionCount
});
$scope.summary.push({
key: 'Completed Task Count',
value: activiti.completedTaskCount
});
$scope.summary.push({
key: 'Completed Activities',
value: activiti.completedActivities
});
$scope.summary.push({
key: 'Open Task Count',
value: activiti.openTaskCount
});
$scope.processes = [];
for (var i = 0; i < activiti.deployedProcessDefinitions.length; i++) {
var process = activiti.deployedProcessDefinitions[i];
var runningProcessInstanceCount = activiti.runningProcessInstanceCount[process];
var completedProcessInstanceCount = activiti.completedProcessInstanceCount[process];
$scope.processes.push({
name: process,
running: runningProcessInstanceCount,
completed: completedProcessInstanceCount
});
}
}).catch(function (response) {
$scope.error = response.data;
module.controller('activitiCtrl', ['$scope', '$http', 'application', function ($scope, $http, application) {
$scope.application = application;
$http.get('api/applications/' + application.id + '/activiti').then(function (response) {
var activiti = response.data;
$scope.summary = [];
$scope.summary.push({
key: 'Completed Task Count Today',
value: activiti.completedTaskCountToday
});
$scope.summary.push({
key: 'Process Definition Count',
value: activiti.processDefinitionCount
});
$scope.summary.push({
key: 'Cached Process Definition Count',
value: activiti.cachedProcessDefinitionCount
});
$scope.summary.push({
key: 'Completed Task Count',
value: activiti.completedTaskCount
});
$scope.summary.push({
key: 'Completed Activities',
value: activiti.completedActivities
});
$scope.summary.push({
key: 'Open Task Count',
value: activiti.openTaskCount
});
$scope.processes = [];
for (var i = 0; i < activiti.deployedProcessDefinitions.length; i++) {
var process = activiti.deployedProcessDefinitions[i];
var runningProcessInstanceCount = activiti.runningProcessInstanceCount[process];
var completedProcessInstanceCount = activiti.completedProcessInstanceCount[process];
$scope.processes.push({
name: process,
running: runningProcessInstanceCount,
completed: completedProcessInstanceCount
});
}]);
}
}).catch(function (response) {
$scope.error = response.data;
});
}]);
module.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('applications.activiti', {
url: '/activiti',
templateUrl: 'applications-activiti/activiti.html',
controller: 'activitiCtrl'
});
}]);
module.run(['ApplicationViews', '$http', function (ApplicationViews, $http) {
ApplicationViews.register({
order: 100,
title: 'Activiti',
state: 'applications.activiti',
show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
module.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('applications.activiti', {
url: '/activiti',
templateUrl: 'applications-activiti/activiti.html',
controller: 'activitiCtrl'
});
}]);
return $http.get('api/applications/' + application.id + '/configprops').then(
function (response) {
return response.data.processEngineEndpoint !== undefined;
}).catch(function () {
return false;
});
}
});
}]);
module.run(['ApplicationViews', '$http', function (ApplicationViews, $http) {
ApplicationViews.register({
order: 100,
title: 'Activiti',
state: 'applications.activiti',
show: function (application) {
return $http.get('api/applications/' + application.id + '/configprops').then(
function (response) {
return response.data.processEngineEndpoint !== undefined;
}).catch(function () {
return false;
});
}
});
}]);
} (sbaModules, angular));
......@@ -59,9 +59,6 @@ module.run(function (ApplicationViews, $sce, $q, $http) {
title: $sce.trustAsHtml('<i class="fa fa-gear fa-fw"></i>Hystrix'),
state: 'applications.hystrix',
show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return isEventSourceAvailable('api/applications/' + application.id + '/hystrix.stream');
}
});
......
......@@ -47,9 +47,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 0,
title: $sce.trustAsHtml('<i class="fa fa-info fa-fw"></i>Details'),
state: 'applications.details',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.details'
});
});
......@@ -36,9 +36,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 10,
title: $sce.trustAsHtml('<i class="fa fa-server fa-fw"></i>Environment'),
state: 'applications.environment',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.environment'
});
});
......@@ -36,9 +36,6 @@ module.run(function (ApplicationViews, $http, $sce) {
title: $sce.trustAsHtml('<i class="fa fa-database fa-fw"></i>Flyway'),
state: 'applications.flyway',
show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return $http.head('api/applications/' + application.id + '/flyway').then(function () {
return true;
}).catch(function () {
......
......@@ -28,9 +28,6 @@ module.run(function ($sce, $http, ApplicationViews) {
href: 'api/applications/{id}/heapdump',
target: '_blank',
show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return $http({ method: 'OPTIONS', url: 'api/applications/' + application.id + '/heapdump' }).then(function (response) {
return response.headers('Allow') === 'GET,HEAD'; //Test the exact headers, in case the DispatcherServlet responses to the request for older boot-versions
}).catch(function () {
......
......@@ -44,9 +44,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 40,
title: $sce.trustAsHtml('<i class="fa fa-cogs fa-fw"></i>JMX'),
state: 'applications.jmx',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.jmx'
});
});
......@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $http, $sce) {
title: $sce.trustAsHtml('<i class="fa fa-database fa-fw"></i>Liquibase'),
state: 'applications.liquibase',
show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return $http.head('api/applications/' + application.id + '/liquibase').then(function () {
return true;
}).catch(function () {
......
......@@ -40,9 +40,6 @@ module.run(function (ApplicationViews, $sce, $http) {
title: $sce.trustAsHtml('<i class="fa fa-file-text-o fa-fw"></i>Log'),
state: 'applications.logfile',
show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return $http.head('api/applications/' + application.id + '/logfile').then(function () {
return true;
}).catch(function () {
......
......@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 30,
title: $sce.trustAsHtml('<i class="fa fa-sliders fa-fw"></i>Logging'),
state: 'applications.logging',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.logging'
});
});
......@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 5,
title: $sce.trustAsHtml('<i class="fa fa-bar-chart fa-fw"></i>Metrics'),
state: 'applications.metrics',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.metrics'
});
});
......@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 50,
title: $sce.trustAsHtml('<i class="fa fa-list fa-fw"></i>Threads'),
state: 'applications.threads',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.threads'
});
});
......@@ -38,9 +38,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({
order: 60,
title: $sce.trustAsHtml('<i class="fa fa-eye fa-fw"></i>Trace'),
state: 'applications.trace',
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
state: 'applications.trace'
});
});
/*
* 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 = {
bindings: {
application: '<detailsFor'
},
controller: function (ApplicationViews) {
'ngInject';
var ctrl = this;
ctrl.primaryView = null;
ctrl.secondaryViews = [];
ctrl.resolveViews = null;
ctrl.$onChanges = function () {
var result = ApplicationViews.getApplicationViews(ctrl.application);
ctrl.primaryView = result.views[0];
ctrl.secondaryViews = result.views.slice(1);
ctrl.resolveViews = result.resolve;
};
},
template: require('./btnDetailViews.tpl.html')
};
<div ng-show="$ctrl.application.managementUrl &amp;&amp; $ctrl.application.statusInfo.status !== null &amp;&amp; $ctrl.application.statusInfo.status !== 'OFFLINE'"
class="btn-group">
<a ng-show="$ctrl.primaryView" ng-href="{{$ctrl.primaryView.href}}" target="{{$ctrl.primaryView.target}}" class="btn btn-success"
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">
<a ng-href="{{view.href}}" target="{{view.target}}" ng-bind-html="view.title"></a>
</li>
</ul>
</div>
\ No newline at end of file
......@@ -15,7 +15,7 @@
*/
'use strict';
module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) {
module.exports = function ($rootScope, $scope, $state, NotificationFilters) {
'ngInject';
$scope.notificationFilters = null;
$scope.notificationFiltersSupported = false;
......@@ -37,10 +37,6 @@ module.exports = function ($rootScope, $scope, $state, ApplicationViews, Notific
descending: false
};
$scope.viewsForApplication = function (application) {
return ApplicationViews.getApplicationViews(application);
};
$scope.orderBy = function (column) {
if (column === $scope.order.column) {
$scope.order.descending = !$scope.order.descending;
......
......@@ -19,5 +19,11 @@ module.exports = function ($scope, application, ApplicationViews) {
'ngInject';
$scope.application = application;
$scope.views = ApplicationViews.getApplicationViews(application);
var views = ApplicationViews.getApplicationViews(application);
views.resolve();
$scope.views = views.views;
};
......@@ -33,7 +33,7 @@
background-color: #666;
border: 0;
padding: 9px 13px 8px 13px;
border-bottom: 1px solid #34302D;
border-bottom: 1px solid transparent;
}
.header--application .nav-tabs > .active > a,
.header--application .nav-tabs > .active > a:hover,
......@@ -46,26 +46,23 @@
border-top: 4px solid #6db33f;
padding: 5px 12px 8px 12px;
}
.header--application .application--status {
font-size: 24px;
line-height: 24px;
margin: 20px;
}
.header--application .application--title {
color: #f1f1f1;
font-size: 24px;
line-height: 24px;
margin: 20px 20px 20px 0;
margin: 20px 20px 0 0;
display: inline-block;
}
.application--urls {
list-style: none;
text-align: right;
font-size: 14px;
color: #888;
margin-top: 20px;
height: 0;
margin: 5px 0 5px 0;
display: block;
float: right;
}
.application--urls > li >a {
.application--urls > li > a {
color: #f1f1f1;
}
.application--urls > li > a:hover,
......
......@@ -38,6 +38,7 @@ 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'));
module.component('sbaBtnDetailViews', require('./components/btnDetailViews.js'));
require('./css/module.css');
......@@ -73,6 +74,10 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
var refresh = function (group, application) {
application.info = {};
if (application.statusInfo.status === 'OFFLINE') {
return;
}
application.refreshing = true;
application.getInfo().then(function (response) {
var info = response.data;
......
......@@ -15,38 +15,53 @@
*/
'use strict';
var angular = require('angular');
module.exports = function ($state, $q) {
'ngInject';
var views = [];
this.register = function (view) {
views.push(view);
views.sort(function (v1, v2) {
return (v1.order || 0) - (v2.order || 0);
});
};
this.getApplicationViews = function (application) {
var applicationViews = [];
var instantiateView = function (view, application) {
var appView = {
order: view.order,
show: view.show || true,
title: view.title
};
views.forEach(function (view) {
$q.when(!view.show || view.show(application)).then(function (result) {
if (result) {
var appView = angular.copy(view);
if (view.state) {
appView.href = $state.href(view.state, {
id: application.id
});
} else {
appView.href = view.href.replace('{id}', application.id);
appView.target = '_blank';
}
if (view.state) {
appView.href = $state.href(view.state, {
id: application.id
});
} else {
appView.href = view.href.replace('{id}', application.id);
appView.target = '_blank';
}
return appView;
};
applicationViews.push(appView);
applicationViews.sort(function (v1, v2) {
return (v1.order || 0) - (v2.order || 0);
this.getApplicationViews = function (application) {
var result = views.map(function (view) {
return instantiateView(view, application);
});
var resolveDynamicViews = function () {
var deferred = $q.defer();
result.forEach(function (view) {
if (typeof view.show === 'function') {
$q.when(view.show(application)).then(function (result) {
view.show = result;
deferred.notify(view);
});
return deferred.promise;
}
});
});
return applicationViews;
};
return { views: result, resolve: resolveDynamicViews };
};
};
<div class="header--application">
<div class="navbar-inner">
<div class="container-fluid">
<div class="application--status pull-left">
<div class="status-{{application.statusInfo.status}}" ng-bind="application.statusInfo.status"></div>
</div>
<div class="application--title pull-left">{{application.name}} <small class="muted">({{application.id}})</small></div>
<ul class="application--urls pull-right">
<div class="application--title">
<span class="application--status status-{{application.statusInfo.status}}" ng-bind="application.statusInfo.status"></span> {{application.name}}
<small class="muted">({{application.id}})</small></div>
<ul class="application--urls">
<li ng-if="application.healthUrl"><a href="{{ application.healthUrl }}" target="_blank" title="Health URL">{{ application.healthUrl }}<i class="fa fa-heartbeat fa-fw" ></i></a></li>
<li ng-if="application.managementUrl"><a href="{{ application.managementUrl }}" target="_blank" title="Management URL">{{ application.managementUrl }}<i class="fa fa-wrench fa-fw"></i></a></li>
<li ng-if="application.serviceUrl"><a href="{{ application.serviceUrl }}" target="_blank" title="Service URL">{{ application.serviceUrl }}<i class="fa fa-home fa-fw" ></i></a></li>
......@@ -13,9 +12,9 @@
</div>
<div class="container-fluid">
<ul class="nav nav-tabs">
<li ng-repeat="view in views" ng-class="{active: $state.includes(view.state)}"><a ng-href="{{view.href}}" target="{{view.target}}"><span ng-bind-html="view.title"></span></a></li>
<li ng-repeat="view in views" ng-class="{active: $state.includes(view.state)}" ng-show="view.show"><a ng-href="{{view.href}}" target="{{view.target}}"><span ng-bind-html="view.title"></span></a></li>
</ul>
</div>
</div>
</div>
<div ui-view></div>
<div ui-view></div>
\ No newline at end of file
<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)"
/>
</div>
<table class="table application-list">
<col style="width: 30px; ">
<col style="width: 300px;">
<col style="width: 150px;">
<col style="width: auto;">
<col style="width: 100px;">
<col style="width: 250px;">
<thead>
<tr>
<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>Status</th>
<th></th>
</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 && !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>
<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-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-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 class="scroll">
<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>
<sba-notification-settings ng-if="notificationFilters" application="application" filters="notificationFilters" refresh-callback="loadFilters()"></sba-notification-settings>
<div class="btn-group">
<a ng-if="views.length > 0" ng-href="{{views[0].href}}" target="{{views[0].target}}" class="btn btn-success" ng-bind-html="views[0].title"></a>
<button class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-if="views.length > 1">
<i class="fa fa-caret-down"></i>
</button>
<ul class="dropdown-menu">
<li ng-repeat="view in views.slice(1)">
<a ng-href="{{view.href}}" target="{{view.target}}" ng-bind-html="view.title"></a>
</li>
</ul>
</div>
<div class="btn-group" title="remove">
<a class="btn btn-danger" ng-click="remove(application)"><i class="fa fa-times"></i></a>
</div>
</td>
</tr>
</tbody>
</table>
<h3>Spring Boot applications</h3>
<div>
<input placeholder="Filter" class="input-xxlarge" type="search" ng-model="searchFilter" ng-keypress="toggleExpandAll(true)"
/>
</div>
<table class="table application-list">
<col style="width: 30px; ">
<col style="width: 300px;">
<col style="width: 150px;">
<col style="width: auto;">
<col style="width: 100px;">
<col style="width: 250px;">
<thead>
<tr>
<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>Status</th>
<th></th>
</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 && !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>
<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-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-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 class="scroll">
<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>
<sba-notification-settings ng-if="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>
</div>
</td>
</tr>
</tbody>
</table>
</div>
\ No newline at end of file
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