Skip to content

Commit 5359d5c

Browse files
authored
feat(#2032): allow to add sub views to top-level views (#2033)
* feat(#2032): allow to add sub views to top-level views * feat(#2032): allow to add sub views to top-level views * no parent for external view * do not set empty path * allow to add sub menu items to user menu by using "user" as parent
1 parent 17fe799 commit 5359d5c

File tree

10 files changed

+234
-78
lines changed

10 files changed

+234
-78
lines changed

spring-boot-admin-docs/src/main/asciidoc/customizing.adoc

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,22 @@ And this is how you register the top-level view.
8181
----
8282
include::{samples-dir}/spring-boot-admin-sample-custom-ui/src/index.js[tags=customization-ui-toplevel]
8383
----
84-
<1> Name of the view and the route to the view
85-
<2> Path where the view will be accessible
84+
<1> Name of the view and the route to the view.
85+
<2> Path where the view will be accessible.
8686
<3> The imported custom component, which will be rendered on the route.
8787
<4> The label for the custom view to be shown in the top navigation bar.
8888
<5> Order for the view. Views in the top navigation bar are sorted by ascending order.
8989

90+
===== Example
91+
[source,javascript]
92+
----
93+
include::{samples-dir}/spring-boot-admin-sample-custom-ui/src/index.js[tags=customization-ui-child]
94+
----
95+
<1> References the name of the parent view.
96+
<2> Router path used to navigate to.
97+
<3> Define whether the path should be registered as child route in parent's route.
98+
When set to `true`, the parent component has to implement `<router-view>`
99+
90100
The `routes.txt` config with the added route:
91101
[source,text]
92102
----
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!--
2+
- Copyright 2014-2018 the original author or authors.
3+
-
4+
- Licensed under the Apache License, Version 2.0 (the "License");
5+
- you may not use this file except in compliance with the License.
6+
- You may obtain a copy of the License at
7+
-
8+
- http://www.apache.org/licenses/LICENSE-2.0
9+
-
10+
- Unless required by applicable law or agreed to in writing, software
11+
- distributed under the License is distributed on an "AS IS" BASIS,
12+
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
- See the License for the specific language governing permissions and
14+
- limitations under the License.
15+
-->
16+
17+
<template>
18+
<h1>Custom Subitem</h1>
19+
</template>
20+
21+
<script>
22+
export default {
23+
};
24+
</script>

spring-boot-admin-samples/spring-boot-admin-sample-custom-ui/src/index.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
/* global SBA */
1818
import custom from './custom';
19+
import customSubitem from './custom-subitem';
1920
import customEndpoint from './custom-endpoint';
2021

2122
// tag::customization-ui-toplevel[]
@@ -32,6 +33,22 @@ SBA.use({
3233
});
3334
// end::customization-ui-toplevel[]
3435

36+
// tag::customization-ui-child[]
37+
SBA.use({
38+
install({viewRegistry}) {
39+
viewRegistry.addView({
40+
name: 'customSub',
41+
parent: 'custom', // <1>
42+
path: 'custom-sub', // <2>
43+
isChildRoute: false, // <3>
44+
component: customSubitem,
45+
label: 'Custom Sub',
46+
order: 1000,
47+
});
48+
}
49+
});
50+
// end::customization-ui-child[]
51+
3552
// tag::customization-ui-endpoint[]
3653
SBA.use({
3754
install({viewRegistry, vueI18n}) {
@@ -48,8 +65,8 @@ SBA.use({
4865

4966
vueI18n.mergeLocaleMessage('en', { // <4>
5067
sidebar: {
51-
custom : {
52-
title : "My Custom Extensions"
68+
custom: {
69+
title: "My Custom Extensions"
5370
}
5471
}
5572
});

spring-boot-admin-server-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"description": "Spring Boot Admin UI",
55
"scripts": {
66
"build": "vue-cli-service build",
7+
"build:dev": "vue-cli-service build --mode development",
78
"test:unit": "vue-cli-service test:unit",
89
"lint": "vue-cli-service lint",
910
"lint:fix": "vue-cli-service lint --fix",

spring-boot-admin-server-ui/src/main/frontend/index.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ installables.forEach(view => view.install({
5050
}));
5151

5252
const routesKnownToBackend = sbaConfig.uiSettings.routes.map(r => new RegExp(`^${r.replace('/**', '(/.*)?')}$`));
53-
const unknownRoutes = viewRegistry.routes.filter(vr => vr.path !== '/' && !routesKnownToBackend.some(br => br.test(vr.path)));
53+
const routes = viewRegistry.routes;
54+
const unknownRoutes = routes.filter(vr => vr.path !== '/' && !routesKnownToBackend.some(br => br.test(vr.path)));
5455
if (unknownRoutes.length > 0) {
5556
console.warn(`The routes ${JSON.stringify(unknownRoutes.map(r => r.path))} aren't known to the backend and may be not properly routed!`)
5657
}
@@ -60,7 +61,7 @@ new Vue({
6061
router: new VueRouter({
6162
mode: 'history',
6263
linkActiveClass: 'is-active',
63-
routes: viewRegistry.routes
64+
routes
6465
}),
6566
el: '#app',
6667
render(h) {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<template>
2+
<a
3+
v-if="view.href"
4+
@click="onClick"
5+
:href="view.href"
6+
:class="{'navbar-item is-arrowless': !hasSubitems, 'navbar-link': hasSubitems}"
7+
target="_blank"
8+
rel="noopener noreferrer"
9+
>
10+
<component :is="view.handle" />
11+
</a>
12+
<router-link
13+
@click.native="onClick"
14+
v-else
15+
:data-sba-to="view.name"
16+
:to="{name: view.name}"
17+
active-class=""
18+
exact-active-class=""
19+
:class="{'navbar-item is-arrowless': !hasSubitems, 'navbar-link': hasSubitems}"
20+
>
21+
<component :is="view.handle" :applications="applications" :error="error" />
22+
</router-link>
23+
</template>
24+
<script>
25+
export default {
26+
name: 'NavbarLink',
27+
props: {
28+
applications: {
29+
type: Array,
30+
default: () => [],
31+
},
32+
error: {
33+
type: Error,
34+
default: null
35+
},
36+
hasSubitems: {
37+
type: Boolean,
38+
default: false
39+
},
40+
view: {
41+
type: Object,
42+
required: true
43+
}
44+
},
45+
methods: {
46+
onClick($event) {
47+
$event?.target?.blur()
48+
}
49+
}
50+
}
51+
</script>

spring-boot-admin-server-ui/src/main/frontend/shell/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
components: {sbaNavbar},
4747
computed: {
4848
mainViews() {
49-
return this.views.filter(view => !view.parent);
49+
return this.views.filter(view => !['instances'].includes(view.parent));
5050
},
5151
activeMainViewName() {
5252
const currentView = this.$route.meta.view;

spring-boot-admin-server-ui/src/main/frontend/shell/navbar.vue

Lines changed: 60 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,49 +18,47 @@
1818
<nav id="navigation" class="navbar is-fixed-top">
1919
<div class="container">
2020
<div class="navbar-brand">
21-
<router-link class="navbar-item logo" to="/" v-html="brand" />
21+
<router-link class="navbar-item logo" to="/" v-html="brand"/>
2222

2323
<div class="navbar-burger burger" @click.stop="showMenu = !showMenu">
24-
<span />
25-
<span />
26-
<span />
24+
<span/>
25+
<span/>
26+
<span/>
2727
</div>
2828
</div>
2929
<div class="navbar-menu" :class="{'is-active' : showMenu}">
3030
<div class="navbar-end">
31-
<template v-for="view in enabledViews">
32-
<a
33-
v-if="view.href"
34-
:key="view.name"
35-
:href="view.href"
36-
class="navbar-item"
37-
target="_blank"
38-
rel="noopener noreferrer"
31+
<template v-for="view in mainViews">
32+
<div class="navbar-item "
33+
:key="view.name"
34+
:class="{'has-dropdown is-hoverable': hasSubitems(view.name)}"
3935
>
40-
<component :is="view.handle" />
41-
</a>
42-
<router-link
43-
v-else
44-
:key="view.name"
45-
:to="{name: view.name}"
46-
class="navbar-item"
47-
>
48-
<component :is="view.handle" :applications="applications" :error="error" />
49-
</router-link>
36+
<navbar-link :applications="applications" :error="error" :has-subitems="hasSubitems(view.name)"
37+
:view="view"/>
38+
39+
<div v-if="hasSubitems(view.name)" class="navbar-dropdown">
40+
<template v-for="subitem in getSubitems(view.name)">
41+
<navbar-link :key="subitem.name" :applications="applications" :error="error" :view="subitem"/>
42+
</template>
43+
</div>
44+
</div>
5045
</template>
46+
5147
<div class="navbar-item has-dropdown is-hoverable" v-if="userName">
5248
<a class="navbar-link">
53-
<font-awesome-icon icon="user-circle" size="lg" />&nbsp;<span v-text="userName" />
49+
<font-awesome-icon icon="user-circle" size="lg"/>&nbsp;<span v-text="userName"/>
5450
</a>
5551
<div class="navbar-dropdown">
56-
<a class="navbar-item">
57-
<form action="logout" method="post">
58-
<input v-if="csrfToken" type="hidden" :name="csrfParameterName" :value="csrfToken">
59-
<button class="button is-icon" type="submit" value="logout">
60-
<font-awesome-icon icon="sign-out-alt" />&nbsp;<span v-text="$t('navbar.logout')" />
61-
</button>
62-
</form>
63-
</a>
52+
<navbar-link v-for="userSubMenuItem in userSubMenuItems" :key="userSubMenuItem.name"
53+
:applications="applications" :error="error" :view="userSubMenuItem"/>
54+
55+
<form action="logout" method="post">
56+
<input v-if="csrfToken" type="hidden" :name="csrfParameterName" :value="csrfToken">
57+
<button class="button is-icon" type="submit" value="logout">
58+
<font-awesome-icon icon="sign-out-alt"/>&nbsp;<span v-text="$t('navbar.logout')"/>
59+
</button>
60+
</form>
61+
6462
</div>
6563
</div>
6664
<navbar-item-language-selector v-if="availableLocales.length > 1" :current-locale="$i18n.locale"
@@ -79,14 +77,15 @@ import {compareBy} from '@/utils/collections';
7977
import {getAvailableLocales} from '@/i18n';
8078
import moment from 'moment';
8179
import NavbarItemLanguageSelector from '@/shell/navbar-item-language-selector';
80+
import NavbarLink from '@/shell/NavbarLink.vue';
8281
8382
const readCookie = (name) => {
8483
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
8584
return (match ? decodeURIComponent(match[3]) : null);
8685
};
8786
8887
export default {
89-
components: {NavbarItemLanguageSelector},
88+
components: {NavbarLink, NavbarItemLanguageSelector},
9089
data: () => ({
9190
showMenu: false,
9291
brand: '<img src="assets/img/icon-spring-boot-admin.svg"><span>Spring Boot Admin</span>',
@@ -115,12 +114,24 @@ export default {
115114
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled())
116115
).sort(compareBy(v => v.order));
117116
},
117+
mainViews() {
118+
return this.enabledViews.filter(v => !v.parent);
119+
},
120+
userSubMenuItems() {
121+
return this.enabledViews.filter(v => v.parent === 'user');
122+
}
118123
},
119124
methods: {
120125
changeLocale(locale) {
121126
this.$i18n.locale = locale;
122127
moment.locale(this.$i18n.locale);
123-
}
128+
},
129+
getSubitems(parent) {
130+
return this.enabledViews.filter(v => v.parent === parent);
131+
},
132+
hasSubitems(parent) {
133+
return this.getSubitems(parent).length > 0;
134+
},
124135
},
125136
created() {
126137
this.brand = sbaConfig.uiSettings.brand;
@@ -155,3 +166,19 @@ export default {
155166
}
156167
}
157168
</style>
169+
170+
<style lang="scss" scoped>
171+
.button {
172+
padding: 0.375rem 3rem 0.375rem 1rem;
173+
font-size: inherit;
174+
color: inherit;
175+
text-align: left;
176+
width: 100%;
177+
display: block;
178+
179+
&:hover {
180+
background-color: hsl(0deg, 0%, 96%);
181+
color: hsl(0deg, 0%, 4%);
182+
}
183+
}
184+
</style>

spring-boot-admin-server-ui/src/main/frontend/viewRegistry.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,9 @@ export default class ViewRegistry {
3838
}
3939

4040
get routes() {
41+
const parentViews = this._views;
4142
return [
42-
...this._toRoutes(this._views, v => v.path && !v.parent),
43+
...this._toRoutes(parentViews, v => v.path && (!v.parent || !v.isChildRoute)),
4344
...this._redirects
4445
]
4546
}
@@ -67,6 +68,9 @@ export default class ViewRegistry {
6768
if (!view.group) {
6869
view.group = VIEW_GROUP.NONE;
6970
}
71+
if (view.isChildRoute === undefined) {
72+
view.isChildRoute = true;
73+
}
7074

7175
if (!view.isEnabled) {
7276
view.isEnabled = () => {
@@ -87,7 +91,7 @@ export default class ViewRegistry {
8791
_toRoutes(views, filter) {
8892
return views.filter(filter).map(
8993
p => {
90-
const children = this._toRoutes(views, v => v.parent === p.name);
94+
const children = this._toRoutes(views, v => v.parent === p.name && v.isChildRoute);
9195
return ({
9296
path: p.path,
9397
name: p.name,

0 commit comments

Comments
 (0)