Commit 2de93933 by Johannes Edmeier

Refactor how routes and views are registered

parent 5e9b1eb6
......@@ -41,10 +41,10 @@
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.20",
"autoprefixer": "^8.6.3",
"autoprefixer": "^8.6.4",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.5",
"babel-jest": "^23.0.1",
"babel-jest": "^23.2.0",
"babel-loader": "^7.1.4",
"babel-plugin-lodash": "^3.3.4",
"babel-polyfill": "^6.26.0",
......@@ -65,7 +65,7 @@
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.30.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.1.0",
"jest": "^23.2.0",
"lodash-webpack-plugin": "^0.11.5",
"node-sass": "^4.9.0",
"optimize-css-assets-webpack-plugin": "^3.2.0",
......
......@@ -17,7 +17,7 @@
const components = [];
// eslint-disable-next-line no-undef
/* global require */
const context = require.context('.', false, /^(?:(?!.*\.spec\.(js|vue)$).)*\.(js|vue)$/);
context.keys().forEach(function (key) {
const name = /^(.\/)+(.*)\.(vue|js)$/.exec(key)[2];
......
......@@ -15,48 +15,36 @@
*/
import '@/assets/css/base.scss';
import logoDanger from '@/assets/img/favicon-danger.png';
import logoOk from '@/assets/img/favicon.png';
import '@/assets/img/icon-spring-boot-admin.svg';
import moment from 'moment';
import Vue from 'vue';
import VueRouter from 'vue-router';
import sbaComponents from './components';
import components from './components';
import Notifications from './notifications';
import sbaShell from './shell';
import Store from './store';
import FontAwesomeIcon from './utils/fontawesome';
import Notifications from './utils/notifications';
import createViews from './views';
import sbaShell from './views/shell';
import ViewRegistry from './viewRegistry';
import views from './views';
moment.locale(window.navigator.language);
Notifications.requestPermissions();
const applicationStore = new Store();
const viewRegistry = new ViewRegistry();
const store = new Store();
Notifications.install({applicationStore});
views.forEach(view => view.install({
viewRegistry,
applicationStore
}));
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
})
}
});
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.use(VueRouter);
Vue.use(sbaComponents);
const router = new VueRouter({
linkActiveClass: 'is-active'
});
Vue.use(components);
new Vue({
router,
router: new VueRouter({
linkActiveClass: 'is-active',
routes: viewRegistry.routes
}),
el: '#app',
render(h) {
return h(sbaShell, {
......@@ -68,8 +56,8 @@ new Vue({
});
},
data: {
views: createViews(router),
applications: store.applications,
views: viewRegistry.views,
applications: applicationStore.applications,
error: null
},
methods: {
......@@ -82,13 +70,13 @@ new Vue({
}
},
created() {
store.addEventListener('connected', this.onConnected);
store.addEventListener('error', this.onError);
store.start();
applicationStore.addEventListener('connected', this.onConnected);
applicationStore.addEventListener('error', this.onError);
applicationStore.start();
},
beforeDestroy() {
store.stop();
store.removeEventListener('connected', this.onConnected);
store.removeEventListener('error', this.onError)
},
applicationStore.stop();
applicationStore.removeEventListener('connected', this.onConnected);
applicationStore.removeEventListener('error', this.onError)
}
});
......@@ -13,31 +13,50 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import logoDanger from '@/assets/img/favicon-danger.png';
import logoOk from '@/assets/img/favicon.png';
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'));
}
const 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);
}
const 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);
}
}
}
};
export default {
install: ({applicationStore}) => {
requestPermissions();
applicationStore.addEventListener('updated', (newVal, oldVal) => {
if (newVal.status !== oldVal.status) {
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
})
}
});
}
}
......@@ -17,7 +17,7 @@
<template>
<div id="app">
<sba-navbar :views="mainViews" :applications="applications" :error="error"/>
<router-view :views="subViews" :applications="applications" :error="error"/>
<router-view :views="childViews" :applications="applications" :error="error"/>
</div>
</template>
......@@ -42,14 +42,14 @@
components: {sbaNavbar},
computed: {
mainViews() {
return this.views.filter(view => !view.name.includes('/'))
return this.views.filter(view => !view.parent);
},
activeMainViewName() {
const idx = this.$route.name.indexOf('/');
return idx < 0 ? this.$route.name : this.$route.name.substr(0, idx);
const currentView = this.$route.meta.view;
return currentView.parent || currentView.name;
},
subViews() {
return this.views.filter(view => view.name.includes(this.activeMainViewName))
childViews() {
return this.views.filter(view => view.parent === this.activeMainViewName);
}
}
}
......
......@@ -29,7 +29,7 @@
<div class="navbar-menu" :class="{'is-active' : showMenu}">
<div class="navbar-start"/>
<div class="navbar-end">
<router-link class="navbar-item" v-for="view in views" :to="{name: view.name}" :key="view.name">
<router-link class="navbar-item" v-for="view in enabledViews" :to="{name: view.name}" :key="view.name">
<component :is="view.handle" :applications="applications" :error="error"/>
</router-link>
......@@ -54,6 +54,8 @@
</template>
<script>
import {compareBy} from '@/utils/collections';
export default {
data: () => ({
showMenu: false,
......@@ -74,6 +76,13 @@
default: null
}
},
computed: {
enabledViews() {
return [...this.views].filter(
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled())
).sort(compareBy(v => v.order));
}
},
created() {
/* global SBA */
if (SBA) {
......
......@@ -60,7 +60,7 @@ export default class {
}
}
dispatchEvent(type, ...args) {
_dispatchEvent(type, ...args) {
if (!(type in this._listeners)) {
return;
}
......@@ -74,9 +74,9 @@ export default class {
const listing = Observable.defer(() => Application.list()).concatMap(message => message.data);
const stream = Application.getStream().map(message => message.data);
this.subscription = listing.concat(stream)
.doFirst(() => this.dispatchEvent('connected'))
.doFirst(() => this._dispatchEvent('connected'))
.retryWhen(errors => errors
.do(error => this.dispatchEvent('error', error))
.do(error => this._dispatchEvent('error', error))
.delay(5000)
).subscribe({
next: application => {
......@@ -85,14 +85,14 @@ export default class {
const oldApplication = this.applications[idx];
if (application.instances.length > 0) {
this.applications.splice(idx, 1, application);
this.dispatchEvent('updated', application, oldApplication);
this._dispatchEvent('updated', application, oldApplication);
} else {
this.applications.splice(idx, 1);
this.dispatchEvent('removed', oldApplication);
this._dispatchEvent('removed', oldApplication);
}
} else {
this.applications.push(application);
this.dispatchEvent('added', application);
this._dispatchEvent('added', application);
}
}
});
......
/*
* 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.
*/
const createVNodeIfNecessary = handle => {
if (typeof handle === 'string') {
return {
render() {
return this._v(handle)
}
}
}
return handle;
};
export default class ViewRegistry {
constructor() {
this._views = [];
this._redirects = [];
}
get views() {
return this._views;
}
get routes() {
return [
...this._toRoutes(this._views, v => !v.parent),
...this._redirects
]
}
addView(...views) {
views.forEach(view => this._addView(view));
}
addRedirect(path, redirectToView) {
this._redirects.push({path, redirect: {name: redirectToView}});
}
_addView(view) {
if (view.handle) {
view.handle = createVNodeIfNecessary(view.handle);
}
this._views.push(view);
}
_toRoutes(views, filter) {
return views.filter(filter).map(
p => {
const children = this._toRoutes(views, v => v.parent === p.name);
return ({
path: p.path,
name: children.length === 0 ? p.name : undefined,
component: p.component,
props: p.props,
meta: {view: p},
children
});
}
)
}
}
......@@ -25,11 +25,13 @@
</p>
<p>
To monitor applications, they must be registered at this server. This is either done by including the
<a href="https://codecentric.github.io/spring-boot-admin/@project.version@/#register-clients-via-spring-boot-admin">
<a
href="https://codecentric.github.io/spring-boot-admin/@project.version@/#register-clients-via-spring-boot-admin">
Spring Boot Admin Client
</a>
or using a
<a href="https://codecentric.github.io/spring-boot-admin/@project.version@/#discover-clients-via-spring-cloud-discovery">
<a
href="https://codecentric.github.io/spring-boot-admin/@project.version@/#discover-clients-via-spring-cloud-discovery">
Spring Cloud Discovery Client
</a> implementation.
</p>
......@@ -76,20 +78,20 @@
</template>
<script>
const component = {
export default {
data: () => ({
// eslint-disable-next-line no-undef
version: __PROJECT_VERSION__
})
};
export default component;
export const view = {
path: '/about',
name: 'about',
handle: 'About',
order: 200,
component: component
}),
install({viewRegistry}) {
viewRegistry.addView({
path: '/about',
name: 'about',
handle: 'About',
order: 200,
component: this
});
}
};
</script>
......
......@@ -133,7 +133,7 @@
}
},
showDetails(instance) {
this.$router.push({name: 'instance/details', params: {instanceId: instance.id}});
this.$router.push({name: 'instances/details', params: {instanceId: instance.id}});
},
async scrollIntoView(id, behavior) {
if (id) {
......
......@@ -66,7 +66,7 @@
import applicationsList from './applications-list';
import handle from './handle';
const component = {
export default {
props: {
applications: {
type: Array,
......@@ -104,17 +104,17 @@
}, 0);
}
},
methods: {}
};
export default component;
export const view = {
path: '/applications/:selected?',
props: true,
name: 'applications',
handle: handle,
order: 0,
component: component
install({viewRegistry}) {
viewRegistry.addView({
path: '/applications/:selected?',
props: true,
name: 'applications',
handle: handle,
order: 0,
component: this
});
viewRegistry.addRedirect('/', 'applications');
}
};
</script>
......
......@@ -14,48 +14,15 @@
* limitations under the License.
*/
import {compareBy} from '@/utils/collections';
import {view as aboutView} from './about';
import {view as applicationView} from './applications';
import instanceViews from './instances';
import {view as journalView} from './journal';
import {view as wallboardView} from './wallboard';
const views = [];
export default router => {
const views = [];
views.register = view => {
if (view.handle) {
if (typeof view.handle === 'string') {
const label = view.handle;
view.handle = {
render() {
return this._v(label)
}
}
}
views.push(view);
}
/* global require */
const context = require.context('.', true, /^\.\/.+\/index\.(js|vue)$/);
context.keys().forEach(function (key) {
const defaultExport = context(key).default;
if (defaultExport && defaultExport.install) {
views.push(defaultExport)
}
});
if (view.component) {
router.addRoutes([{
path: view.path,
children: view.children,
component: view.component,
props: view.props,
name: view.name
}]
)
}
};
views.register(applicationView);
views.register(journalView);
views.register(aboutView);
views.register(wallboardView);
instanceViews.forEach(views.register);
views.sort(compareBy(v => v.order));
router.addRoutes([{path: '/', redirect: {name: 'applications'}}]);
return views;
}
export default views;
......@@ -38,13 +38,13 @@
</td>
<td v-if="hasSessionEndpoint && event.principal">
<router-link v-text="event.principal"
:to="{ name: 'instance/sessions', params: { 'instanceId' : instance.id }, query: { username : event.principal} }"/>
:to="{ name: 'instances/sessions', params: { 'instanceId' : instance.id }, query: { username : event.principal} }"/>
</td>
<td v-else v-text="event.principal"/>
<td v-text="event.remoteAddress"/>
<td v-if="hasSessionEndpoint && event.sessionId">
<router-link v-text="event.sessionId"
:to="{ name: 'instance/sessions', params: { 'instanceId' : instance.id }, query: { sessionId : event.sessionId } }"/>
:to="{ name: 'instances/sessions', params: { 'instanceId' : instance.id }, query: { sessionId : event.sessionId } }"/>
</td>
<td v-else v-text="event.sessionId"/>
</tr>
......
......@@ -121,6 +121,18 @@
addEvents(events) {
this.events = _.uniqBy(this.events ? events.concat(this.events) : events, event => event.key);
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/auditevents',
parent: 'instances',
path: 'auditevents',
component: this,
props: true,
handle: 'Audit Log',
order: 600,
isEnabled: ({instance}) => instance.hasEndpoint('auditevents')
});
}
}
</script>
......@@ -145,6 +145,17 @@
this.hasLoaded = true;
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/details',
parent: 'instances',
path: '',
component: this,
props: true,
handle: 'Details',
order: 0
});
}
}
</script>
......@@ -132,6 +132,18 @@
this.hasEnvManagerSupport = false;
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/env',
parent: 'instances',
path: 'env',
component: this,
props: true,
handle: 'Environment',
order: 100,
isEnabled: ({instance}) => instance.hasEndpoint('env')
});
}
}
</script>
......@@ -121,6 +121,18 @@
return 'is-light';
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/flyway',
parent: 'instances',
path: 'flyway',
component: this,
props: true,
handle: 'Flyway',
order: 900,
isEnabled: ({instance}) => instance.hasEndpoint('flyway')
});
}
}
</script>
......@@ -19,7 +19,7 @@
<div>
<div class="message is-warning">
<div class="message-body">
A heap dump may contain <strong>sensitive data</strong>.<br>Please handle with care.
A heap dump may contain <strong>sensitive data</strong>. Please handle with care.
</div>
</div>
<div class="message is-warning">
......@@ -43,6 +43,18 @@
type: Instance,
required: true
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/heapdump',
parent: 'instances',
path: 'heapdump',
component: this,
props: true,
handle: 'Heap Dump',
order: 800,
isEnabled: ({instance}) => instance.hasEndpoint('heapdump')
});
}
}
</script>
......
......@@ -231,6 +231,18 @@
}
return filterFn;
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/httptrace',
parent: 'instances',
path: 'httptrace',
component: this,
props: true,
handle: 'Http Traces',
order: 500,
isEnabled: ({instance}) => instance.hasEndpoint('httptrace')
});
}
}
</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 sbaInstancesAuditevents from './auditevents';
import sbaInstancesDetails from './details';
import sbaInstancesEnv from './env';
import sbaInstancesFlyway from './flyway';
import sbaInstancesHeapdump from './heapdump';
import sbaInstancesTrace from './httptrace';
import sbaInstancesJolokia from './jolokia';
import sbaInstancesLiquibase from './liquibase';
import sbaInstancesLogfile from './logfile';
import sbaInstancesLoggers from './loggers';
import sbaInstancesMetrics from './metrics';
import sbaInstancesSessions from './sessions';
import sbaInstancesShell from './shell';
import sbaInstancesThreaddump from './threaddump';
export default [{
path: '/instances/:instanceId', component: sbaInstancesShell, props: true,
children: [{
path: '', component: sbaInstancesDetails, props: true, name: 'instance/details'
}, {
path: 'metrics', component: sbaInstancesMetrics, props: true, name: 'instance/metrics'
}, {
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: 'jolokia', component: sbaInstancesJolokia, props: true, name: 'instance/jolokia'
}, {
path: 'httptrace', component: sbaInstancesTrace, props: true, name: 'instance/httptrace'
}, {
path: 'auditevents', component: sbaInstancesAuditevents, props: true, name: 'instance/auditevents'
}, {
path: 'sessions', 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'
}, {
path: 'heapdump', component: sbaInstancesHeapdump, props: true, name: 'instance/heapdump'
}]
}, {
name: 'instance/details',
handle: 'Details',
order: 0
}, {
name: 'instance/metrics',
handle: 'Metrics',
order: 50,
isActive: ({instance}) => instance.hasEndpoint('metrics')
}, {
name: 'instance/env',
handle: 'Environment',
order: 100,
isActive: ({instance}) => instance.hasEndpoint('env')
}, {
name: 'instance/logfile',
handle: 'Logfile',
order: 200,
isActive: ({instance}) => instance.hasEndpoint('logfile')
}, {
name: 'instance/loggers',
handle: 'Loggers',
order: 300,
isActive: ({instance}) => instance.hasEndpoint('loggers')
}, {
name: 'instance/jolokia',
handle: 'JMX',
order: 350,
isActive: ({instance}) => instance.hasEndpoint('jolokia')
}, {
name: 'instance/threaddump',
handle: 'Threads',
order: 400,
isActive: ({instance}) => instance.hasEndpoint('threaddump')
}, {
name: 'instance/httptrace',
handle: 'Http Traces',
order: 500,
isActive: ({instance}) => instance.hasEndpoint('httptrace')
}, {
name: 'instance/auditevents',
handle: 'Audit Log',
order: 600,
isActive: ({instance}) => instance.hasEndpoint('auditevents')
}, {
name: 'instance/sessions',
handle: 'Sessions',
order: 700,
isActive: ({instance}) => instance.hasEndpoint('sessions')
}, {
name: 'instance/heapdump',
handle: 'Heap Dump',
order: 800,
isActive: ({instance}) => instance.hasEndpoint('heapdump')
}, {
name: 'instance/liquibase',
handle: 'Liquibase',
order: 900,
isActive: ({instance}) => instance.hasEndpoint('liquibase')
}, {
name: 'instance/flyway',
handle: 'Flyway',
order: 900,
isActive: ({instance}) => instance.hasEndpoint('flyway')
}];
......@@ -89,12 +89,12 @@
</template>
<script>
import sticksBelow from '@/directives/sticks-below';
import Instance from '@/services/instance';
import _ from 'lodash';
import {directive as onClickaway} from 'vue-clickaway';
import mBeanAttributes from './m-bean-attributes';
import mBeanOperations from './m-bean-operations';
import sticksBelow from '@/directives/sticks-below';
const getOperationName = (name, descriptor) => {
const params = descriptor.args.map(arg => arg.type).join(',');
......@@ -171,7 +171,7 @@
selected() {
if (!_.isEqual(this.selected, !this.$route.query)) {
this.$router.replace({
name: 'instance/jolokia',
name: 'instances/jolokia',
query: this.selected
});
}
......@@ -220,6 +220,18 @@
view: view || (mBean ? (mBean.attr ? 'attributes' : (mBean.op ? 'operations' : null)) : null)
};
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/jolokia',
parent: 'instances',
path: 'jolokia',
component: this,
props: true,
handle: 'JMX',
order: 350,
isEnabled: ({instance}) => instance.hasEndpoint('jolokia')
});
}
}
</script>
......
......@@ -141,6 +141,18 @@
return 'is-info';
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/liquibase',
parent: 'instances',
path: 'liquibase',
component: this,
props: true,
handle: 'Liquibase',
order: 900,
isEnabled: ({instance}) => instance.hasEndpoint('liquibase')
});
}
}
</script>å
......@@ -122,6 +122,18 @@
scrollToBottom() {
document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight;
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/logfile',
parent: 'instances',
path: 'logfile',
component: this,
props: true,
handle: 'Logfile',
order: 200,
isEnabled: ({instance}) => instance.hasEndpoint('logfile')
});
}
}
</script>
......
......@@ -199,6 +199,18 @@
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/loggers',
parent: 'instances',
path: 'loggers',
component: this,
props: true,
handle: 'Loggers',
order: 300,
isEnabled: ({instance}) => instance.hasEndpoint('loggers')
});
}
}
</script>
......
......@@ -132,7 +132,7 @@
deep: true,
handler() {
this.$router.replace({
name: 'instance/metrics',
name: 'instances/metrics',
query: stringify(this.metrics)
})
}
......@@ -211,6 +211,18 @@
this.stateFetchingTags = 'failed';
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/metrics',
parent: 'instances',
path: 'metrics',
component: this,
props: true,
handle: 'Metrics',
order: 50,
isEnabled: ({instance}) => instance.hasEndpoint('metrics')
});
}
}
</script>
......
......@@ -139,13 +139,25 @@
const query = {[this.filter.type]: this.filter.value};
if (!_.isEqual(query, !this.$route.query)) {
this.$router.replace({
name: 'instance/sessions',
name: 'instances/sessions',
query: query
});
}
this.fetch();
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/sessions',
parent: 'instances',
path: 'sessions',
component: this,
props: true,
handle: 'Sessions',
order: 700,
isEnabled: ({instance}) => instance.hasEndpoint('sessions')
});
}
}
</script>
......@@ -40,7 +40,7 @@
<tr v-for="session in sessions" :key="session.id">
<td>
<router-link v-text="session.id"
:to="{ name: 'instance/sessions', params: { 'instanceId' : instance.id}, query: { sessionId : session.id } }"/>
:to="{ name: 'instances/sessions', params: { 'instanceId' : instance.id}, query: { sessionId : session.id } }"/>
</td>
<td v-text="session.creationTime.format('L HH:mm:ss.SSS')"/>
<td v-text="session.lastAccessedTime.format('L HH:mm:ss.SSS')"/>
......
......@@ -74,6 +74,14 @@
}
return 'is-light';
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances',
path: '/instances/:instanceId',
component: this,
props: true
});
}
}
</script>
......
......@@ -28,7 +28,7 @@
<nav class="instance-tabs__tabs tabs is-boxed">
<ul>
<li v-if="instance" v-for="view in activeViews" :key="view.name"
<li v-if="instance" v-for="view in enabledViews" :key="view.name"
:class="{'is-active' : $route.name === view.name}">
<a v-if="view.href" :href="view.href({ 'instanceId' : instance.id })"
target="_blank">
......@@ -49,6 +49,7 @@
<script>
import Application from '@/services/application';
import Instance from '@/services/instance';
import {compareBy} from '@/utils/collections';
export default {
props: {
......@@ -69,14 +70,14 @@
isStuck: false
}),
computed: {
activeViews() {
if (!this.instance || !this.views) {
enabledViews() {
if (!this.instance) {
return [];
}
return this.views.filter(
view => typeof view.isActive === 'undefined' || view.isActive({instance: this.instance})
);
return [...this.views].filter(
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled({instance: this.instance}))
).sort(compareBy(v => v.order));
}
},
methods: {
......
......@@ -125,6 +125,18 @@
}
});
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/threaddump',
parent: 'instances',
path: 'threaddump',
component: this,
props: true,
handle: 'Threads',
order: 400,
isEnabled: ({instance}) => instance.hasEndpoint('threaddump')
});
}
}
</script>
......@@ -81,7 +81,7 @@
}
}
const component = {
export default {
mixins: [subscribing],
data: () => ({
events: [],
......@@ -126,15 +126,15 @@
console.warn('Fetching events failed:', error);
this.error = error;
}
},
install({viewRegistry}) {
viewRegistry.addView({
path: '/journal',
name: 'journal',
handle: 'Journal',
order: 100,
component: this
});
}
};
export default component;
export const view = {
path: '/journal',
name: 'journal',
handle: 'Journal',
order: 100,
component: component
};
</script>
......@@ -34,7 +34,7 @@
<script>
import hexMesh from './hex-mesh';
const component = {
export default {
components: {hexMesh},
props: {
applications: {
......@@ -70,22 +70,22 @@
},
select(application) {
if (application.instances.length === 1) {
this.$router.push({name: 'instance/details', params: {instanceId: application.instances[0].id}});
this.$router.push({name: 'instances/details', params: {instanceId: application.instances[0].id}});
} else {
this.$router.push({name: 'applications', params: {selected: application.name}});
}
},
},
install({viewRegistry}) {
viewRegistry.addView({
path: '/wallboard',
name: 'wallboard',
handle: 'Wallboard',
order: -100,
component: this
});
}
};
export default component;
export const view = {
path: '/wallboard',
name: 'wallboard',
handle: 'Wallboard',
order: -100,
component: component
};
</script>
<style lang="scss">
......
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