Commit 75a960a7 by Johannes Edmeier

Add api to contribute custom views and a sample for custom views

parent abbcc7eb
......@@ -33,7 +33,17 @@
<module>spring-boot-admin-sample-reactive</module>
<module>spring-boot-admin-sample-war</module>
<module>spring-boot-admin-sample-hazelcast</module>
<module>spring-boot-admin-sample-custom-ui</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-sample-custom-ui</artifactId>
<version>${revision}</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<!-- Turn on filtering by default for application properties -->
<resources>
......
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "spring-boot-admin-sample-custom-ui",
"private": true,
"scripts": {
"build": "vue-cli-service build",
"watch": "vue-cli-service build --watch",
"lint": "vue-cli-service lint"
},
"peerDependencies": {
"vue": "^2.5.16"
},
"dependencies": {},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.0.0-rc.5",
"@vue/cli-plugin-eslint": "^3.0.0-rc.5",
"@vue/cli-service": "^3.0.0-rc.5",
"@vue/eslint-config-standard": "^3.0.0-rc.5",
"vue-template-compiler": "^2.5.16"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"eslint:recommended",
"plugin:vue/strongly-recommended"
],
"rules": {},
"parserOptions": {
"parser": "babel-eslint"
}
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-boot-admin-sample-custom-ui</artifactId>
<name>Spring Boot Admin Server custom UI</name>
<description>Spring Boot Admin Server custom UI</description>
<parent>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-samples</artifactId>
<version>${revision}</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<executions>
<execution>
<id>npm-install</id>
<phase>validate</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>npm</executable>
<arguments>
<argument>install</argument>
</arguments>
</configuration>
</execution>
<execution>
<id>npm-build</id>
<phase>generate-resources</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>npm</executable>
<arguments>
<argument>run</argument>
<argument>build</argument>
</arguments>
<environmentVariables>
<PROJECT_VERSION>${project.version}</PROJECT_VERSION>
</environmentVariables>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
</plugin>
</plugins>
<resources>
<resource>
<directory>target/dist</directory>
<targetPath>META-INF/spring-boot-admin-server-ui/extensions/custom</targetPath>
<filtering>false</filtering>
</resource>
</resources>
</build>
</project>
<!--
- 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.
-->
<template>
<div class="custom">
Instance: <span v-text="instance.id"/>
Output: <span v-text="text"/>
</div>
</template>
<script>
export default {
props: {
instance: {
type: Object,
required: true
}
},
data: () => ({
text: ''
}),
async created() {
const response = await this.instance.axios.get('actuator/custom');
this.text = response.data;
}
};
</script>
<style>
.custom {
font-size: 20px;
width: 80%;
}
</style>
<!--
- 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.
-->
<template>
<pre v-text="stringify(applications, null, 4)"/>
</template>
<script>
export default {
props: {
applications: {
type: Array,
required: true
}
},
methods: {
stringify: JSON.stringify
}
};
</script>
<style>
</style>
/*
* 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 custom from './custom';
import customEndpoint from './custom-endpoint';
/* global SBA */
SBA.extensions.push({
install({viewRegistry}) {
viewRegistry.addView({
name: 'custom',
path: '/custom',
component: custom,
props: true,
label: 'Custom',
order: 1000,
});
viewRegistry.addView({
name: 'instances/custom',
parent: 'instances',
path: 'custom',
component: customEndpoint,
props: true,
label: 'Custom',
order: 1000,
isEnabled: ({instance}) => instance.hasEndpoint('custom')
});
}
});
/*
* 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.
*/
module.exports = {
outputDir: 'target/dist',
chainWebpack: config => {
config.entryPoints.delete('app');
config.entry('custom').add('./src/index.js');
config.externals({
vue: {
commonjs: 'vue',
commonjs2: 'vue',
root: 'Vue'
}
});
config.output.libraryTarget('var');
config.optimization.splitChunks(false);
config.module
.rule('vue')
.use('vue-loader')
.loader('vue-loader')
.tap(options => ({
...options,
hotReload: false
}));
config.plugins.delete('html');
config.plugins.delete('preload');
config.plugins.delete('prefetch');
}
};
......@@ -30,6 +30,10 @@
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-sample-custom-ui</artifactId>
</dependency>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
......
/*
* 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.
*/
package de.codecentric.boot.admin;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@Endpoint(id = "custom")
public class CustomEndpoint {
@ReadOperation
public String invoke() {
return "Hello World!";
}
}
......@@ -48,7 +48,6 @@ import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@EnableAutoConfiguration
@EnableAdminServer
public class SpringBootAdminApplication {
private static final Logger log = LoggerFactory.getLogger(SpringBootAdminApplication.class);
public static void main(String[] args) {
......@@ -71,7 +70,7 @@ public class SpringBootAdminApplication {
}
@Profile("secure")
// tag::configuration-spring-security[]
// tag::configuration-spring-security[]
@Configuration
public static class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
private final String adminContextPath;
......@@ -104,7 +103,7 @@ public class SpringBootAdminApplication {
// @formatter:on
}
}
// end::configuration-spring-security[]
// end::configuration-spring-security[]
@Bean
public InstanceExchangeFilterFunction auditLog() {
......@@ -121,6 +120,11 @@ public class SpringBootAdminApplication {
return new LoggingNotifier(repository);
}
@Bean
public CustomEndpoint customEndpoint() {
return new CustomEndpoint();
}
// tag::configuration-filtering-notifier[]
@Configuration
public static class NotifierConfig {
......@@ -147,5 +151,5 @@ public class SpringBootAdminApplication {
return notifier;
}
}
// end::configuration-filtering-notifier[]
// end::configuration-filtering-notifier[]
}
......@@ -36,9 +36,10 @@ spring:
ui:
cache:
no-cache: true
template-location: file://@project.basedir@/../../spring-boot-admin-server-ui/target/dist/
resource-locations: file://@project.basedir@/../../spring-boot-admin-server-ui/target/dist/
template-location: file:@project.basedir@/../../spring-boot-admin-server-ui/target/dist/
resource-locations: file:@project.basedir@/../../spring-boot-admin-server-ui/target/dist/
cache-templates: false
extension-resource-locations: file:@project.basedir@/../spring-boot-admin-sample-custom-ui/target/dist/
---
spring:
......
......@@ -9,11 +9,11 @@
"test:watch": "jest --watch"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.0",
"@fortawesome/free-brands-svg-icons": "^5.1.0",
"@fortawesome/free-regular-svg-icons": "^5.1.0",
"@fortawesome/free-solid-svg-icons": "^5.1.0",
"@fortawesome/vue-fontawesome": "^0.1.0",
"@fortawesome/fontawesome-svg-core": "^1.2.1",
"@fortawesome/free-brands-svg-icons": "^5.1.1",
"@fortawesome/free-regular-svg-icons": "^5.1.1",
"@fortawesome/free-solid-svg-icons": "^5.1.1",
"@fortawesome/vue-fontawesome": "^0.1.1",
"ansi_up": "^3.0.0",
"axios": "^0.18.0",
"bulma": "^0.7.1",
......@@ -33,7 +33,7 @@
"popper.js": "^1.14.3",
"pretty-bytes": "^5.1.0",
"resize-observer-polyfill": "^1.5.0",
"rxjs": "^6.2.1",
"rxjs": "^6.2.2",
"vue": "^2.5.16",
"vue-clickaway": "^2.2.1",
"vue-router": "^3.0.1",
......@@ -41,10 +41,10 @@
},
"devDependencies": {
"@vue/test-utils": "^1.0.0-beta.20",
"autoprefixer": "^8.6.4",
"autoprefixer": "^8.6.5",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.5",
"babel-jest": "^23.2.0",
"babel-eslint": "^8.2.6",
"babel-jest": "^23.4.0",
"babel-loader": "^7.1.5",
"babel-plugin-lodash": "^3.3.4",
"babel-polyfill": "^6.26.0",
......@@ -52,26 +52,25 @@
"babel-preset-stage-2": "^6.24.1",
"clean-webpack-plugin": "^0.1.19",
"cross-env": "^5.2.0",
"css-hot-loader": "^1.3.9",
"css-hot-loader": "^1.4.0",
"css-loader": "^0.28.11",
"css-mqpacker": "^6.0.2",
"eslint": "^5.0.1",
"eslint-loader": "^1.9.0",
"eslint": "^4.19.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.5",
"eslint-plugin-vue": "^4.5.0",
"eslint-plugin-vue": "^4.7.0",
"extract-text-webpack-plugin": "^3.0.2",
"file-loader": "^1.1.11",
"glob": "^7.1.2",
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.30.0",
"html-webpack-plugin": "^3.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^23.3.0",
"jest": "^23.4.1",
"lodash-webpack-plugin": "^0.11.5",
"node-sass": "^4.9.1",
"node-sass": "^4.9.2",
"optimize-css-assets-webpack-plugin": "^3.2.0",
"postcss-loader": "^2.1.5",
"sass-loader": "^6.0.7",
"style-loader": "^0.20.3",
"postcss-loader": "^2.1.6",
"sass-loader": "^7.0.3",
"style-loader": "^0.21.0",
"url-loader": "^0.6.2",
"vue-jest": "^2.6.0",
"vue-loader": "^14.2.3",
......
......@@ -119,6 +119,7 @@
<nonFilteredFileExtension>ico</nonFilteredFileExtension>
<nonFilteredFileExtension>png</nonFilteredFileExtension>
</nonFilteredFileExtensions>
<includeEmptyDirs>true</includeEmptyDirs>
</configuration>
</plugin>
</plugins>
......
......@@ -24,6 +24,9 @@
<meta name="theme-color" content="#42d3a5">
<link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
<link href="assets/css/sba-core.css" rel="stylesheet">
<th:block th:each="cssFile : ${cssExtensions}">
<link th:href="${cssFile.resourcePath}" rel="stylesheet">
</th:block>
<title th:text="${uiSettings.title}">Spring Boot Admin</title>
</head>
<body>
......@@ -33,10 +36,14 @@
<script th:inline="javascript">
var SBA = {
uiSettings: /*[[${uiSettings}]]*/ {},
user: /*[[${user}]]*/ null
user: /*[[${user}]]*/ null,
extensions: []
}
</script>
<script lang="javascript" src="assets/js/vendors.js" defer></script>
<th:block th:each="jsFile : ${jsExtensions}">
<script lang="javascript" th:src="${jsFile.resourcePath}" defer></script>
</th:block>
<script lang="javascript" src="assets/js/sba-core.js" defer></script>
</body>
</html>
......@@ -31,8 +31,14 @@ moment.locale(window.navigator.language);
const applicationStore = new Store();
const viewRegistry = new ViewRegistry();
Notifications.install({applicationStore});
views.forEach(view => view.install({
const installables = [
Notifications,
...views,
/* global SBA */
...SBA.extensions
];
installables.forEach(view => view.install({
viewRegistry,
applicationStore
}));
......
......@@ -25,6 +25,9 @@ class Application {
constructor(name) {
this.name = name;
this.axios = axios.create({
baseURL: uri`applications/${this.name}/`
})
}
findInstance(instanceId) {
......@@ -36,7 +39,7 @@ class Application {
}
async unregister() {
return axios.delete(uri`applications/${this.name}`)
return this.axios.delete('')
}
static async list() {
......
......@@ -30,6 +30,9 @@ const actuatorMimeTypes = [
class Instance {
constructor(id) {
this.id = id;
this.axios = axios.create({
baseURL: uri`instances/${this.id}/`
})
}
hasEndpoint(endpointId) {
......@@ -41,17 +44,17 @@ class Instance {
}
async unregister() {
return axios.delete(uri`instances/${this.id}`);
return this.axios.delete('');
}
async fetchInfo() {
return axios.get(uri`instances/${this.id}/actuator/info`, {
return this.axios.get(uri`actuator/info`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchMetrics() {
return axios.get(uri`instances/${this.id}/actuator/metrics`, {
return this.axios.get(uri`actuator/metrics`, {
headers: {'Accept': actuatorMimeTypes}
});
}
......@@ -63,7 +66,7 @@ class Instance {
.map(([name, value]) => `${name}:${value}`)
.join(',')
} : {};
return axios.get(uri`instances/${this.id}/actuator/metrics/${metric}`, {
return this.axios.get(uri`actuator/metrics/${metric}`, {
headers: {'Accept': actuatorMimeTypes},
params
});
......@@ -71,7 +74,7 @@ class Instance {
async fetchHealth() {
try {
return await axios.get(uri`instances/${this.id}/actuator/health`, {
return await this.axios.get(uri`actuator/health`, {
headers: {'Accept': actuatorMimeTypes}
});
} catch (error) {
......@@ -83,99 +86,99 @@ class Instance {
}
async fetchEnv(name) {
return axios.get(uri`instances/${this.id}/actuator/env/${name || '' }`, {
return this.axios.get(uri`actuator/env/${name || '' }`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async hasEnvManagerSupport() {
const response = await axios.options(uri`instances/${this.id}/actuator/env`);
const response = await this.axios.options(uri`actuator/env`);
return response.headers['allow'] && response.headers['allow'].includes('POST');
}
async resetEnv() {
return axios.delete(uri`instances/${this.id}/actuator/env`);
return this.axios.delete(uri`actuator/env`);
}
async setEnv(name, value) {
return axios.post(uri`instances/${this.id}/actuator/env`, {name, value}, {
return this.axios.post(uri`actuator/env`, {name, value}, {
headers: {'Content-Type': 'application/json'}
});
}
async refreshContext() {
return axios.post(uri`instances/${this.id}/actuator/refresh`);
return this.axios.post(uri`actuator/refresh`);
}
async fetchLiquibase() {
return axios.get(uri`instances/${this.id}/actuator/liquibase`, {
return this.axios.get(uri`actuator/liquibase`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchFlyway() {
return axios.get(uri`instances/${this.id}/actuator/flyway`, {
return this.axios.get(uri`actuator/flyway`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchLoggers() {
return axios.get(uri`instances/${this.id}/actuator/loggers`, {
return this.axios.get(uri`actuator/loggers`, {
headers: {'Accept': actuatorMimeTypes},
transformResponse: Instance._toLoggers
});
}
async configureLogger(name, level) {
return axios.post(uri`instances/${this.id}/actuator/loggers/${name}`, {configuredLevel: level}, {
return this.axios.post(uri`actuator/loggers/${name}`, {configuredLevel: level}, {
headers: {'Content-Type': 'application/json'}
});
}
async fetchHttptrace() {
return axios.get(uri`instances/${this.id}/actuator/httptrace`, {
return this.axios.get(uri`actuator/httptrace`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchThreaddump() {
return axios.get(uri`instances/${this.id}/actuator/threaddump`, {
return this.axios.get(uri`actuator/threaddump`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchAuditevents(after) {
return axios.get(uri`instances/${this.id}/actuator/auditevents`, {
return this.axios.get(uri`actuator/auditevents`, {
headers: {'Accept': actuatorMimeTypes},
params: {after: after.toISOString()}
});
}
async fetchSessionsByUsername(username) {
return axios.get(uri`instances/${this.id}/actuator/sessions`, {
return this.axios.get(uri`actuator/sessions`, {
headers: {'Accept': actuatorMimeTypes},
params: {username}
});
}
async fetchSession(sessionId) {
return axios.get(uri`instances/${this.id}/actuator/sessions/${sessionId}`, {
return this.axios.get(uri`actuator/sessions/${sessionId}`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async deleteSession(sessionId) {
return axios.delete(uri`instances/${this.id}/actuator/sessions/${sessionId}`, {
return this.axios.delete(uri`actuator/sessions/${sessionId}`, {
headers: {'Accept': actuatorMimeTypes}
});
}
streamLogfile(interval) {
return logtail(uri`instances/${this.id}/actuator/logfile`, interval);
return logtail(uri`actuator/logfile`, interval);
}
async listMBeans() {
return axios.get(uri`instances/${this.id}/actuator/jolokia/list`, {
return this.axios.get(uri`actuator/jolokia/list`, {
headers: {'Accept': 'application/json'},
params: {canonicalNaming: false},
transformResponse: Instance._toMBeans
......@@ -188,7 +191,7 @@ class Instance {
mbean: `${domain}:${mBean}`,
config: {ignoreErrors: true}
};
return axios.post(uri`instances/${this.id}/actuator/jolokia`, body, {
return this.axios.post(uri`actuator/jolokia`, body, {
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
});
}
......@@ -200,7 +203,7 @@ class Instance {
attribute,
value
};
return axios.post(uri`instances/${this.id}/actuator/jolokia`, body, {
return this.axios.post(uri`actuator/jolokia`, body, {
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
});
}
......@@ -212,7 +215,7 @@ class Instance {
operation,
'arguments': args
};
return axios.post(uri`instances/${this.id}/actuator/jolokia`, body, {
return this.axios.post(uri`actuator/jolokia`, body, {
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
});
}
......
......@@ -30,7 +30,7 @@
<div class="navbar-start"/>
<div class="navbar-end">
<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"/>
<component :is="view.label" :applications="applications" :error="error"/>
</router-link>
<div class="navbar-item has-dropdown is-hoverable" v-if="userName">
......@@ -79,7 +79,7 @@
computed: {
enabledViews() {
return [...this.views].filter(
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled())
view => view.label && (typeof view.isEnabled === 'undefined' || view.isEnabled())
).sort(compareBy(v => v.order));
}
},
......
......@@ -51,8 +51,8 @@ export default class ViewRegistry {
}
_addView(view) {
if (view.handle) {
view.handle = createVNodeIfNecessary(view.handle);
if (view.label) {
view.label = createVNodeIfNecessary(view.label);
}
this._views.push(view);
}
......
......@@ -87,7 +87,7 @@
viewRegistry.addView({
path: '/about',
name: 'about',
handle: 'About',
label: 'About',
order: 200,
component: this
});
......
......@@ -64,7 +64,7 @@
<script>
import * as _ from 'lodash';
import applicationsList from './applications-list';
import handle from './handle';
import label from './label';
export default {
props: {
......@@ -109,7 +109,7 @@
path: '/applications/:selected?',
props: true,
name: 'applications',
handle: handle,
label,
order: 0,
component: this
});
......
......@@ -131,7 +131,7 @@
path: 'auditevents',
component: this,
props: true,
handle: 'Audit Log',
label: 'Audit Log',
order: 600,
isEnabled: ({instance}) => instance.hasEndpoint('auditevents')
});
......
......@@ -153,7 +153,7 @@
path: '',
component: this,
props: true,
handle: 'Details',
label: 'Details',
order: 0
});
}
......
......@@ -140,7 +140,7 @@
path: 'env',
component: this,
props: true,
handle: 'Environment',
label: 'Environment',
order: 100,
isEnabled: ({instance}) => instance.hasEndpoint('env')
});
......
......@@ -129,7 +129,7 @@
path: 'flyway',
component: this,
props: true,
handle: 'Flyway',
label: 'Flyway',
order: 900,
isEnabled: ({instance}) => instance.hasEndpoint('flyway')
});
......
......@@ -51,7 +51,7 @@
path: 'heapdump',
component: this,
props: true,
handle: 'Heap Dump',
label: 'Heap Dump',
order: 800,
isEnabled: ({instance}) => instance.hasEndpoint('heapdump')
});
......
......@@ -241,7 +241,7 @@
path: 'httptrace',
component: this,
props: true,
handle: 'Http Traces',
label: 'Http Traces',
order: 500,
isEnabled: ({instance}) => instance.hasEndpoint('httptrace')
});
......
......@@ -228,7 +228,7 @@
path: 'jolokia',
component: this,
props: true,
handle: 'JMX',
label: 'JMX',
order: 350,
isEnabled: ({instance}) => instance.hasEndpoint('jolokia')
});
......
......@@ -149,7 +149,7 @@
path: 'liquibase',
component: this,
props: true,
handle: 'Liquibase',
label: 'Liquibase',
order: 900,
isEnabled: ({instance}) => instance.hasEndpoint('liquibase')
});
......
......@@ -132,7 +132,7 @@
path: 'logfile',
component: this,
props: true,
handle: 'Logfile',
label: 'Logfile',
order: 200,
isEnabled: ({instance}) => instance.hasEndpoint('logfile')
});
......
......@@ -207,7 +207,7 @@
path: 'loggers',
component: this,
props: true,
handle: 'Loggers',
label: 'Loggers',
order: 300,
isEnabled: ({instance}) => instance.hasEndpoint('loggers')
});
......
......@@ -219,7 +219,7 @@
path: 'metrics',
component: this,
props: true,
handle: 'Metrics',
label: 'Metrics',
order: 50,
isEnabled: ({instance}) => instance.hasEndpoint('metrics')
});
......
......@@ -154,7 +154,7 @@
path: 'sessions',
component: this,
props: true,
handle: 'Sessions',
label: 'Sessions',
order: 700,
isEnabled: ({instance}) => instance.hasEndpoint('sessions')
});
......
......@@ -30,13 +30,8 @@
<ul>
<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">
<component :is="view.handle"/>
</a>
<router-link v-else
:to="{ name: view.name, params: { 'instanceId' : instance.id } }">
<component :is="view.handle"/>
<router-link :to="{ name: view.name, params: { 'instanceId' : instance.id } }">
<component :is="view.label"/>
</router-link>
</li>
</ul>
......@@ -76,7 +71,7 @@
}
return [...this.views].filter(
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled({instance: this.instance}))
view => view.label && (typeof view.isEnabled === 'undefined' || view.isEnabled({instance: this.instance}))
).sort(compareBy(v => v.order));
}
},
......
......@@ -133,7 +133,7 @@
path: 'threaddump',
component: this,
props: true,
handle: 'Threads',
label: 'Threads',
order: 400,
isEnabled: ({instance}) => instance.hasEndpoint('threaddump')
});
......
......@@ -131,7 +131,7 @@
viewRegistry.addView({
path: '/journal',
name: 'journal',
handle: 'Journal',
label: 'Journal',
order: 100,
component: this
});
......
......@@ -80,7 +80,7 @@
viewRegistry.addView({
path: '/wallboard',
name: 'wallboard',
handle: 'Wallboard',
label: 'Wallboard',
order: -100,
component: this
});
......
......@@ -19,9 +19,15 @@ package de.codecentric.boot.admin.server.ui.config;
import de.codecentric.boot.admin.server.config.AdminServerMarkerConfiguration;
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import de.codecentric.boot.admin.server.config.AdminServerWebConfiguration;
import de.codecentric.boot.admin.server.ui.extensions.UiExtension;
import de.codecentric.boot.admin.server.ui.extensions.UiExtensionsScanner;
import de.codecentric.boot.admin.server.ui.web.UiController;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
......@@ -41,6 +47,7 @@ import org.thymeleaf.templatemode.TemplateMode;
@AutoConfigureAfter(AdminServerWebConfiguration.class)
@EnableConfigurationProperties(AdminServerUiProperties.class)
public class AdminServerUiAutoConfiguration {
private static final Logger log = LoggerFactory.getLogger(AdminServerUiAutoConfiguration.class);
private final AdminServerUiProperties uiProperties;
private final AdminServerProperties adminServerProperties;
private final ApplicationContext applicationContext;
......@@ -55,10 +62,20 @@ public class AdminServerUiAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public UiController homeUiController() {
return new UiController(adminServerProperties.getContextPath(),
uiProperties.getTitle(),
uiProperties.getBrand());
public UiController homeUiController() throws IOException {
return new UiController(
this.adminServerProperties.getContextPath(),
this.uiProperties.getTitle(),
this.uiProperties.getBrand(),
this.uiExtensions()
);
}
private List<UiExtension> uiExtensions() throws IOException {
UiExtensionsScanner scanner = new UiExtensionsScanner(this.applicationContext);
List<UiExtension> uiExtensions = scanner.scan(this.uiProperties.getExtensionResourceLocations());
uiExtensions.forEach(e -> log.info("Loaded Spring Boot Admin UI Extension: " + e));
return uiExtensions;
}
@Bean
......@@ -87,6 +104,7 @@ public class AdminServerUiAutoConfiguration {
public void addResourceHandlers(org.springframework.web.reactive.config.ResourceHandlerRegistry registry) {
registry.addResourceHandler(adminServerProperties.getContextPath() + "/**")
.addResourceLocations(uiProperties.getResourceLocations())
.addResourceLocations(uiProperties.getExtensionResourceLocations())
.setCacheControl(uiProperties.getCache().toCacheControl());
}
}
......@@ -103,6 +121,7 @@ public class AdminServerUiAutoConfiguration {
public void addResourceHandlers(org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry registry) {
registry.addResourceHandler(adminServerProperties.getContextPath() + "/**")
.addResourceLocations(uiProperties.getResourceLocations())
.addResourceLocations(uiProperties.getExtensionResourceLocations())
.setCacheControl(uiProperties.getCache().toCacheControl());
}
}
......
......@@ -27,6 +27,7 @@ import org.springframework.http.CacheControl;
@ConfigurationProperties("spring.boot.admin.ui")
public class AdminServerUiProperties {
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {"classpath:/META-INF/spring-boot-admin-server-ui/"};
private static final String[] CLASSPATH_EXTENSION_RESOURCE_LOCATIONS = {"classpath:/META-INF/spring-boot-admin-server-ui/extensions/"};
/**
* Locations of SBA ui resources.
......@@ -34,6 +35,11 @@ public class AdminServerUiProperties {
private String[] resourceLocations = CLASSPATH_RESOURCE_LOCATIONS;
/**
* Locations of SBA ui exentsion resources.
*/
private String[] extensionResourceLocations = CLASSPATH_EXTENSION_RESOURCE_LOCATIONS;
/**
* Locations of SBA ui template.
*/
private String templateLocation = CLASSPATH_RESOURCE_LOCATIONS[0];
......
/*
* 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.
*/
package de.codecentric.boot.admin.server.ui.extensions;
@lombok.Data
public class UiExtension {
private final String resourcePath;
private final String resourceLocation;
}
/*
* 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.
*/
package de.codecentric.boot.admin.server.ui.extensions;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.ResourcePatternResolver;
public class UiExtensionsScanner {
private final ResourcePatternResolver resolver;
public UiExtensionsScanner(ResourcePatternResolver resolver) {
this.resolver = resolver;
}
public List<UiExtension> scan(String... locations) throws IOException {
List<UiExtension> extensions = new ArrayList<>();
for (String location : locations) {
for (Resource resource : this.resolver.getResources(toPattern(location))) {
String resourcePath = this.getResourcePath(location, resource);
if (resourcePath != null && isExtension(resource)) {
extensions.add(new UiExtension(resourcePath, location + resourcePath));
}
}
}
return extensions;
}
private String toPattern(String location) {
return location + "**";
}
private boolean isExtension(Resource resource) {
return resource.isReadable() && resource.getFilename().endsWith(".css") || resource.getFilename().endsWith(".js");
}
private String getResourcePath(String location, Resource resource) throws IOException {
String locationWithouPrefix = location.replaceFirst("^[^:]+:", "");
Matcher m = Pattern.compile(Pattern.quote(locationWithouPrefix) + "(.+)$")
.matcher(resource.getURI().toString());
if (m.find()) {
return m.group(1);
} else {
return null;
}
}
}
......@@ -16,11 +16,14 @@
package de.codecentric.boot.admin.server.ui.web;
import de.codecentric.boot.admin.server.ui.extensions.UiExtension;
import de.codecentric.boot.admin.server.web.AdminController;
import java.security.Principal;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
......@@ -31,13 +34,21 @@ import static java.util.Collections.singletonMap;
@AdminController
public class UiController {
private final String adminContextPath;
private final List<UiExtension> cssExtensions;
private final List<UiExtension> jsExtensions;
private final Map<String, Object> uiSettings;
public UiController(String adminContextPath, String title, String brand) {
public UiController(String adminContextPath, String title, String brand, List<UiExtension> uiExtensions) {
this.adminContextPath = adminContextPath;
this.uiSettings = new HashMap<>();
this.uiSettings.put("title", title);
this.uiSettings.put("brand", brand);
this.cssExtensions = uiExtensions.stream()
.filter(e -> e.getResourcePath().endsWith(".css"))
.collect(Collectors.toList());
this.jsExtensions = uiExtensions.stream()
.filter(e -> e.getResourcePath().endsWith(".js"))
.collect(Collectors.toList());
}
@ModelAttribute(value = "adminContextPath", binding = false)
......@@ -50,6 +61,16 @@ public class UiController {
return uiSettings;
}
@ModelAttribute(value = "cssExtensions", binding = false)
public List<UiExtension> getCssExtensions() {
return cssExtensions;
}
@ModelAttribute(value = "jsExtensions", binding = false)
public List<UiExtension> getJsExtensions() {
return jsExtensions;
}
@ModelAttribute(value = "user", binding = false)
public Map<String, Object> getUiSettings(Principal principal) {
if (principal != null) {
......
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