Commit a0cc96c7 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 cbb2dcb4
...@@ -15,80 +15,76 @@ ...@@ -15,80 +15,76 @@
*/ */
(function (sbaModules, angular) { (function (sbaModules, angular) {
'use strict'; 'use strict';
var module = angular.module('sba-applications-activiti', ['sba-applications']); var module = angular.module('sba-applications-activiti', ['sba-applications']);
sbaModules.push(module.name); sbaModules.push(module.name);
module.controller('activitiCtrl', ['$scope', '$http', 'application', function ($scope, $http, application) { module.controller('activitiCtrl', ['$scope', '$http', 'application', function ($scope, $http, application) {
$scope.application = application; $scope.application = application;
$http.get('api/applications/' + application.id + '/activiti').then(function (response) { $http.get('api/applications/' + application.id + '/activiti').then(function (response) {
var activiti = response.data; var activiti = response.data;
$scope.summary = []; $scope.summary = [];
$scope.summary.push({ $scope.summary.push({
key: 'Completed Task Count Today', key: 'Completed Task Count Today',
value: activiti.completedTaskCountToday value: activiti.completedTaskCountToday
}); });
$scope.summary.push({ $scope.summary.push({
key: 'Process Definition Count', key: 'Process Definition Count',
value: activiti.processDefinitionCount value: activiti.processDefinitionCount
}); });
$scope.summary.push({ $scope.summary.push({
key: 'Cached Process Definition Count', key: 'Cached Process Definition Count',
value: activiti.cachedProcessDefinitionCount value: activiti.cachedProcessDefinitionCount
}); });
$scope.summary.push({ $scope.summary.push({
key: 'Completed Task Count', key: 'Completed Task Count',
value: activiti.completedTaskCount value: activiti.completedTaskCount
}); });
$scope.summary.push({ $scope.summary.push({
key: 'Completed Activities', key: 'Completed Activities',
value: activiti.completedActivities value: activiti.completedActivities
}); });
$scope.summary.push({ $scope.summary.push({
key: 'Open Task Count', key: 'Open Task Count',
value: activiti.openTaskCount value: activiti.openTaskCount
}); });
$scope.processes = []; $scope.processes = [];
for (var i = 0; i < activiti.deployedProcessDefinitions.length; i++) { for (var i = 0; i < activiti.deployedProcessDefinitions.length; i++) {
var process = activiti.deployedProcessDefinitions[i]; var process = activiti.deployedProcessDefinitions[i];
var runningProcessInstanceCount = activiti.runningProcessInstanceCount[process]; var runningProcessInstanceCount = activiti.runningProcessInstanceCount[process];
var completedProcessInstanceCount = activiti.completedProcessInstanceCount[process]; var completedProcessInstanceCount = activiti.completedProcessInstanceCount[process];
$scope.processes.push({ $scope.processes.push({
name: process, name: process,
running: runningProcessInstanceCount, running: runningProcessInstanceCount,
completed: completedProcessInstanceCount completed: completedProcessInstanceCount
});
}
}).catch(function (response) {
$scope.error = response.data;
}); });
}]); }
}).catch(function (response) {
$scope.error = response.data;
});
}]);
module.config(['$stateProvider', function ($stateProvider) { module.config(['$stateProvider', function ($stateProvider) {
$stateProvider.state('applications.activiti', { $stateProvider.state('applications.activiti', {
url: '/activiti', url: '/activiti',
templateUrl: 'applications-activiti/activiti.html', templateUrl: 'applications-activiti/activiti.html',
controller: 'activitiCtrl' 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;
}
return $http.get('api/applications/' + application.id + '/configprops').then( module.run(['ApplicationViews', '$http', function (ApplicationViews, $http) {
function (response) { ApplicationViews.register({
return response.data.processEngineEndpoint !== undefined; order: 100,
}).catch(function () { title: 'Activiti',
return false; 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)); } (sbaModules, angular));
...@@ -59,9 +59,6 @@ module.run(function (ApplicationViews, $sce, $q, $http) { ...@@ -59,9 +59,6 @@ module.run(function (ApplicationViews, $sce, $q, $http) {
title: $sce.trustAsHtml('<i class="fa fa-gear fa-fw"></i>Hystrix'), title: $sce.trustAsHtml('<i class="fa fa-gear fa-fw"></i>Hystrix'),
state: 'applications.hystrix', state: 'applications.hystrix',
show: function (application) { show: function (application) {
if (!application.managementUrl || !application.statusInfo.status || application.statusInfo.status === 'OFFLINE') {
return false;
}
return isEventSourceAvailable('api/applications/' + application.id + '/hystrix.stream'); return isEventSourceAvailable('api/applications/' + application.id + '/hystrix.stream');
} }
}); });
......
...@@ -47,9 +47,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -47,9 +47,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 0, order: 0,
title: $sce.trustAsHtml('<i class="fa fa-info fa-fw"></i>Details'), title: $sce.trustAsHtml('<i class="fa fa-info fa-fw"></i>Details'),
state: 'applications.details', state: 'applications.details'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
...@@ -36,9 +36,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -36,9 +36,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 10, order: 10,
title: $sce.trustAsHtml('<i class="fa fa-server fa-fw"></i>Environment'), title: $sce.trustAsHtml('<i class="fa fa-server fa-fw"></i>Environment'),
state: 'applications.environment', state: 'applications.environment'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
...@@ -36,9 +36,6 @@ module.run(function (ApplicationViews, $http, $sce) { ...@@ -36,9 +36,6 @@ module.run(function (ApplicationViews, $http, $sce) {
title: $sce.trustAsHtml('<i class="fa fa-database fa-fw"></i>Flyway'), title: $sce.trustAsHtml('<i class="fa fa-database fa-fw"></i>Flyway'),
state: 'applications.flyway', state: 'applications.flyway',
show: function (application) { 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 $http.head('api/applications/' + application.id + '/flyway').then(function () {
return true; return true;
}).catch(function () { }).catch(function () {
......
...@@ -28,9 +28,6 @@ module.run(function ($sce, $http, ApplicationViews) { ...@@ -28,9 +28,6 @@ module.run(function ($sce, $http, ApplicationViews) {
href: 'api/applications/{id}/heapdump', href: 'api/applications/{id}/heapdump',
target: '_blank', target: '_blank',
show: function (application) { 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 $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 return response.headers('Allow') === 'GET,HEAD'; //Test the exact headers, in case the DispatcherServlet responses to the request for older boot-versions
}).catch(function () { }).catch(function () {
......
...@@ -44,9 +44,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -44,9 +44,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 40, order: 40,
title: $sce.trustAsHtml('<i class="fa fa-cogs fa-fw"></i>JMX'), title: $sce.trustAsHtml('<i class="fa fa-cogs fa-fw"></i>JMX'),
state: 'applications.jmx', state: 'applications.jmx'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $http, $sce) { ...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $http, $sce) {
title: $sce.trustAsHtml('<i class="fa fa-database fa-fw"></i>Liquibase'), title: $sce.trustAsHtml('<i class="fa fa-database fa-fw"></i>Liquibase'),
state: 'applications.liquibase', state: 'applications.liquibase',
show: function (application) { 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 $http.head('api/applications/' + application.id + '/liquibase').then(function () {
return true; return true;
}).catch(function () { }).catch(function () {
......
...@@ -40,9 +40,6 @@ module.run(function (ApplicationViews, $sce, $http) { ...@@ -40,9 +40,6 @@ module.run(function (ApplicationViews, $sce, $http) {
title: $sce.trustAsHtml('<i class="fa fa-file-text-o fa-fw"></i>Log'), title: $sce.trustAsHtml('<i class="fa fa-file-text-o fa-fw"></i>Log'),
state: 'applications.logfile', state: 'applications.logfile',
show: function (application) { 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 $http.head('api/applications/' + application.id + '/logfile').then(function () {
return true; return true;
}).catch(function () { }).catch(function () {
......
...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 30, order: 30,
title: $sce.trustAsHtml('<i class="fa fa-sliders fa-fw"></i>Logging'), title: $sce.trustAsHtml('<i class="fa fa-sliders fa-fw"></i>Logging'),
state: 'applications.logging', state: 'applications.logging'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 5, order: 5,
title: $sce.trustAsHtml('<i class="fa fa-bar-chart fa-fw"></i>Metrics'), title: $sce.trustAsHtml('<i class="fa fa-bar-chart fa-fw"></i>Metrics'),
state: 'applications.metrics', state: 'applications.metrics'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -37,9 +37,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 50, order: 50,
title: $sce.trustAsHtml('<i class="fa fa-list fa-fw"></i>Threads'), title: $sce.trustAsHtml('<i class="fa fa-list fa-fw"></i>Threads'),
state: 'applications.threads', state: 'applications.threads'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
...@@ -38,9 +38,6 @@ module.run(function (ApplicationViews, $sce) { ...@@ -38,9 +38,6 @@ module.run(function (ApplicationViews, $sce) {
ApplicationViews.register({ ApplicationViews.register({
order: 60, order: 60,
title: $sce.trustAsHtml('<i class="fa fa-eye fa-fw"></i>Trace'), title: $sce.trustAsHtml('<i class="fa fa-eye fa-fw"></i>Trace'),
state: 'applications.trace', state: 'applications.trace'
show: function (application) {
return application.managementUrl && application.statusInfo.status !== null && application.statusInfo.status !== 'OFFLINE';
}
}); });
}); });
/*
* 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 @@ ...@@ -15,7 +15,7 @@
*/ */
'use strict'; 'use strict';
module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) { module.exports = function ($rootScope, $scope, $state, NotificationFilters) {
'ngInject'; 'ngInject';
$scope.notificationFilters = null; $scope.notificationFilters = null;
$scope.notificationFiltersSupported = false; $scope.notificationFiltersSupported = false;
...@@ -37,10 +37,6 @@ module.exports = function ($rootScope, $scope, $state, ApplicationViews, Notific ...@@ -37,10 +37,6 @@ module.exports = function ($rootScope, $scope, $state, ApplicationViews, Notific
descending: false descending: false
}; };
$scope.viewsForApplication = function (application) {
return ApplicationViews.getApplicationViews(application);
};
$scope.orderBy = function (column) { $scope.orderBy = function (column) {
if (column === $scope.order.column) { if (column === $scope.order.column) {
$scope.order.descending = !$scope.order.descending; $scope.order.descending = !$scope.order.descending;
......
...@@ -19,5 +19,11 @@ module.exports = function ($scope, application, ApplicationViews) { ...@@ -19,5 +19,11 @@ module.exports = function ($scope, application, ApplicationViews) {
'ngInject'; 'ngInject';
$scope.application = application; $scope.application = application;
$scope.views = ApplicationViews.getApplicationViews(application);
var views = ApplicationViews.getApplicationViews(application);
views.resolve();
$scope.views = views.views;
}; };
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
background-color: #666; background-color: #666;
border: 0; border: 0;
padding: 9px 13px 8px 13px; 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,
.header--application .nav-tabs > .active > a:hover, .header--application .nav-tabs > .active > a:hover,
...@@ -46,26 +46,23 @@ ...@@ -46,26 +46,23 @@
border-top: 4px solid #6db33f; border-top: 4px solid #6db33f;
padding: 5px 12px 8px 12px; padding: 5px 12px 8px 12px;
} }
.header--application .application--status {
font-size: 24px;
line-height: 24px;
margin: 20px;
}
.header--application .application--title { .header--application .application--title {
color: #f1f1f1; color: #f1f1f1;
font-size: 24px; font-size: 24px;
line-height: 24px; line-height: 24px;
margin: 20px 20px 20px 0; margin: 20px 20px 0 0;
display: inline-block;
} }
.application--urls { .application--urls {
list-style: none; list-style: none;
text-align: right; text-align: right;
font-size: 14px; font-size: 14px;
color: #888; color: #888;
margin-top: 20px; margin: 5px 0 5px 0;
height: 0; display: block;
float: right;
} }
.application--urls > li >a { .application--urls > li > a {
color: #f1f1f1; color: #f1f1f1;
} }
.application--urls > li > a:hover, .application--urls > li > a:hover,
......
...@@ -39,6 +39,7 @@ module.component('sbaNotificationSettings', require('./components/notificationSe ...@@ -39,6 +39,7 @@ module.component('sbaNotificationSettings', require('./components/notificationSe
module.component('sbaPopover', require('./components/popover.js')); module.component('sbaPopover', require('./components/popover.js'));
module.component('sbaLimitedText', require('./components/limitedText.js')); module.component('sbaLimitedText', require('./components/limitedText.js'));
module.component('sbaStatusInfo', require('./components/statusInfo.js')); module.component('sbaStatusInfo', require('./components/statusInfo.js'));
module.component('sbaBtnDetailViews', require('./components/btnDetailViews.js'));
require('./css/module.css'); require('./css/module.css');
...@@ -74,6 +75,10 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG ...@@ -74,6 +75,10 @@ module.run(function ($rootScope, $state, Notification, Application, ApplicationG
var refresh = function (group, application) { var refresh = function (group, application) {
application.info = {}; application.info = {};
if (application.statusInfo.status === 'OFFLINE') {
return;
}
application.refreshing = true; application.refreshing = true;
application.getInfo().then(function (response) { application.getInfo().then(function (response) {
var info = response.data; var info = response.data;
......
...@@ -15,38 +15,53 @@ ...@@ -15,38 +15,53 @@
*/ */
'use strict'; 'use strict';
var angular = require('angular');
module.exports = function ($state, $q) { module.exports = function ($state, $q) {
'ngInject'; 'ngInject';
var views = []; var views = [];
this.register = function (view) { this.register = function (view) {
views.push(view); views.push(view);
views.sort(function (v1, v2) {
return (v1.order || 0) - (v2.order || 0);
});
}; };
this.getApplicationViews = function (application) { var instantiateView = function (view, application) {
var applicationViews = []; var appView = {
order: view.order,
show: view.show || true,
title: view.title
};
views.forEach(function (view) { if (view.state) {
$q.when(!view.show || view.show(application)).then(function (result) { appView.href = $state.href(view.state, {
if (result) { id: application.id
var appView = angular.copy(view); });
if (view.state) { } else {
appView.href = $state.href(view.state, { appView.href = view.href.replace('{id}', application.id);
id: application.id appView.target = '_blank';
}); }
} else {
appView.href = view.href.replace('{id}', application.id); return appView;
appView.target = '_blank'; };
}
applicationViews.push(appView); this.getApplicationViews = function (application) {
applicationViews.sort(function (v1, v2) { var result = views.map(function (view) {
return (v1.order || 0) - (v2.order || 0); 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="header--application">
<div class="navbar-inner"> <div class="navbar-inner">
<div class="container-fluid"> <div class="container-fluid">
<div class="application--status pull-left"> <div class="application--title">
<div class="status-{{application.statusInfo.status}}" ng-bind="application.statusInfo.status"></div> <span class="status-{{application.statusInfo.status}}" ng-bind="application.statusInfo.status"></span>
{{application.name}} <small class="muted">({{application.id}})</small>
</div> </div>
<div class="application--title pull-left">{{application.name}} <small class="muted">({{application.id}})</small></div> <ul class="application--urls">
<ul class="application--urls pull-right">
<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.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.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> <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,7 +13,7 @@ ...@@ -13,7 +13,7 @@
</div> </div>
<div class="container-fluid"> <div class="container-fluid">
<ul class="nav nav-tabs"> <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> </ul>
</div> </div>
</div> </div>
......
...@@ -49,18 +49,8 @@ ...@@ -49,18 +49,8 @@
</td> </td>
<td> <td>
<sba-notification-settings ng-if="notificationFilters" application="application" filters="notificationFilters" refresh-callback="loadFilters()"></sba-notification-settings> <sba-notification-settings ng-if="notificationFilters" application="application" filters="notificationFilters" refresh-callback="loadFilters()"></sba-notification-settings>
<div class="btn-group"> <sba-btn-detail-views details-for="application"></sba-btn-detail-views>
<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> <div class="btn-group" title="remove">
<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" ng-if="application.source == 'http-api'">
<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>
</div> </div>
</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