Commit 4c8196c5 by Johannes Edmeier

Add environment manager

parent 43a8fb1d
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2014-2017 the original author or authors.
~ 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.
......@@ -16,57 +16,65 @@
-->
<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-eureka</artifactId>
<name>Spring Boot Admin Sample Eureka</name>
<description>Spring Boot Admin Sample using Eureka</description>
<parent>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-samples</artifactId>
<version>${revision}</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<!-- tag::dependency-eureka[] -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- end::dependency-eureka[] -->
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>de.codecentric.boot.admin.SpringBootAdminApplication</mainClass>
<addResources>false</addResources>
</configuration>
</plugin>
</plugins>
</build>
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-eureka</artifactId>
<name>Spring Boot Admin Sample Eureka</name>
<description>Spring Boot Admin Sample using Eureka</description>
<parent>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-samples</artifactId>
<version>${revision}</version>
<relativePath>..</relativePath>
</parent>
<dependencies>
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- tag::dependency-eureka[] -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- end::dependency-eureka[] -->
<dependency>
<groupId>org.jolokia</groupId>
<artifactId>jolokia-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
<goal>build-info</goal>
</goals>
</execution>
</executions>
<configuration>
<mainClass>de.codecentric.boot.admin.SpringBootAdminApplication</mainClass>
<addResources>false</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
/*
* Copyright 2014-2017 the original author or authors.
* 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.
......@@ -21,6 +21,8 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
// tag::application-eureka[]
@Configuration
......@@ -31,5 +33,15 @@ public class SpringBootAdminApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootAdminApplication.class, args);
}
@Configuration
public static class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll()//
.and().csrf().disable();
}
}
}
// end::application-eureka[]
spring:
application:
name: eureka-example
cloud:
config:
enabled: false
# tag::configuration-eureka[]
eureka: #<1>
eureka:
instance:
leaseRenewalIntervalInSeconds: 10
client:
registryFetchIntervalSeconds: 5
serviceUrl:
defaultZone: ${EUREKA_SERVICE_URL:http://localhost:8761}/eureka/
management.security.enabled: false #<2>
# end::configuration-eureka[]
# tag::configuration-ui-hystrix[]
spring.boot.admin.routes.endpoints: env,metrics,dump,jolokia,info,configprops,trace,logfile,refresh,flyway,liquibase,heapdump,loggers,auditevents,hystrix.stream
# end::configuration-ui-hystrix[]
# tag::configuration-ui-turbine[]
spring.boot.admin.turbine:
clusters: default
location: turbine #<1>
# end::configuration-ui-turbine[]
management:
endpoints:
web:
expose: "*"
endpoint:
health:
show-details: true
......@@ -40,7 +40,7 @@
"autoprefixer": "^7.2.5",
"babel-core": "^6.25.0",
"babel-eslint": "^8.2.1",
"babel-jest": "^22.1.0",
"babel-jest": "^22.2.0",
"babel-loader": "^7.1.1",
"babel-plugin-lodash": "^3.3.2",
"babel-polyfill": "^6.26.0",
......@@ -60,7 +60,7 @@
"html-loader": "^0.5.5",
"html-webpack-plugin": "^2.30.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^22.1.4",
"jest": "^22.2.1",
"jest-vue": "^0.8.2",
"lodash-webpack-plugin": "^0.11.4",
"node-sass": "^4.7.2",
......
......@@ -42,7 +42,8 @@
}
}
.is-loading {
.section.is-loading,
.content.is-loading {
&::after {
@include loader;
height: 5rem;
......
......@@ -37,11 +37,11 @@
},
click(event) {
if (this.confirm) {
event.target.style.width = null;
this.$el.style.width = null;
this.$emit('click', event);
} else {
const width = event.target.getBoundingClientRect().width;
event.target.style.width = `${width}px`;
const width = this.$el.getBoundingClientRect().width;
this.$el.style.width = `${width}px`;
}
this.confirm = !this.confirm;
}
......
......@@ -16,32 +16,40 @@
<template>
<button type="button" class="icon-button" :disabled="disabled">
<slot></slot>
<font-awesome-icon :icon="icon" :size="size" :class="iconClass"></font-awesome-icon>
</button>
</template>
<script>
export default {
props: {
disabled: Boolean,
},
props: ['disabled', 'icon', 'size', 'icon-class']
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.icon-button {
background: none;
border: none;
padding: 0;
font-size: 1em;
color: inherit;
&:not([disabled]) {
cursor: pointer;
&:hover {
color: $black;
}
& svg {
fill: currentcolor;
}
}
&:disabled {
opacity: 0.5;
pointer-events: none;
}
......
......@@ -34,8 +34,6 @@
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.panel {
margin-bottom: 1.5rem;
}
......
......@@ -46,9 +46,9 @@ store.addEventListener('updated', (newVal, oldVal) => {
}
});
Vue.component('font-awesome-icon', FontAwesomeIcon);
Vue.use(VueRouter);
Vue.use(sbaComponents);
Vue.component('font-awesome-icon', FontAwesomeIcon);
const router = new VueRouter({
linkActiveClass: 'is-active'
......
......@@ -52,6 +52,7 @@ class Instance {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchMetric(metric, tags) {
const params = tags ? {tag: _.entries(tags).map(([name, value]) => `${name}:${value}`).join(',')} : {};
return axios.get(`instances/${this.id}/actuator/metrics/${metric}`, {
......@@ -67,25 +68,46 @@ class Instance {
}
async fetchEnv(name) {
return await axios.get(`instances/${this.id}/actuator/env${name ? `/${name}` : '' }`, {
return axios.get(`instances/${this.id}/actuator/env${name ? `/${name}` : '' }`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async hasEnvManagerSupport() {
const response = await axios.options(`instances/${this.id}/actuator/env`);
return response.headers['allow'] && response.headers['allow'].indexOf('POST') >= 0;
}
async resetEnv() {
return axios.delete(`instances/${this.id}/actuator/env`);
}
async setEnv(name, value) {
return axios.post(`instances/${this.id}/actuator/env`, {name, value}, {
headers: {
'Content-Type': 'application/json'
}
});
}
async refreshContext() {
return axios.post(`instances/${this.id}/actuator/refresh`);
}
async fetchLiquibase() {
return await axios.get(`instances/${this.id}/actuator/liquibase`, {
return axios.get(`instances/${this.id}/actuator/liquibase`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchFlyway() {
return await axios.get(`instances/${this.id}/actuator/flyway`, {
return axios.get(`instances/${this.id}/actuator/flyway`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchLoggers() {
return await axios.get(`instances/${this.id}/actuator/loggers`, {
return axios.get(`instances/${this.id}/actuator/loggers`, {
headers: {
'Accept': actuatorMimeTypes
},
......@@ -94,7 +116,7 @@ class Instance {
}
async configureLogger(name, level) {
return await axios.post(`instances/${this.id}/actuator/loggers/${name}`, {configuredLevel: level}, {
return axios.post(`instances/${this.id}/actuator/loggers/${name}`, {configuredLevel: level}, {
headers: {
'Content-Type': 'application/json'
}
......@@ -102,19 +124,19 @@ class Instance {
}
async fetchHttptrace() {
return await axios.get(`instances/${this.id}/actuator/httptrace`, {
return axios.get(`instances/${this.id}/actuator/httptrace`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchThreaddump() {
return await axios.get(`instances/${this.id}/actuator/threaddump`, {
return axios.get(`instances/${this.id}/actuator/threaddump`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchAuditevents(after) {
return await axios.get(`instances/${this.id}/actuator/auditevents`, {
return axios.get(`instances/${this.id}/actuator/auditevents`, {
headers: {'Accept': actuatorMimeTypes},
params: {
after: after.toISOString()
......@@ -123,7 +145,7 @@ class Instance {
}
async fetchSessions(username) {
return await axios.get(`instances/${this.id}/actuator/sessions`, {
return axios.get(`instances/${this.id}/actuator/sessions`, {
headers: {'Accept': actuatorMimeTypes},
params: {
username
......@@ -132,13 +154,13 @@ class Instance {
}
async fetchSession(sessionId) {
return await axios.get(`instances/${this.id}/actuator/sessions/${sessionId}`, {
return axios.get(`instances/${this.id}/actuator/sessions/${sessionId}`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async deleteSession(sessionId) {
return await axios.delete(`instances/${this.id}/actuator/sessions/${sessionId}`, {
return axios.delete(`instances/${this.id}/actuator/sessions/${sessionId}`, {
headers: {'Accept': actuatorMimeTypes}
});
}
......@@ -148,7 +170,7 @@ class Instance {
}
static async fetchEvents() {
return await axios.get(`instances/events`);
return axios.get(`instances/events`);
}
static getEventStream() {
......@@ -166,7 +188,7 @@ class Instance {
}
static async get(id) {
return await axios.get(`instances/${id}`, {
return axios.get(`instances/${id}`, {
transformResponse: Instance._toInstance
});
}
......
......@@ -25,6 +25,7 @@ import faDownload from '@fortawesome/fontawesome-free-solid/faDownload';
import faExclamation from '@fortawesome/fontawesome-free-solid/faExclamation';
import faExclamationTriangle from '@fortawesome/fontawesome-free-solid/faExclamationTriangle';
import faMinusCircle from '@fortawesome/fontawesome-free-solid/faMinusCircle';
import faPencilAlt from '@fortawesome/fontawesome-free-solid/faPencilAlt';
import faQuestionCircle from '@fortawesome/fontawesome-free-solid/faQuestionCircle';
import faSignOutAlt from '@fortawesome/fontawesome-free-solid/faSignOutAlt';
import faStepBackward from '@fortawesome/fontawesome-free-solid/faStepBackward';
......@@ -34,6 +35,6 @@ import faTrash from '@fortawesome/fontawesome-free-solid/faTrash';
import FontAwesomeIcon from '@fortawesome/vue-fontawesome';
fontawesome.library.add(faGithub, faStackOverflow, faGitter, faTrash, faDownload, faStepForward, faStepBackward, faCheck, faQuestionCircle, faBan, faTimesCircle, faMinusCircle, faExclamation,
faBook, faSignOutAlt, faExclamationTriangle);
faBook, faSignOutAlt, faExclamationTriangle, faPencilAlt);
export default FontAwesomeIcon;
\ No newline at end of file
......@@ -19,12 +19,12 @@ import 'rxjs/add/observable/empty';
import 'rxjs/add/observable/from';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/concat';
import 'rxjs/add/operator/concatAll';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/ignoreElements';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/retryWhen';
......@@ -32,6 +32,14 @@ import 'rxjs/add/operator/retryWhen';
import {Observable} from 'rxjs/Observable';
import {animationFrame} from 'rxjs/scheduler/animationFrame';
Observable.prototype.doOnSubscribe = function (onSubscribe) {
let source = this;
return Observable.defer(() => {
onSubscribe();
return source;
});
};
Observable.prototype.doFirst = function (doFirst) {
let source = this;
let triggered = false;
......@@ -46,6 +54,22 @@ Observable.prototype.doFirst = function (doFirst) {
});
};
Observable.prototype.listen = function (callbackFn) {
let handle = null;
return this.doOnSubscribe(() => handle = setTimeout(() => callbackFn('executing'), 150))
.do({
complete: () => {
handle && clearTimeout(handle);
callbackFn('completed');
},
error: (error) => {
console.warn("Operation failed:", error);
handle && clearTimeout(handle);
callbackFn('failed');
}
});
};
export {
Observable,
animationFrame
......
......@@ -36,10 +36,9 @@
<div class="applications-list-item__text"
v-text="application.version"></div>
<div class="applications-list-item__actions">
<sba-icon-button
v-if="application.isUnregisterable"
@click.native.stop="unregister(application)">
<font-awesome-icon icon="trash"></font-awesome-icon>
<sba-icon-button icon="trash"
v-if="application.isUnregisterable"
@click.native.stop="unregister(application)">
</sba-icon-button>
</div>
</div>
......@@ -50,10 +49,9 @@
@click.stop="deselect()">
<div class="applications-list-item__header-text" v-text="application.name"></div>
<div class="applications-list-item__header-actions">
<sba-icon-button
v-if="application.isUnregisterable"
@click.native.stop="unregister(application)">
<font-awesome-icon icon="trash"></font-awesome-icon>
<sba-icon-button icon="trash"
v-if="application.isUnregisterable"
@click.native.stop="unregister(application)">
</sba-icon-button>
</div>
</div>
......@@ -74,9 +72,9 @@
<span v-text="instance.info.version"
class="applications-list-item__text"></span>
<div class="applications-list-item__actions">
<sba-icon-button v-if="instance.isUnregisterable"
<sba-icon-button icon="trash"
v-if="instance.isUnregisterable"
@click.native.stop="unregister(instance)">
<font-awesome-icon icon="trash"></font-awesome-icon>
</sba-icon-button>
</div>
</li>
......
......@@ -35,10 +35,15 @@
</div>
</div>
</div>
<div class="content" v-if="env && hasEnvManagerSupport">
<sba-env-manager :instance="instance" :property-sources="env.propertySources"
@refresh="fetchEnv()" @update="fetchEnv" @reset="fetchEnv()">
</sba-env-manager>
</div>
<div class="content" v-if="env">
<div class="field has-addons">
<p class="control is-expanded">
<input class="input" type="text" placeholder="name / value" v-model="filter">
<input class="input" type="search" placeholder="name / value filter" v-model="filter">
</p>
</div>
</div>
......@@ -65,6 +70,7 @@
<script>
import _ from 'lodash';
import sbaEnvManager from './env-manager';
const filterProperty = (needle) => (property, name) => {
return name.toString().toLowerCase().indexOf(needle) >= 0 || property.value.toString().toLowerCase().indexOf(needle) >= 0;
......@@ -82,12 +88,13 @@
export default {
props: ['instance'],
components: {sbaEnvManager},
data: () => ({
hasLoaded: false,
error: null,
env: null,
filter: null
filter: null,
hasEnvManagerSupport: false
}),
computed: {
propertySources() {
......@@ -101,10 +108,12 @@
},
created() {
this.fetchEnv();
this.determineEnvManagerSupport();
},
watch: {
instance() {
this.fetchEnv();
this.determineEnvManagerSupport();
}
},
methods: {
......@@ -120,6 +129,16 @@
}
this.hasLoaded = true;
}
},
async determineEnvManagerSupport() {
if (this.instance) {
try {
this.hasEnvManagerSupport = await this.instance.hasEnvManagerSupport();
} catch (error) {
console.warn('Determine env manager support failed:', error);
this.hasEnvManagerSupport = false;
}
}
}
}
}
......
......@@ -29,7 +29,7 @@
<div class="field-body">
<div class="field has-addons">
<p class="control is-expanded">
<input class="input" type="text" placeholder="path" v-model="filter">
<input class="input" type="search" placeholder="path filter" v-model="filter">
</p>
<p class="control">
<span class="button is-static">
......
......@@ -26,11 +26,11 @@
</div>
<div class="logfile-view-actions" v-if="hasLoaded">
<div class="logfile-view-actions__navigation">
<sba-icon-button :disabled="atTop" @click.native="scrollToTop">
<font-awesome-icon icon="step-backward" size="lg" class="rotated"></font-awesome-icon>
<sba-icon-button :disabled="atTop" @click.native="scrollToTop" icon="step-backward" size="lg"
icon-class="rotated">
</sba-icon-button>
<sba-icon-button :disabled="atBottom" @click.native="scrollToBottom">
<font-awesome-icon icon="step-forward" size="lg" class="rotated"></font-awesome-icon>
<sba-icon-button :disabled="atBottom" @click.native="scrollToBottom" icon="step-forward" size="lg"
icon-class="rotated">
</sba-icon-button>
</div>
<a class="button" v-if="instance" :href="`instances/${instance.id}/logfile`" target="_blank">
......
......@@ -29,7 +29,7 @@
<div class="field-body">
<div class="field has-addons">
<p class="control is-expanded">
<input class="input" type="text" placeholder="name" v-model="filter">
<input class="input" type="search" placeholder="name filter" v-model="filter">
</p>
<p class="control">
<span class="button is-static">
......
......@@ -23,7 +23,7 @@
</button>
</div>
<div class="control">
<button class="button" :class="{ 'is-loading' : isLoading === null }"
<button class="button is-light" :class="{ 'is-loading' : isLoading === null }"
:disabled="!configured || !allowReset" @click.stop="selectLevel(null)">
Reset
</button>
......
......@@ -115,12 +115,13 @@
await vm.instance.deleteSession(sessionId);
return sessionId;
})
.do(sessionId => vm.$set(vm.deleting, sessionId, 'deleted'))
.catch(error => {
vm.$set(vm.deleting, sessionId, 'failed');
console.warn(`Deleting session ${sessionId} failed:`, error);
throw error;
})
.do({
next: sessionId => vm.$set(vm.deleting, sessionId, 'deleted'),
error: error => {
console.warn(`Deleting session ${sessionId} failed:`, error);
vm.$set(vm.deleting, sessionId, 'failed');
}
});
}
}
}
......
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