Commit 6e3a6c00 by Johannes Edmeier

Add ui for managing notification filters

parent 46e319ee
...@@ -11,5 +11,8 @@ target/ ...@@ -11,5 +11,8 @@ target/
*.iml *.iml
*.iws *.iws
#vscode
.vscode/
# gnupg keyring # gnupg keyring
/.gnupg /.gnupg
/*
* 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';
var angular = require('angular');
module.exports = {
bindings: {
application: '<application',
filters: '<filters',
refreshFilters: '&refreshCallback'
},
controller: function (NotificationFilters) {
'ngInject';
var ctrl = this;
ctrl.$onInit = function () {
ctrl.ttl = '-1';
ctrl.filterType = 'by-name';
};
ctrl.$onChanges = function () {
ctrl.activeFilters = NotificationFilters.getActiveFilters(ctrl.filters, ctrl.application);
ctrl.hasActiveFilter = !angular.equals({}, ctrl.activeFilters);
if (!ctrl.hasActiveFilter) {
ctrl.showPopover = false;
}
};
ctrl.removeFilter = function (filterId) {
return NotificationFilters.removeFilter(filterId).then(function () {
ctrl.refreshFilters();
});
};
ctrl.addFilter = function () {
var ttl = parseInt(ctrl.ttl);
if (ttl >= 0) {
ttl = ttl * 60000;
}
var promise = null;
switch (ctrl.filterType) {
case 'by-name':
promise = NotificationFilters.addFilterByName(ctrl.application.name, ttl);
break;
case 'by-id':
promise = NotificationFilters.addFilterById(ctrl.application.id, ttl);
break;
default:
return;
}
return promise.then(function () {
ctrl.showPopover = false;
ctrl.refreshFilters();
});
};
},
template: require('./notificationSettings.tpl.html')
};
\ No newline at end of file
<button class="btn" ng-click="$ctrl.showPopover = !$ctrl.showPopover">
<i class="fa fa-fw {{$ctrl.hasActiveFilter ? 'fa-bell-slash' : 'fa-bell muted'}}"></i>
</button>
<sba-popover popover-title="Notification Filters" popover-toggle="$ctrl.showPopover">
<button class="btn btn-link pull-right" ng-click="$ctrl.refreshFilters()"><i class="fa fa-fw fa-repeat"></i></button>
<table class="table">
<tr>
<th>Type</th>
<th>Expiry</th>
<th>remove</th>
</tr>
<tr ng-repeat="(key, value) in $ctrl.activeFilters">
<td ng-if="value.name">by name ({{value.name}})</td>
<td ng-if="value.id">by id ({{value.id}})</td>
<td ng-if="value.expiry >=0" ng-bind="value.expiry | date:'HH:mm:ss'"></td>
<td ng-if="value.expiry <0">unlimited</td>
<td><button class="btn btn-danger btn-small" ng-click="$ctrl.removeFilter(key)"><i class="fa fa-times"></i></button>
</tr>
<tr>
<td>
<select class="input-xlarge" ng-model="$ctrl.filterType">
<option value="{{'by-name'}}">by name ({{$ctrl.application.name}})</option>
<option value="{{'by-id'}}">by id ({{$ctrl.application.id}})</option>
</select>
</td>
<td>
<select class="input-small" ng-model="$ctrl.ttl">
<option value="{{'5'}}">5 min</option>
<option value="{{'15'}}">15 min</option>
<option value="{{'30'}}">30 min</option>
<option value="{{'60'}}">1 hr</option>
<option value="{{'-1'}}">unlimited</option>
</select>
</td>
<td>
<button class="btn btn-success btn-small" value="Add filter" ng-click="$ctrl.addFilter()"><i class="fa fa-plus"></i></button>
</td>
</tr>
</table>
</sba-popover>
\ No newline at end of file
...@@ -14,26 +14,31 @@ ...@@ -14,26 +14,31 @@
* limitations under the License. * limitations under the License.
*/ */
'use strict'; 'use strict';
var angular = require('angular');
module.exports = { module.exports = {
transclude: true,
bindings: { bindings: {
application: '<application', title: '@popoverTitle',
filters: '<filters' toggle: '<popoverToggle'
}, },
controller: function () { controllerAs: '$popover',
controller: function ($element) {
'ngInject';
var ctrl = this; var ctrl = this;
ctrl.offset = null;
var notificationActive = function () { ctrl.$onChanges = function (changes) {
var active = true; if (changes.toggle.currentValue) {
angular.forEach(ctrl.filters, function (value) { ctrl.offset = $element.parent().offset();
active = active && (value.id !== ctrl.application.id && value.name !== ctrl.application.name || value.expired); ctrl.offset.left -= (($element.children().first().outerWidth() - $element.parent().outerWidth()) / 2);
}); ctrl.offset.top += + (($element.parent().outerHeight()));
return active; }
}; };
ctrl.getCssClass = function () { ctrl.isVisible = function () {
return notificationActive() ? 'fa-bell muted' : 'fa-bell-slash '; return ctrl.toggle;
}; };
}, },
template: '<i class="fa {{$ctrl.getCssClass()}}"></i>' template: require('./popover.tpl.html')
}; };
<div class="popover fade bottom" ng-class="{'in' : $popover.isVisible()}" 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
...@@ -18,8 +18,10 @@ ...@@ -18,8 +18,10 @@
module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) { module.exports = function ($rootScope, $scope, $state, ApplicationViews, NotificationFilters) {
'ngInject'; 'ngInject';
$scope.applications = $rootScope.applications; $scope.applications = $rootScope.applications;
$scope.notificationFilters = {}; $scope.notificationFilters = null;
$scope.notificationFiltersSupported = false;
$scope.remove = function (application) { $scope.remove = function (application) {
application.$remove(); application.$remove();
...@@ -51,7 +53,10 @@ module.exports = function ($rootScope, $scope, $state, ApplicationViews, Notific ...@@ -51,7 +53,10 @@ module.exports = function ($rootScope, $scope, $state, ApplicationViews, Notific
} }
}; };
NotificationFilters.getFilters().then(function (filters) { $scope.loadFilters = function () {
$scope.notificationFilters = filters.data; return NotificationFilters.getFilters().then(function (filters) {
}); $scope.notificationFilters = filters;
});
};
$scope.loadFilters();
}; };
...@@ -133,3 +133,9 @@ ...@@ -133,3 +133,9 @@
.accordion-group { .accordion-group {
border: 1px solid #34302D; border: 1px solid #34302D;
} }
/* ---------- */
.popover {
max-width: none !important;
}
...@@ -34,7 +34,8 @@ module.filter('limitLines', require('./filters/limitLines.js')); ...@@ -34,7 +34,8 @@ module.filter('limitLines', require('./filters/limitLines.js'));
module.component('sbaInfoPanel', require('./components/infoPanel.js')); module.component('sbaInfoPanel', require('./components/infoPanel.js'));
module.component('sbaAccordion', require('./components/accordion.js')); module.component('sbaAccordion', require('./components/accordion.js'));
module.component('sbaAccordionGroup', require('./components/accordionGroup.js')); module.component('sbaAccordionGroup', require('./components/accordionGroup.js'));
module.component('sbaNotificationFilter', require('./components/notificationFilter.js')); module.component('sbaNotificationSettings', require('./components/notificationSettings.js'));
module.component('sbaPopover', require('./components/popover.js'));
require('./css/module.css'); require('./css/module.css');
......
...@@ -15,30 +15,47 @@ ...@@ -15,30 +15,47 @@
*/ */
'use strict'; 'use strict';
var angular = require('angular');
module.exports = function ($http) { module.exports = function ($http) {
'ngInject'; 'ngInject';
this.getFilters = function () { this.getFilters = function () {
return $http.get('api/notifications/filters'); return $http.get('api/notifications/filters').then(function (response) {
return response.data;
}).catch(function () {
return null;
});
};
this.getActiveFilters = function (filters, application) {
var appFilters = {};
angular.forEach(filters, function (value, key) {
if ((value.expired === false || value.expired === undefined) && (value.id === application.id || value.name === application.name)) {
appFilters[key] = value;
}
});
return appFilters;
}; };
this.addFilterByName = function (name, ttl) { this.addFilterByName = function (name, ttl) {
return $http.post('api/notifications/filters', { return $http.post('api/notifications/filters', null, {
name: name, params: {
ttl: ttl name: name,
ttl: ttl
}
}); });
}; };
this.addFilterById = function (id, ttl) { this.addFilterById = function (id, ttl) {
return $http.post('api/notifications/filters', { return $http.post('api/notifications/filters', null, {
id: id, params: {
ttl: ttl id: id,
ttl: ttl
}
}); });
}; };
this.removeFilter = function (id) { this.removeFilter = function (id) {
return $http.delete('api/notifications/filters', { return $http.delete('api/notifications/filters/' + id);
id: id
});
}; };
}; };
...@@ -34,13 +34,13 @@ ...@@ -34,13 +34,13 @@
<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>
<sba-notification-filter application="application" filters="notificationFilters"></sba-notification-filter> <sba-notification-settings ng-if="notificationFilters" application="application" filters="notificationFilters" refresh-callback="loadFilters()"></sba-notification-settings>
</td> </td>
<td> <td>
<div class="pull-right"> <div class="pull-right">
<div class="btn-group"> <div class="btn-group">
<a ng-if="application.capabilities.logfile && application.managementUrl && application.statusInfo.status != null && application.statusInfo.status != 'OFFLINE'" target="_blank" class="btn btn-success" ng-href="{{application.capabilities.logfile ? 'api/applications/' + application.id + '/logfile' :''}}"><i class="fa fa-file-text-o"></i> Log</a> <a ng-if="application.capabilities.logfile && application.managementUrl && application.statusInfo.status != null && application.statusInfo.status != 'OFFLINE'" target="_blank" class="btn btn-success" ng-href="{{application.capabilities.logfile ? 'api/applications/' + application.id + '/logfile' :''}}"><i class="fa fa-file-text-o"></i> Log</a>
<a ng-if="views.length > 0" ng-href="{{views[0].href}}" class="btn btn-success" ng-bind-html="views[0].title">/a> <a ng-if="views.length > 0" ng-href="{{views[0].href}}" class="btn btn-success" ng-bind-html="views[0].title"></a>
<a class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-if="views.length > 1"> <a class="btn btn-success dropdown-toggle" data-toggle="dropdown" ng-if="views.length > 1">
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
......
...@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.notify.filter.web; ...@@ -17,6 +17,7 @@ package de.codecentric.boot.admin.notify.filter.web;
import static org.springframework.util.StringUtils.hasText; import static org.springframework.util.StringUtils.hasText;
import java.util.Collections;
import java.util.Map; import java.util.Map;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
...@@ -53,11 +54,13 @@ public class NotificationFilterController { ...@@ -53,11 +54,13 @@ public class NotificationFilterController {
@RequestMapping(path = "/api/notifications/filters", method = { @RequestMapping(path = "/api/notifications/filters", method = {
RequestMethod.POST }, produces = MimeTypeUtils.APPLICATION_JSON_VALUE) RequestMethod.POST }, produces = MimeTypeUtils.APPLICATION_JSON_VALUE)
public ResponseEntity<String> addFilter(@RequestParam(name = "id", required = false) String id, public ResponseEntity<?> addFilter(@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "name", required = false) String name, @RequestParam(name = "name", required = false) String name,
@RequestParam(name = "ttl", required = false, defaultValue = "-1") long ttl) { @RequestParam(name = "ttl", required = false, defaultValue = "-1") long ttl) {
if (hasText(id) || hasText(name)) { if (hasText(id) || hasText(name)) {
return ResponseEntity.ok(filteringNotifier.addFilter(createFilter(id, name, ttl))); NotificationFilter filter = createFilter(id, name, ttl);
String filterId = filteringNotifier.addFilter(filter);
return ResponseEntity.ok(Collections.singletonMap(filterId, filter));
} else { } else {
return ResponseEntity.badRequest().body("Either 'id' or 'name' must be set"); return ResponseEntity.badRequest().body("Either 'id' or 'name' must be set");
} }
...@@ -78,6 +81,7 @@ public class NotificationFilterController { ...@@ -78,6 +81,7 @@ public class NotificationFilterController {
NotificationFilter filter = hasText(id) ? new ApplicationIdNotificationFilter(id, expiry) NotificationFilter filter = hasText(id) ? new ApplicationIdNotificationFilter(id, expiry)
: new ApplicationNameNotificationFilter(name, expiry); : new ApplicationNameNotificationFilter(name, expiry);
return filter; return filter;
} }
} }
...@@ -9,10 +9,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. ...@@ -9,10 +9,16 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import java.io.IOException;
import java.util.Map;
import org.junit.Test; import org.junit.Test;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.codecentric.boot.admin.notify.LoggingNotifier; import de.codecentric.boot.admin.notify.LoggingNotifier;
import de.codecentric.boot.admin.notify.filter.FilteringNotifier; import de.codecentric.boot.admin.notify.filter.FilteringNotifier;
...@@ -35,10 +41,12 @@ public class NotificationFilterControllerTest { ...@@ -35,10 +41,12 @@ public class NotificationFilterControllerTest {
@Test @Test
public void test_post_delete() throws Exception { public void test_post_delete() throws Exception {
String id = mvc.perform(post("/api/notifications/filters?id=1337&ttl=10000")) String response = mvc.perform(post("/api/notifications/filters?id=1337&ttl=10000"))
.andExpect(status().isOk()).andExpect(content().string(not(isEmptyString()))) .andExpect(status().isOk()).andExpect(content().string(not(isEmptyString())))
.andReturn().getResponse().getContentAsString(); .andReturn().getResponse().getContentAsString();
String id = extractId(response);
mvc.perform(get("/api/notifications/filters")).andExpect(status().isOk()) mvc.perform(get("/api/notifications/filters")).andExpect(status().isOk())
.andExpect(jsonPath("$..id").value("1337")); .andExpect(jsonPath("$..id").value("1337"));
...@@ -47,4 +55,9 @@ public class NotificationFilterControllerTest { ...@@ -47,4 +55,9 @@ public class NotificationFilterControllerTest {
mvc.perform(get("/api/notifications/filters")).andExpect(status().isOk()) mvc.perform(get("/api/notifications/filters")).andExpect(status().isOk())
.andExpect(jsonPath("$").isEmpty()); .andExpect(jsonPath("$").isEmpty());
} }
private String extractId(String response) throws JsonProcessingException, IOException {
Map<?, ?> map = new ObjectMapper().readerFor(Map.class).readValue(response);
return map.keySet().iterator().next().toString();
}
} }
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