Commit 4c56589a by Johannes Edmeier

Display notifications and red favicon when applications goes down

parent 1c5ba86d
......@@ -49,7 +49,7 @@
"clean-webpack-plugin": "^0.1.17",
"cross-env": "^5.1.3",
"css-hot-loader": "^1.3.5",
"css-loader": "^0.28.8",
"css-loader": "^0.28.9",
"css-mqpacker": "^6.0.1",
"eslint": "^4.15.0",
"eslint-plugin-html": "^4.0.1",
......@@ -57,10 +57,10 @@
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.6",
"glob": "^7.1.2",
"html-loader": "^0.5.4",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.30.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^22.1.1",
"jest": "^22.1.3",
"jest-vue": "^0.8.2",
"lodash-webpack-plugin": "^0.11.4",
"node-sass": "^4.7.2",
......
This diff was suppressed by a .gitattributes entry.
......@@ -18,23 +18,13 @@ import moment from 'moment';
import Vue from 'vue';
import VueRouter from 'vue-router';
import './assets/css/base.scss';
import './assets/img/favicon.png';
import logoDanger from './assets/img/favicon-danger.png';
import logoOk from './assets/img/favicon.png';
import sbaComponents from './components'
import Store from './store';
import FontAwesomeIcon from './utils/fontawesome';
import sbaAbout from './views/about';
import sbaApplications from './views/applications';
import sbaInstancesAuditevents from './views/instances/auditevents';
import sbaInstancesDetails from './views/instances/details';
import sbaInstancesEnv from './views/instances/env';
import sbaInstancesFlyway from './views/instances/flyway';
import sbaInstancesLiquibase from './views/instances/liquibase';
import sbaInstancesLogfile from './views/instances/logfile';
import sbaInstancesLoggers from './views/instances/loggers';
import sbaInstancesSessions from './views/instances/sessions';
import sbaInstancesShell from './views/instances/shell';
import sbaInstancesThreaddump from './views/instances/threaddump';
import sbaInstancesTrace from './views/instances/trace';
import sbaJournal from './views/journal';
import Notifications from './utils/notifications';
import createViews from './views';
import sbaShell from './views/shell';
moment.locale(window.navigator.language);
......@@ -47,127 +37,24 @@ const router = new VueRouter({
linkActiveClass: 'is-active'
});
const views = [];
views.register = view => {
if (view.template) {
views.push(view);
}
Notifications.requestPermissions();
if (view.component) {
router.addRoutes([{
path: view.path,
children: view.children,
component: view.component,
props: view.props,
name: view.name
}]
)
}
};
const store = new Store();
views.register({
path: '/applications',
name: 'applications',
template: 'Applications',
order: 0,
component: sbaApplications
});
views.register({path: '/journal', name: 'journal', template: 'Journal', order: 100, component: sbaJournal});
views.register({path: '/about', name: 'about', template: 'About', order: 200, component: sbaAbout});
views.register({
path: '/instances/:instanceId', component: sbaInstancesShell, props: true,
children: [{
path: '', component: sbaInstancesDetails, props: true, name: 'instance/details',
}, {
path: 'env', component: sbaInstancesEnv, props: true, name: 'instance/env',
}, {
path: 'logfile', component: sbaInstancesLogfile, props: true, name: 'instance/logfile',
}, {
path: 'loggers', component: sbaInstancesLoggers, props: true, name: 'instance/loggers',
}, {
path: 'trace', component: sbaInstancesTrace, props: true, name: 'instance/trace',
}, {
path: 'auditevents', component: sbaInstancesAuditevents, props: true, name: 'instance/auditevents',
}, {
path: 'sessions/:sessionId?', component: sbaInstancesSessions, props: true, name: 'instance/sessions',
}, {
path: 'liquibase', component: sbaInstancesLiquibase, props: true, name: 'instance/liquibase',
}, {
path: 'flyway', component: sbaInstancesFlyway, props: true, name: 'instance/flyway',
}, {
path: 'threaddump', component: sbaInstancesThreaddump, props: true, name: 'instance/threaddump',
}]
});
views.register({
name: 'instance/details',
template: 'Details',
order: 0,
});
views.register({
name: 'instance/env',
template: 'Environment',
order: 100,
isActive: ({instance}) => instance.hasEndpoint('env')
});
views.register({
name: 'instance/logfile',
template: 'Logfile',
order: 200,
isActive: ({instance}) => instance.hasEndpoint('logfile')
});
views.register({
name: 'instance/loggers',
template: 'Loggers',
order: 300,
isActive: ({instance}) => instance.hasEndpoint('loggers')
});
views.register({
name: 'instance/threaddump',
template: 'Threads',
order: 400,
isActive: ({instance}) => instance.hasEndpoint('threaddump')
});
views.register({
name: 'instance/trace',
template: 'Traces',
order: 500,
isActive: ({instance}) => instance.hasEndpoint('traces')
});
views.register({
name: 'instance/auditevents',
template: 'Audit Log',
order: 600,
isActive: ({instance}) => instance.hasEndpoint('auditevents')
});
views.register({
name: 'instance/sessions',
template: 'Sessions',
order: 700,
isActive: ({instance}) => instance.hasEndpoint('sessions')
});
views.register({
name: 'instance/heapdump',
href: params => `instances/${params.instanceId}/actuator/heapdump`,
template: 'Heapdump',
order: 800,
isActive: ({instance}) => instance.hasEndpoint('heapdump')
});
views.register({
name: 'instance/liquibase',
template: 'Liquibase',
order: 900,
isActive: ({instance}) => instance.hasEndpoint('liquibase')
});
views.register({
name: 'instance/flyway',
template: 'Flyway',
order: 900,
isActive: ({instance}) => instance.hasEndpoint('flyway')
store.addEventListener('updated', (newVal, oldVal) => {
if (newVal.status !== oldVal.status) {
Notifications.notify(`${newVal.name} is now ${newVal.status}`, {
tag: `${newVal.name}-${newVal.status}`,
lang: 'en',
body: `was ${oldVal.status}.`,
icon: newVal.status === 'UP' ? logoOk : logoDanger,
renotify: true,
timeout:
10000
})
}
});
router.addRoutes([{path: '/', redirect: {name: 'applications'}}]);
//Fire root Vue up
new Vue({
router,
el: '#app',
......@@ -175,6 +62,24 @@ new Vue({
return h(sbaShell);
},
data: {
views
}
views: createViews(router),
applications: store.applications
},
computed: {
allUp() {
return this.applications.findIndex(application => application.status !== 'UP') < 0;
}
},
watch: {
allUp(newVal) {
const link = document.querySelector('link[rel*="icon"]');
link.href = newVal ? logoOk : logoDanger;
}
},
created() {
store.start();
},
beforeDestroy() {
store.stop();
},
});
......@@ -40,20 +40,20 @@ class Application {
});
}
static async getStream() {
await waitForPolyfill();
static getStream() {
return Observable.from(waitForPolyfill()).ignoreElements().concat(
Observable.create(observer => {
const eventSource = new EventSource('applications');
eventSource.onmessage = message => observer.next({
...message,
data: Application._transformResponse(message.data)
});
return Observable.create(observer => {
const eventSource = new EventSource('applications');
eventSource.onmessage = message => observer.next({
...message,
data: Application._transformResponse(message.data)
});
eventSource.onerror = err => observer.error(err);
return () => {
eventSource.close();
};
});
eventSource.onerror = err => observer.error(err);
return () => {
eventSource.close();
};
}));
}
static _transformResponse(data) {
......
......@@ -146,10 +146,8 @@ class Instance {
return await axios.get(`instances/events`);
}
static async getEventStream() {
await waitForPolyfill();
return Observable.create(observer => {
static getEventStream() {
return Observable.from(waitForPolyfill()).ignoreElements().concat(Observable.create(observer => {
const eventSource = new EventSource('instances/events');
eventSource.onmessage = message => observer.next({
...message,
......@@ -159,7 +157,7 @@ class Instance {
return () => {
eventSource.close();
};
});
}));
}
static async get(id) {
......
/*
* Copyright 2014-2018 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.
*/
import Application from '@/services/application';
import {Observable} from '@/utils/rxjs';
export default class {
constructor() {
this.applications = [];
this.listeners = {
added: [],
updated: [],
removed: []
};
}
indexOf(name) {
return this.applications.findIndex(application => application.name === name)
}
addEventListener(type, listener) {
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
this.listeners[type].push(listener);
}
removeEventListener(type, listener) {
if (!(type in this.listeners)) {
return;
}
const idx = this.listeners[type].indexOf(listener);
if (idx > 0) {
this.listeners[type].splice(idx, 1);
}
}
dispatchEvent(type, ...args) {
if (!(type in this.listeners)) {
return;
}
const target = this;
this.listeners[type].forEach(
listener => listener.call(target, ...args)
)
}
start() {
const initialListing = Observable.from(Application.list())
.concatMap(message => message.data);
const stream = Application.getStream()
.map(message => message.data);
this.subscription = initialListing.concat(stream)
.subscribe({
next: application => {
const idx = this.indexOf(application.name);
if (idx >= 0) {
const oldVal = this.applications[idx];
if (application.instances.length > 0) {
this.applications.splice(idx, 1, application);
this.dispatchEvent('updated', application, oldVal);
} else {
this.applications.splice(idx, 1);
this.dispatchEvent('removed', oldVal);
}
} else {
this.applications.push(application);
this.dispatchEvent('added', application);
}
},
error: err => {
}
});
}
stop() {
if (this.subscription) {
try {
this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
}
}
\ No newline at end of file
......@@ -14,11 +14,9 @@
* limitations under the License.
*/
const waitForPolyfill = async () => {
export default async () => {
if (typeof window.EventSource === 'undefined') {
return import(/* webpackChunkName: "event-source-polyfill" */ 'event-source-polyfill');
}
return Promise.resolve();
};
export default waitForPolyfill;
\ No newline at end of file
};
\ No newline at end of file
/*
* Copyright 2014-2018 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.
*/
let granted = false;
export default {
requestPermissions() {
if ('Notification' in window) {
granted = (window.Notification.permission === 'granted');
if (!granted && window.Notification.permission !== 'denied') {
window.Notification.requestPermission(permission => granted = (permission === 'granted'));
}
}
},
notify(title, options) {
if (granted) {
const note = new window.Notification(title, options);
if (options.url !== null) {
note.onclick = () => {
window.focus();
window.open(options.url, '_self');
};
}
if (options.timeout > 0) {
note.onshow = () => setTimeout(() => note.close(), options.timeout);
}
}
}
}
......@@ -20,9 +20,10 @@ import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/concatAll';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/finally';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mergeMap';
\ No newline at end of file
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/map';
\ No newline at end of file
......@@ -54,13 +54,10 @@
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Application from '@/services/application'
import * as _ from 'lodash';
import applicationsList from './applications-list';
export default {
mixins: [subscribing],
components: {
applicationsList,
},
......@@ -70,7 +67,7 @@
}),
computed: {
applications() {
return this.$data._applications.filter(application => application.instances.length > 0);
return this.$root.applications;
},
statusGroups() {
const byStatus = _.groupBy(this.applications, application => application.status);
......@@ -91,27 +88,7 @@
}, 0);
}
},
methods: {
async createSubscription() {
try {
this.$data._applications = (await Application.list()).data;
} catch (e) {
this.errors.push(e);
}
return (await Application.getStream()).subscribe({
next: message => {
const idx = this.$data._applications.findIndex(application => application.name === message.data.name);
if (idx >= 0) {
this.$data._applications.splice(idx, 1, message.data);
} else {
this.$data._applications.push(message.data);
}
},
error: err => this.errors.push(err)
});
}
}
methods: {}
}
</script>
......
/*
* Copyright 2014-2018 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.
*/
import sbaAbout from './about';
import sbaApplications from './applications';
import sbaInstancesAuditevents from './instances/auditevents';
import sbaInstancesDetails from './instances/details';
import sbaInstancesEnv from './instances/env';
import sbaInstancesFlyway from './instances/flyway';
import sbaInstancesLiquibase from './instances/liquibase';
import sbaInstancesLogfile from './instances/logfile';
import sbaInstancesLoggers from './instances/loggers';
import sbaInstancesSessions from './instances/sessions';
import sbaInstancesShell from './instances/shell';
import sbaInstancesThreaddump from './instances/threaddump';
import sbaInstancesTrace from './instances/trace';
import sbaJournal from './journal';
export default router => {
const views = [];
views.register = view => {
if (view.template) {
views.push(view);
}
if (view.component) {
router.addRoutes([{
path: view.path,
children: view.children,
component: view.component,
props: view.props,
name: view.name
}]
)
}
};
views.register({
path: '/applications',
name: 'applications',
template: 'Applications',
order: 0,
component: sbaApplications
});
views.register({path: '/journal', name: 'journal', template: 'Journal', order: 100, component: sbaJournal});
views.register({path: '/about', name: 'about', template: 'About', order: 200, component: sbaAbout});
views.register({
path: '/instances/:instanceId', component: sbaInstancesShell, props: true,
children: [{
path: '', component: sbaInstancesDetails, props: true, name: 'instance/details',
}, {
path: 'env', component: sbaInstancesEnv, props: true, name: 'instance/env',
}, {
path: 'logfile', component: sbaInstancesLogfile, props: true, name: 'instance/logfile',
}, {
path: 'loggers', component: sbaInstancesLoggers, props: true, name: 'instance/loggers',
}, {
path: 'trace', component: sbaInstancesTrace, props: true, name: 'instance/trace',
}, {
path: 'auditevents', component: sbaInstancesAuditevents, props: true, name: 'instance/auditevents',
}, {
path: 'sessions/:sessionId?', component: sbaInstancesSessions, props: true, name: 'instance/sessions',
}, {
path: 'liquibase', component: sbaInstancesLiquibase, props: true, name: 'instance/liquibase',
}, {
path: 'flyway', component: sbaInstancesFlyway, props: true, name: 'instance/flyway',
}, {
path: 'threaddump', component: sbaInstancesThreaddump, props: true, name: 'instance/threaddump',
}]
});
views.register({
name: 'instance/details',
template: 'Details',
order: 0,
});
views.register({
name: 'instance/env',
template: 'Environment',
order: 100,
isActive: ({instance}) => instance.hasEndpoint('env')
});
views.register({
name: 'instance/logfile',
template: 'Logfile',
order: 200,
isActive: ({instance}) => instance.hasEndpoint('logfile')
});
views.register({
name: 'instance/loggers',
template: 'Loggers',
order: 300,
isActive: ({instance}) => instance.hasEndpoint('loggers')
});
views.register({
name: 'instance/threaddump',
template: 'Threads',
order: 400,
isActive: ({instance}) => instance.hasEndpoint('threaddump')
});
views.register({
name: 'instance/trace',
template: 'Traces',
order: 500,
isActive: ({instance}) => instance.hasEndpoint('trace')
});
views.register({
name: 'instance/auditevents',
template: 'Audit Log',
order: 600,
isActive: ({instance}) => instance.hasEndpoint('auditevents')
});
views.register({
name: 'instance/sessions',
template: 'Sessions',
order: 700,
isActive: ({instance}) => instance.hasEndpoint('sessions')
});
views.register({
name: 'instance/heapdump',
href: params => `instances/${params.instanceId}/actuator/heapdump`,
template: 'Heapdump',
order: 800,
isActive: ({instance}) => instance.hasEndpoint('heapdump')
});
views.register({
name: 'instance/liquibase',
template: 'Liquibase',
order: 900,
isActive: ({instance}) => instance.hasEndpoint('liquibase')
});
views.register({
name: 'instance/flyway',
template: 'Flyway',
order: 900,
isActive: ({instance}) => instance.hasEndpoint('flyway')
});
router.addRoutes([{path: '/', redirect: {name: 'applications'}}]);
return views;
}
\ No newline at end of file
......@@ -96,8 +96,8 @@
}), {});
_.assign(this.instanceNames, newInstanceNames);
},
async createSubscription() {
return (await Instance.getEventStream()).subscribe({
createSubscription() {
return Instance.getEventStream().subscribe({
next: message => {
this.addEvents([message.data])
},
......
......@@ -54,7 +54,8 @@ public class DefaultApplicationFactory implements ApplicationFactory {
public DefaultApplicationFactory(InstanceProperties instance,
ManagementServerProperties management,
ServerProperties server, PathMappedEndpoints pathMappedEndpoints,
ServerProperties server,
PathMappedEndpoints pathMappedEndpoints,
WebEndpointProperties webEndpoint) {
this.instance = instance;
this.management = management;
......
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