🔥 Removing code or files. 删除 hystrix 监控

This commit is contained in:
冷冷 committed 2019-11-04 12:54:25 +08:00
1 parent a16150ba59
commit e3468b5c8d
173 files changed
+11 -40953

No files matched your search

-21
View File
@@ -1,21 +0,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.
*/
module.exports = {
presets: [
'@vue/app'
]
};
File diff suppressed because it is too large. Load diff
-133
View File
@@ -1,133 +0,0 @@
{
"name": "spring-boot-admin-server-ui",
"description": "Spring Boot Admin UI",
"private": true,
"scripts": {
"watch": "vue-cli-service build --watch",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.8",
"@fortawesome/free-brands-svg-icons": "^5.5.0",
"@fortawesome/free-regular-svg-icons": "^5.5.0",
"@fortawesome/free-solid-svg-icons": "^5.5.0",
"@fortawesome/vue-fontawesome": "^0.1.2",
"ansi_up": "^3.0.0",
"autolinker": "^1.7.1",
"axios": "^0.18.0",
"bulma": "^0.7.2",
"bulma-badge": "^2.0.0",
"d3-array": "^1.2.4",
"d3-axis": "^1.0.12",
"d3-brush": "^1.0.6",
"d3-scale": "^2.1.2",
"d3-selection": "^1.3.2",
"d3-shape": "^1.2.2",
"d3-time": "^1.0.10",
"element-ui": "^2.4.10",
"event-source-polyfill": "^1.0.3",
"lodash": "^4.17.11",
"moment": "^2.22.2",
"moment-shortformat": "^2.1.0",
"popper.js": "^1.14.4",
"pretty-bytes": "^5.1.0",
"resize-observer-polyfill": "^1.5.0",
"rxjs": "^6.3.3",
"vue": "^2.5.17",
"vue-clickaway": "2.2.1",
"vue-router": "^3.0.1"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^3.1.1",
"@vue/cli-plugin-eslint": "^3.1.4",
"@vue/cli-plugin-unit-jest": "^3.1.1",
"@vue/cli-service": "^3.1.2",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/test-utils": "^1.0.0-beta.20",
"babel-core": "7.0.0-bridge.0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"eslint": "^5.8.0",
"eslint-plugin-vue": "^5.0.0-0",
"html-loader": "^0.5.5",
"node-sass": "^4.9.4",
"sass-loader": "^7.0.1",
"vue-template-compiler": "^2.5.17",
"webpack-bundle-analyzer": "^3.0.3"
},
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"eslint:recommended",
"plugin:vue/strongly-recommended"
],
"rules": {
"quotes": [
"error",
"single"
],
"no-console": [
"error",
{
"allow": [
"warn"
]
}
],
"vue/max-attributes-per-line": "off"
},
"parserOptions": {
"parser": "babel-eslint"
},
"overrides": [
{
"files": [
"*.spec.js"
],
"env": {
"jest": true
}
}
]
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
],
"jest": {
"setupFiles": [
"<rootDir>/src/test/frontend/globals.js"
],
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"transform": {
"^.+\\.vue$": "vue-jest",
".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub",
"^.+\\.js$": "babel-jest"
},
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/main/frontend/$1"
},
"snapshotSerializers": [
"jest-serializer-vue"
],
"testMatch": [
"**/*.spec.js"
],
"testURL": "http://example.com"
}
}
+5 -16
View File
@@ -48,22 +48,6 @@
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>${spring-boot-admin.version}</version>
</dependency>
<!--hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
<!--turbine-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -79,6 +63,11 @@
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--sentinel 依赖-->
<dependency>
<groupId>com.pig4cloud</groupId>
<artifactId>pigx-common-sentinel</artifactId>
</dependency>
<!--web 模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -1,47 +0,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.
*/
// Import the initial variables
@import "~bulma/sass/utilities/initial-variables";
@import "~bulma/sass/utilities/functions";
$turquoise: rgb(66, 211, 165);
// overrides
$primary: $turquoise;
$primary-invert: $white;
// Add new color variables to the color map.
@import "~bulma/sass/utilities/derived-variables.sass";
$addColors: ();
$colors: map-merge($colors, $addColors);
$link: $primary;
$modal-card-head-background-color: $white;
$modal-content-width: calc(100vh - 40px);
$navbar-background-color: $black-ter;
$navbar-item-color: $grey;
$navbar-item-hover-color: $white;
$navbar-item-hover-background-color: $navbar-background-color;
$navbar-item-active-color: $white;
$navbar-item-active-background-color: $navbar-background-color;
@import "~bulma/sass/utilities/mixins.sass";
// for calculations / originally rem
$navbar-height-px: 52px;
@@ -1,140 +0,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.
*/
@import "utilities";
// Import the rest of Bulma
@import "~bulma/bulma";
@import "~bulma-badge/dist/css/bulma-badge";
html {
min-height: 100vh;
display: flex;
flex-direction: column;
}
body {
flex-grow: 1;
display: flex;
flex-direction: column;
& > div {
flex-grow: 1;
display: flex;
flex-direction: column;
}
}
.is-breakable {
word-break: break-all;
}
pre.is-breakable {
white-space: pre-wrap;
}
.monospaced {
font-family: $family-monospace;
}
.is-muted {
color: $grey-light;
}
.is-selectable {
cursor: pointer;
}
.button.is-icon {
background: transparent;
border-color: transparent;
}
.navbar-link::after {
border-color: currentColor;
}
.navbar .badge {
&[data-badge]::after {
box-shadow: 0 0 0 $badge-border-size $navbar-background-color;
}
}
//Loading spinner
.section.is-loading,
.content.is-loading {
&::after {
@include loader;
height: 5rem;
width: 5rem;
left: calc(50% - 2.5em);
top: calc(50% - 2.5em);
border-width: 0.5rem;
}
}
p.is-loading {
&::before {
display: inline-block;
right: 0.25em;
@include loader;
}
}
//hero as card-header
.card .hero {
padding: 0.75rem;
}
//Bullets
.has-bullet::before {
background: currentColor;
content: '';
width: 0.75em;
height: 0.75em;
display: inline-block;
margin-right: 0.25em;
}
@each $name, $pair in $colors {
.has-bullet.has-bullet-#{$name}::before {
$color: nth($pair, 1);
background-color: $color;
}
}
//D3 Charts
.tick {
& line {
stroke: currentColor;
}
& text {
fill: currentColor;
font-size: $size-7;
}
}
path.domain {
stroke: currentColor;
}
//for login.html
.login {
background-color: $black-ter;
&--logo {
margin-left: auto;
margin-right: auto;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

@@ -1,9 +0,0 @@
<svg viewBox="0 0 135.297 120.36666" xmlns="http://www.w3.org/2000/svg">
<defs>
<style>.hex{fill:#42d3a5;} .line{fill:#fff;}</style>
</defs>
<path d="M133.605291 66.312792l-27.6913077 47.9628009c-2.1826121 3.7799055-6.2171779 6.1091806-10.5824021 6.1091806H39.9489658c-4.3652242 0-8.3984567-2.3292751-10.5824021-6.1091806L1.6765893 66.312792c-2.1826121-3.7799055-2.1826121-8.4384557 0-12.2196945L29.3665637 6.1316299C31.5505091 2.3503911 35.5837416.021116 39.9489658.021116h55.3826154c4.3652242 0 8.39979 2.3292751 10.5824021 6.1105139l27.6913077 47.9614676c2.1826121 3.7812388 2.1826121 8.439789 0 12.2196945"
class="hex"/>
<path d="M61.53275952 19.96761733c-1.41466463.03066923-2.66193345.92969675-3.14052148 2.26164345l-17.8355541 49.69575758-5.85535361-9.759556c-.6159846-1.02927427-1.72965676-1.6601585-2.92962676-1.6601585H.05149659c.04970976 2.00834979.5878153 4.00699982 1.62695933 5.80645483l.59373516 1.02727432h27.56531085l8.57008574 14.28264293c.62131447 1.0359741 1.73705657 1.66015849 2.9276268 1.66015849.1119972 0 .22197446-.00559986.33397166-.01757956 1.31466713-.1279968 2.43673908-1.00337492 2.88472788-2.24604385l17.25756855-48.0947976 18.95252617 46.4108397c.48531787 1.18667033 1.5956601 2.00194995 2.87292818 2.11124722 1.26926826.11332717 2.5074373-.50440739 3.1874203-1.59376016l15.14962126-24.28039297 8.8435789 11.5637109c.6479838.84530887 1.65345867 1.34176645 2.71473213 1.34176645h18.8235294l1.24996875-2.16594585c.84245894-1.45896352 1.35666609-3.0480238 1.54876128-4.6678833h-19.93350165l-10.8157296-14.14464637c-.68131296-.88930777-1.7675558-1.39116522-2.87692807-1.33786655-1.119972.05332866-2.14294643.6515037-2.7362316 1.60345991L84.56351375 70.56637232 64.76700867 22.0885843c-.5359866-1.3079673-1.82695433-2.15894603-3.2362191-2.12294693z"
class="line"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,93 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`application-status should match the snapshot with custom status 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--?"
/>
<!---->
</div>
`;
exports[`application-status should match the snapshot with status DOWN 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--DOWN"
icon="times-circle"
/>
<!---->
</div>
`;
exports[`application-status should match the snapshot with status OFFLINE 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--OFFLINE"
icon="minus-circle"
/>
<!---->
</div>
`;
exports[`application-status should match the snapshot with status OUT_OF_SERVICE 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--OUT_OF_SERVICE"
icon="ban"
/>
<!---->
</div>
`;
exports[`application-status should match the snapshot with status RESTRICTED 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--RESTRICTED"
icon="exclamation"
/>
<!---->
</div>
`;
exports[`application-status should match the snapshot with status UNKNOWN 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--UNKNOWN"
icon="question-circle"
/>
<!---->
</div>
`;
exports[`application-status should match the snapshot with status UP with Timestamp 1`] = `
<div
class="application-status"
>
<font-awesome-icon-stub
class="application-status__icon application-status__icon--UP"
icon="check"
/>
<small>
14m
</small>
</div>
`;
@@ -1,5 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`time-ago given a short time passed time should match the snapshot 1`] = `14m`;
exports[`time-ago given a long time passed time should match the snapshot 1`] = `Jul 7`;
@@ -1,74 +0,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.
*/
import {library} from '@fortawesome/fontawesome-svg-core';
import {faGithub} from '@fortawesome/free-brands-svg-icons/faGithub';
import {faGitter} from '@fortawesome/free-brands-svg-icons/faGitter';
import {faStackOverflow} from '@fortawesome/free-brands-svg-icons/faStackOverflow';
import {faTimesCircle as farTimesCircle} from '@fortawesome/free-regular-svg-icons/faTimesCircle';
import {faBan} from '@fortawesome/free-solid-svg-icons/faBan';
import {faBell} from '@fortawesome/free-solid-svg-icons/faBell';
import {faBellSlash} from '@fortawesome/free-solid-svg-icons/faBellSlash';
import {faBook} from '@fortawesome/free-solid-svg-icons/faBook';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload';
import {faExclamation} from '@fortawesome/free-solid-svg-icons/faExclamation';
import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
import {faHeartbeat} from '@fortawesome/free-solid-svg-icons/faHeartbeat';
import {faHome} from '@fortawesome/free-solid-svg-icons/faHome';
import {faMinusCircle} from '@fortawesome/free-solid-svg-icons/faMinusCircle';
import {faPencilAlt} from '@fortawesome/free-solid-svg-icons/faPencilAlt';
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons/faQuestionCircle';
import {faSignOutAlt} from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
import {faStepBackward} from '@fortawesome/free-solid-svg-icons/faStepBackward';
import {faStepForward} from '@fortawesome/free-solid-svg-icons/faStepForward';
import {faTimesCircle} from '@fortawesome/free-solid-svg-icons/faTimesCircle';
import {faTrash} from '@fortawesome/free-solid-svg-icons/faTrash';
import {faUserCircle} from '@fortawesome/free-solid-svg-icons/faUserCircle';
import {faWrench} from '@fortawesome/free-solid-svg-icons/faWrench';
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome';
library.add(
//solid
faTrash,
faDownload,
faStepForward,
faStepBackward,
faCheck,
faQuestionCircle,
faBan,
faTimesCircle,
faMinusCircle,
faExclamation,
faBook,
faSignOutAlt,
faExclamationTriangle,
faPencilAlt,
faBell,
faBellSlash,
faUserCircle,
faHeartbeat,
faHome,
faWrench,
//regular,
farTimesCircle,
//brands
faGithub,
faStackOverflow,
faGitter
);
export default FontAwesomeIcon;
@@ -1,31 +0,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.
*/
const components = [];
/* global require */
const context = require.context('.', false, /^(?:(?!.*\.spec\.(js|vue)$).)*\.(js|vue)$/);
context.keys().forEach(function (key) {
const name = /^(.\/)+(.*)\.(vue|js)$/.exec(key)[2];
components.push({name, component: context(key).default})
});
export default {
install(Vue) {
components.forEach(component => Vue.component(component.name, component.component));
}
}
@@ -1,57 +0,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.
-->
<template>
<button @click="click" class="confirm-button" :class="{ 'is-warning' : confirm }" v-on-clickaway="abort">
<slot name="confirm" v-if="confirm">Confirm</slot>
<slot v-else />
</button>
</template>
<script>
import {directive as onClickaway} from 'vue-clickaway';
export default {
directives: {onClickaway},
data: () => ({
confirm: false
}),
methods: {
abort() {
this.confirm = false;
},
click(event) {
if (this.confirm) {
this.$el.style.width = null;
this.$emit('click', event);
} else {
const width = this.$el.getBoundingClientRect().width;
this.$el.style.width = `${width}px`;
}
this.confirm = !this.confirm;
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.confirm-button {
transition: all $easing 150ms;
}
</style>
@@ -1,50 +0,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.
-->
<template>
<div v-html="formatted" class="formatted" />
</template>
<script>
import {Autolink} from '@/utils/autolink';
import objToString from '@/utils/objToString';
const autolink = new Autolink({truncate: {
length: 50,
location: 'smart'
}});
export default {
props: {
value: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: null,
required: true
},
},
computed: {
formatted() {
return autolink(objToString(this.value));
}
}
}
</script>
<style lang="scss">
.formatted {
white-space: pre;
}
</style>
@@ -1,77 +0,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.
-->
<template>
<button type="button" class="icon-button" :disabled="disabled" v-on="$listeners">
<font-awesome-icon :icon="icon" :size="size" :class="iconClass" />
</button>
</template>
<script>
export default {
props: {
disabled: {
type: Boolean,
default: false,
},
icon: {
type: [String, Array],
required: true,
},
size: {
type: String,
default: null
},
iconClass: {
type: String,
default: null
}
}
}
</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;
}
&:active {
outline: none;
}
}
</style>
@@ -1,87 +0,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.
-->
<template>
<div class="card panel">
<header
v-if="title || $slots['header']"
class="card-header"
:class="{'panel__header--sticky': headerSticksBelow}"
v-sticks-below="headerSticksBelow"
>
<p class="card-header-title">
<span v-text="title" />
<slot name="header" />
</p>
<div class="panel__close">
<sba-icon-button v-if="closeable" :icon="['far', 'times-circle']" @click.stop="close" />
</div>
</header>
<div v-if="$slots['default']" class="card-content">
<slot />
</div>
</div>
</template>
<script>
import sticksBelow from '@/directives/sticks-below';
import SbaIconButton from './sba-icon-button';
export default {
components: {SbaIconButton},
directives: {sticksBelow},
props: {
title: {
type: String,
required: true
},
closeable: {
type: Boolean,
default: false
},
headerSticksBelow: {
type: Array,
default: undefined
}
},
methods: {
close(event) {
this.$emit('close', event);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.panel {
margin-bottom: 1.5rem;
&__close {
margin-right: 0.75em;
color: $grey-light;
display: flex;
align-items: center;
justify-self: flex-end;
}
&__header--sticky {
position: sticky;
background-color: $white;
}
}
</style>
@@ -1,66 +0,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.
*/
import {mount} from '@vue/test-utils';
import moment from 'moment';
import sbaStatus from './sba-status';
moment.now = () => +new Date(1318781879406);
describe('application-status', () => {
const testSnapshotForStatus = (status, date) => {
const wrapper = mount(sbaStatus, {
propsData: {
status,
date
},
stubs: {
'font-awesome-icon': true
}
});
expect(wrapper.vm.$el).toMatchSnapshot();
};
it('should match the snapshot with status UP with Timestamp', () => {
testSnapshotForStatus('UP', 1318781000000);
});
it('should match the snapshot with status RESTRICTED', () => {
testSnapshotForStatus('RESTRICTED');
});
it('should match the snapshot with status OUT_OF_SERVICE', () => {
testSnapshotForStatus('OUT_OF_SERVICE');
});
it('should match the snapshot with status DOWN', () => {
testSnapshotForStatus('DOWN');
});
it('should match the snapshot with status UNKNOWN', () => {
testSnapshotForStatus('UNKNOWN');
});
it('should match the snapshot with status OFFLINE', () => {
testSnapshotForStatus('OFFLINE');
});
it('should match the snapshot with custom status', () => {
testSnapshotForStatus('?');
});
});
@@ -1,94 +0,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.
-->
<template>
<div class="application-status">
<font-awesome-icon :icon="icon" class="application-status__icon"
:class="`application-status__icon--${status}`"
/>
<small v-if="date">
<sba-time-ago :date="date" />
</small>
</div>
</template>
<script>
import moment from 'moment';
import sbaTimeAgo from './sba-time-ago';
const icons = {
'UP': 'check',
'RESTRICTED': 'exclamation',
'OUT_OF_SERVICE': 'ban',
'DOWN': 'times-circle',
'OFFLINE': 'minus-circle',
'UNKNOWN': 'question-circle'
};
export default {
// eslint-disable-next-line vue/no-unused-components
components: {sbaTimeAgo},
props: {
status: {
type: String,
default: 'UNKNOWN'
},
date: {
type: [String, Date, Number, moment],
default: null
},
},
computed: {
icon() {
return icons[this.status];
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.application-status {
text-align: center;
line-height: 1rem;
display: inline-flex;
flex-direction: column;
&__icon {
color: gray;
margin: 0 auto;
&--UP {
color: $success;
}
&--RESTRICTED {
color: $warning;
}
&--OUT_OF_SERVICE,
&--DOWN {
color: $danger;
}
&--UNKNOWN,
&--OFFLINE {
color: $grey;
}
}
}
</style>
@@ -1,42 +0,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.
-->
<template>
<div class="field is-grouped is-grouped-multiline" v-if="hasTags">
<div class="control" v-for="(value, key) in tags" :key="key">
<div class="tags has-addons">
<span class="tag is-dark" v-text="key" />
<span class="tag is-light" v-text="value" />
</div>
</div>
</div>
</template>
<script>
export default {
props: {
tags: {
type: Object,
required: true
}
},
computed: {
hasTags() {
return Object.keys(this.tags).length > 0;
}
}
}
</script>
@@ -1,48 +0,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.
*/
import moment from 'moment-shortformat';
export default {
props: {
date: {
type: [String, Date, Number, moment],
default: null
}
},
data: () => ({
now: moment(),
timer: null,
}),
computed: {
timeAgo() {
return moment(this.date).short(true, this.now);
}
},
created() {
this.timer = window.setInterval(() => {
this.now = moment();
}, 1000);
},
render() {
return this._v(this.timeAgo);
},
beforeDestroy() {
if (this.timer) {
window.clearInterval(this.timer);
}
}
}
@@ -1,47 +0,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.
*/
import {shallowMount} from '@vue/test-utils';
import moment from 'moment';
import sbaTimeAgo from './sba-time-ago.js';
moment.now = () => +new Date(1318781879406);
describe('time-ago', () => {
describe('given a short time passed time', () => {
it('should match the snapshot', () => {
const wrapper = shallowMount(sbaTimeAgo, {
propsData: {
date: moment(1318781000000)
}
});
expect(wrapper.vm.$el).toMatchSnapshot();
});
});
describe('given a long time passed time', () => {
it('should match the snapshot', () => {
const wrapper = shallowMount(sbaTimeAgo, {
propsData: {
date: moment(1310000000000)
}
});
expect(wrapper.vm.$el).toMatchSnapshot();
});
});
});
@@ -1,47 +0,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.
*/
import ResizeObserver from 'resize-observer-polyfill';
const observers = new WeakMap();
const bind = (el, binding) => {
unbind(el);
const observer = new ResizeObserver(binding.value);
observer.observe(el);
observers.set(el, observer);
};
const unbind = (el) => {
const observer = observers.get(el);
if (observer) {
observer.disconnect();
observers.delete(el);
}
};
export default {
bind,
update(el, binding) {
if (binding.value === binding.oldValue) {
return
}
bind(el, binding)
},
unbind
}
@@ -1,47 +0,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.
*/
import Popper from 'popper.js';
const poppers = new WeakMap();
const bind = (el, binding) => {
const reference = typeof binding.value === 'string' ? document.getElementById(binding.value) : binding.value;
if (reference) {
const popper = new Popper(reference, el);
poppers.set(el, popper);
}
};
const unbind = (el) => {
const popper = poppers.get(el);
if (popper) {
popper.destroy(el);
}
};
export default {
bind,
update(el, binding) {
if (binding.value === binding.oldValue) {
return
}
bind(el, binding)
},
unbind
}
@@ -1,38 +0,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.
*/
const bind = (el, binding) => {
if (!binding.value) {
return;
}
const top = binding.value.map(v => document.querySelector(v))
.filter(v => Boolean(v))
.map(v => v.getBoundingClientRect().height)
.reduce((a, b) => a + b, 0);
el.style.top = `${top}px`;
el.style.position = 'sticky';
};
export default {
bind,
update(el, binding) {
if (binding.value === binding.oldValue) {
return
}
bind(el, binding)
}
}
@@ -1,66 +0,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.
-->
<!DOCTYPE html>
<html>
<head>
<base th:href="@{'/'}" href="/">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="format-detection" content="telephone=no,email=no">
<meta name="theme-color" content="#42d3a5">
<!-- include extensions -->
<th:block th:each="cssFile : ${cssExtensions}">
<link rel="preload" th:href="${cssFile.resourcePath}" as="style">
</th:block>
<th:block th:each="jsFile : ${jsExtensions}">
<link rel="preload" th:href="${jsFile.resourcePath}" as="script">
</th:block>
<th:block th:each="cssFile : ${cssExtensions}">
<link th:href="${cssFile.resourcePath}" rel="stylesheet">
</th:block>
<link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
<title th:text="${uiSettings.title}">服务监控</title>
</head>
<body>
<div id="app"></div>
<script th:inline="javascript">
var SBA = {
uiSettings: /*[[${uiSettings}]]*/ {},
user: /*[[${user}]]*/ null,
extensions: [],
/*[#th:block th:if="${_csrf}"]*/
csrf: {
parameterName: /*[[${_csrf.parameterName}]]*/ null,
headerName: /*[[${_csrf.headerName}]]*/ null
},
/*[/th:block]*/
use: function (ext) {
this.extensions.push(ext);
}
}
</script>
<!-- include extensions -->
<th:block th:each="jsFile : ${jsExtensions}">
<script lang="javascript" th:src="${jsFile.resourcePath}"></script>
</th:block>
</body>
</html>
@@ -1,89 +0,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.
*/
import '@/assets/css/base.scss';
import moment from 'moment';
import Vue from 'vue';
import VueRouter from 'vue-router';
import components from './components';
import Notifications from './notifications';
import sbaShell from './shell';
import Store from './store';
import ViewRegistry from './viewRegistry';
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'
import views from './views';
moment.locale(window.navigator.language);
Vue.use(VueRouter);
Vue.use(components);
Vue.use(ElementUI)
const applicationStore = new Store();
const viewRegistry = new ViewRegistry();
const installables = [
Notifications,
...views,
...global.SBA.extensions
];
installables.forEach(view => view.install({
viewRegistry,
applicationStore,
vue: Vue
}));
new Vue({
router: new VueRouter({
linkActiveClass: 'is-active',
routes: viewRegistry.routes
}),
el: '#app',
render(h) {
return h(sbaShell, {
props: {
views: this.views,
applications: this.applications,
error: this.error
}
});
},
data: {
views: viewRegistry.views,
applications: applicationStore.applications,
error: null
},
methods: {
onError(error) {
console.warn('Connection to server failed:', error);
this.error = error;
},
onConnected() {
this.error = null;
}
},
created() {
applicationStore.addEventListener('connected', this.onConnected);
applicationStore.addEventListener('error', this.onError);
applicationStore.start();
},
beforeDestroy() {
applicationStore.stop();
applicationStore.removeEventListener('connected', this.onConnected);
applicationStore.removeEventListener('error', this.onError)
}
});
@@ -1,78 +0,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.
-->
<!DOCTYPE html>
<html>
<head>
<base th:href="@{'/'}" href="/">
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="format-detection" content="telephone=no,email=no">
<meta name="theme-color" content="#42d3a5">
<link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
<title>Spring Boot Admin - Login</title>
</head>
<body class="login">
<section class="hero is-fullheight">
<div class="hero-body">
<div class="container has-text-centered">
<div class="column is-4 is-offset-4">
<div class="box">
<figure class="image is-128x128 login--logo">
<img src="assets/img/icon-spring-boot-admin.svg">
</figure>
<h1 class="title has-text-primary">PigX 服务状态监控</h1>
<form method="post">
<input type="hidden"
th:if="${_csrf}"
th:name="${_csrf.parameterName}"
th:value="${_csrf.token}"/>
<div class="field">
<p class="is-medium has-text-danger" th:unless="${param.error == null}">
Invalid username or password
</p>
<p class="is-medium" th:unless="${param.logout == null}">
Logout successful
</p>
</div>
<div class="field">
<div class="control">
<input class="input is-medium" type="input" name="username" placeholder="Username"
autofocus th:classappend="${param.error != null} ? is-danger">
</div>
</div>
<div class="field">
<div class="control">
<input class="input is-medium" type="password" name="password"
placeholder="Password" th:classappend="${param.error != null} ? is-danger">
</div>
</div>
<div class="field">
<div class="control">
<input type="submit" class="button is-block is-primary is-medium is-fullwidth"
value="Login">
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</section>
</body>
</html>
@@ -1,17 +0,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.
*/
import '@/assets/css/base.scss';
@@ -1,40 +0,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.
*/
export default {
created() {
this.subscribe();
},
beforeDestroy() {
this.unsubscribe();
},
methods: {
async subscribe() {
if (!this.subscription) {
this.subscription = await this.createSubscription();
}
},
unsubscribe() {
if (this.subscription) {
try {
!this.subscription.closed && this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
}
}
}
@@ -1,59 +0,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.
*/
let granted = false;
const requestPermissions = () => {
if ('Notification' in window) {
granted = (window.Notification.permission === 'granted');
if (!granted && window.Notification.permission !== 'denied') {
window.Notification.requestPermission(permission => granted = (permission === 'granted'));
}
}
};
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' ? 'assets/img/favicon.png' : 'assets/img/favicon-danger.png',
renotify: true,
timeout: 10000
})
}
});
}
}
@@ -1,94 +0,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.
*/
import axios, {redirectOn401} from '@/utils/axios';
import waitForPolyfill from '@/utils/eventsource-polyfill';
import {concat, from, ignoreElements, Observable} from '@/utils/rxjs';
import uri from '@/utils/uri';
import sortBy from 'lodash/sortBy';
import Instance from './instance';
class Application {
constructor(name) {
this.name = name;
this.axios = axios.create({
baseURL: uri`applications/${this.name}/`
});
this.axios.interceptors.response.use(
response => response,
redirectOn401()
);
}
findInstance(instanceId) {
return this.instances.find(instance => instance.id === instanceId);
}
get isUnregisterable() {
return this.instances.findIndex(i => i.isUnregisterable) >= 0;
}
async unregister() {
return this.axios.delete('')
}
static async list() {
return await axios.get('applications', {
transformResponse: Application._transformResponse
});
}
static getStream() {
return concat(
from(waitForPolyfill()).pipe(ignoreElements()),
Observable.create(observer => {
const eventSource = new EventSource('applications');
eventSource.onmessage = message => observer.next({
...message,
data: Application._transformResponse(message.data)
});
eventSource.onerror = err => observer.error(err);
return () => {
eventSource.close();
};
})
);
}
static _transformResponse(data) {
if (!data) {
return data;
}
const json = JSON.parse(data);
if (json instanceof Array) {
const applications = json.map(Application._toApplication);
return sortBy(applications, [item => item.name]);
}
return Application._toApplication(json);
}
static _toApplication(application) {
const instances = application.instances.map(instance => Object.assign(new Instance(instance.id), instance));
return Object.assign(new Application(application.name), {
...application,
instances: sortBy(instances, [instance => instance.registration.healthUrl])
});
}
}
export default Application;
@@ -1,332 +0,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.
*/
import axios, {redirectOn401} from '@/utils/axios';
import waitForPolyfill from '@/utils/eventsource-polyfill';
import logtail from '@/utils/logtail';
import {concat, from, ignoreElements, Observable} from '@/utils/rxjs';
import uri from '@/utils/uri';
import transform from 'lodash/transform';
const actuatorMimeTypes = [
'application/vnd.spring-boot.actuator.v2+json',
'application/vnd.spring-boot.actuator.v1+json',
'application/json'
];
const isInstanceActuatorRequest = url => url.match(/^instances[/][^/]+[/]actuator([/].*)?$/);
class Instance {
constructor(id) {
this.id = id;
this.axios = axios.create({
baseURL: uri`instances/${this.id}/`,
});
this.axios.interceptors.response.use(
response => response,
redirectOn401(error => !isInstanceActuatorRequest(error.config.url))
);
}
hasEndpoint(endpointId) {
return this.endpoints.findIndex(endpoint => endpoint.id === endpointId) >= 0;
}
get isUnregisterable() {
return this.registration.source === 'http-api';
}
async unregister() {
return this.axios.delete('');
}
async fetchInfo() {
return this.axios.get(uri`actuator/info`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchMetrics() {
return this.axios.get(uri`actuator/metrics`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchMetric(metric, tags) {
const params = tags ? {
tag: Object.entries(tags)
.filter(([, value]) => typeof value !== 'undefined' && value !== null)
.map(([name, value]) => `${name}:${value}`)
.join(',')
} : {};
return this.axios.get(uri`actuator/metrics/${metric}`, {
headers: {'Accept': actuatorMimeTypes},
params
});
}
async fetchHealth() {
return await this.axios.get(uri`actuator/health`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchEnv(name) {
return this.axios.get(uri`actuator/env/${name || '' }`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchConfigprops() {
return this.axios.get(uri`actuator/configprops`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async hasEnvManagerSupport() {
const response = await this.axios.options(uri`actuator/env`);
return response.headers['allow'] && response.headers['allow'].includes('POST');
}
async resetEnv() {
return this.axios.delete(uri`actuator/env`);
}
async setEnv(name, value) {
return this.axios.post(uri`actuator/env`, {name, value}, {
headers: {'Content-Type': 'application/json'}
});
}
async refreshContext() {
return this.axios.post(uri`actuator/refresh`);
}
async fetchLiquibase() {
return this.axios.get(uri`actuator/liquibase`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchScheduledTasks() {
return this.axios.get(uri`actuator/scheduledtasks`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchCaches() {
return this.axios.get(uri`actuator/caches`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async clearCaches() {
return this.axios.delete(uri`actuator/caches`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async clearCache(name, cacheManager) {
return this.axios.delete(uri`actuator/caches/${name}` , {
params: {'cacheManager': cacheManager},
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchFlyway() {
return this.axios.get(uri`actuator/flyway`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchLoggers() {
return this.axios.get(uri`actuator/loggers`, {
headers: {'Accept': actuatorMimeTypes},
transformResponse: Instance._toLoggers
});
}
async configureLogger(name, level) {
return this.axios.post(uri`actuator/loggers/${name}`, {configuredLevel: level}, {
headers: {'Content-Type': 'application/json'}
});
}
async fetchHttptrace() {
return this.axios.get(uri`actuator/httptrace`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchBeans() {
return this.axios.get(uri`actuator/beans`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchThreaddump() {
return this.axios.get(uri`actuator/threaddump`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async fetchAuditevents({after, type, principal}) {
return this.axios.get(uri`actuator/auditevents`, {
headers: {'Accept': actuatorMimeTypes},
params: {
after: after.toISOString(),
type: type || undefined,
principal: principal || undefined
}
});
}
async fetchSessionsByUsername(username) {
return this.axios.get(uri`actuator/sessions`, {
headers: {'Accept': actuatorMimeTypes},
params: {
username: username || undefined
}
});
}
async fetchSession(sessionId) {
return this.axios.get(uri`actuator/sessions/${sessionId}`, {
headers: {'Accept': actuatorMimeTypes}
});
}
async deleteSession(sessionId) {
return this.axios.delete(uri`actuator/sessions/${sessionId}`, {
headers: {'Accept': actuatorMimeTypes}
});
}
streamLogfile(interval) {
return logtail(opt => this.axios.get(uri`actuator/logfile`, opt), interval);
}
async listMBeans() {
return this.axios.get(uri`actuator/jolokia/list`, {
headers: {'Accept': 'application/json'},
params: {canonicalNaming: false},
transformResponse: Instance._toMBeans
});
}
async readMBeanAttributes(domain, mBean) {
const body = {
type: 'read',
mbean: `${domain}:${mBean}`,
config: {ignoreErrors: true}
};
return this.axios.post(uri`actuator/jolokia`, body, {
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
});
}
async writeMBeanAttribute(domain, mBean, attribute, value) {
const body = {
type: 'write',
mbean: `${domain}:${mBean}`,
attribute,
value
};
return this.axios.post(uri`actuator/jolokia`, body, {
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
});
}
async invokeMBeanOperation(domain, mBean, operation, args) {
const body = {
type: 'exec',
mbean: `${domain}:${mBean}`,
operation,
'arguments': args
};
return this.axios.post(uri`actuator/jolokia`, body, {
headers: {'Accept': 'application/json', 'Content-Type': 'application/json'}
});
}
async fetchMappings() {
return this.axios.get(uri`actuator/mappings`, {
headers: {'Accept': actuatorMimeTypes}
});
}
static async fetchEvents() {
return axios.get('instances/events');
}
static getEventStream() {
return concat(
from(waitForPolyfill()).pipe(ignoreElements()),
Observable.create(observer => {
const eventSource = new EventSource('instances/events');
eventSource.onmessage = message => observer.next({
...message,
data: JSON.parse(message.data)
});
eventSource.onerror = err => observer.error(err);
return () => {
eventSource.close();
};
})
);
}
static async get(id) {
return axios.get(uri`instances/${id}`, {
transformResponse: Instance._toInstance
});
}
static _toInstance(data) {
if (!data) {
return data;
}
const instance = JSON.parse(data);
return Object.assign(new Instance(instance.id), instance);
}
static _toLoggers(data) {
if (!data) {
return data;
}
const raw = JSON.parse(data);
const loggers = transform(raw.loggers, (result, value, key) => {
return result.push({name: key, ...value});
}, []);
return {levels: raw.levels, loggers};
}
static _toMBeans(data) {
if (!data) {
return data;
}
const raw = JSON.parse(data);
return Object.entries(raw.value).map(([domain, mBeans]) => ({
domain,
mBeans: Object.entries(mBeans).map(([descriptor, mBean]) => ({
descriptor: descriptor,
...mBean
}))
}))
}
}
export default Instance;
@@ -1,102 +0,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.
*/
import axios from '@/utils/axios';
import uri from '@/utils/uri';
import moment from 'moment';
class NotificationFilter {
constructor({expiry, ...filter}) {
Object.assign(this, filter);
this.expiry = expiry ? moment(expiry) : null;
}
affects(obj) {
if (!obj) {
return false;
}
if (this.isApplicationFilter) {
return this.applicationName === obj.name;
}
if (this.isInstanceFilter) {
return this.instanceId === obj.id;
}
return false;
}
get isApplicationFilter() {
return this.hasOwnProperty('applicationName');
}
get isInstanceFilter() {
return this.hasOwnProperty('instanceId');
}
async delete() {
return axios.delete(uri`notifications/filters/${this.id}`);
}
static async isSupported() {
try {
const response = await axios.options('notifications/filters');
return response.headers['allow'] && response.headers['allow'].includes('POST');
} catch (error) {
return false;
}
}
static async getFilters() {
return axios.get('notifications/filters', {
transformResponse: NotificationFilter._transformResponse
});
}
static async addFilter(object, ttl) {
const params = {ttl};
if (object.hasOwnProperty('name')) {
params.applicationName = object.name;
} else if (object.hasOwnProperty('id')) {
params.instanceId = object.id;
}
return axios.post('notifications/filters', null, {
params,
transformResponse: NotificationFilter._transformResponse
});
}
static _transformResponse(data) {
if (!data) {
return data;
}
const json = JSON.parse(data);
if (json instanceof Array) {
return json.map(NotificationFilter._toNotificationFilters).filter(f => !f.expired);
}
return NotificationFilter._toNotificationFilters(json);
}
static _toNotificationFilters(notificationFilter) {
return new NotificationFilter(notificationFilter);
}
}
export default NotificationFilter;
@@ -1,57 +0,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.
-->
<template>
<div id="app">
<sba-navbar :views="mainViews" :applications="applications" :error="error" />
<router-view :views="childViews" :applications="applications" :error="error" />
</div>
</template>
<script>
import sbaNavbar from './navbar';
export default {
props: {
views: {
type: Array,
default: () => []
},
applications: {
type: Array,
default: () => [],
},
error: {
type: Error,
default: null
}
},
// eslint-disable-next-line vue/no-unused-components
components: {sbaNavbar},
computed: {
mainViews() {
return this.views.filter(view => !view.parent);
},
activeMainViewName() {
const currentView = this.$route.meta.view;
return currentView && (currentView.parent || currentView.name);
},
childViews() {
return this.views.filter(view => view.parent === this.activeMainViewName);
}
}
}
</script>
@@ -1,132 +0,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.
-->
<template>
<nav id="navigation" class="navbar is-fixed-top">
<div class="container">
<div class="navbar-brand">
<router-link class="navbar-item logo" to="/" v-html="brand" />
<div class="navbar-burger burger" @click.stop="showMenu = !showMenu">
<span />
<span />
<span />
</div>
</div>
<div class="navbar-menu" :class="{'is-active' : showMenu}">
<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" />
</router-link>
<div class="navbar-item has-dropdown is-hoverable" v-if="userName">
<a class="navbar-link">
<font-awesome-icon icon="user-circle" size="lg" />&nbsp;<span v-text="userName" />
</a>
<div class="navbar-dropdown">
<a class="navbar-item">
<form action="logout" method="post">
<input v-if="csrfToken" type="hidden" :name="csrfParameterName" :value="csrfToken">
<button class="button is-icon" type="submit" value="logout">
<font-awesome-icon icon="sign-out-alt" />&nbsp;Log out
</button>
</form>
</a>
</div>
</div>
</div>
</div>
</div>
</nav>
</template>
<script>
import {compareBy} from '@/utils/collections';
const readCookie = (name) => {
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'));
return (match ? decodeURIComponent(match[3]) : null);
};
export default {
data: () => ({
showMenu: false,
brand: '<img src="assets/img/icon-spring-boot-admin.svg"><span>PigX 服务状态监控</span>',
userName: null,
csrfToken: null,
csrfParameterName: null
}),
props: {
views: {
type: Array,
default: () => []
},
applications: {
type: Array,
default: () => [],
},
error: {
type: Error,
default: null
}
},
computed: {
enabledViews() {
return [...this.views].filter(
view => view.handle && (typeof view.isEnabled === 'undefined' || view.isEnabled())
).sort(compareBy(v => v.order));
}
},
created() {
if (global.SBA) {
if (global.SBA.uiSettings) {
this.brand = global.SBA.uiSettings.brand || this.brand;
}
if (global.SBA.user) {
this.userName = global.SBA.user.name;
}
}
this.csrfToken = readCookie('XSRF-TOKEN');
this.csrfParameterName = (global.SBA && global.SBA.csrf && global.SBA.csrf.parameterName) || '_csrf';
},
mounted() {
document.documentElement.classList.add('has-navbar-fixed-top');
},
beforeDestroy() {
document.documentElement.classList.remove('has-navbar-fixed-top')
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.logo {
align-self: center;
flex-basis: 12em;
flex-basis: content;
max-height: 2.25em;
padding: 0.5em 1em 0.5em 0.5em;
font-size: 1.5em;
font-weight: 600;
white-space: nowrap;
img {
margin-right: 0.5em;
}
}
</style>
@@ -1,116 +0,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.
*/
import Application from '@/services/application';
import {concat, concatMap, defer, delay, doFirst, map, retryWhen, tap} from '@/utils/rxjs';
export default class {
constructor() {
this.applications = [];
this.applications.findInstance = (instanceId) => {
for (let application of this.applications) {
const instance = application.findInstance(instanceId);
if (instance) {
return instance;
}
}
return undefined;
};
this.applications.findApplicationForInstance = (instanceId) => {
return this.applications.find(application => !!application.findInstance(instanceId));
};
this.applications.indexOfApplication = (name) => {
return this.applications.findIndex(application => application.name === name);
};
this._listeners = {
added: [],
updated: [],
removed: []
};
}
addEventListener(type, listener) {
if (!(type in this._listeners)) {
this._listeners[type] = [];
}
this._listeners[type].push(listener);
}
removeEventListener(type, listener) {
if (!(type in this._listeners)) {
return;
}
const idx = this._listeners[type].indexOf(listener);
if (idx > 0) {
this._listeners[type].splice(idx, 1);
}
}
_dispatchEvent(type, ...args) {
if (!(type in this._listeners)) {
return;
}
const target = this;
this._listeners[type].forEach(
listener => listener.call(target, ...args)
)
}
start() {
const list = defer(() => Application.list())
.pipe(concatMap(message => message.data));
const stream = Application.getStream()
.pipe(map(message => message.data));
this.subscription = concat(list, stream)
.pipe(
doFirst(() => this._dispatchEvent('connected')),
retryWhen(
errors => errors.pipe(
tap(error => this._dispatchEvent('error', error)),
delay(5000)
)
)
).subscribe({
next: application => {
const idx = this.applications.indexOfApplication(application.name);
if (idx >= 0) {
const oldApplication = this.applications[idx];
if (application.instances.length > 0) {
this.applications.splice(idx, 1, application);
this._dispatchEvent('updated', application, oldApplication);
} else {
this.applications.splice(idx, 1);
this._dispatchEvent('removed', oldApplication);
}
} else {
this.applications.push(application);
this._dispatchEvent('added', application);
}
}
});
}
stop() {
if (this.subscription) {
try {
!this.subscription.closed && this.subscription.unsubscribe();
} finally {
this.subscription = null;
}
}
}
}
@@ -1,47 +0,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.
*/
import _Autolinker from 'autolinker';
export const defaults = {
urls: {
schemeMatches: true,
wwwMatches: false,
tldMatches: false
},
email: false,
phone: false,
mention: false,
hashtag: false,
stripPrefix: false,
stripTrailingSlash: false,
newWindow: true,
truncate: {
length: 0,
location: 'smart'
},
className: ''
};
const autolinker = new _Autolinker(defaults);
export default s => autolinker.link(s);
export function Autolink(cfg) {
this.autolinker = new _Autolinker({...defaults, ...cfg});
return s => this.autolinker.link(s)
}
@@ -1,46 +0,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.
*/
import autolink, {Autolink} from './autolink'
describe('autolink should', () => {
it('return the input string for normal text', () => {
const str = 'This is just a normal text containing no hyperlinks';
expect(autolink(str)).toBe(str);
});
it('return string with anchor tag for the hyperlink', () => {
const str = 'Please visit http://example.com.';
expect(autolink(str)).toBe('Please visit <a href="http://example.com" target="_blank" rel="noopener noreferrer">http://example.com</a>.');
});
it('return string with anchor tag with shortened text for the hyperlink', () => {
const str = 'Please visit http://extraordinary.com/very/very/log/hyperlink.';
const customAutolink = new Autolink({
truncate: {
length: 30,
location: 'smart'
},
});
expect(customAutolink(str)).toBe('Please visit <a href="http://extraordinary.com/very/very/log/hyperlink" target="_blank" rel="noopener noreferrer" title="http://extraordinary.com/very/very/log/hyperlink">extraordinary.com/very&hellip;rlink</a>.');
});
it('return string with anchor for hyperlinks in dense json', () => {
const str = '{"name":"John Smith","links":[{"rel":"random-link1","href":"https://localhost:8000/api/123/query?action=do_something&age=21","hreflang":null,"media":null,"title":null,"type":null,"deprecation":null}]}';
expect(autolink(str)).toBe('{"name":"John Smith","links":[{"rel":"random-link1","href":"<a href="https://localhost:8000/api/123/query?action=do_something&age=21" target="_blank" rel="noopener noreferrer">https://localhost:8000/api/123/query?action=do_something&age=21</a>","hreflang":null,"media":null,"title":null,"type":null,"deprecation":null}]}');
});
});
@@ -1,36 +0,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.
*/
import axios from 'axios';
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
if (global.SBA && global.SBA.csrf && global.SBA.csrf.headerName) {
axios.defaults.xsrfHeaderName = global.SBA.csrf.headerName;
}
export const redirectOn401 = (predicate = () => true) => error => {
if (error.response && error.response.status === 401 && predicate(error)) {
window.location.assign(`login?redirectTo=${encodeURIComponent(window.location.href)}`);
}
return Promise.reject(error);
};
const instance = axios.create();
instance.interceptors.response.use(response => response, redirectOn401());
instance.create = axios.create;
export default instance;
@@ -1,73 +0,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.
*/
import {redirectOn401} from './axios';
describe('redirectOn401', () => {
it('should not redirect on 500', async () => {
window.location.assign = jest.fn();
const error = {
response: {
status: 500
}
};
try {
await redirectOn401()(error);
} catch (e) {
expect(e).toBe(error);
}
expect(window.location.assign).not.toBeCalled();
});
it('should redirect on 401', async () => {
window.location.assign = jest.fn();
const error = {
response: {
status: 401
}
};
try {
await redirectOn401()(error);
} catch (e) {
expect(e).toBe(error);
}
expect(window.location.assign).toBeCalledWith('login?redirectTo=http%3A%2F%2Fexample.com%2F');
});
it('should not redirect on 401 for predicate yields false', async () => {
window.location.assign = jest.fn();
const error = {
response: {
status: 401
}
};
try {
await redirectOn401(() => false)(error);
} catch (e) {
expect(e).toBe(error);
}
expect(window.location.assign).not.toBeCalled();
});
});
@@ -1,22 +0,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.
*/
export const compareBy = mapper => (a, b) => {
const valA = mapper(a);
const valB = mapper(b);
return valA > valB ? 1 : valA < valB ? -1 : 0;
};
-33
View File
@@ -1,33 +0,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.
*/
import * as array from 'd3-array';
import * as axis from 'd3-axis';
import * as brush from 'd3-brush';
import * as scale from 'd3-scale';
import * as selection from 'd3-selection';
import * as shape from 'd3-shape';
import * as time from 'd3-time';
export default {
...array,
...axis,
...brush,
...scale,
...selection,
...shape,
...time
}
@@ -1,22 +0,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.
*/
export default async () => {
if (typeof window.EventSource === 'undefined') {
return import(/* webpackChunkName: "event-source-polyfill" */ 'event-source-polyfill');
}
return Promise.resolve();
};
@@ -1,68 +0,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.
*/
import {concatMap, EMPTY, of, timer} from '@/utils/rxjs';
export default (getFn, interval, initialSize = 300 * 1024) => {
let range = `bytes=-${initialSize}`;
let size = 0;
return timer(0, interval)
.pipe(
concatMap(() => getFn({headers: {range, 'Accept': 'text/plain, */*'}})),
concatMap(response => {
const initial = size === 0;
const contentLength = response.data.length;
if (response.status === 200) {
if (!initial) {
throw 'Expected 206 - Partial Content on subsequent requests.';
}
size = contentLength;
} else if (response.status === 206) {
size = parseInt(response.headers['content-range'].split('/')[1]);
} else {
throw 'Unexpected response status: ' + response.status;
}
// Reload the last byte to avoid a 416: Range unsatisfiable.
// If the response has length = 1 the file hasn't beent changed.
// If the response status is 416 the logfile has been truncated.
range = `bytes=${size - 1}-`;
let addendum = null;
let skipped = 0;
if (initial) {
if (contentLength >= size) {
addendum = response.data;
} else {
// In case of a partial response find the first line break.
addendum = response.data.substring(response.data.indexOf('\n') + 1);
skipped = size - addendum.length;
}
} else if (response.data.length > 1) {
// Remove the first byte which has been part of the previos response.
addendum = response.data.substring(1);
}
return addendum ? of({
totalBytes: size,
skipped,
addendum
}) : EMPTY;
})
);
}
@@ -1,62 +0,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.
*/
import isEmpty from 'lodash/isEmpty';
import isObject from 'lodash/isObject';
const nonEmptyComplexValue = value => (Array.isArray(value) || isObject(value)) && !isEmpty(value);
const objToString = (obj, indent = '') => {
if (Array.isArray(obj)) {
if (isEmpty(obj)) {
return indent + '[]';
}
return obj.map(value => {
if (nonEmptyComplexValue(value)) {
return `${indent}-\n${objToString(value, indent + ' ')}`;
}
return `${indent}- ${objToString(value, '')}`;
}).join('\n');
}
if (isObject(obj)) {
if (isEmpty(obj)) {
return indent + '{}';
}
return Object.entries(obj)
.map(([key, value]) => {
if (nonEmptyComplexValue(value)) {
return `${indent}${key}:\n${objToString(value, indent + ' ')}`;
}
return `${indent}${key}: ${objToString(value, '')}`;
})
.join('\n');
}
if (obj === null) {
return indent + 'null';
}
if (typeof obj === 'undefined' || obj === '') {
return '';
}
return indent + obj;
};
export default objToString;
@@ -1,63 +0,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.
*/
import objToString from './objToString'
describe('objToString should', () => {
it('return the input string for normal text', () => {
const obj = {
a: 'start',
b: 1,
c: true,
d: [1, 2, [3, 4]],
e: {
f: '',
g: 1,
h: null,
i: undefined,
j: {},
k: [1],
l: [{a:1, b:'foo'}, {b:2}]
}
};
const str = `a: start
b: 1
c: true
d:
- 1
- 2
-
- 3
- 4
e:
f:
g: 1
h: null
i:
j: {}
k:
- 1
l:
-
a: 1
b: foo
-
b: 2`;
expect(objToString(obj)).toBe(str);
});
});
@@ -1,78 +0,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.
*/
import {defer} from 'rxjs/internal/observable/defer';
import {tap} from 'rxjs/internal/operators/tap';
export {throwError} from 'rxjs/internal/observable/throwError';
export {of} from 'rxjs/internal/observable/of';
export {defer} from 'rxjs/internal/observable/defer';
export {concat} from 'rxjs/internal/observable/concat';
export {EMPTY} from 'rxjs/internal/observable/empty';
export {from} from 'rxjs/internal/observable/from';
export {timer} from 'rxjs/internal/observable/timer';
export {Observable} from 'rxjs/internal/Observable';
export {Subject} from 'rxjs/internal/Subject';
export {animationFrame as animationFrameScheduler} from 'rxjs/internal/scheduler/animationFrame';
export {concatMap} from 'rxjs/internal/operators/concatMap';
export {delay} from 'rxjs/internal/operators/delay';
export {debounceTime} from 'rxjs/internal/operators/debounceTime';
export {merge} from 'rxjs/internal/operators/merge';
export {map} from 'rxjs/internal/operators/map';
export {retryWhen} from 'rxjs/internal/operators/retryWhen';
export {tap} from 'rxjs/internal/operators/tap';
export {filter} from 'rxjs/internal/operators/filter';
export {concatAll} from 'rxjs/internal/operators/concatAll';
export {ignoreElements} from 'rxjs/internal/operators/ignoreElements';
export const doOnSubscribe = cb => source =>
defer(() => {
cb();
return source
});
export const doFirst = cb => source => {
let triggered;
return defer(() => {
triggered = false;
return source;
}).pipe(
tap(v => {
if (!triggered) {
triggered = true;
cb(v);
}
})
);
};
export const listen = (cb, execDelay = 150) => source => {
let handle = null;
return source.pipe(
doOnSubscribe(() => handle = setTimeout(() => cb('executing'), execDelay)),
tap({
complete: () => {
handle && clearTimeout(handle);
cb('completed');
},
error: (error) => {
console.warn('Operation failed:', error);
handle && clearTimeout(handle);
cb('failed');
}
})
)
};
@@ -1,121 +0,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.
*/
import {concat, EMPTY, of, throwError} from '@/utils/rxjs';
import {delay, doFirst, doOnSubscribe, listen} from './rxjs';
describe('doOnSubscribe', () => {
it('should call callback when subscribing', done => {
const cb = jest.fn();
EMPTY.pipe(
doOnSubscribe(cb)
).subscribe({
complete: () => {
expect(cb).toHaveBeenCalledTimes(1);
done();
}
});
});
});
describe('doFirst', () => {
it('should not call callback when empty', done => {
const cb = jest.fn();
EMPTY.pipe(
doFirst(cb)
).subscribe({
complete: () => {
expect(cb).not.toBeCalled();
done();
}
});
});
it('should call callback on first item', done => {
const cb = jest.fn();
of(1,2,3).pipe(
doFirst(cb)
).subscribe({
complete: () => {
expect(cb).toHaveBeenCalledTimes(1);
done();
}
});
});
});
describe('listen', () => {
it('should call callback with complete', done => {
const cb = jest.fn();
EMPTY.pipe(
listen(cb)
).subscribe({
complete: () => {
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith('completed');
done();
}
});
});
it('should call callback with executing and complete', done => {
const cb = jest.fn();
of(1).pipe(
delay(10),
listen(cb, 1)
).subscribe({
complete: () => {
expect(cb).toHaveBeenCalledTimes(2);
expect(cb).toHaveBeenCalledWith('executing');
expect(cb).toHaveBeenCalledWith('completed');
done();
}
});
});
it('should call callback with failed', done => {
const cb = jest.fn();
throwError(new Error('test')).pipe(
listen(cb)
).subscribe({
error: () => {
expect(cb).toHaveBeenCalledTimes(1);
expect(cb).toHaveBeenCalledWith('failed');
done();
}
});
});
it('should call callback with executing and failed', done => {
const cb = jest.fn();
concat(
of(1).pipe(delay(10)),
throwError(new Error('test'))
).pipe(
listen(cb, 1)
).subscribe({
error: () => {
expect(cb).toHaveBeenCalledTimes(2);
expect(cb).toHaveBeenCalledWith('executing');
expect(cb).toHaveBeenCalledWith('failed');
done();
}
});
});
});
@@ -1,33 +0,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.
*/
export default (fullName, targetLength) => {
if (!fullName || fullName.length < targetLength) {
return fullName;
}
const tokens = fullName.split('.');
let shortened = tokens.pop();
while (tokens.length > 0) {
const next = tokens.pop();
if (next.length + 1 + shortened.length < targetLength) {
shortened = next + '.' + shortened;
} else {
shortened = next[0] + '.' + shortened;
}
}
return shortened;
};
@@ -1,32 +0,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.
*/
import shortenClassname from './shortenClassname';
describe('shortenClassname', () => {
it('should shorten when too long', () => {
expect(shortenClassname('de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', 40)).toBe('d.c.b.a.s.config.AdminServerAutoConfiguration')
});
it('should not shorten when string is small enough', () => {
expect(shortenClassname('de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration', 300)).toBe('de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration')
});
it('should not shorten when no package is present', () => {
expect(shortenClassname('AdminServerAutoConfiguration', 1)).toBe('AdminServerAutoConfiguration')
});
});
@@ -1,23 +0,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.
*/
export default (strings, ...values) => {
let result = strings[0];
for (let i = 0; i < values.length; ++i) {
result += encodeURIComponent(values[i]) + strings[i + 1];
}
return result;
}
@@ -1,30 +0,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.
*/
import uri from './uri';
describe('uri', () => {
it('should escape uris properly', () => {
expect(uri`http://app/${'foo/bar'}?q=${'???'}`).toBe('http://app/foo%2Fbar?q=%3F%3F%3F');
expect(uri`http://app/${'foo/bar'}?q=1`).toBe('http://app/foo%2Fbar?q=1');
expect(uri`http://app/${'foo/bar'}`).toBe('http://app/foo%2Fbar');
expect(uri`http://app/foo`).toBe('http://app/foo');
expect(uri``).toBe('');
});
});
@@ -1,72 +0,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.
*/
const createTextVNode = label => {
return {
render() {
return this._v(label)
}
}
};
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.label && !view.handle) {
view.handle = createTextVNode(view.label);
}
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
});
}
)
}
}
@@ -1,327 +0,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.
-->
<template>
<div class="applications-list">
<div class="application-list__item card" :class="{'is-active': selected === application.name}"
v-for="application in applications" :key="application.name" :id=" application.name"
v-on-clickaway="(event) => deselect(event, application.name)"
>
<header class="hero application-list__item__header"
:class="getHeaderClass(application)"
@click.stop="select(application.name)"
>
<template v-if="selected !== application.name">
<sba-status :status="application.status"
:date="application.statusTimestamp"
class="application-list__item__header__status"
/>
<p class="application-list__item__header__name">
<span v-text="application.name" /><br>
<span class="is-muted">
<a v-if="application.instances.length === 1"
v-text="application.instances[0].registration.serviceUrl || application.instances[0].registration.healthUrl"
:href="application.instances[0].registration.serviceUrl || application.instances[0].registration.healthUrl"
/>
<span v-else
v-text="`${application.instances.length} instances`"
/>
</span>
</p>
<p class="application-list__item__header__version" v-text="application.buildVersion" />
</template>
<template v-else>
<h1 class="title is-size-5 application-list__item__header__name" v-text="application.name" />
</template>
<div class="application-list__item__header__actions">
<sba-icon-button :id="`nf-settings-${application.name}`"
v-if="hasNotificationFiltersSupport"
@click.stop="toggleNotificationFilterSettingsFor(application)"
:icon="hasActiveFilter(application) ? 'bell-slash' : 'bell'"
/>&nbsp;
<sba-icon-button icon="trash"
v-if="application.isUnregisterable"
@click.stop="unregister(application)"
/>
</div>
</header>
<div class="card-content" v-if="selected === application.name">
<table class="table is-hoverable is-selectable is-fullwidth application__instances">
<tbody>
<tr v-for="instance in application.instances" :key="instance.id" @click.stop="showDetails(instance)">
<td class="instance__status">
<sba-status :status="instance.statusInfo.status" :date="instance.statusTimestamp" />
</td>
<td class="is-narrow">
<a v-text="instance.registration.serviceUrl || instance.registration.healthUrl"
:href="instance.registration.serviceUrl || instance.registration.healthUrl"
@click.stop
/><br>
<span class="is-muted" v-text="instance.id" />
</td>
<td>
<sba-tags :tags="instance.tags" />
</td>
<td>
<span v-text="instance.buildVersion" />
</td>
<td class="instance__actions">
<sba-icon-button :id="`nf-settings-${instance.id}`"
v-if="hasNotificationFiltersSupport"
@click.stop="toggleNotificationFilterSettingsFor(instance)"
:icon="hasActiveFilter(instance) ? 'bell-slash' : 'bell'"
/>
<sba-icon-button icon="trash"
v-if="instance.isUnregisterable"
@click.stop="unregister(instance)"
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<notification-filter-settings v-if="showNotificationFilterSettingsObject"
v-popper="`nf-settings-${showNotificationFilterSettingsObject.id || showNotificationFilterSettingsObject.name}`"
:notification-filters="notificationFilters"
:object="showNotificationFilterSettingsObject"
@filter-added="handleFilterChange"
@filter-deleted="handleFilterChange"
/>
</div>
</template>
<script>
import Popper from '@/directives/popper';
import subscribing from '@/mixins/subscribing';
import NotificationFilter from '@/services/notification-filter';
import {concatMap, merge, Subject, timer} from '@/utils/rxjs';
import {directive as onClickaway} from 'vue-clickaway';
import NotificationFilterSettings from './notification-filter-settings';
export default {
props: {
applications: {
type: Array,
default: () => []
},
selected: {
type: String,
default: null
}
},
directives: {onClickaway, Popper},
mixins: [subscribing],
components: {NotificationFilterSettings},
data: () => ({
errors: [],
hasNotificationFiltersSupport: false,
notificationFilters: [],
showNotificationFilterSettingsObject: null
}),
computed: {},
methods: {
select(name) {
this.$router.replace({name: 'applications', params: {selected: name}});
},
deselect(event, expectedSelected) {
this.toggleNotificationFilterSettingsFor(null);
if (event && event.target instanceof HTMLAnchorElement) {
return;
}
if (this.selected === expectedSelected || !expectedSelected) {
this.$router.replace({name: 'applications'});
}
},
showDetails(instance) {
this.$router.push({name: 'instances/details', params: {instanceId: instance.id}});
},
async scrollIntoView(id, behavior) {
if (id) {
await this.$nextTick();
const el = document.getElementById(id);
if (el) {
const top = el.getBoundingClientRect().top + window.scrollY - 100;
window.scroll({top, left: window.scrollX, behavior: behavior || 'smooth'});
}
}
},
getHeaderClass(application) {
if (this.selected !== application.name) {
return 'is-selectable';
}
if (application.status === 'UP') {
return 'is-primary';
}
if (application.status === 'RESTRICTED') {
return 'is-warning';
}
if (application.status === 'DOWN') {
return 'is-danger';
}
if (application.status === 'OUT_OF_SERVICE') {
return 'is-danger';
}
if (application.status === 'OFFLINE') {
return 'is-light';
}
return 'is-light';
},
async unregister(item) {
try {
item.unregister();
} catch (e) {
this.errors.push(e);
}
},
createSubscription() {
const vm = this;
vm.notificationFilterSubject = new Subject();
return timer(0, 60000)
.pipe(
merge(vm.notificationFilterSubject),
concatMap(this.fetchNotificationFilters),
)
.subscribe({
next: data => {
vm.notificationFilters = data;
},
error: error => {
console.warn('Fetching notification filters failed:', error);
vm.errors.push(error);
}
});
},
async fetchNotificationFilters() {
if (this.hasNotificationFiltersSupport) {
const response = await NotificationFilter.getFilters();
return response.data;
}
return [];
},
handleFilterChange(event) {
this.toggleNotificationFilterSettingsFor(null);
this.notificationFilterSubject.next(event);
},
toggleNotificationFilterSettingsFor(obj) {
this.showNotificationFilterSettingsObject = obj ? obj : null;
},
hasActiveFilter(object) {
return this.notificationFilters.findIndex(f => f.affects(object)) >= 0;
}
},
async mounted() {
this.scrollIntoView(this.selected, 'instant');
this.hasNotificationFiltersSupport = await NotificationFilter.isSupported();
this.fetchNotificationFilters();
},
watch: {
selected(newVal) {
this.scrollIntoView(newVal);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.application-list__item {
transition: all $easing $speed;
&.is-active {
margin: 0.75rem -0.75rem;
max-width: unset;
}
&__header {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
*:not(.is-active) > &:hover {
background-color: $white-bis;
}
& > *:not(:first-child) {
margin-left: 12px;
}
&__status {
width: $gap;
}
&__name,
&__version {
flex-grow: 1;
flex-basis: 50%;
}
&__name.title {
margin: 0.75rem 0;
}
&__actions {
justify-self: end;
opacity: 0;
transition: all $easing $speed;
will-change: opacity;
margin-right: ($gap / 2);
display: flex;
*:hover > &,
*.is-active & {
opacity: 1;
}
& > * {
width: ($gap / 2);
height: ($gap / 2);
}
}
}
}
.application__instances td {
vertical-align: middle;
}
.instance {
&__status {
width: $gap;
}
&__actions {
text-align: right;
opacity: 0;
transition: all $easing $speed;
will-change: opacity;
margin-right: $gap;
*:hover > & {
opacity: 1;
}
& > * {
width: ($gap / 2);
height: ($gap / 2);
}
}
}
</style>
@@ -1,60 +0,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.
-->
<template>
<span>
<span class="has-text-warning" v-if="error">
<font-awesome-icon icon="exclamation-triangle" />
</span>
<span :class="{ 'badge is-badge-danger' : downCount > 0 }" :data-badge="downCount">
应用
</span>
</span>
</template>
<script>
export default {
props: {
applications: {
type: Array,
default: () => [],
},
error: {
type: Error,
default: null
}
},
computed: {
downCount() {
return this.applications.reduce((current, next) => {
return current + (next.instances.filter(instance => instance.statusInfo.status !== 'UP').length);
}, 0);
}
},
watch: {
downCount(newVal, oldVal) {
if ((newVal === 0) !== (oldVal === 0)) {
this.updateFavicon(newVal === 0);
}
}
},
methods: {
updateFavicon(up) {
document.querySelector('link[rel*="icon"]').href = up ? 'assets/img/favicon.png' : 'assets/img/favicon-danger.png';
}
}
};
</script>
@@ -1,130 +0,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.
-->
<template>
<section class="section">
<div class="container">
<div v-if="error" class="message is-warning">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
Server connection failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level applications-stats">
<div class="level-item has-text-centered">
<div>
<p class="heading">应用</p>
<p class="title" v-text="applicationsCount">1</p>
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">实例</p>
<p class="title" v-text="instancesCount">1</p>
</div>
</div>
<div class="level-item has-text-centered">
<div v-if="downCount === 0">
<p class="heading">状态</p>
<p class="title has-text-success">all up</p>
</div>
<div v-else>
<p class="heading">下线</p>
<p class="title has-text-danger" v-text="downCount" />
</div>
</div>
</div>
<div class="application-group" v-for="group in statusGroups" :key="group.status">
<p class="heading" v-text="group.status" />
<applications-list :applications="group.applications" :selected="selected" />
</div>
<div v-if="statusGroups.length === 0">
<p class="is-muted">No applications registered.</p>
</div>
</div>
</section>
</template>
<script>
import groupBy from 'lodash/groupBy';
import sortBy from 'lodash/sortBy';
import transform from 'lodash/transform';
import applicationsList from './applications-list';
import handle from './handle';
export default {
props: {
applications: {
type: Array,
default: () => [],
},
error: {
type: Error,
default: null
},
selected: {
type: String,
default: null
}
},
// eslint-disable-next-line vue/no-unused-components
components: {applicationsList},
computed: {
statusGroups() {
const byStatus = groupBy(this.applications, application => application.status);
const list = transform(byStatus, (result, value, key) => {
result.push({status: key, applications: sortBy(value, [application => application.name])})
}, []);
return sortBy(list, [item => item.status]);
},
applicationsCount() {
return this.applications.length;
},
instancesCount() {
return this.applications.reduce((current, next) => current + next.instances.length, 0);
},
downCount() {
return this.applications.reduce((current, next) => {
return current + (next.instances.filter(instance => instance.statusInfo.status !== 'UP').length);
}, 0);
}
},
install({viewRegistry}) {
viewRegistry.addView({
path: '/applications/:selected?',
props: true,
name: 'applications',
label: '应用',
handle,
order: 0,
component: this
});
viewRegistry.addRedirect('/', 'applications');
}
};
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.application-group {
margin: $gap 0;
}
</style>
@@ -1,122 +0,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.
-->
<template>
<div class="box">
<template v-if="!activeFilter">
<div class="field">
<p class="control has-inline-text">
Suppress notifications on <code v-text="object.id || object.name" /> for
<span class="select">
<select v-model="ttl" @click.stop>
<option v-for="option in ttlOptions"
:key="`ttl-instance-${option.value}`"
:value="option.value" v-text="option.label"
/>
</select>
</span>
</p>
</div>
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-warning" :class="{'is-loading' : actionState === 'executing'}"
@click.stop="addFilter"
><font-awesome-icon icon="bell-slash" />&nbsp;Suppress
</button>
</div>
</div>
</template>
<template v-else>
<div class="field">
<p class="control has-inline-text">
Notifications on <code v-text="object.id || object.name" /> are suppressed for
<strong v-text="activeFilter.expiry ? activeFilter.expiry.locale('en').fromNow(true) : 'ever' " />.
</p>
</div>
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button" :class="{'is-loading' : actionState === 'executing'}" @click.stop="deleteActiveFilter">
<font-awesome-icon icon="bell" />&nbsp;Unsuppress
</button>
</div>
</div>
</template>
</div>
</template>
<script>
import NotificationFilter from '@/services/notification-filter';
export default {
props: {
object: {
type: Object,
required: true
},
notificationFilters: {
type: Array,
required: true
}
},
data: () => ({
ttl: 5 * 60 * 1000,
ttlOptions: [
{label: '5 minutes', value: 5 * 60 * 1000},
{label: '15 minutes', value: 15 * 60 * 1000},
{label: '30 minutes', value: 30 * 60 * 1000},
{label: '1 hour', value: 60 * 60 * 1000},
{label: '3 hours', value: 3 * 60 * 60 * 1000},
{label: '8 hours', value: 8 * 60 * 60 * 1000},
{label: '24 hours', value: 24 * 60 * 60 * 1000},
{label: 'ever', value: -1}
],
actionState: null
}),
computed: {
activeFilter() {
return this.notificationFilters.find(f => f.affects(this.object));
}
},
methods: {
async addFilter() {
this.actionState = 'executing';
try {
const response = await NotificationFilter.addFilter(this.object, this.ttl);
this.actionState = 'completed';
this.$emit('filter-added', response.data)
} catch (error) {
console.warn('Adding notification filter failed:', error);
}
},
async deleteActiveFilter() {
this.actionState = 'executing';
try {
await this.activeFilter.delete();
this.actionState = 'completed';
this.$emit('filter-deleted', this.activeFilter.id);
} catch (error) {
this.actionState = 'failed';
console.warn('Deleting notification filter failed:', error);
}
}
}
}
</script>
<style lang="scss">
.control.has-inline-text {
line-height: 2.25em;
}
</style>
@@ -1,28 +0,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.
*/
const views = [];
/* 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)
}
});
export default views;
@@ -1,123 +0,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.
-->
<template>
<table class="auditevents table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Timestamp</th>
<th>Event</th>
<th>Principal</th>
<th>Remote address</th>
<th>Session Id</th>
</tr>
</thead>
<tbody>
<template v-for="event in events">
<tr class="is-selectable"
:class="{ 'auditevents__event--is-detailed' : showDetails[event.key] }"
@click="showDetails[event.key] ? $delete(showDetails, event.key) : $set(showDetails, event.key, true)"
:key="event.key"
>
<td v-text="event.timestamp.format('L HH:mm:ss.SSS')" />
<td>
<span v-text="event.type" class="tag"
:class="{ 'is-success' : event.isSuccess(), 'is-danger' : event.isFailure() }"
/>
</td>
<td v-if="hasSessionEndpoint && event.principal">
<router-link v-text="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: 'instances/sessions', params: { 'instanceId' : instance.id }, query: { sessionId : event.sessionId } }"
/>
</td>
<td v-else v-text="event.sessionId" />
</tr>
<tr :key="`${event.key}-detail`" v-if="showDetails[event.key]">
<td colspan="5">
<pre class="auditevents__event-detail" v-text="toJson(event.data)" />
</td>
</tr>
</template>
<tr v-if="events.length === 0">
<td class="is-muted" colspan="5">
<p v-if="isLoading" class="is-loading">Loading Audit Events...</p>
<p v-else>No Audit Events found.</p>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Instance from '@/services/instance';
import prettyBytes from 'pretty-bytes';
export default {
props: {
events: {
type: Array,
default: () => []
},
instance: {
type: Instance,
required: true
},
isLoading: {
type: Boolean,
default: false
}
},
data: () => ({
showDetails: {}
}),
computed: {
hasSessionEndpoint() {
return this.instance.hasEndpoint('sessions');
}
},
methods: {
prettyBytes,
toJson(obj) {
return JSON.stringify(obj, null, 4);
}
}
}
</script>
<style lang="scss">
.auditevents {
table-layout: fixed;
td {
vertical-align: middle;
}
&__event--is-detailed td {
border: none !important;
}
&__event-detail {
overflow: auto;
}
}
</style>
@@ -1,214 +0,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.
-->
<template>
<section class="section">
<div class="field is-horizontal">
<div class="field-body">
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="search"
placeholder="Principal"
v-model.trim="filter.principal"
>
</p>
</div>
<div class="field">
<p class="control is-expanded">
<input
list="auditevent-type"
class="input"
type="search"
placeholder="Type"
v-model="filter.type"
>
<datalist id="auditevent-type">
<option value="AUTHENTICATION_FAILURE" />
<option value="AUTHENTICATION_SUCCESS" />
<option value="AUTHENTICATION_SWITCH" />
<option value="AUTHORIZATION_FAILURE" />
</datalist>
</p>
</div>
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="datetime-local"
placeholder="Date"
:value="formatDate(filter.after)"
@input="filter.after = parseDate($event.target.value)"
>
</p>
</div>
</div>
</div>
<template>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon
class="has-text-danger"
icon="exclamation-triangle"
/>
Fetching audit events failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div
v-if="isOldAuditevents"
class="message is-warning"
>
<div class="message-body">
Audit Log is not supported for Spring Boot 1.x applications.
</div>
</div>
<auditevents-list
:instance="instance"
:events="events"
:is-loading="isLoading"
/>
</template>
</section>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, debounceTime, merge, Subject, tap, timer} from '@/utils/rxjs';
import AuditeventsList from '@/views/instances/auditevents/auditevents-list';
import uniqBy from 'lodash/uniqBy';
import moment from 'moment';
class Auditevent {
constructor({timestamp, ...event}) {
Object.assign(this, event);
this.timestamp = moment(timestamp);
}
get key() {
return `${this.timestamp}-${this.type}-${this.principal}`;
}
get remoteAddress() {
return this.data && this.data.details && this.data.details.remoteAddress || null;
}
get sessionId() {
return this.data && this.data.details && this.data.details.sessionId || null;
}
isSuccess() {
return this.type.toLowerCase().includes('success');
}
isFailure() {
return this.type.toLowerCase().includes('failure');
}
}
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
components: {AuditeventsList},
data: () => ({
isLoading: false,
error: null,
events: [],
filter: {
after: moment().startOf('day'),
type: null,
principal: null
},
isOldAuditevents: false
}),
watch: {
filter: {
deep: true,
handler() {
this.filterChanged.next();
}
}
},
methods: {
formatDate(value) {
return value.format(moment.HTML5_FMT.DATETIME_LOCAL);
},
parseDate(value) {
return moment(value, moment.HTML5_FMT.DATETIME_LOCAL, true);
},
async fetchAuditevents() {
this.isLoading = true;
const response = await this.instance.fetchAuditevents(this.filter);
const converted = response.data.events.map(event => new Auditevent(event));
converted.reverse();
this.isLoading = false;
return converted;
},
createSubscription() {
const vm = this;
vm.filterChanged = new Subject();
vm.error = null;
return timer(0, 5000)
.pipe(
merge(vm.filterChanged.pipe(
debounceTime(250),
tap({
next: () => vm.events = []
})
)),
concatMap(this.fetchAuditevents)
)
.subscribe({
next: events => {
vm.addEvents(events);
},
error: error => {
console.warn('Fetching audit events failed:', error);
if (error.response.headers['content-type'].includes('application/vnd.spring-boot.actuator.v2')) {
vm.error = error;
} else {
vm.isOldAuditevents = true;
}
}
});
},
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,
label: 'Audit Log',
group: 'Security',
order: 600,
isEnabled: ({instance}) => instance.hasEndpoint('auditevents')
});
}
}
</script>
@@ -1,141 +0,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.
-->
<template>
<table class="beans table is-fullwidth is-hoverable">
<tbody>
<template v-for="bean in beans">
<tr
class="is-selectable"
:key="bean.name"
@click="showDetails[bean.name] ? $delete(showDetails, bean.name) : $set(showDetails, bean.name, true)"
>
<td class="is-breakable">
<span
v-text="bean.shortName"
:title="bean.name"
/><br>
<small
class="is-muted"
v-text="bean.shortType"
:title="bean.type"
/>
</td>
<td><span
v-text="bean.scope"
class="tag"
/></td>
</tr>
<tr
:key="`${bean.name}-detail`"
v-if="showDetails[bean.name]"
>
<td
colspan="2"
class="has-background-white-ter"
>
<table class="table is-narrow is-fullwidth beans__bean-detail">
<tbody>
<tr v-if="bean.name !== bean.shortName">
<th>
<small>Name</small>
</th>
<td
class="is-breakable"
v-text="bean.name"
/>
</tr>
<tr
v-for="(alias, idx) in bean.aliases"
:key="alias"
>
<th
v-if="idx === 0"
:rowspan="bean.aliases.length"
>
<small>Aliases</small>
</th>
<td
class="is-breakable"
v-text="alias"
/>
</tr>
<tr v-if="bean.type !== bean.shortType">
<th>
<small>Type</small>
</th>
<td
class="is-breakable"
v-text="bean.type"
/>
</tr>
<tr v-if="bean.resource">
<th>
<small>Resource</small>
</th>
<td
class="is-breakable"
v-text="bean.resource"
/>
</tr>
<tr
v-for="(dependency, idx) in bean.dependencies"
:key="dependency"
>
<th
v-if="idx === 0"
:rowspan="bean.dependencies.length"
>
<small>Dependencies</small>
</th>
<td
class="is-breakable"
v-text="dependency"
/>
</tr>
</tbody>
</table>
</td>
</tr>
</template>
</tbody>
</table>
</template>
<script>
export default {
props: {
beans: {
type: Array,
default: () => []
}
},
data: () => ({
showDetails: {}
})
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.beans {
table.beans__bean-detail {
tbody tr {
pointer-events: none;
background-color: $white-ter;
}
}
}
</style>
@@ -1,150 +0,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.
-->
<template>
<section class="section" :class="{isLoading: isLoading}">
<div class="container">
<div
v-if="error"
class="message is-danger"
>
<div class="message-body">
<strong>
<font-awesome-icon
class="has-text-danger"
icon="exclamation-triangle"
/>
Fetching beans failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="field">
<p class="control is-expanded">
<input
class="input"
type="search"
placeholder="filter"
v-model="filter"
>
</p>
</div>
<template v-for="context in filteredContexts">
<h3 class="title" v-text="context.name" :key="context.name" />
<beans-list :beans="context.beans" :key="`${context.name}-beans`" />
</template>
</div>
</section>
</template>
<script>
import Instance from '@/services/instance';
import {compareBy} from '@/utils/collections';
import shortenClassname from '@/utils/shortenClassname';
import BeansList from '@/views/instances/beans/beans-list';
import isEmpty from 'lodash/isEmpty';
class Bean {
constructor(name, bean) {
Object.assign(this, bean);
this.name = name;
this.shortName = shortenClassname(this.name, 80);
this.shortType = shortenClassname(this.type, 80);
}
}
const flattenBeans = beans => {
return Object.keys(beans)
.map((key) => {
return new Bean(key, beans[key]);
});
};
const flattenContexts = beanData => {
if (isEmpty(beanData.contexts)) {
return [];
}
return Object.keys(beanData.contexts)
.map((key) => ({
beans: flattenBeans(beanData.contexts[key].beans),
name: key,
parent: beanData.contexts[key].parentId
}));
};
export default {
components: {BeansList},
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
isLoading: false,
error: null,
contexts: [],
filter: '',
}),
computed: {
filteredContexts() {
const filterFn = this.getFilterFn();
return this.contexts.map(ctx => ({
...ctx,
beans: ctx.beans.filter(filterFn).sort(compareBy(bean => bean.name))
}));
}
},
methods: {
getFilterFn() {
if (!this.filter || this.filter === '') {
return () => true;
}
const regex = new RegExp(this.filter, 'i');
return bean => (bean.name.match(regex) || bean.aliases.some(alias => alias.match(regex)));
},
async fetchBeans() {
this.error = null;
this.isLoading = true;
try {
const res = await this.instance.fetchBeans();
this.contexts = flattenContexts(res.data);
} catch (error) {
console.warn('Fetching beans failed:', error);
this.error = error;
}
this.isLoading = false;
}
},
created() {
this.fetchBeans();
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/beans',
parent: 'instances',
path: 'beans',
group: 'Insights',
component: this,
label: 'Beans',
order: 110,
isEnabled: ({instance}) => instance.hasEndpoint('beans')
});
}
}
</script>
@@ -1,146 +0,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.
-->
<template>
<table class="caches table is-fullwidth">
<thead>
<tr>
<th>Name</th>
<th>Cache Manager</th>
<th class="is-narrow">
<sba-confirm-button
class="button"
:class="{'is-loading' : clearingAll === 'executing', 'is-info' : clearingAll === 'completed', 'is-danger' : clearingAll === 'failed'}"
:disabled="clearingAll !== null"
@click="clearCaches"
>
<span v-if="clearingAll === 'completed'">Cleared</span>
<span v-else-if="clearingAll === 'failed'">Failed</span>
<span v-else><font-awesome-icon icon="trash" />&nbsp;Clear</span>
</sba-confirm-button>
</th>
</tr>
</thead>
<tbody>
<tr
v-for="cache in caches"
:key="cache.key"
>
<td>
<span
class="is-breakable"
v-text="cache.name"
/>
</td>
<td>
<span
class="is-breakable"
v-text="cache.cacheManager"
/>
</td>
<td class="is-narrow">
<button class="button"
:class="{ 'is-loading' : clearing[cache.name] === 'executing', 'is-info' : clearing[cache.name] === 'completed', 'is-danger' : clearing[cache.name] === 'failed' }"
:disabled="cache.name in clearing" @click="clearCache(cache.name)"
>
<span v-if="clearing[cache.name] === 'completed'">Cleared</span>
<span v-else-if="clearing[cache.name] === 'failed'">Failed</span>
<span v-else><font-awesome-icon icon="trash" />&nbsp;Clear</span>
</button>
</td>
</tr>
<tr v-if="caches.length === 0">
<td class="is-muted" colspan="3 ">
<p v-if="isLoading" class="is-loading">Loading Caches...</p>
<p v-else>No caches found.</p>
</td>
</tr>
</tbody>
</table>
</template>
<script>
import Instance from '@/services/instance';
import {concatMap, from, listen, of, tap} from '@/utils/rxjs';
export default {
name: 'CachesList',
props: {
caches: {
type: Array,
default: () => []
},
instance: {
type: Instance,
required: true
},
isLoading: {
type: Boolean,
default: false
}
},
data: () => ({
clearing: {},
clearingAll: null,
}),
methods: {
clearCaches() {
const vm = this;
from(vm.instance.clearCaches())
.pipe(listen(status => vm.clearingAll = status))
.subscribe({
complete: () => {
setTimeout(() => vm.clearingAll = null, 2500);
return vm.$emit('cleared', '*');
}
});
},
clearCache(cache) {
const vm = this;
vm._clearCache(cache)
.pipe(listen(status => vm.$set(vm.clearing, cache, status)))
.subscribe({
complete: () => {
setTimeout(() => vm.$delete(vm.clearing, cache), 2500);
return vm.$emit('cleared', cache);
},
});
},
_clearCache(cache) {
const vm = this;
return of(cache)
.pipe(
concatMap(async cache => {
await vm.instance.clearCache(cache);
return cache;
}),
tap({
error: error => {
console.warn(`Clearing cache ${cache} failed:`, error);
}
})
);
}
}
}
</script>
<style lang="scss">
.caches {
td, th {
vertical-align: middle;
}
}
</style>
@@ -1,138 +0,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.
-->
<template>
<section class="section">
<div
class="container"
>
<div
v-if="error"
class="message is-danger"
>
<div class="message-body">
<strong>
<font-awesome-icon
class="has-text-danger"
icon="exclamation-triangle"
/>
Fetching caches failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="field-body">
<div class="field has-addons">
<p class="control is-expanded">
<input
class="input"
type="search"
placeholder="name filter"
v-model="filter"
>
</p>
<p class="control">
<span class="button is-static">
<span v-text="filteredCaches.length" />
/
<span v-text="caches.length" />
</span>
</p>
</div>
</div>
<caches-list :instance="instance" :caches="filteredCaches" :is-loading="isLoading" />
</div>
</section>
</template>
<script>
import Instance from '@/services/instance';
import CachesList from '@/views/instances/caches/caches-list';
import isEmpty from 'lodash/isEmpty';
const flattenCaches = cacheData => {
if (isEmpty(cacheData.cacheManagers)) {
return [];
}
const mappend = Object.entries(cacheData.cacheManagers).flatMap(
([cacheManagerName, v]) => Object.keys(v.caches)
.map(cacheName => ({cacheManager: cacheManagerName, name: cacheName, key: `${cacheManagerName}:${cacheName}`}))
);
return mappend.sort((c1, c2) => c1.key.localeCompare(c2.key));
};
export default {
components: {CachesList},
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
isLoading: false,
error: null,
caches: [],
filter: '',
}),
computed: {
filteredCaches() {
let filterFn = this.getFilterFn();
return filterFn ? this.caches.filter(filterFn) : this.caches;
}
},
methods: {
async fetchCaches() {
this.error = null;
this.isLoading = true;
try {
const res = await this.instance.fetchCaches();
this.caches = flattenCaches(res.data);
} catch (error) {
console.warn('Fetching caches failed:', error);
this.error = error;
}
this.isLoading = false;
},
getFilterFn() {
let filterFn = null;
if (this.filter) {
const normalizedFilter = this.filter.toLowerCase();
filterFn = cache => cache.name.toLowerCase().includes(normalizedFilter);
}
return filterFn;
}
},
created() {
this.fetchCaches();
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/caches',
parent: 'instances',
path: 'caches',
group: 'Data',
component: this,
label: 'Caches',
order: 970,
isEnabled: ({instance}) => instance.hasEndpoint('caches')
});
}
}
</script>
@@ -1,172 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching configuration properties failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="field has-addons" v-if="configprops">
<p class="control is-expanded">
<input class="input" type="search" placeholder="name / value filter" v-model="filter">
</p>
</div>
<sba-panel :header-sticks-below="['#navigation']"
v-for="bean in configurationPropertiesBeans"
:key="bean.name"
:title=" bean.name"
>
<table class="table is-fullwidth"
v-if="Object.keys(bean.properties).length > 0"
>
<tr v-for="(value, name) in bean.properties" :key="`${bean.name}-${name}`">
<td v-text="name" />
<td class="is-breakable" v-text="value" />
</tr>
</table>
<p class="is-muted" v-else>No properties set</p>
</sba-panel>
</section>
</template>
<script>
import Instance from '@/services/instance';
import isEmpty from 'lodash/isEmpty';
import mapKeys from 'lodash/mapKeys';
import pickBy from 'lodash/pickBy';
const filterProperty = (needle) => (value, name) => {
return name.toString().toLowerCase().includes(needle) || value.toString().toLowerCase().includes(needle);
};
const filterProperties = (needle, properties) => pickBy(properties, filterProperty(needle));
const filterConfigurationProperties = (needle) => (propertySource) => {
if (!propertySource || !propertySource.properties) {
return null;
}
return {
...propertySource,
properties: filterProperties(needle, propertySource.properties)
};
};
function flattenBean(obj, prefix = '') {
if (Object(obj) !== obj) {
return {[prefix]: obj};
}
if (Array.isArray(obj)) {
if (obj.length === 0) {
return {[prefix]: []};
} else {
return obj.map(
(value, idx) => flattenBean(value, `${prefix}[${idx}]`)
).reduce((c, n) => ({...c, ...n}), {});
}
} else {
if (isEmpty(obj)) {
return {[prefix]: {}};
} else {
return Object.entries(obj).map(
([name, value]) => flattenBean(value, prefix ? `${prefix}.${name}` : name)
).reduce((c, n) => ({...c, ...n}), {});
}
}
}
const flattenConfigurationPropertiesBeans = (configprops) => {
const propertySources = [];
const contextNames = Object.keys(configprops.contexts);
for (const contextName of contextNames) {
const context = configprops.contexts[contextName];
const beanNames = Object.keys(context.beans);
for (const beanName of beanNames) {
const bean = context.beans[beanName];
const properties = mapKeys(flattenBean(bean.properties), (value, key) => `${bean.prefix}.${key}`);
propertySources.push({
name: contextNames.length > 1 ? `${contextName}: ${beanName}` : beanName,
properties
});
}
}
return propertySources;
};
export default {
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
hasLoaded: false,
error: null,
configprops: null,
filter: null
}),
computed: {
configurationPropertiesBeans() {
if (!this.configprops) {
return [];
}
const configurationProperties = flattenConfigurationPropertiesBeans(this.configprops);
if (!this.filter) {
return configurationProperties;
}
return configurationProperties
.map(filterConfigurationProperties(this.filter.toLowerCase()))
.filter(ps => ps && Object.keys(ps.properties).length > 0);
}
},
created() {
this.fetchConfigprops();
},
methods: {
async fetchConfigprops() {
this.error = null;
try {
const configprops = await this.instance.fetchConfigprops();
this.configprops = configprops.data;
} catch (error) {
console.warn('Fetching configuration properties failed:', error);
this.error = error;
}
this.hasLoaded = true;
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/configprops',
parent: 'instances',
path: 'configprops',
component: this,
label: 'Configuration Properties',
group: 'Insights',
order: 110,
isEnabled: ({instance}) => instance.hasEndpoint('configprops')
});
}
}
</script>
@@ -1,139 +0,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.
-->
<template>
<div class="cache-chart">
<svg class="cache-chart__svg" />
</div>
</template>
<script>
import d3 from '@/utils/d3';
import moment from 'moment';
export default {
props: {
data: {
type: Array,
default: () => []
}
},
data: () => ({}),
methods: {
drawChart(_data) {
const vm = this;
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
///setup x and y scale
const extent = d3.extent(data, d => d.timestamp);
const x = d3.scaleTime()
.range([0, vm.width])
.domain(extent);
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.total) * 1.05]);
//draw areas
const miss = vm.areas.selectAll('.cache-chart__area--miss')
.data([data]);
miss.enter().append('path')
.merge(miss)
.attr('class', 'cache-chart__area--miss')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(d => y(d.hit))
.y1(d => y(d.total)));
miss.exit().remove();
const hit = vm.areas.selectAll('.cache-chart__area--hit')
.data([data]);
hit.enter().append('path')
.merge(hit)
.attr('class', 'cache-chart__area--hit')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(y(0))
.y1(d => y(d.hit)));
hit.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(5)
.tickFormat(d => moment(d).format('HH:mm:ss'))
);
vm.yAxis.call(d3.axisLeft(y)
.ticks(5)
.tickFormat(d => d <= 1000 ? d : (d / 1000).toFixed(1) + 'K')
);
},
},
mounted() {
const margin = {
top: 5,
right: 5,
bottom: 30,
left: 50,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.cache-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'cache-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'cache-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.drawChart(this.data);
},
watch: {
data: 'drawChart'
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.cache-chart {
&__svg {
height: 159px;
width: 100%;
}
&__area {
&--miss {
fill: $warning;
opacity: 0.8;
}
&--hit {
fill: $info;
opacity: 0.8;
}
}
}
</style>
@@ -1,136 +0,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.
-->
<template>
<div class="datasource-chart">
<svg class="datasource-chart__svg" />
</div>
</template>
<script>
import d3 from '@/utils/d3';
import moment from 'moment';
export default {
props: {
data: {
type: Array,
default: () => []
}
},
methods: {
drawChart(_data) {
const vm = this;
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
///setup x and y scale
const extent = d3.extent(data, d => d.timestamp);
const x = d3.scaleTime()
.range([0, vm.width])
.domain(extent);
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.active) * 1.05]);
//draw max
const max = vm.areas.selectAll('.datasource-chart__line--max')
.data([data]);
max.enter().append('path')
.merge(max)
.attr('class', 'datasource-chart__line--max')
.attr('d', d3.line()
.x(d => x(d.timestamp))
.y(d => y(d.max)));
max.exit().remove();
//draw areas
const active = vm.areas.selectAll('.datasource-chart__area--active')
.data([data]);
active.enter().append('path')
.merge(active)
.attr('class', 'datasource-chart__area--active')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(y(0))
.y1(d => y(d.active)));
active.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(5)
.tickFormat(d => moment(d).format('HH:mm:ss'))
);
vm.yAxis.call(d3.axisLeft(y)
.ticks(5)
);
},
},
mounted() {
const margin = {
top: 5,
right: 5,
bottom: 30,
left: 50,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.datasource-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'datasource-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'datasource-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.drawChart(this.data);
},
watch: {
data: 'drawChart'
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.datasource-chart {
&__svg {
height: 159px;
width: 100%;
}
&__area {
&--active {
fill: $info;
opacity: 0.8;
}
}
&__line--max {
stroke: $grey;
}
}
</style>
@@ -1,144 +0,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.
-->
<template>
<sba-panel :title="`Cache: ${cacheName}`" v-if="hasLoaded">
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching cache metrics failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level cache-current" v-if="current">
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-info">Hits</p>
<p v-text="current.hit" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-warning">Misses</p>
<p v-text="current.miss" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Hit ratio</p>
<p v-text="ratio" />
</div>
</div>
<div v-if="current.size" class="level-item has-text-centered">
<div>
<p class="heading">Size</p>
<p v-text="current.size" />
</div>
</div>
</div>
<cache-chart v-if="chartData.length > 0" :data="chartData" />
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import moment from 'moment';
import cacheChart from './cache-chart';
export default {
props: {
instance: {
type: Instance,
required: true
},
cacheName: {
type: String,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {cacheChart},
data: () => ({
hasLoaded: false,
error: null,
current: null,
disableSize: false,
chartData: [],
}),
computed: {
ratio() {
if (this.current.total > 0) {
return (this.current.hit / this.current.total * 100).toFixed(2) + '%';
}
return '-';
}
},
methods: {
async fetchMetrics() {
const responseHit = this.instance.fetchMetric('cache.gets', {name: this.cacheName, result: 'hit'});
const responseMiss = this.instance.fetchMetric('cache.gets', {name: this.cacheName, result: 'miss'});
let size = undefined;
if (!this.disableSize) {
const responsSize = this.instance.fetchMetric('cache.size', {name: this.cacheName});
try {
size = (await responsSize).data.measurements[0].value;
} catch (error) {
this.disableSize = true;
console.warn('Fetching cache size failed - error is ignored', error)
}
}
const hit = (await responseHit).data.measurements[0].value;
const miss = (await responseMiss).data.measurements[0].value;
return {
hit,
miss,
total: hit + miss,
size
};
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(vm.fetchMetrics))
.subscribe({
next: data => {
vm.hasLoaded = true;
vm.current = data;
vm.chartData.push({...data, timestamp: moment().valueOf()});
},
error: error => {
vm.hasLoaded = true;
console.warn(`Fetching cache ${vm.cacheName} metrics failed:`, error);
vm.error = error;
}
});
}
}
}
</script>
<style lang="scss">
.datasource-current {
margin-bottom: 0 !important;
}
</style>
@@ -1,65 +0,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.
-->
<template>
<div>
<details-cache v-for="cache in caches" :key="cache"
:instance="instance" :cache-name="cache"
/>
</div>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import uniq from 'lodash/uniq';
import detailsCache from './details-cache';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {detailsCache},
data: () => ({
caches: [],
}),
methods: {
async fetchCaches() {
const response = await this.instance.fetchMetric('cache.gets');
return uniq(response.data.availableTags.filter(tag => tag.tag === 'name')[0].values);
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(this.fetchCaches))
.subscribe({
next: names => {
vm.caches = names
},
error: error => {
console.warn('Fetching caches failed:', error);
}
});
}
}
}
</script>
@@ -1,119 +0,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.
-->
<template>
<sba-panel :title="`Datasource: ${dataSource}`" v-if="hasLoaded">
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching datasource metrics failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level datasource-current" v-if="current">
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-info">Active connections</p>
<p v-text="current.active" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Min connections</p>
<p v-text="current.min" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Max connections</p>
<p v-if="current.max >= 0" v-text="current.max" />
<p v-else>unlimited</p>
</div>
</div>
</div>
<datasource-chart v-if="chartData.length > 0" :data="chartData" />
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import moment from 'moment';
import datasourceChart from './datasource-chart';
export default {
props: {
instance: {
type: Instance,
required: true
},
dataSource: {
type: String,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {datasourceChart},
data: () => ({
hasLoaded: false,
error: null,
current: null,
chartData: [],
}),
methods: {
async fetchMetrics() {
const responseActive = this.instance.fetchMetric('data.source.active.connections', {name: this.dataSource});
const responseMin = this.instance.fetchMetric('data.source.min.connections', {name: this.dataSource});
const responseMax = this.instance.fetchMetric('data.source.max.connections', {name: this.dataSource});
return {
active: (await responseActive).data.measurements[0].value,
min: (await responseMin).data.measurements[0].value,
max: (await responseMax).data.measurements[0].value
};
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(vm.fetchMetrics))
.subscribe({
next: data => {
vm.hasLoaded = true;
vm.current = data;
vm.chartData.push({...data, timestamp: moment().valueOf()});
},
error: error => {
vm.hasLoaded = true;
console.warn(`Fetching datasource ${vm.dataSource} metrics failed:`, error);
vm.error = error;
}
});
}
}
}
</script>
<style lang="scss">
.datasource-current {
margin-bottom: 0 !important;
}
</style>
@@ -1,64 +0,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.
-->
<template>
<div>
<details-datasource v-for="dataSource in dataSources" :key="dataSource"
:instance="instance" :data-source="dataSource"
/>
</div>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import detailsDatasource from './details-datasource';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {detailsDatasource},
data: () => ({
dataSources: [],
}),
methods: {
async fetchDataSources() {
const response = await this.instance.fetchMetric('data.source.active.connections');
return response.data.availableTags.filter(tag => tag.tag === 'name')[0].values;
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(this.fetchDataSources))
.subscribe({
next: names => {
vm.dataSources = names
},
error: error => {
console.warn('Fetching datasources failed:', error);
}
});
}
}
}
</script>
@@ -1,107 +0,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.
-->
<template>
<sba-panel title="Garbage Collection Pauses" v-if="hasLoaded">
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching GC metrics failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level" v-if="current">
<div class="level-item has-text-centered">
<div>
<p class="heading">Count</p>
<p v-text="current.count" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Total time spent</p>
<p v-text="`${current.total_time.asSeconds().toFixed(4)}s`" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Max time spent</p>
<p v-text="`${current.max.asSeconds().toFixed(4)}s`" />
</div>
</div>
</div>
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import moment from 'moment';
import {toMillis} from '../metrics/metric';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
data: () => ({
hasLoaded: false,
error: null,
current: null,
}),
methods: {
async fetchMetrics() {
const response = await this.instance.fetchMetric('jvm.gc.pause');
const measurements = response.data.measurements.reduce(
(current, measurement) => ({
...current,
[measurement.statistic.toLowerCase()]: measurement.value
}),
{}
);
return {
...measurements,
total_time: moment.duration(toMillis(measurements.total_time, response.baseUnit)),
max: moment.duration(toMillis(measurements.max, response.baseUnit)),
};
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(this.fetchMetrics))
.subscribe({
next: data => {
vm.hasLoaded = true;
vm.current = data;
},
error: error => {
vm.hasLoaded = true;
console.warn('Fetching GC metrics failed:', error);
vm.error = error;
}
});
}
}
}
</script>
@@ -1,72 +0,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.
-->
<template>
<sba-panel title="Health">
<div>
<div v-if="error" class="message is-warning">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
Fetching live health status failed. This is the last known information.
</strong>
<p v-text="error.message" />
</div>
</div>
<health-details name="Instance" :health="health" />
</div>
</sba-panel>
</template>
<script>
import Instance from '@/services/instance';
import healthDetails from './health-details';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
// eslint-disable-next-line vue/no-unused-components
components: {healthDetails},
data: () => ({
error: null,
liveHealth: null,
}),
created() {
this.fetchHealth();
},
computed: {
health() {
return this.liveHealth || this.instance.statusInfo;
}
},
methods: {
async fetchHealth() {
this.error = null;
try {
const res = await this.instance.fetchHealth();
this.liveHealth = res.data;
} catch (error) {
console.warn('Fetching live health failed:', error);
this.error = error;
}
}
}
}
</script>
@@ -1,95 +0,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.
-->
<template>
<sba-panel title="Info">
<div>
<div v-if="error" class="message is-warning">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-warning" icon="exclamation-triangle" />
Fetching live info failed. This is the last known information.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="content info">
<table class="table" v-if="!isEmptyInfo">
<tr v-for="(value, key) in info" :key="key">
<td class="info__key" v-text="key" />
<td>
<sba-formatted-obj :value="value" />
</td>
</tr>
</table>
<p v-else class="is-muted">No info provided.</p>
</div>
</div>
</sba-panel>
</template>
<script>
import Instance from '@/services/instance';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
error: null,
liveInfo: null
}),
computed: {
info() {
return this.liveInfo || this.instance.info
},
isEmptyInfo() {
return Object.keys(this.info).length <= 0;
}
},
created() {
this.fetchInfo();
},
methods: {
async fetchInfo() {
if (this.instance.hasEndpoint('info')) {
try {
this.error = null;
const res = await this.instance.fetchInfo();
this.liveInfo = res.data;
} catch (error) {
this.error = error;
console.warn('Fetching info failed:', error);
}
}
}
}
}
</script>
<style lang="scss">
.info {
overflow: auto;
&__key {
vertical-align: top;
}
}
</style>
@@ -1,143 +0,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.
-->
<template>
<sba-panel :title="`Memory: ${name}`" v-if="hasLoaded">
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching memory metrics failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level memory-current" v-if="current">
<div class="level-item has-text-centered" v-if="current.metaspace">
<div>
<p class="heading has-bullet has-bullet-primary">Metaspace</p>
<p v-text="prettyBytes(current.metaspace)" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-info">Used</p>
<p v-text="prettyBytes(current.used)" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-warning">Size</p>
<p v-text="prettyBytes(current.committed)" />
</div>
</div>
<div class="level-item has-text-centered" v-if="current.max >= 0">
<div>
<p class="heading">Max</p>
<p v-text="prettyBytes(current.max)" />
</div>
</div>
</div>
<mem-chart v-if="chartData.length > 0" :data="chartData" />
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import moment from 'moment';
import prettyBytes from 'pretty-bytes';
import memChart from './mem-chart';
export default {
props: {
instance: {
type: Instance,
required: true
},
type: {
type: String,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {memChart},
data: () => ({
hasLoaded: false,
error: null,
current: null,
chartData: [],
}),
computed: {
name() {
switch (this.type) {
case 'heap':
return 'Heap';
case 'nonheap':
return 'Non heap';
default:
return this.type;
}
}
},
methods: {
prettyBytes,
async fetchMetrics() {
const responseMax = this.instance.fetchMetric('jvm.memory.max', {area: this.type});
const responseUsed = this.instance.fetchMetric('jvm.memory.used', {area: this.type});
const responeMetaspace = this.type === 'nonheap'
? this.instance.fetchMetric('jvm.memory.used', {area: this.type, id: 'Metaspace'})
: null;
const responseCommitted = this.instance.fetchMetric('jvm.memory.committed', {area: this.type});
return {
max: (await responseMax).data.measurements[0].value,
used: (await responseUsed).data.measurements[0].value,
metaspace: responeMetaspace ? (await responeMetaspace).data.measurements[0].value : null,
committed: (await responseCommitted).data.measurements[0].value
};
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(this.fetchMetrics))
.subscribe({
next: data => {
vm.hasLoaded = true;
vm.current = data;
vm.chartData.push({...data, timestamp: moment().valueOf()});
},
error: error => {
vm.hasLoaded = true;
console.warn('Fetching memory metrics failed:', error);
vm.error = error;
}
});
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.memory-current {
margin-bottom: 0 !important;
}
</style>
@@ -1,62 +0,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.
-->
<template>
<sba-panel title="Metadata">
<div class="content metadata" v-if="metadata">
<table class="table" v-if="!isEmptyMetadata">
<tr v-for="(value, key) in metadata" :key="key">
<td class="metadata__key" v-text="key" />
<td>
<sba-formatted-obj :value="value" />
</td>
</tr>
</table>
<p v-else class="is-muted">No metadata provided.</p>
</div>
</sba-panel>
</template>
<script>
import Instance from '@/services/instance';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
computed: {
metadata() {
return this.instance.registration.metadata;
},
isEmptyMetadata() {
return Object.keys(this.metadata).length <= 0;
}
}
}
</script>
<style lang="scss">
.metadata {
overflow: auto;
&__key {
vertical-align: top;
}
}
</style>
@@ -1,171 +0,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.
-->
<template>
<sba-panel title="Process" v-if="hasLoaded">
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching process metrics failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level">
<div class="level-item has-text-centered" v-if="pid">
<div>
<p class="heading">PID</p>
<p v-text="pid" />
</div>
</div>
<div class="level-item has-text-centered" v-if="uptime">
<div>
<p class="heading">Uptime</p>
<p>
<process-uptime :value="toMillis(uptime.value, uptime.baseUnit)" />
</p>
</div>
</div>
<div class="level-item has-text-centered" v-if="processCpuLoad">
<div>
<p class="heading">Process CPU Usage</p>
<p v-text="processCpuLoad.toFixed(2)" />
</div>
</div>
<div class="level-item has-text-centered" v-if="systemCpuLoad">
<div>
<p class="heading">System CPU Usage</p>
<p v-text="systemCpuLoad.toFixed(2)" />
</div>
</div>
<div class="level-item has-text-centered" v-if="systemCpuCount">
<div>
<p class="heading">CPUs</p>
<p v-text="systemCpuCount" />
</div>
</div>
</div>
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import {toMillis} from '../metrics/metric';
import processUptime from './process-uptime';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {processUptime},
data: () => ({
hasLoaded: false,
error: null,
pid: null,
uptime: {value: null, baseUnit: null},
systemCpuLoad: null,
processCpuLoad: null,
systemCpuCount: null
}),
created() {
this.fetchPid();
this.fetchUptime();
this.fetchCpuCount();
},
methods: {
toMillis,
async fetchUptime() {
try {
const response = await this.fetchMetric('process.uptime');
this.uptime = {value: response.measurements[0].value, baseUnit: response.baseUnit};
} catch (error) {
this.error = error;
console.warn('Fetching Uptime failed:', error);
}
this.hasLoaded = true;
},
async fetchPid() {
if (this.instance.hasEndpoint('env')) {
try {
const response = await this.instance.fetchEnv('PID');
this.pid = response.data.property.value;
} catch (error) {
console.warn('Fetching PID failed:', error);
}
this.hasLoaded = true;
}
},
async fetchCpuCount() {
try {
this.systemCpuCount = (await this.fetchMetric('system.cpu.count')).measurements[0].value;
} catch (error) {
console.warn('Fetching Cpu Count failed:', error);
}
this.hasLoaded = true;
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(this.fetchCpuLoadMetrics))
.subscribe({
next: data => {
vm.processCpuLoad = data.processCpuLoad;
vm.systemCpuLoad = data.systemCpuLoad;
},
error: error => {
vm.hasLoaded = true;
console.warn('Fetching CPU Usage metrics failed:', error);
vm.error = error;
}
});
},
async fetchCpuLoadMetrics() {
const fetchProcessCpuLoad = this.fetchMetric('process.cpu.usage');
const fetchSystemCpuLoad = this.fetchMetric('system.cpu.usage');
let processCpuLoad;
let systemCpuLoad;
try {
processCpuLoad = (await fetchProcessCpuLoad).measurements[0].value
} catch (error) {
console.warn('Fetching Process CPU Load failed:', error);
}
try {
systemCpuLoad = (await fetchSystemCpuLoad).measurements[0].value
} catch (error) {
console.warn('Fetching Sytem CPU Load failed:', error);
}
return {
processCpuLoad,
systemCpuLoad
};
},
async fetchMetric(name) {
const response = await this.instance.fetchMetric(name);
return response.data;
}
}
}
</script>
@@ -1,114 +0,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.
-->
<template>
<sba-panel title="Threads" v-if="hasLoaded">
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching threads metrics failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="level threads-current" v-if="current">
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-warning">Live</p>
<p v-text="current.live" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading has-bullet has-bullet-info">Daemon</p>
<p v-text="current.daemon" />
</div>
</div>
<div class="level-item has-text-centered">
<div>
<p class="heading">Peak Live</p>
<p v-text="current.peak" />
</div>
</div>
</div>
<threads-chart v-if="chartData.length > 0" :data="chartData" />
</div>
</sba-panel>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import moment from 'moment';
import threadsChart from './threads-chart';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {threadsChart},
data: () => ({
hasLoaded: false,
error: null,
current: null,
chartData: [],
}),
methods: {
async fetchMetrics() {
const responseLive = this.instance.fetchMetric('jvm.threads.live');
const responsePeak = this.instance.fetchMetric('jvm.threads.peak');
const responseDaemon = this.instance.fetchMetric('jvm.threads.daemon');
return {
live: (await responseLive).data.measurements[0].value,
peak: (await responsePeak).data.measurements[0].value,
daemon: (await responseDaemon).data.measurements[0].value
};
},
createSubscription() {
const vm = this;
return timer(0, 2500)
.pipe(concatMap(this.fetchMetrics))
.subscribe({
next: data => {
vm.hasLoaded = true;
vm.current = data;
vm.chartData.push({...data, timestamp: moment().valueOf()});
},
error: error => {
vm.hasLoaded = true;
console.warn('Fetching threads metrics failed:', error);
vm.error = error;
}
});
}
}
}
</script>
<style lang="scss">
.threads-current {
margin-bottom: 0 !important;
}
</style>
@@ -1,127 +0,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.
-->
<template>
<table class="health-details table is-fullwidth">
<tr>
<th colspan="2">
<span v-text="name" />
<span class="health-details__status" :class="`health-details__status--${health.status}`"
v-text="health.status"
/>
</th>
</tr>
<tr v-if="details && details.length > 0">
<td class="health-details__nested" colspan="2">
<table class="health-details table is-fullwidth">
<tr class="health-details__detail" v-for="detail in details" :key="detail.name">
<td v-text="detail.name" />
<td v-if="name === 'diskSpace'" v-text="prettyBytes(detail.value)" />
<td class="is-breakable" v-else v-text="detail.value" />
</tr>
</table>
</td>
</tr>
<tr v-for="child in childHealth" :key="child.name">
<td class="health-details__nested" colspan="2">
<health-details :name="child.name" :health="child.value" />
</td>
</tr>
</table>
</template>
<script>
import prettyBytes from 'pretty-bytes';
const isChildHealth = (value) => {
return value !== null && typeof value === 'object' && value.hasOwnProperty('status');
};
export default {
name: 'HealthDetails',
props: {
name: {
type: String,
required: true
},
health: {
type: Object,
required: true
}
},
methods: {
prettyBytes
},
computed: {
details() {
if (this.health.details) {
return Object.entries(this.health.details)
.filter(([, value]) => !isChildHealth(value))
.map(([name, value]) => ({name, value}));
}
return [];
},
childHealth() {
if (this.health.details) {
return Object.entries(this.health.details)
.filter(([, value]) => isChildHealth(value))
.map(([name, value]) => ({name, value}));
}
return [];
}
},
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
td.health-details__nested {
padding: 0 0 0 0.75em;
}
td.health-details__nested {
border-bottom: 0;
}
.health-details__nested .table {
margin-bottom: 0.5em;
}
.health-details__status {
float: right;
&--UP {
color: $success;
}
&--RESTRICTED {
color: $warning;
}
&--OUT_OF_SERVICE,
&--DOWN {
color: $danger;
}
&--UNKNOWN,
&--OFFLINE {
color: $grey;
}
}
</style>
@@ -1,198 +0,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.
-->
<template>
<section class="section">
<div class="details-header">
<h1 class="title" v-if="instance" v-text="instance.registration.name" />
<h2 class="subtitle is-5" v-if="instance">Id: <span v-text="instance.id" /></h2>
<sba-tags v-if="instance" :tags="instance.tags" />
<div class="details-header__urls">
<a class="button is-small " :href="instance.registration.serviceUrl">
<font-awesome-icon icon="home" />&nbsp;
<span v-text="instance.registration.serviceUrl" />
</a>
<a class="button is-small " :href="instance.registration.managementUrl">
<font-awesome-icon icon="wrench" />&nbsp;
<span v-text="instance.registration.managementUrl" />
</a>
<a class="button is-small " :href="instance.registration.healthUrl">
<font-awesome-icon icon="heartbeat" />&nbsp;
<span v-text="instance.registration.healthUrl" />
</a>
</div>
</div>
<hr>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching metrics index failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="columns is-desktop">
<div class="column is-half-desktop">
<details-info v-if="hasInfo" :instance="instance" />
<details-metadata v-if="hasMetadata" :instance="instance" />
</div>
<div class="column is-half-desktop">
<details-health :instance="instance" />
</div>
</div>
<div class="columns is-desktop">
<div class="column is-half-desktop">
<details-process v-if="hasProcess" :instance="instance" />
<details-gc v-if="hasGc" :instance="instance" />
</div>
<div class="column is-half-desktop">
<details-threads v-if="hasThreads" :instance="instance" />
</div>
</div>
<div class="columns is-desktop">
<div class="column is-half-desktop">
<details-memory v-if="hasMemory" :instance="instance" type="heap" />
</div>
<div class="column is-half-desktop">
<details-memory v-if="hasMemory" :instance="instance" type="nonheap" />
</div>
</div>
<div class="columns is-desktop">
<div class="column is-half-desktop">
<details-datasources v-if="hasDatasources" :instance="instance" />
</div>
<div class="column is-half-desktop">
<details-caches v-if="hasCaches" :instance="instance" />
</div>
</div>
</section>
</template>
<script>
import Instance from '@/services/instance';
import detailsCaches from './details-caches';
import detailsDatasources from './details-datasources';
import detailsGc from './details-gc';
import detailsHealth from './details-health';
import detailsInfo from './details-info';
import detailsMemory from './details-memory';
import detailsMetadata from './details-metadata';
import detailsProcess from './details-process';
import detailsThreads from './details-threads';
export default {
/* eslint-disable vue/no-unused-components */
components: {
detailsHealth,
detailsInfo,
detailsProcess,
detailsThreads,
detailsDatasources,
detailsMemory,
detailsGc,
detailsCaches,
detailsMetadata
},
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
hasLoaded: false,
error: null,
metrics: []
}),
computed: {
hasCaches() {
return this.hasMetric('cache.gets');
},
hasDatasources() {
return this.hasMetric('data.source.active.connections');
},
hasGc() {
return this.hasMetric('jvm.gc.pause');
},
hasInfo() {
return this.instance.hasEndpoint('info');
},
hasMemory() {
return this.hasMetric('jvm.memory.max');
},
hasProcess() {
return this.hasMetric('process.uptime');
},
hasThreads() {
return this.hasMetric('jvm.threads.live');
},
hasMetadata() {
return this.instance.registration && this.instance.registration.metadata;
}
},
created() {
this.fetchMetricIndex();
},
methods: {
hasMetric(metric) {
return this.metrics && this.metrics.includes(metric);
},
async fetchMetricIndex() {
if (this.instance.hasEndpoint('metrics')) {
this.error = null;
try {
const res = await this.instance.fetchMetrics();
this.metrics = res.data.names;
} catch (error) {
console.warn('Fetching metric index failed:', error);
this.error = error;
}
this.hasLoaded = true;
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/details',
parent: 'instances',
path: 'details',
component: this,
label: 'Details',
group: 'Insights',
order: 0
});
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.details-header {
margin-bottom: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
&__urls {
width: 100%;
text-align: center;
}
}
</style>
@@ -1,170 +0,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.
-->
<template>
<div class="mem-chart">
<svg class="mem-chart__svg" />
</div>
</template>
<script>
import d3 from '@/utils/d3';
import moment from 'moment';
import prettyBytes from 'pretty-bytes';
export default {
props: {
data: {
type: Array,
default: () => []
}
},
data: () => ({}),
methods: {
drawChart(_data) {
const vm = this;
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
///setup x and y scale
const extent = d3.extent(data, d => d.timestamp);
const x = d3.scaleTime()
.range([0, vm.width])
.domain(extent);
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.committed) * 1.05]);
//draw max
const max = vm.areas.selectAll('.mem-chart__line--max')
.data([data]);
max.enter().append('path')
.merge(max)
.attr('class', 'mem-chart__line--max')
.attr('d', d3.line()
.x(d => x(d.timestamp))
.y(d => y(d.max)));
max.exit().remove();
//draw areas
const committed = vm.areas.selectAll('.mem-chart__area--committed')
.data([data]);
committed.enter().append('path')
.merge(committed)
.attr('class', 'mem-chart__area--committed')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(d => y(d.used))
.y1(d => y(d.committed)));
committed.exit().remove();
const used = vm.areas.selectAll('.mem-chart__area--used')
.data([data]);
used.enter().append('path')
.merge(used)
.attr('class', 'mem-chart__area--used')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(d => y(d.metaspace || 0))
.y1(d => y(d.used)));
used.exit().remove();
const metaspace = vm.areas.selectAll('.mem-chart__area--metaspace')
.data([data]);
metaspace.enter().append('path')
.merge(metaspace)
.attr('class', 'mem-chart__area--metaspace')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(y(0))
.y1(d => y(d.metaspace)));
metaspace.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(5)
.tickFormat(d => moment(d).format('HH:mm:ss'))
);
vm.yAxis.call(d3.axisLeft(y)
.ticks(5)
.tickFormat(prettyBytes)
);
},
},
mounted() {
const margin = {
top: 5,
right: 5,
bottom: 30,
left: 50,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.mem-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'mem-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'mem-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.drawChart(this.data);
},
watch: {
data: 'drawChart'
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.mem-chart {
&__svg {
height: 159px;
width: 100%;
}
&__area {
&--committed {
fill: $warning;
opacity: 0.8;
}
&--used {
fill: $info;
opacity: 0.8;
}
&--metaspace {
fill: $primary;
opacity: 0.8;
}
}
&__line--max {
stroke: $grey;
}
}
</style>
@@ -1,57 +0,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.
*/
import subscribing from '@/mixins/subscribing';
import {timer} from '@/utils/rxjs';
import moment from 'moment';
export default {
props: ['value'],
mixins: [subscribing],
data: () => ({
startTs: null,
offset: null
}),
render() {
return this._v(this.clock);
},
computed: {
clock() {
if (!this.value) {
return null;
}
const duration = moment.duration(this.value + this.offset);
return `${Math.floor(duration.asDays())}d ${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;
}
},
watch: {
value: 'subscribe'
},
methods: {
createSubscription() {
if (this.value) {
const vm = this;
vm.startTs = moment();
vm.offset = 0;
return timer(0, 1000).subscribe({
next: () => {
vm.offset = moment().valueOf() - vm.startTs.valueOf();
}
})
}
}
}
}
@@ -1,138 +0,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.
-->
<template>
<div class="threads-chart">
<svg class="threads-chart__svg" />
</div>
</template>
<script>
import d3 from '@/utils/d3';
import moment from 'moment';
export default {
props: {
data: {
type: Array,
default: () => []
}
},
methods: {
drawChart(_data) {
const vm = this;
const data = _data.length === 1 ? _data.concat([{..._data[0], timestamp: _data[0].timestamp + 1}]) : _data;
///setup x and y scale
const extent = d3.extent(data, d => d.timestamp);
const x = d3.scaleTime()
.range([0, vm.width])
.domain(extent);
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.live) * 1.05]);
//draw max
const live = vm.areas.selectAll('.threads-chart__area--live')
.data([data]);
live.enter().append('path')
.merge(live)
.attr('class', 'threads-chart__area--live')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(d => y(d.daemon))
.y1(d => y(d.live)));
live.exit().remove();
//draw areas
const daemon = vm.areas.selectAll('.threads-chart__area--daemon')
.data([data]);
daemon.enter().append('path')
.merge(daemon)
.attr('class', 'threads-chart__area--daemon')
.attr('d', d3.area()
.x(d => x(d.timestamp))
.y0(y(0))
.y1(d => y(d.daemon)));
daemon.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(5)
.tickFormat(d => moment(d).format('HH:mm:ss'))
);
vm.yAxis.call(d3.axisLeft(y)
.ticks(5)
);
},
},
mounted() {
const margin = {
top: 5,
right: 5,
bottom: 30,
left: 50,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.threads-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'threads-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'threads-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.drawChart(this.data);
},
watch: {
data: 'drawChart'
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.threads-chart {
&__svg {
height: 159px;
width: 100%;
}
&__area {
&--live {
fill: $warning;
opacity: 0.8;
}
&--daemon {
fill: $info;
opacity: 0.8;
}
}
}
</style>
@@ -1,260 +0,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.
-->
<template>
<div class="box">
<h1 class="is-size-5">Environment Manager</h1>
<datalist id="allPropertyNames">
<option v-for="name in allPropertyNames" :key="name" v-text="name" />
</datalist>
<div class="field is-horizontal" v-for="(prop, index) in managedProperties" :key="`managed-${index}`">
<div class="field-body">
<div class="field">
<div class="control">
<input class="input" type="text" placeholder="Property name" list="allPropertyNames"
v-model="prop.name" @input="handlePropertyNameChange(prop, index)"
>
</div>
<p class="help is-danger" v-text="prop.validation" />
</div>
<div class="field">
<div class="control has-icons-right" :class="{'is-loading' : prop.status === 'executing'}">
<input class="input" type="text" placeholder="Value" v-model="prop.input"
@input="prop.status = null"
>
<span class="icon is-right has-text-success" v-if="prop.status === 'completed'">
<font-awesome-icon icon="check" />
</span>
<span class="icon is-right has-text-warning" v-else-if="prop.status === 'failed'">
<font-awesome-icon icon="exclamation-triangle" />
</span>
<span class="icon is-right" v-else-if="prop.input !== prop.value">
<font-awesome-icon icon="pencil-alt" />
</span>
</div>
</div>
</div>
</div>
<div class="field is-horizontal">
<div class="field-body" v-if="instance.hasEndpoint('refresh')">
<div class="field">
<div class="control">
<sba-confirm-button class="button is-light"
:class="{'is-loading' : refreshStatus === 'executing', 'is-danger' : refreshStatus === 'failed', 'is-info' : refreshStatus === 'completed'}"
:disabled="refreshStatus === 'executing'"
@click="refreshContext"
>
<span v-if="refreshStatus === 'completed'">Context refreshed</span>
<span v-else-if="refreshStatus === 'failed'">Failed</span>
<span v-else>Refresh Context</span>
</sba-confirm-button>
</div>
</div>
</div>
<div class="field-body">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-light"
:class="{'is-loading' : resetStatus === 'executing', 'is-danger' : resetStatus === 'failed', 'is-success' : resetStatus === 'completed'}"
:disabled="!hasManagedProperty || resetStatus === 'executing'"
@click="resetEnvironment"
>
<span v-if="resetStatus === 'completed'">Resetted</span>
<span v-else-if="resetStatus === 'failed'">Failed</span>
<span v-else>Reset</span>
</button>
</div>
<div class="control">
<button class="button is-primary"
:class="{'is-loading' : updateStatus === 'executing', 'is-danger' : updateStatus === 'failed', 'is-success' : updateStatus === 'completed'}"
:disabled="hasErrorProperty || !hasChangedProperty || updateStatus === 'executing'"
@click="updateEnvironment"
>
<span v-if="updateStatus === 'completed'">Updated</span>
<span v-else-if="updateStatus === 'failed'">Failed</span>
<span v-else>Update</span>
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import Instance from '@/services/instance';
import {concatMap, filter, from, listen} from '@/utils/rxjs';
import debounce from 'lodash/debounce';
import uniq from 'lodash/uniq';
export default {
props: {
instance: {
type: Instance,
required: true
},
propertySources: {
type: Array,
default: () => []
}
},
data: () => ({
error: null,
refreshStatus: null,
resetStatus: null,
updateStatus: null,
managedProperties: [{
name: null,
input: null,
value: null,
status: null,
validation: null
}]
}),
computed: {
allPropertyNames() {
return uniq(this.propertySources.map(ps => Object.keys(ps.properties))
.reduce((result, names) => result.concat(names))
.sort());
},
managerPropertySource() {
return this.propertySources.find(ps => ps.name === 'manager') || {name: 'manager', properties: {}};
},
hasManagedProperty() {
return this.managedProperties.findIndex(property => !!property.name) >= 0;
},
hasChangedProperty() {
return this.managedProperties.findIndex(property => property.input !== property.value) >= 0;
},
hasErrorProperty() {
return this.managedProperties.findIndex(property => property.validation !== null) >= 0;
}
},
methods: {
handlePropertyNameChange: debounce(function (prop, idx) {
if (prop.name && idx === this.managedProperties.length - 1) {
this.managedProperties.push({
name: null,
input: null,
value: null,
status: null,
validation: null
});
}
}, 250),
refreshContext() {
const vm = this;
from(vm.instance.refreshContext())
.pipe(listen(status => vm.refreshStatus = status))
.subscribe({
complete: () => {
setTimeout(() => vm.refreshStatus = null, 2500);
return vm.$emit('reset');
},
error: () => vm.$emit('reset')
});
},
updateEnvironment() {
const vm = this;
from(vm.managedProperties)
.pipe(
filter(property => !!property.name && property.input !== property.value),
listen(status => vm.updateStatus = status),
concatMap(
property => from(vm.instance.setEnv(property.name, property.input))
.pipe(listen(status => property.status = status))
)
)
.subscribe({
complete: () => {
setTimeout(() => vm.updateStatus = null, 2500);
return vm.$emit('update');
},
error: () => vm.$emit('update')
});
},
resetEnvironment() {
const vm = this;
from(vm.instance.resetEnv())
.pipe(listen(status => vm.resetStatus = status))
.subscribe({
complete: () => {
vm.managedProperties = [{
name: null,
input: null,
value: null,
status: null,
validation: null
}];
setTimeout(() => vm.resetStatus = null, 2500);
return vm.$emit('refresh');
},
error: () => vm.$emit('refresh')
});
},
updateManagedProperties(manager) {
Object.entries(manager.properties).forEach(([name, property]) => {
const managedProperty = this.managedProperties.find(property => property.name === name);
if (managedProperty) {
managedProperty.value = property.value
} else {
const idx = this.managedProperties.length - 1;
this.managedProperties.splice(idx, 0, {
name,
input: property.value,
value: property.value,
status: null,
validation: null
})
}
});
}
},
watch: {
managerPropertySource: {
handler: 'updateManagedProperties',
immediate: true
},
managedProperties: {
deep: true,
handler() {
const counts = this.managedProperties.reduce(
(acc, v) => {
if (v.name) {
acc[v.name] = (acc[v.name] || 0) + 1;
}
return acc;
}, {});
this.managedProperties.forEach(property => {
if (!property.name) {
if (property.input) {
property.validation = 'Property name is required';
}
return;
}
const count = counts[property.name] || 0;
if (count > 1) {
property.validation = 'Property name must be unique';
return;
}
property.validation = null;
});
}
}
}
}
</script>
@@ -1,151 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching environment failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="field is-grouped is-grouped-multiline" v-if="env && env.activeProfiles.length > 0">
<div class="control" v-for="profile in env.activeProfiles" :key="profile">
<div class="tags has-addons">
<span class="tag is-medium is-primary">Profile</span>
<span class="tag is-medium" v-text="profile" />
</div>
</div>
</div>
<sba-env-manager v-if="env && hasEnvManagerSupport"
:instance="instance" :property-sources="env.propertySources"
@refresh="fetchEnv()" @update="fetchEnv" @reset="fetchEnv()"
/>
<div class="field has-addons" v-if="env">
<p class="control is-expanded">
<input class="input" type="search" placeholder="name / value filter" v-model="filter">
</p>
</div>
<sba-panel :header-sticks-below="['#navigation']"
v-for="propertySource in propertySources" :key="propertySource.name"
:title="propertySource.name"
>
<table class="table is-fullwidth"
v-if="Object.keys(propertySource.properties).length > 0"
>
<tr v-for="(value, name) in propertySource.properties" :key="`${propertySource.name}-${name}`">
<td>
<span v-text="name" /><br>
<small class="is-muted" v-if="value.origin" v-text="value.origin" />
</td>
<td class="is-breakable" v-text="value.value" />
</tr>
</table>
<p class="is-muted" v-else>No properties set</p>
</sba-panel>
</section>
</template>
<script>
import Instance from '@/services/instance';
import pickBy from 'lodash/pickBy';
import sbaEnvManager from './env-manager';
const filterProperty = (needle) => (property, name) => {
return name.toString().toLowerCase().includes(needle) || property.value.toString().toLowerCase().includes(needle);
};
const filterProperties = (needle, properties) => pickBy(properties, filterProperty(needle));
const filterPropertySource = (needle) => (propertySource) => {
if (!propertySource || !propertySource.properties) {
return null;
}
return {
...propertySource,
properties: filterProperties(needle, propertySource.properties)
};
};
export default {
props: {
instance: {
type: Instance,
required: true
}
},
// eslint-disable-next-line vue/no-unused-components
components: {sbaEnvManager},
data: () => ({
hasLoaded: false,
error: null,
env: null,
filter: null,
hasEnvManagerSupport: false
}),
computed: {
propertySources() {
if (!this.env) {
return [];
}
if (!this.filter) {
return this.env.propertySources;
}
return this.env.propertySources
.map(filterPropertySource(this.filter.toLowerCase()))
.filter(ps => ps && Object.keys(ps.properties).length > 0);
}
},
created() {
this.fetchEnv();
this.determineEnvManagerSupport();
},
methods: {
async fetchEnv() {
this.error = null;
try {
const res = await this.instance.fetchEnv();
this.env = res.data;
} catch (error) {
console.warn('Fetching environment failed:', error);
this.error = error;
}
this.hasLoaded = true;
},
async determineEnvManagerSupport() {
try {
this.hasEnvManagerSupport = await this.instance.hasEnvManagerSupport();
} catch (error) {
console.warn('Determine env manager support failed:', error);
this.hasEnvManagerSupport = false;
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/env',
parent: 'instances',
path: 'env',
component: this,
label: 'Environment',
group: 'Insights',
order: 100,
isEnabled: ({instance}) => instance.hasEndpoint('env')
});
}
}
</script>
@@ -1,140 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
<template v-if="hasLoaded">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching Flyway reports failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<template v-for="(context, ctxName) in contexts">
<h3 class="title" v-text="ctxName" :key="ctxName" />
<sba-panel v-for="(report, name) in context.flywayBeans" :key="`${ctxName}-${name}`" :title="name"
:header-sticks-below="['#navigation']"
class="migration"
>
<table class="table">
<thead>
<tr>
<th>Type</th>
<th>Checksum</th>
<th>Version</th>
<th>Description</th>
<th>Script</th>
<th>State</th>
<th>Installed by</th>
<th>Installed on</th>
<th>Installed rank</th>
<th>Execution Time</th>
</tr>
</thead>
<tbody>
<tr v-for="migration in report.migrations" :key="migration.checksum">
<td v-text="migration.type" />
<td v-text="migration.checksum" />
<td v-text="migration.version" />
<td v-text="migration.description" />
<td v-text="migration.script" />
<td><span v-text="migration.state" class="tag"
:class="stateClass(migration.state)"
/></td>
<td v-text="migration.installedBy" />
<td v-text="migration.installedOn" />
<td v-text="migration.installedRank" />
<td v-text="`${migration.executionTime}ms`" />
</tr>
</tbody>
</table>
</sba-panel>
</template>
</template>
</section>
</template>
<script>
import Instance from '@/services/instance';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
hasLoaded: false,
error: null,
contexts: null
}),
computed: {},
created() {
this.fetchFlyway();
},
methods: {
async fetchFlyway() {
this.error = null;
try {
const res = await this.instance.fetchFlyway();
this.contexts = res.data.contexts;
} catch (error) {
console.warn('Fetching flyway reports failed:', error);
this.error = error;
}
this.hasLoaded = true;
},
stateClass(state) {
switch (state) {
case 'BASELINE' :
case 'MISSING_SUCCESS' :
case 'SUCCESS' :
case 'OUT_OF_ORDER' :
case 'FUTURE_SUCCESS' :
return 'is-success';
case 'PENDING':
case 'ABOVE_TARGET':
case 'PREINIT':
case 'BELOW_BASELINE':
case 'IGNORED':
return 'is-warning';
case 'MISSING_FAILED':
case 'FAILED':
case 'FUTURE_FAILED':
return 'is-danger';
default:
return 'is-light';
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/flyway',
parent: 'instances',
path: 'flyway',
component: this,
label: 'Flyway',
group: 'Data',
order: 900,
isEnabled: ({instance}) => instance.hasEndpoint('flyway')
});
}
}
</script>
@@ -1,72 +0,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.
-->
<template>
<section class="section heapdump">
<div>
<div class="message is-warning">
<div class="message-body">
A heap dump may contain <strong>sensitive data</strong>. Please handle with care.
</div>
</div>
<div class="message is-warning">
<div class="message-body">
Dumping the heap may be expensive in terms of cpu and disk space.
</div>
</div>
<a class="button is-primary" :href="`instances/${instance.id}/actuator/heapdump`" target="_blank">
<font-awesome-icon icon="download" />&nbsp;Download Heap Dump
</a>
</div>
</section>
</template>
<script>
import Instance from '@/services/instance';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/heapdump',
parent: 'instances',
path: 'heapdump',
component: this,
label: 'Heap Dump',
group: 'JVM',
order: 800,
isEnabled: ({instance}) => instance.hasEndpoint('heapdump')
});
}
}
</script>
<style lang="scss">
.heapdump {
display: flex;
justify-content: space-around;
& > div {
display: flex;
flex-direction: column;
}
}
</style>
@@ -1,249 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded}">
<template v-if="hasLoaded">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching traces failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<template v-if="traces">
<div class="field-body">
<div class="field has-addons">
<p class="control is-expanded">
<input class="input" type="search" placeholder="path filter" v-model="filter">
</p>
<p class="control">
<span class="button is-static">
<span v-text="filteredTraces.length" />
/
<span v-text="traces.length" />
</span>
</p>
</div>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="showSuccess">
success
</label>
</div>
</div>
<div class="field is-narrow">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="showClientErrors">
client errors
</label>
</div>
</div>
<div class="field is-narrow">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="showServerErrors">
server errors
</label>
</div>
</div>
<div class="field is-narrow" v-if="actuatorPath">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="excludeActuator">
exclude <span v-text="actuatorPath" />/**
</label>
</div>
</div>
</div>
<sba-traces-chart :traces="filteredTraces" @selected="(d) => selection = d" />
<sba-traces-list :traces="selectedTraces" />
</template>
</template>
</section>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import {concatMap, timer} from '@/utils/rxjs';
import moment from 'moment';
import sbaTracesChart from './traces-chart';
import sbaTracesList from './traces-list';
const addToFilter = (oldFilter, addedFilter) =>
!oldFilter
? addedFilter
: (val, key) => oldFilter(val, key) && addedFilter(val, key);
class Trace {
constructor({timestamp, ...trace}) {
Object.assign(this, trace);
this.timestamp = moment(timestamp);
}
get key() {
return `${this.timestamp}-${this.request.method}-${this.request.uri}`;
}
get contentLength() {
const contentLength = this.response.headers['Content-Length'] && this.response.headers['Content-Length'][0];
if (contentLength && /^\d+$/.test(contentLength)) {
return parseInt(contentLength);
}
return null;
}
get contentType() {
const contentType = this.response.headers['Content-Type'] && this.response.headers['Content-Type'][0];
if (contentType) {
const idx = contentType.indexOf(';');
return idx >= 0 ? contentType.substring(0, idx) : contentType;
}
return null;
}
compareTo(other) {
return this.timestamp - other.timestamp;
}
isSuccess() {
return this.response.status <= 399
}
isClientError() {
return this.response.status >= 400 && this.response.status <= 499
}
isServerError() {
return this.response.status >= 500 && this.response.status <= 599
}
}
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
// eslint-disable-next-line vue/no-unused-components
components: {sbaTracesList, sbaTracesChart},
data: () => ({
hasLoaded: false,
error: null,
traces: null,
filter: null,
excludeActuator: true,
showSuccess: true,
showClientErrors: true,
showServerErrors: true,
selection: null
}),
computed: {
actuatorPath() {
if (this.instance.registration.managementUrl.includes(this.instance.registration.serviceUrl)) {
const appendix = this.instance.registration.managementUrl.substring(this.instance.registration.serviceUrl.length);
if (appendix.length > 0) {
return appendix.startsWith('/') ? appendix : `/${appendix}`;
}
}
return null;
},
filteredTraces() {
const filterFn = this.getFilterFn();
return filterFn ? this.traces.filter(filterFn) : this.traces;
},
selectedTraces() {
if (this.selection === null) {
return this.filteredTraces;
}
const [start, end] = this.selection;
return this.filteredTraces.filter(trace => !trace.timestamp.isBefore(start) && !trace.timestamp.isAfter(end));
}
},
methods: {
async fetchHttptrace() {
const response = await this.instance.fetchHttptrace();
const traces = response.data.traces.map(trace => new Trace(trace)).filter(
trace => trace.timestamp.isAfter(this.lastTimestamp)
);
traces.sort((a, b) => -1 * a.compareTo(b));
if (traces.length > 0) {
this.lastTimestamp = traces[0].timestamp;
}
return traces;
},
createSubscription() {
const vm = this;
vm.lastTimestamp = moment(0);
vm.error = null;
return timer(0, 5000)
.pipe(concatMap(vm.fetchHttptrace))
.subscribe({
next: traces => {
vm.hasLoaded = true;
vm.traces = vm.traces ? traces.concat(vm.traces) : traces;
},
error: error => {
vm.hasLoaded = true;
console.warn('Fetching traces failed:', error);
vm.error = error;
}
});
},
getFilterFn() {
let filterFn = null;
if (this.actuatorPath !== null && this.excludeActuator) {
filterFn = addToFilter(filterFn, (trace) => !trace.request.uri.includes(this.actuatorPath));
}
if (this.filter) {
const normalizedFilter = this.filter.toLowerCase();
filterFn = addToFilter(filterFn, (trace) => trace.request.uri.toLowerCase().includes(normalizedFilter));
}
if (!this.showSuccess) {
filterFn = addToFilter(filterFn, (trace) => !trace.isSuccess());
}
if (!this.showClientErrors) {
filterFn = addToFilter(filterFn, (trace) => !trace.isClientError());
}
if (!this.showServerErrors) {
filterFn = addToFilter(filterFn, (trace) => !trace.isServerError());
}
return filterFn;
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/httptrace',
parent: 'instances',
path: 'httptrace',
component: this,
label: 'Http Traces',
group: 'Web',
order: 500,
isEnabled: ({instance}) => instance.hasEndpoint('httptrace')
});
}
}
</script>
@@ -1,364 +0,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.
-->
<template>
<div class="trace-chart">
<div class="trace-chart__tooltip"
v-if="tooltipSelection "
:class="`trace-chart__tooltip--${this.x(tooltipSelection[0]) > this.width / 2 ? 'left' : 'right'}`"
>
<table class="is-narrow is-size-7">
<tr>
<th>total requests</th>
<td v-text="tooltipContent.totalCount" />
</tr>
<tr>
<th>successful</th>
<td v-text="tooltipContent.totalSuccess" />
</tr>
<tr>
<th>status 4xx</th>
<td v-text="tooltipContent.totalClientErrors" />
</tr>
<tr>
<th>status 5xx</th>
<td v-text="tooltipContent.totalServerErrors" />
</tr>
<tr>
<th>max duration</th>
<td v-text="`${tooltipContent.maxTime}ms`" />
</tr>
<tr>
<th>ø duration</th>
<td v-text="`${tooltipContent.avgTime}ms`" />
</tr>
</table>
</div>
<svg class="trace-chart__svg" />
</div>
</template>
<script>
//see https://github.com/d3/d3/issues/2733#issuecomment-190743489
import d3 from '@/utils/d3';
import {event as d3Event} from 'd3-selection';
import moment from 'moment';
const interval = 1000;
export default {
props: {
traces: {
type: Array,
default: () => []
}
},
data: () => ({
brushSelection: null,
hovered: null
}),
computed: {
chartData() {
if (this.traces.length <= 0) {
return [];
}
const chartData = [];
const now = moment().valueOf();
let idx = this.traces.length - 1;
const oldest = this.traces[this.traces.length - 1].timestamp.valueOf();
for (let time = Math.floor(oldest.valueOf() / interval) * interval; time < now; time += interval) {
const bucket = {
timeStart: time,
timeEnd: time + interval,
totalCount: 0,
totalSuccess: 0,
totalClientErrors: 0,
totalServerErrors: 0,
totalTime: 0,
maxTime: 0
};
while (idx >= 0 && this.traces[idx].timestamp.valueOf() < time + interval) {
const trace = this.traces[idx];
bucket.totalCount++;
if (trace.isSuccess()) {
bucket.totalSuccess++;
} else if (trace.isClientError()) {
bucket.totalClientErrors++;
} else if (trace.isServerError()) {
bucket.totalServerErrors++;
}
if (trace.timeTaken) {
bucket.totalTime += trace.timeTaken;
bucket.maxTime = Math.max(bucket.maxTime, trace.timeTaken);
}
idx--;
}
chartData.push(bucket);
}
return chartData;
},
tooltipSelection() {
return this.brushSelection ? this.brushSelection : this.hovered ? [this.hovered, this.hovered + interval] : null;
},
tooltipContent() {
const selection = this.tooltipSelection;
const totals = this.chartData.filter(
bucket => bucket.timeStart.valueOf() >= selection[0] && bucket.timeStart.valueOf() < selection[1]
).reduce(
(current, next) => ({
totalCount: current.totalCount + next.totalCount,
totalSuccess: current.totalSuccess + next.totalSuccess,
totalClientErrors: current.totalClientErrors + next.totalClientErrors,
totalServerErrors: current.totalServerErrors + next.totalServerErrors,
totalTime: current.totalTime + next.totalTime,
maxTime: Math.max(current.maxTime, next.maxTime)
}), {
totalCount: 0,
totalSuccess: 0,
totalClientErrors: 0,
totalServerErrors: 0,
totalTime: 0,
maxTime: 0
});
return {
...totals,
avgTime: totals.totalCount > 0 ? Math.floor(totals.totalTime / totals.totalCount) : 0
};
}
},
methods: {
drawChart(data) {
const vm = this;
///setup x and y scale
const x = d3.scaleTime()
.range([0, vm.width])
.domain(d3.extent(data, d => d.timeStart));
this.x = x;
const y = d3.scaleLinear()
.range([vm.height, 0])
.domain([0, d3.max(data, d => d.totalCount)]);
//draw areas
const area = d3.area()
.x(d => x(d.data.timeStart))
.y0(d => y(d[0]))
.y1(d => y(d[1]));
const stack = d3.stack()
.keys(['totalSuccess', 'totalClientErrors', 'totalServerErrors']);
const d = vm.areas.selectAll('.trace-chart__area')
.data(stack(data));
d.enter().append('path')
.merge(d)
.attr('class', d => `trace-chart__area trace-chart__area--${d.key}`)
.attr('d', area);
d.exit().remove();
//draw axis
vm.xAxis.call(d3.axisBottom(x)
.ticks(10)
.tickFormat(d => moment(d).format('HH:mm:ss'))
);
vm.yAxis.call(d3.axisRight(y)
.ticks(Math.min(5, d3.max(data, d => d.totalCount)))
.tickSize(this.width)
).call(
axis => axis.selectAll('.tick text')
.attr('x', -2)
.attr('dy', 2)
.attr('text-anchor', 'end')
);
//draw brush selection
const brush = d3.brushX()
.extent([[0, 0], [vm.width, vm.height]])
.on('start', () => {
if (d3Event.selection) {
vm.isBrushing = true;
vm.hovered = null;
}
})
.on('brush', function () {
if (d3Event.sourceEvent === null || d3Event.sourceEvent.type === 'brush') {
return;
}
if (d3Event.selection) {
const floor = Math.floor(x.invert(d3Event.selection[0]) / interval) * interval;
const ceil = Math.ceil(x.invert(d3Event.selection[1]) / interval) * interval;
d3.select(this).call(d3Event.target.move, [floor, ceil].map(x));
vm.brushSelection = [floor, ceil];
}
})
.on('end', () => {
vm.isBrushing = false;
if (!d3Event.selection) {
vm.brushSelection = null;
}
});
vm.brushGroup.call(brush)
.on('mousemove', () => {
if (vm.isBrushing) {
return;
}
const mouseX = d3.mouse(vm.brushGroup.select('.overlay').node())[0];
vm.hovered = Math.floor(x.invert(mouseX) / interval) * interval;
}).on('mouseout', () => {
vm.hovered = null;
});
brush.move(vm.brushGroup, vm.brushSelection ? vm.brushSelection.map(x) : null);
},
},
mounted() {
const margin = {
top: 20,
right: 20,
bottom: 30,
left: 20,
};
this.width = this.$el.getBoundingClientRect().width - margin.left - margin.right;
this.height = this.$el.getBoundingClientRect().height - margin.top - margin.bottom;
this.chartLayer = d3.select(this.$el.querySelector('.trace-chart__svg'))
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);
this.xAxis = this.chartLayer.append('g')
.attr('class', 'trace-chart__axis-x')
.attr('transform', `translate(0,${this.height})`);
this.yAxis = this.chartLayer.append('g')
.attr('class', 'trace-chart__axis-y')
.attr('stroke', null);
this.areas = this.chartLayer.append('g');
this.hover = this.chartLayer.append('path')
.attr('class', 'trace-chart__hover')
.attr('opacity', 0);
this.brushGroup = this.chartLayer.append('g')
.attr('class', 'trace-chart__brush');
this.drawChart(this.chartData);
},
watch: {
chartData: 'drawChart',
hovered(newVal) {
if (newVal) {
this.hover.attr('opacity', 1)
.attr('d', `M${this.x(newVal)},${this.height} ${this.x(newVal)},0`);
} else {
this.hover.attr('opacity', 0);
}
},
brushSelection(newVal) {
this.$emit('selected', newVal);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.trace-chart {
&__svg {
height: 200px;
width: 100%;
}
&__hover {
stroke: $grey-light;
stroke-width: 1px;
}
&__tooltip {
position: absolute;
background: $black;
opacity: 0.8;
pointer-events: none;
border-radius: $radius-large;
padding: 0.825em;
width: 200px;
& table th,
& table td {
border: none;
color: $grey-light;
padding: 0.25em 0.75em;
}
& table td {
text-align: right;
}
&--left {
left: 5px;
}
&--right {
right: 5px;
}
}
& .selection {
stroke: none;
fill: rgba(0, 0, 0, 0.2);
fill-opacity: 1;
}
&__axis-y {
& .domain {
stroke: none;
}
.tick:not(:first-of-type) {
& line {
stroke-dasharray: 2, 2;
stroke: $grey-light;
}
}
}
&__area {
&--totalSuccess {
fill: $success;
opacity: 0.8;
}
&--totalClientErrors {
fill: $warning;
opacity: 0.8;
}
&--totalServerErrors {
fill: $danger;
opacity: 0.8;
}
}
}
</style>
@@ -1,140 +0,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.
-->
<template>
<table class="httptraces table is-hoverable is-fullwidth">
<thead>
<tr>
<th class="httptraces__trace-timestamp">Timestamp</th>
<th class="httptraces__trace-method">Method</th>
<th class="httptraces__trace-uri">Path</th>
<th class="httptraces__trace-status">Status</th>
<th class="httptraces__trace-contentType">Content-Type</th>
<th class="httptraces__trace-contentLength">Length</th>
<th class="httptraces__trace-timeTaken">Time</th>
</tr>
</thead>
<tbody>
<template v-for="trace in traces">
<tr class="is-selectable"
:class="{ 'httptraces__trace---is-detailed' : showDetails[trace.key] }"
@click="showDetails[trace.key] ? $delete(showDetails, trace.key) : $set(showDetails, trace.key, true)"
:key="trace.key"
>
<td class="httptraces__trace-timestamp" v-text="trace.timestamp.format('L HH:mm:ss.SSS')" />
<td class="httptraces__trace-method" v-text="trace.request.method" />
<td class="httptraces__trace-uri" v-text="trace.request.uri" />
<td class="httptraces__trace-status">
<span v-text="trace.response.status" class="tag"
:class="{ 'is-success' : trace.isSuccess(), 'is-warning' : trace.isClientError(), 'is-danger' : trace.isServerError() }"
/>
</td>
<td class="httptraces__trace-contentType" v-text="trace.contentType" />
<td class="httptraces__trace-contentLength"
v-text="trace.contentLength ? prettyBytes(trace.contentLength) : ''"
/>
<td class="httptraces__trace-timeTaken"
v-text="trace.timeTaken !== null && typeof trace.timeTaken !== 'undefined' ? `${trace.timeTaken} ms` : ''"
/>
</tr>
<tr :key="`${trace.key}-detail`" v-if="showDetails[trace.key]">
<td colspan="7">
<pre class="httptraces__trace-detail" v-text="toJson(trace)" />
</td>
</tr>
</template>
<tr v-if="traces.length === 0">
<td class="is-muted" colspan="7">No traces found.</td>
</tr>
</tbody>
</table>
</template>
<script>
import prettyBytes from 'pretty-bytes';
export default {
props: {
traces: {
type: Array,
default: () => []
}
},
data: () => ({
showDetails: {}
}),
methods: {
prettyBytes,
toJson(obj) {
return JSON.stringify(obj, null, 4);
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.httptraces {
table-layout: fixed;
td {
vertical-align: middle;
overflow: hidden;
word-wrap: break-word;
}
&__trace {
&--is-detailed td {
border: none !important;
}
&-timestamp {
width: 130px;
}
&-method {
width: 100px;
}
&-uri {
width: auto;
}
&-status {
width: 80px;
}
&-contentType {
width: 200px;
}
&-contentLength {
width: 100px;
}
&-timeTaken {
width: 120px;
}
&-detail {
overflow: auto;
}
}
}
</style>
@@ -1,294 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching JMX Beans failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="columns">
<div class="column" v-if="selectedDomain">
<h1 class="heading">MBeans</h1>
<div class="m-bean card" :class="{'is-active': mBean === selectedMBean}"
v-for="mBean in selectedDomain.mBeans" :key="mBean.descriptor.raw" :id="mBean.descriptor.raw"
v-on-clickaway="() => mBean === selectedMBean && select(selectedDomain)"
>
<header class="m-bean--header hero"
:class="{'is-primary': mBean === selectedMBean, 'is-selectable' : mBean !== selectedMBean }"
@click="select(selectedDomain, mBean)"
>
<div class="level is-clipped">
<div class="level-left">
<div class="level-item is-narrow"
v-for="attribute in mBean.descriptor.attributes"
:key="`mBean-desc-${attribute.name}`"
>
<div class="is-clipped" :title="`${attribute.name} ${attribute.value}`">
<p class="heading" v-text="attribute.name" />
<p class="title is-size-6" v-text="attribute.value" />
</div>
</div>
</div>
</div>
<sba-icon-button v-if="mBean === selectedMBean" :icon="['far', 'times-circle']"
class="m-bean--header--close has-text-white"
@click.stop="select(selectedDomain)"
/>
<div class="hero-foot tabs is-boxed" v-if="mBean === selectedMBean">
<ul>
<li v-if="mBean.attr" :class="{'is-active' : selected.view === 'attributes' }">
<a @click.stop="select(selectedDomain, selectedMBean, 'attributes')">Attributes</a>
</li>
<li v-if="mBean.op" :class="{'is-active' : selected.view === 'operations' }">
<a @click.stop="select(selectedDomain, selectedMBean, 'operations')">Operations</a>
</li>
</ul>
</div>
</header>
<div class="card-content" v-if="mBean === selectedMBean">
<m-bean-attributes v-if="selected.view === 'attributes'" :instance="instance"
:domain="selectedDomain.domain" :m-bean="mBean"
/>
<m-bean-operations v-if="selected.view === 'operations'" :instance="instance"
:domain="selectedDomain.domain" :m-bean="mBean"
/>
</div>
</div>
</div>
<div class="column is-narrow">
<nav class="menu" v-sticks-below="['#navigation']">
<p class="menu-label">domains</p>
<ul class="menu-list">
<li>
<a class="" v-for="domain in domains" :key="domain.domain"
:class="{'is-active' : domain === selectedDomain}"
v-text="domain.domain" @click="select(domain)"
/>
</li>
</ul>
</nav>
</div>
</div>
</section>
</template>
<script>
import sticksBelow from '@/directives/sticks-below';
import Instance from '@/services/instance';
import fromPairs from 'lodash/fromPairs';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import sortBy from 'lodash/sortBy';
import {directive as onClickaway} from 'vue-clickaway';
import mBeanAttributes from './m-bean-attributes';
import mBeanOperations from './m-bean-operations';
const getOperationName = (name, descriptor) => {
const params = descriptor.args.map(arg => arg.type).join(',');
return `${name}(${params})`;
};
class MBeanDescriptor {
constructor(raw) {
Object.assign(this, MBeanDescriptor.parse(raw));
this.raw = raw;
}
static parse(raw) {
const attributes = raw.split(',')
.map(attribute => attribute.split('='))
.map(([name, value]) => ({name, value}));
const displayName = attributes.map(({value}) => value).join(' ').trim();
return {attributes, displayName}
}
}
export class MBean {
constructor({descriptor, op, ...mBean}) {
Object.assign(this, mBean);
this.descriptor = new MBeanDescriptor(descriptor);
const flattenedOps = Object.entries(op || {}).flatMap(([name, value]) => {
if (Array.isArray(value)) {
return value.map(v => [name, v]);
} else {
return [[name, value]];
}
}).map(([name, operation]) => [getOperationName(name, operation), operation]);
this.op = flattenedOps.length > 0 ? fromPairs(flattenedOps) : null;
}
}
export default {
props: {
instance: {
type: Instance,
required: true
}
},
// eslint-disable-next-line vue/no-unused-components
components: {mBeanOperations, mBeanAttributes},
directives: {onClickaway, sticksBelow},
data: () => ({
hasLoaded: false,
error: null,
domains: [],
selected: {
domain: null,
mBean: null,
view: null
}
}),
computed: {
selectedDomain() {
return this.domains.find(d => d.domain === this.selected.domain)
},
selectedMBean() {
return this.selectedDomain && this.selectedDomain.mBeans.find(b => b.descriptor.raw === this.selected.mBean)
}
},
created() {
this.fetchMBeans();
},
watch: {
'$route': {
immediate: true,
handler() {
if (!isEmpty(this.$route.query)) {
this.selected = this.$route.query;
} else if (this.domains.length > 0) {
this.select(this.domains[0]);
}
}
},
selected() {
if (!isEqual(this.selected, this.$route.query)) {
this.$router.replace({
name: 'instances/jolokia',
query: this.selected
});
}
},
async selectedMBean(newVal) {
if (newVal) {
await this.$nextTick();
const el = document.getElementById(newVal.descriptor.raw);
if (el) {
const scrollingEl = document.scrollingElement;
const navigation = document.querySelector('#navigation');
const navbarOffset = (navigation ? navigation.getBoundingClientRect().bottom : 120) + 10;
const top = scrollingEl.scrollTop + el.getBoundingClientRect().top - navbarOffset;
if (scrollingEl.scrollTo) {
scrollingEl.scrollTo({top, behavior: 'smooth'})
} else {
scrollingEl.scrollTop = top;
}
}
}
}
},
methods: {
async fetchMBeans() {
this.error = null;
try {
const res = await this.instance.listMBeans();
const domains = sortBy(res.data, [d => d.domain]);
this.domains = domains.map(domain => ({
...domain,
mBeans: sortBy(domain.mBeans.map(mBean => new MBean(mBean)), [b => b.descriptor.displayName])
}));
if (!this.selectedDomain && this.domains.length > 0) {
this.select(this.domains[0]);
}
} catch (error) {
console.warn('Fetching MBeans failed:', error);
this.error = error;
}
this.hasLoaded = true;
},
select(domain, mBean, view) {
this.selected = {
domain: domain && domain.domain,
mBean: mBean && mBean.descriptor.raw,
view: view || (mBean ? (mBean.attr ? 'attributes' : (mBean.op ? 'operations' : null)) : null)
};
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/jolokia',
parent: 'instances',
path: 'jolokia',
component: this,
label: 'JMX',
group: 'JVM',
order: 350,
isEnabled: ({instance}) => instance.hasEndpoint('jolokia')
});
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.m-bean {
transition: all $easing $speed;
&.is-active {
margin: 0.75rem -0.75rem;
max-width: unset;
}
&.is-active .m-bean--header {
padding-bottom: 0;
}
&:not(.is-active) .m-bean--header:hover {
background-color: $white-bis;
}
&--header {
& .level .level-left {
width: 100%;
& .level-item {
min-width: 0;
flex-shrink: 1;
& p {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
}
&--close {
position: absolute;
right: 0.75rem;
top: 0.75rem;
}
}
}
</style>
@@ -1,129 +0,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.
-->
<template>
<div class="field">
<label class="label">
<span v-text="name" />
<small class="is-muted has-text-weight-normal" v-text="descriptor.type" />
</label>
<div class="field-body">
<div class="field is-expanded">
<div class="field has-addons">
<div class="control">
<button class="button" :disabled="!descriptor.rw" @click="edit">
<font-awesome-icon icon="pencil-alt" />
</button>
</div>
<div class="control is-expanded has-icons-right">
<template v-if="!editing">
<input v-if="!hasComplexValue" class="input" type="text" readonly :value="value" @dblclick="edit">
<textarea v-else class="input m-bean-attribute--text" readonly v-text="jsonValue" />
<span class="icon is-right has-text-warning" v-if="error">
<font-awesome-icon icon="exclamation-triangle" />
</span>
</template>
<template v-else>
<input v-if="!hasComplexValue" class="input" type="text" ref="input"
v-model="input" @keyup.esc="cancel" @keyup.enter="save"
>
</template>
</div>
</div>
<div class="help" v-text="descriptor.desc" />
</div>
</div>
<div class="control" v-if="editing">
<button class="button is-light is-small" @click="cancel">Cancel</button>
<button class="button is-primary is-small" :class="{'is-loading' : saving}" @click="save"
:disabled="value === input"
>Save
</button>
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true
},
descriptor: {
type: Object,
required: true
},
value: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: null,
default: null
},
onSaveValue: {
type: Function,
required: true
}
},
data: () => ({
input: null,
editing: false,
saving: false,
error: null
}),
computed: {
hasComplexValue() {
return this.value !== null && typeof this.value === 'object';
},
jsonValue() {
return JSON.stringify(this.value, null, 4);
},
},
methods: {
async edit() {
if (this.descriptor.rw && !this.hasComplexValue) {
this.input = this.value;
this.editing = true;
await this.$nextTick();
this.$refs.input.focus();
}
},
cancel() {
this.editing = false;
},
async save() {
this.saving = true;
try {
await this.onSaveValue(this.input);
} catch (error) {
console.warn(`Error saving attribute ${this.name}`, error);
this.error = error;
} finally {
this.saving = false;
this.editing = false;
}
}
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.m-bean-attribute--text {
resize: vertical;
min-height: 120px;
}
</style>
@@ -1,84 +0,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.
-->
<template>
<div>
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching attributes failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<m-bean-attribute v-for="(attribute, name) in mBean.attr" :key="`attr-${name}`"
:name="name" :descriptor="attribute" :value="attributeValues && attributeValues[name]"
:on-save-value="value => writeAttribute(name, value)"
/>
</div>
</template>
<script>
import Instance from '@/services/instance';
import {MBean} from './index';
import mBeanAttribute from './m-bean-attribute';
export default {
props: {
domain: {
type: String,
required: true
},
mBean: {
type: MBean,
required: true
},
instance: {
type: Instance,
required: true
}
},
// eslint-disable-next-line vue/no-unused-components
components: {mBeanAttribute},
data: () => ({
attributeValues: null,
error: null
}),
computed: {},
methods: {
async readAttributes() {
try {
const response = await this.instance.readMBeanAttributes(this.domain, this.mBean.descriptor.raw);
this.attributeValues = response.data.value;
} catch (error) {
console.warn('Fetching MBean attributes failed:', error);
this.error = error;
}
},
async writeAttribute(attribute, value) {
try {
await this.instance.writeMBeanAttribute(this.domain, this.mBean.descriptor.raw, attribute, value);
} finally {
await this.readAttributes();
}
}
},
created() {
this.readAttributes();
},
}
</script>
@@ -1,204 +0,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.
-->
<template>
<div class="modal is-active">
<div class="modal-background" @click="abort" />
<div class="modal-content">
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title" v-text="name" />
</header>
<template v-if="state === 'input-args'">
<section class="modal-card-body" @keyup.ctrl.enter="invoke(args)">
<div class="field" v-for="(arg, idx) in descriptor.args" :key="arg.name">
<label class="label">
<span v-text="arg.name" />
<small class="is-muted has-text-weight-normal" v-text="arg.type" />
</label>
<div class="control">
<input type="text" class="input" v-model="args[idx]">
</div>
<p class="help" v-text="arg.desc" />
</div>
</section>
<footer class="modal-card-foot">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-primary" @click="invoke(args)">Execute</button>
</div>
</div>
</footer>
</template>
<template v-else-if="state === 'executing'">
<section class="modal-card-body">
<section class="section is-loading">
<p>Executing...</p>
</section>
</section>
</template>
<template v-else-if="state === 'completed'">
<section class="modal-card-body">
<div class="message is-success">
<div class="message-body">
<strong>Execution successful.</strong>
</div>
</div>
<pre v-if="descriptor.ret !== 'void'" v-text="prettyPrintedResult" />
</section>
<footer class="modal-card-foot">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-light" @click="abort"> Close</button>
</div>
</div>
</footer>
</template>
<template v-else-if="state === 'failed'">
<section class="modal-card-body">
<div class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger"
icon="exclamation-triangle"
/>
Execution failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<pre v-if="error.stacktrace"
v-text="error.stacktrace"
/>
<pre v-if="error.response && error.response.data"
v-text="error.response.data"
/>
</section>
<footer class="modal-card-foot">
<div class="field is-grouped is-grouped-right">
<div class="control">
<button class="button is-light" @click="abort"> Close</button>
</div>
</div>
</footer>
</template>
</div>
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true
},
descriptor: {
type: Object,
required: true
},
value: {
// eslint-disable-next-line vue/require-prop-type-constructor
type: null,
default: null
},
onClose: {
type: Function,
required: true
},
onExecute: {
type: Function,
required: true
}
},
data: () => ({
state: null,
error: null,
args: null,
result: null
}),
computed: {
prettyPrintedResult() {
if (this.result && typeof this.result === 'string') {
try {
const o = JSON.parse(this.result);
return JSON.stringify(o, undefined, 4);
} catch (e) {
return this.result;
}
} else if (typeof result === 'object') {
return JSON.stringify(this.result, undefined, 4);
}
return this.result;
}
},
methods: {
abort() {
this.onClose();
},
invoke(args) {
this.state = (args || this.descriptor.args.length === 0) ? 'prepared' : 'input-args';
this.args = args || new Array(this.descriptor.args.length);
this.error = null;
this.result = null;
if (this.state === 'prepared') {
this.execute()
}
},
async execute() {
this.state = 'executing';
try {
const result = await this.onExecute(this.args);
if (result.data.status < 400) {
this.result = result.data.value;
this.state = 'completed';
} else {
const error = new Error(`Execution failed: ${result.data.error}`);
error.stacktrace = result.data.stacktrace;
this.state = 'failed';
this.error = error;
console.warn('Invocation failed', error);
}
} catch (error) {
this.state = 'failed';
this.error = error;
console.warn('Invocation failed', error);
}
},
keyHandler(event) {
if (event.keyCode === 27) {
this.abort()
}
}
},
created() {
this.invoke();
},
mounted() {
document.addEventListener('keyup', this.keyHandler)
},
beforeDestroy() {
document.removeEventListener('keyup', this.keyHandler)
},
}
</script>
@@ -1,41 +0,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.
-->
<template>
<div class="field">
<div class="control">
<button class="button is-light is-fullwidth" @click="$emit('click', $event)">
<small class="is-light is-muted" v-text="descriptor.ret" />&nbsp;<span v-text="name" />
</button>
<p class="help" v-text="descriptor.desc" />
</div>
</div>
</template>
<script>
export default {
props: {
name: {
type: String,
required: true
},
descriptor: {
type: Object,
required: true
}
},
}
</script>
@@ -1,67 +0,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.
-->
<template>
<div>
<mBeanOperation v-for="(operation, name) in mBean.op" :key="`op-${name}`"
:name="name" :descriptor="operation" @click="invoke(name, operation)"
/>
<m-bean-operation-invocation v-if="invocation" :name="invocation.name" :descriptor="invocation.descriptor"
:on-execute="execute" :on-close="closeInvocation"
/>
</div>
</template>
<script>
import Instance from '@/services/instance';
import {MBean} from './index';
import mBeanOperation from './m-bean-operation';
import mBeanOperationInvocation from './m-bean-operation-invocation';
export default {
props: {
domain: {
type: String,
required: true
},
mBean: {
type: MBean,
required: true
},
instance: {
type: Instance,
required: true
}
},
// eslint-disable-next-line vue/no-unused-components
components: {mBeanOperation, mBeanOperationInvocation},
data: () => ({
invocation: null
}),
computed: {},
methods: {
closeInvocation() {
this.invocation = null
},
invoke(name, descriptor) {
this.invocation = {name, descriptor};
},
execute(args) {
return this.instance.invokeMBeanOperation(this.domain, this.mBean.descriptor.raw, this.invocation.name, args);
}
}
}
</script>
@@ -1,161 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
<template v-if="hasLoaded">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching Liquibase migrations failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<template v-for="(context, ctxName) in contexts">
<h3 class="title" v-text="ctxName" :key="ctxName" />
<template v-for="(report, name) in context.liquibaseBeans">
<sba-panel :key="`${ctxName}-${name}`" :title="`name`" class="change-set"
:header-sticks-below="['#navigation']"
>
<table class="table is-hoverable is-fullwidth">
<thead>
<tr>
<th>Id</th>
<th>Execution</th>
<th>Description</th>
<th>Tag</th>
<th>Contexts</th>
<th>Labels</th>
</tr>
</thead>
<tbody>
<template v-for="changeSet in report.changeSets">
<tr :key="`${ctxName}-${name}-${changeSet.id}`" class="is-selectable"
@click="showDetails[changeSet.checksum] ? $delete(showDetails, changeSet.checksum) : $set(showDetails, changeSet.checksum, true)"
>
<td v-text="changeSet.id" />
<td>
<span v-text="changeSet.execType" class="tag" :class="execClass(execType)" />
</td>
<td v-text="changeSet.description" />
<td v-text="changeSet.tag" />
<td v-text="changeSet.contexts.join(', ')" />
<td>
<span v-for="label in changeSet.labels" :key="`${ctxName}-${name}-${changeSet.id}-${label}`"
class="tag is-info" v-text="label"
/>
</td>
</tr>
<tr v-if="showDetails[changeSet.checksum]" :key="`${ctxName}-${name}-${changeSet.id}-details`">
<td colspan="6">
<table class="table is-fullwidth">
<tr>
<th>Changelog</th>
<td colspan="3" v-text="changeSet.changeLog" />
<th>Author</th>
<td v-text="changeSet.author" />
</tr>
<tr>
<th>Checksum</th>
<td v-text="changeSet.checksum" />
<th>Comments</th>
<td colspan="3" v-text="changeSet.comments" />
</tr>
<tr>
<th>Execution Order</th>
<td v-text="changeSet.orderExecuted" />
<th>ExecutionDate</th>
<td v-text="changeSet.dateExecuted" />
<th>DeploymentId</th>
<td v-text="changeSet.deploymentId" />
</tr>
</table>
</td>
</tr>
</template>
</tbody>
</table>
</sba-panel>
</template>
</template>
</template>
</section>
</template>
<script>
import Instance from '@/services/instance';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
hasLoaded: false,
error: null,
contexts: null,
showDetails: {}
}),
computed: {},
created() {
this.fetchLiquibase();
},
methods: {
async fetchLiquibase() {
this.error = null;
try {
const res = await this.instance.fetchLiquibase();
this.contexts = res.data.contexts;
} catch (error) {
console.warn('Fetching Liquibase changeSets failed:', error);
this.error = error;
}
this.hasLoaded = true;
},
execClass(execType) {
switch (execType) {
case 'EXECUTED':
return 'is-success';
case 'FAILED':
return 'is-danger';
case 'SKIPPED':
return 'is-light';
case 'RERAN':
case 'MARK_RAN':
return 'is-warning';
default:
return 'is-info';
}
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/liquibase',
parent: 'instances',
path: 'liquibase',
component: this,
label: 'Liquibase',
group: 'Data',
order: 900,
isEnabled: ({instance}) => instance.hasEndpoint('liquibase')
});
}
}
</script>å
@@ -1,176 +0,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.
-->
<template>
<div class="section logfile-view" :class="{ 'is-loading' : !hasLoaded }">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching logfile failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<div class="logfile-view-actions" v-if="hasLoaded">
<div class="logfile-view-actions__navigation">
<sba-icon-button :disabled="atTop" @click="scrollToTop" icon="step-backward" size="lg"
icon-class="rotated"
/>
<sba-icon-button :disabled="atBottom" @click="scrollToBottom" icon="step-forward" size="lg"
icon-class="rotated"
/>
</div>
<a class="button" :href="`instances/${instance.id}/actuator/logfile`" target="_blank">
<font-awesome-icon icon="download" />&nbsp;Download
</a>
</div>
<p v-if="skippedBytes" v-text="`skipped ${prettyBytes(skippedBytes)}`" />
<!-- log will be appended here -->
</div>
</template>
<script>
import subscribing from '@/mixins/subscribing';
import Instance from '@/services/instance';
import autolink from '@/utils/autolink';
import {animationFrameScheduler, concatAll, concatMap, map, of, tap} from '@/utils/rxjs';
import AnsiUp from 'ansi_up';
import chunk from 'lodash/chunk';
import prettyBytes from 'pretty-bytes';
export default {
props: {
instance: {
type: Instance,
required: true
}
},
mixins: [subscribing],
data: () => ({
hasLoaded: false,
error: null,
atBottom: true,
atTop: false,
skippedBytes: null
}),
created() {
this.ansiUp = new AnsiUp();
},
mounted() {
window.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
},
methods: {
prettyBytes,
createSubscription() {
const vm = this;
vm.error = null;
return this.instance.streamLogfile(1000)
.pipe(
tap(part => vm.skippedBytes = vm.skippedBytes || part.skipped),
concatMap(part => chunk(part.addendum.split(/\r?\n/), 250)),
map(lines => of(lines, animationFrameScheduler)),
concatAll()
)
.subscribe({
next: lines => {
vm.hasLoaded = true;
lines.forEach(line => {
const child = document.createElement('pre');
child.innerHTML = autolink(this.ansiUp.ansi_to_html(line));
vm.$el.appendChild(child);
});
if (vm.atBottom) {
vm.scrollToBottom();
}
},
error: error => {
vm.hasLoaded = true;
console.warn('Fetching logfile failed:', error);
vm.error = error;
}
});
},
onScroll() {
const scrollingEl = document.scrollingElement;
const visibleHeight = document.documentElement.clientHeight;
this.atBottom = visibleHeight === scrollingEl.scrollHeight - scrollingEl.scrollTop;
this.atTop = scrollingEl.scrollTop <= 0;
},
scrollToTop() {
document.scrollingElement.scrollTop = 0;
},
scrollToBottom() {
document.scrollingElement.scrollTop = document.scrollingElement.scrollHeight;
}
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/logfile',
parent: 'instances',
path: 'logfile',
component: this,
label: 'Logfile',
group: 'Logging',
order: 200,
isEnabled: ({instance}) => instance.hasEndpoint('logfile')
});
}
}
</script>
<style lang="scss">
@import "~@/assets/css/utilities";
.logfile-view {
padding: 1.5em;
pre {
word-break: break-all;
padding: 0;
white-space: pre-wrap;
width: 100%;
&:hover {
background: $grey-lighter;
}
}
&-actions {
top: (($gap / 2) + $navbar-height-px);
right: ($gap /2);
display: flex;
align-items: center;
position: sticky;
float: right;
&__navigation {
display: inline-flex;
flex-direction: column;
justify-content: space-between;
margin-right: 0.5rem;
}
}
}
.rotated {
transform: rotate(90deg);
}
</style>
@@ -1,219 +0,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.
-->
<template>
<section class="section" :class="{ 'is-loading' : !hasLoaded }">
<template v-if="hasLoaded">
<div v-if="error" class="message is-danger">
<div class="message-body">
<strong>
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
Fetching loggers failed.
</strong>
<p v-text="error.message" />
</div>
</div>
<template v-if="loggerConfig">
<div class="field-body">
<div class="field has-addons">
<p class="control is-expanded">
<input class="input" type="search" placeholder="name filter" v-model="filter">
</p>
<p class="control">
<span class="button is-static">
<span v-text="filteredLoggers.length" />
/
<span v-text="loggerConfig.loggers.length" />
</span>
</p>
</div>
</div>
<div class="field-body">
<div class="field is-narrow">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="showClassLoggersOnly">
class only
</label>
</div>
</div>
<div class="field is-narrow">
<div class="control">
<label class="checkbox">
<input type="checkbox" v-model="showConfiguredLoggersOnly">
configured
</label>
</div>
</div>
</div>
</template>
<table v-if="loggerConfig" class="table is-hoverable is-fullwidth">
<tbody>
<tr v-for="logger in limitedLoggers" :key="logger.name">
<td>
<span class="is-breakable" v-text="logger.name" />
<span class="has-text-danger" v-if="logger.name in failed">
<font-awesome-icon class="has-text-danger" icon="exclamation-triangle" />
<span v-text="`Configuring ${failed[logger.name]} failed.`" />
</span>
<sba-logger-control class="is-pulled-right"
:level-options="levels"
:effective-level="logger.effectiveLevel"
:configured-level="logger.configuredLevel"
:is-loading="loading[logger.name]"
:has-failed="failed[logger.name]"
:allow-reset="logger.name !== 'ROOT'"
@input="level => configureLogger(logger, level)"
/>
</td>
</tr>
<tr v-if="limitedLoggers.length === 0">
<td class="is-muted" colspan="5">No loggers found.
</td>
</tr>
</tbody>
</table>
</template>
</section>
</template>
<script>
import Instance from '@/services/instance';
import sbaLoggerControl from './logger-control';
const isClassName = name => /\.[A-Z]/.test(name);
const addToFilter = (oldFilter, addedFilter) =>
!oldFilter
? addedFilter
: (val, key) => oldFilter(val, key) && addedFilter(val, key);
export default {
// eslint-disable-next-line vue/no-unused-components
components: {sbaLoggerControl},
props: {
instance: {
type: Instance,
required: true
}
},
data: () => ({
hasLoaded: false,
error: null,
loggerConfig: null,
filter: '',
showClassLoggersOnly: false,
showConfiguredLoggersOnly: false,
visibleLimit: 25,
loading: {},
failed: {}
}),
computed: {
levels() {
return this.loggerConfig.levels;
},
limitedLoggers() {
if (this.visibleLimit > 0) {
return this.filteredLoggers.slice(0, this.visibleLimit);
} else {
return this.filteredLoggers;
}
},
filteredLoggers() {
let filterFn = this.getFilterFn();
return filterFn ? this.loggerConfig.loggers.filter(filterFn) : this.loggerConfig.loggers;
}
},
methods: {
async configureLogger(logger, level) {
const vm = this;
const setLoadingHandle = setTimeout(() => vm.$set(vm.loading, logger.name, level), 150);
try {
await this.instance.configureLogger(logger.name, level);
vm.$delete(vm.failed, logger.name, level)
} catch (error) {
console.warn(`Configuring logger '${logger.name}' failed:`, error);
vm.$set(vm.failed, logger.name, level);
}
try {
await this.fetchLoggers();
} finally {
vm.$delete(vm.loading, logger.name);
clearTimeout(setLoadingHandle);
}
},
async fetchLoggers() {
this.error = null;
try {
const res = await this.instance.fetchLoggers();
this.loggerConfig = res.data;
} catch (error) {
console.warn('Fetching loggers failed:', error);
this.error = error;
}
this.hasLoaded = true;
},
onScroll() {
if (this.loggerConfig && this.$el.getBoundingClientRect().bottom - 400 <= window.innerHeight && this.visibleLimit < this.filteredLoggers.length) {
this.visibleLimit += 25;
}
},
getFilterFn() {
let filterFn = null;
if (this.showClassLoggersOnly) {
filterFn = addToFilter(filterFn, logger => isClassName(logger.name));
}
if (this.showConfiguredLoggersOnly) {
filterFn = addToFilter(filterFn, logger => !!logger.configuredLevel);
}
if (this.filter) {
const normalizedFilter = this.filter.toLowerCase();
filterFn = addToFilter(filterFn, logger => logger.name.toLowerCase().includes(normalizedFilter));
}
return filterFn;
}
},
created() {
this.fetchLoggers();
},
mounted() {
window.addEventListener('scroll', this.onScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.onScroll);
},
install({viewRegistry}) {
viewRegistry.addView({
name: 'instances/loggers',
parent: 'instances',
path: 'loggers',
component: this,
label: 'Loggers',
group: 'Logging',
order: 300,
isEnabled: ({instance}) => instance.hasEndpoint('loggers')
});
}
}
</script>
<style lang="scss">
</style>
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff. Show more