Initial version of sasiedzi generated by generator-jhipster@8.7.2
@@ -0,0 +1,58 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Page Not Found</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="favicon.ico" />
|
||||
<style>
|
||||
* {
|
||||
line-height: 1.2;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
color: #888;
|
||||
display: table;
|
||||
font-family: sans-serif;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
margin: 2em auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #555;
|
||||
font-size: 2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 auto;
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 280px) {
|
||||
body,
|
||||
p {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 1.5em;
|
||||
margin: 0 0 0.3em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Page Not Found</h1>
|
||||
<p>Sorry, but the page you were trying to view does not exist.</p>
|
||||
</body>
|
||||
</html>
|
||||
<!-- IE needs 512+ bytes: http://blogs.msdn.com/b/ieinternals/archive/2010/08/19/http-error-pages-in-internet-explorer.aspx -->
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<web-app
|
||||
xmlns="http://java.sun.com/xml/ns/javaee"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
|
||||
version="3.0">
|
||||
|
||||
<mime-mapping>
|
||||
<extension>html</extension>
|
||||
<mime-type>text/html;charset=utf-8</mime-type>
|
||||
</mime-mapping>
|
||||
|
||||
</web-app>
|
||||
@@ -0,0 +1,109 @@
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import AccountService from './account.service';
|
||||
import { type AccountStore, useStore } from '@/store';
|
||||
|
||||
const resetStore = (store: AccountStore) => {
|
||||
store.$reset();
|
||||
};
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
post: sinon.stub(axios, 'post'),
|
||||
};
|
||||
|
||||
createTestingPinia({ stubActions: false });
|
||||
const store = useStore();
|
||||
|
||||
describe('Account Service test suite', () => {
|
||||
let accountService: AccountService;
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
|
||||
axiosStub.get.reset();
|
||||
resetStore(store);
|
||||
});
|
||||
|
||||
it('should init service and do not retrieve account', async () => {
|
||||
axiosStub.get.resolves({});
|
||||
axiosStub.get
|
||||
.withArgs('management/info')
|
||||
.resolves({ status: 200, data: { 'display-ribbon-on-profiles': 'dev', activeProfiles: ['dev', 'test'] } });
|
||||
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
expect(store.logon).toBe(null);
|
||||
expect(accountService.authenticated).toBe(false);
|
||||
expect(store.account).toBe(null);
|
||||
expect(axiosStub.get.calledWith('management/info')).toBeTruthy();
|
||||
expect(store.activeProfiles[0]).toBe('dev');
|
||||
expect(store.activeProfiles[1]).toBe('test');
|
||||
expect(store.ribbonOnProfiles).toBe('dev');
|
||||
});
|
||||
|
||||
it('should init service and retrieve profiles if already logged in before but no account found', async () => {
|
||||
axiosStub.get.resolves({});
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
expect(store.logon).toBe(null);
|
||||
expect(accountService.authenticated).toBe(false);
|
||||
expect(store.account).toBe(null);
|
||||
expect(axiosStub.get.calledWith('management/info')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should init service and retrieve profiles if already logged in before but exception occurred and should be logged out', async () => {
|
||||
axiosStub.get.resolves({});
|
||||
axiosStub.get.withArgs('api/account').rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
expect(accountService.authenticated).toBe(false);
|
||||
expect(store.account).toBe(null);
|
||||
expect(axiosStub.get.calledWith('management/info')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should init service and check for authority after retrieving account but getAccount failed', async () => {
|
||||
axiosStub.get.rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('USER').then((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should init service and check for authority after retrieving account', async () => {
|
||||
axiosStub.get.resolves({ status: 200, data: { authorities: ['USER'], langKey: 'en', login: 'ADMIN' } });
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('USER').then((value: boolean) => {
|
||||
expect(value).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should init service as not authentified and not return any authorities admin and not retrieve account', async () => {
|
||||
axiosStub.get.rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('ADMIN').then((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should init service as not authentified and return authority user', async () => {
|
||||
axiosStub.get.rejects();
|
||||
accountService = new AccountService(store);
|
||||
await accountService.update();
|
||||
|
||||
return accountService.hasAnyAuthorityAndCheckAuth('USER').then((value: boolean) => {
|
||||
expect(value).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import axios from 'axios';
|
||||
|
||||
import { type AccountStore } from '@/store';
|
||||
|
||||
export default class AccountService {
|
||||
constructor(private store: AccountStore) {}
|
||||
|
||||
public async update(): Promise<void> {
|
||||
if (!this.store.profilesLoaded) {
|
||||
await this.retrieveProfiles();
|
||||
this.store.setProfilesLoaded();
|
||||
}
|
||||
await this.loadAccount();
|
||||
}
|
||||
|
||||
public async retrieveProfiles(): Promise<boolean> {
|
||||
try {
|
||||
const res = await axios.get<any>('management/info');
|
||||
if (res.data && res.data.activeProfiles) {
|
||||
this.store.setRibbonOnProfiles(res.data['display-ribbon-on-profiles']);
|
||||
this.store.setActiveProfiles(res.data.activeProfiles);
|
||||
}
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async retrieveAccount(): Promise<boolean> {
|
||||
try {
|
||||
const response = await axios.get<any>('api/account');
|
||||
if (response.status === 200 && response.data?.login) {
|
||||
const account = response.data;
|
||||
this.store.setAuthentication(account);
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// Ignore error
|
||||
}
|
||||
|
||||
this.store.logout();
|
||||
return false;
|
||||
}
|
||||
|
||||
public async loadAccount() {
|
||||
if (this.store.logon) {
|
||||
return this.store.logon;
|
||||
}
|
||||
if (this.authenticated && this.userAuthorities) {
|
||||
return;
|
||||
}
|
||||
|
||||
const promise = this.retrieveAccount();
|
||||
this.store.authenticate(promise);
|
||||
promise.then(() => this.store.authenticate(null));
|
||||
await promise;
|
||||
}
|
||||
|
||||
public async hasAnyAuthorityAndCheckAuth(authorities: any): Promise<boolean> {
|
||||
if (typeof authorities === 'string') {
|
||||
authorities = [authorities];
|
||||
}
|
||||
|
||||
return this.checkAuthorities(authorities);
|
||||
}
|
||||
|
||||
public get authenticated(): boolean {
|
||||
return this.store.authenticated;
|
||||
}
|
||||
|
||||
public get userAuthorities(): string[] {
|
||||
return this.store.account?.authorities;
|
||||
}
|
||||
|
||||
private checkAuthorities(authorities: string[]): boolean {
|
||||
if (this.userAuthorities) {
|
||||
for (const authority of authorities) {
|
||||
if (this.userAuthorities.includes(authority)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
import LoginService from './login.service';
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
post: sinon.stub(axios, 'post'),
|
||||
};
|
||||
|
||||
describe('Login Service test suite', () => {
|
||||
let loginService: LoginService;
|
||||
|
||||
beforeEach(() => {
|
||||
loginService = new LoginService();
|
||||
});
|
||||
|
||||
it('should build url for login', () => {
|
||||
const loc = { href: '', hostname: 'localhost', pathname: '/' };
|
||||
|
||||
loginService.login(loc);
|
||||
|
||||
expect(loc.href).toBe('//localhost/oauth2/authorization/oidc');
|
||||
});
|
||||
|
||||
it('should build url for login with loc.pathname equals to /accessdenied', () => {
|
||||
const loc = { href: '', hostname: 'localhost', pathname: '/accessdenied' };
|
||||
|
||||
loginService.login(loc);
|
||||
|
||||
expect(loc.href).toBe('//localhost/oauth2/authorization/oidc');
|
||||
});
|
||||
|
||||
it('should build url for login with loc.pathname equals to /forbidden', () => {
|
||||
const loc = { href: '', hostname: 'localhost', pathname: '/forbidden' };
|
||||
|
||||
loginService.login(loc);
|
||||
|
||||
expect(loc.href).toBe('//localhost/oauth2/authorization/oidc');
|
||||
});
|
||||
|
||||
it('should build url for login with loc.pathname equals to /accessdenied', () => {
|
||||
const loc = { href: '', hostname: 'localhost', pathname: '/accessdenied' };
|
||||
|
||||
loginService.login(loc);
|
||||
|
||||
expect(loc.href).toBe('//localhost/oauth2/authorization/oidc');
|
||||
});
|
||||
|
||||
it('should build url for login with loc.pathname equals to /forbidden', () => {
|
||||
const loc = { href: '', hostname: 'localhost', pathname: '/forbidden' };
|
||||
|
||||
loginService.login(loc);
|
||||
|
||||
expect(loc.href).toBe('//localhost/oauth2/authorization/oidc');
|
||||
});
|
||||
|
||||
it('should build url for login behind client proxy', () => {
|
||||
const loc = { href: '', port: '8080', hostname: 'localhost', pathname: '/' };
|
||||
|
||||
loginService.login(loc);
|
||||
|
||||
expect(loc.href).toBe('//localhost:8080/oauth2/authorization/oidc');
|
||||
});
|
||||
|
||||
it('should call global logout when asked to', () => {
|
||||
loginService.logout();
|
||||
|
||||
expect(axiosStub.post.calledWith('api/logout')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import axios, { type AxiosPromise } from 'axios';
|
||||
|
||||
export default class LoginService {
|
||||
public login(loc: { href: string; hostname: string; pathname: string; port?: string } = window.location) {
|
||||
const port = loc.port ? `:${loc.port}` : '';
|
||||
let contextPath = loc.pathname;
|
||||
if (contextPath.endsWith('accessdenied')) {
|
||||
contextPath = contextPath.substring(0, contextPath.indexOf('accessdenied'));
|
||||
}
|
||||
if (contextPath.endsWith('forbidden')) {
|
||||
contextPath = contextPath.substring(0, contextPath.indexOf('forbidden'));
|
||||
}
|
||||
if (!contextPath.endsWith('/')) {
|
||||
contextPath = `${contextPath}/`;
|
||||
}
|
||||
// If you have configured multiple OIDC providers, then, you can update this URL to /login.
|
||||
// It will show a Spring Security generated login page with links to configured OIDC providers.
|
||||
loc.href = `//${loc.hostname}${port}${contextPath}oauth2/authorization/oidc`;
|
||||
}
|
||||
|
||||
public logout(): AxiosPromise<any> {
|
||||
return axios.post('api/logout');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Configuration from './configuration.vue';
|
||||
|
||||
type ConfigurationComponentType = InstanceType<typeof Configuration>;
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
};
|
||||
|
||||
describe('Configuration Component', () => {
|
||||
let configuration: ConfigurationComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosStub.get.reset();
|
||||
axiosStub.get.resolves({
|
||||
data: { contexts: [{ beans: [{ prefix: 'A' }, { prefix: 'B' }] }], propertySources: [{ properties: { key1: { value: 'value' } } }] },
|
||||
});
|
||||
const wrapper = shallowMount(Configuration);
|
||||
configuration = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('OnRouteEnter', () => {
|
||||
it('should set all default values correctly', () => {
|
||||
expect(configuration.configKeys).toEqual([]);
|
||||
expect(configuration.filtered).toBe('');
|
||||
expect(configuration.orderProp).toBe('prefix');
|
||||
expect(configuration.reverse).toBe(false);
|
||||
});
|
||||
it('Should call load all on init', async () => {
|
||||
// WHEN
|
||||
configuration.init();
|
||||
await configuration.$nextTick();
|
||||
|
||||
// THEN
|
||||
expect(axiosStub.get.calledWith('management/env')).toBeTruthy();
|
||||
expect(axiosStub.get.calledWith('management/configprops')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('keys method', () => {
|
||||
it('should return the keys of an Object', () => {
|
||||
// GIVEN
|
||||
const data = {
|
||||
key1: 'test',
|
||||
key2: 'test2',
|
||||
};
|
||||
|
||||
// THEN
|
||||
expect(configuration.keys(data)).toEqual(['key1', 'key2']);
|
||||
expect(configuration.keys(undefined)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeOrder function', () => {
|
||||
it('should change order', () => {
|
||||
// GIVEN
|
||||
const rev = configuration.reverse;
|
||||
|
||||
// WHEN
|
||||
configuration.changeOrder('prefix');
|
||||
|
||||
// THEN
|
||||
expect(configuration.orderProp).toBe('prefix');
|
||||
expect(configuration.reverse).toBe(!rev);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import { type Ref, computed, defineComponent, inject, ref } from 'vue';
|
||||
|
||||
import ConfigurationService from './configuration.service';
|
||||
import { orderAndFilterBy } from '@/shared/computables';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiConfiguration',
|
||||
setup() {
|
||||
const configurationService = inject('configurationService', () => new ConfigurationService(), true);
|
||||
|
||||
const orderProp = ref('prefix');
|
||||
const reverse = ref(false);
|
||||
const allConfiguration: Ref<any> = ref({});
|
||||
const configuration: Ref<any[]> = ref([]);
|
||||
const configKeys: Ref<any[]> = ref([]);
|
||||
const filtered = ref('');
|
||||
|
||||
const filteredConfiguration = computed(() =>
|
||||
orderAndFilterBy(configuration.value, {
|
||||
filterByTerm: filtered.value,
|
||||
orderByProp: orderProp.value,
|
||||
reverse: reverse.value,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
configurationService,
|
||||
orderProp,
|
||||
reverse,
|
||||
allConfiguration,
|
||||
configuration,
|
||||
configKeys,
|
||||
filtered,
|
||||
filteredConfiguration,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init(): void {
|
||||
this.configurationService.loadConfiguration().then(res => {
|
||||
this.configuration = res;
|
||||
|
||||
for (const config of this.configuration) {
|
||||
if (config.properties !== undefined) {
|
||||
this.configKeys.push(Object.keys(config.properties));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.configurationService.loadEnvConfiguration().then(res => {
|
||||
this.allConfiguration = res;
|
||||
});
|
||||
},
|
||||
changeOrder(prop: string): void {
|
||||
this.orderProp = prop;
|
||||
this.reverse = !this.reverse;
|
||||
},
|
||||
keys(dict: any): string[] {
|
||||
return dict === undefined ? [] : Object.keys(dict);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import axios from 'axios';
|
||||
|
||||
export default class ConfigurationService {
|
||||
public loadConfiguration(): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
axios.get('management/configprops').then(res => {
|
||||
const properties = [];
|
||||
const propertiesObject = this.getConfigPropertiesObjects(res.data);
|
||||
for (const key in propertiesObject) {
|
||||
if (Object.hasOwn(propertiesObject, key)) {
|
||||
properties.push(propertiesObject[key]);
|
||||
}
|
||||
}
|
||||
|
||||
properties.sort((propertyA, propertyB) => {
|
||||
const comparePrefix = propertyA.prefix < propertyB.prefix ? -1 : 1;
|
||||
return propertyA.prefix === propertyB.prefix ? 0 : comparePrefix;
|
||||
});
|
||||
resolve(properties);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public loadEnvConfiguration(): Promise<any> {
|
||||
return new Promise(resolve => {
|
||||
axios.get<any>('management/env').then(res => {
|
||||
const properties = {};
|
||||
const propertySources = res.data.propertySources;
|
||||
|
||||
for (const propertyObject of propertySources) {
|
||||
const name = propertyObject.name;
|
||||
const detailProperties = propertyObject.properties;
|
||||
const vals = [];
|
||||
for (const keyDetail in detailProperties) {
|
||||
if (Object.hasOwn(detailProperties, keyDetail)) {
|
||||
vals.push({ key: keyDetail, val: detailProperties[keyDetail].value });
|
||||
}
|
||||
}
|
||||
properties[name] = vals;
|
||||
}
|
||||
resolve(properties);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private getConfigPropertiesObjects(res): any {
|
||||
// This code is for Spring Boot 2
|
||||
if (res.contexts !== undefined) {
|
||||
for (const key in res.contexts) {
|
||||
// If the key is not bootstrap, it will be the ApplicationContext Id
|
||||
// For default app, it is baseName
|
||||
// For microservice, it is baseName-1
|
||||
if (!key.startsWith('bootstrap')) {
|
||||
return res.contexts[key].beans;
|
||||
}
|
||||
}
|
||||
}
|
||||
// by default, use the default ApplicationContext Id
|
||||
return res.contexts.sasiedzi.beans;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2 id="configuration-page-heading" data-cy="configurationPageHeading">Configuration</h2>
|
||||
|
||||
<div v-if="allConfiguration && configuration">
|
||||
<span>Filter (by prefix)</span> <input type="text" v-model="filtered" class="form-control" />
|
||||
<h3>Spring configuration</h3>
|
||||
<table class="table table-striped table-bordered table-responsive d-table" aria-describedby="Configuration">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-40" @click="changeOrder('prefix')" scope="col">
|
||||
<span>Prefix</span>
|
||||
</th>
|
||||
<th class="w-60" @click="changeOrder('properties')" scope="col">
|
||||
<span>Properties</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entry in filteredConfiguration" :key="entry.prefix">
|
||||
<td>
|
||||
<span>{{ entry.prefix }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<div class="row" v-for="key in keys(entry.properties)" :key="key">
|
||||
<div class="col-md-4">{{ key }}</div>
|
||||
<div class="col-md-8">
|
||||
<span class="float-right badge-secondary break">{{ entry.properties[key] }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-for="key in keys(allConfiguration)" :key="key">
|
||||
<h4>
|
||||
<span>{{ key }}</span>
|
||||
</h4>
|
||||
<table class="table table-sm table-striped table-bordered table-responsive d-table" aria-describedby="Properties">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="w-40" scope="col">Property</th>
|
||||
<th class="w-60" scope="col">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="item of allConfiguration[key]" :key="item.key">
|
||||
<td class="break">{{ item.key }}</td>
|
||||
<td class="break">
|
||||
<span class="float-right badge-secondary break">{{ item.val }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./configuration.component.ts"></script>
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiDocs',
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<iframe
|
||||
src="/swagger-ui/index.html"
|
||||
width="100%"
|
||||
height="900"
|
||||
seamless
|
||||
target="_top"
|
||||
title="Swagger UI"
|
||||
class="border-0"
|
||||
data-cy="swagger-frame"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./docs.component.ts"></script>
|
||||
@@ -0,0 +1,89 @@
|
||||
import { vitest } from 'vitest';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import HealthModal from './health-modal.vue';
|
||||
|
||||
type HealthModalComponentType = InstanceType<typeof HealthModal>;
|
||||
|
||||
const healthService = { getBaseName: vitest.fn(), getSubSystemName: vitest.fn() };
|
||||
|
||||
describe('Health Modal Component', () => {
|
||||
let healthModal: HealthModalComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
const wrapper = shallowMount(HealthModal, {
|
||||
propsData: {
|
||||
currentHealth: {},
|
||||
},
|
||||
global: {
|
||||
stubs: {
|
||||
'font-awesome-icon': true,
|
||||
},
|
||||
provide: {
|
||||
healthService,
|
||||
},
|
||||
},
|
||||
});
|
||||
healthModal = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('baseName and subSystemName', () => {
|
||||
it('should use healthService', () => {
|
||||
healthModal.baseName('base');
|
||||
|
||||
expect(healthService.getBaseName).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use healthService', () => {
|
||||
healthModal.subSystemName('base');
|
||||
|
||||
expect(healthService.getSubSystemName).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('readableValue should transform data', () => {
|
||||
it('to string when is an object', () => {
|
||||
const result = healthModal.readableValue({ data: 1000 });
|
||||
|
||||
expect(result).toBe('{"data":1000}');
|
||||
});
|
||||
|
||||
it('to string when is a string', () => {
|
||||
const result = healthModal.readableValue('data');
|
||||
|
||||
expect(result).toBe('data');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Modal Component for diskSpace', () => {
|
||||
let healthModal: HealthModalComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
const wrapper = shallowMount(HealthModal, {
|
||||
propsData: {
|
||||
currentHealth: { name: 'diskSpace' },
|
||||
},
|
||||
global: {
|
||||
provide: {
|
||||
healthService,
|
||||
},
|
||||
},
|
||||
});
|
||||
healthModal = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('readableValue should transform data', () => {
|
||||
it('to GB when needed', () => {
|
||||
const result = healthModal.readableValue(2147483648);
|
||||
|
||||
expect(result).toBe('2.00 GB');
|
||||
});
|
||||
|
||||
it('to MB when needed', () => {
|
||||
const result = healthModal.readableValue(214748);
|
||||
|
||||
expect(result).toBe('0.20 MB');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { defineComponent, inject } from 'vue';
|
||||
import HealthService from './health.service';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiHealthModal',
|
||||
props: {
|
||||
currentHealth: {},
|
||||
},
|
||||
setup() {
|
||||
const healthService = inject('healthService', () => new HealthService(), true);
|
||||
|
||||
return {
|
||||
healthService,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
baseName(name: string): any {
|
||||
return this.healthService.getBaseName(name);
|
||||
},
|
||||
subSystemName(name: string): any {
|
||||
return this.healthService.getSubSystemName(name);
|
||||
},
|
||||
readableValue(value: any): string {
|
||||
if (this.currentHealth.name === 'diskSpace') {
|
||||
// Should display storage space in an human readable unit
|
||||
const val = value / 1073741824;
|
||||
if (val > 1) {
|
||||
// Value
|
||||
return `${val.toFixed(2)} GB`;
|
||||
}
|
||||
return `${(value / 1048576).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return value.toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="modal-body pad">
|
||||
<div v-if="currentHealth && currentHealth.details">
|
||||
<h5>Properties</h5>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="Health">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" scope="col">Name</th>
|
||||
<th class="text-left" scope="col">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(item, index) in currentHealth.details.details" :key="index">
|
||||
<td class="text-left">{{ index }}</td>
|
||||
<td class="text-left">{{ readableValue(item) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="currentHealth && currentHealth.error">
|
||||
<h4>Error</h4>
|
||||
<pre>{{ currentHealth.error }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./health-modal.component.ts"></script>
|
||||
@@ -0,0 +1,92 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Health from './health.vue';
|
||||
import HealthService from './health.service';
|
||||
|
||||
type HealthComponentType = InstanceType<typeof Health>;
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
};
|
||||
|
||||
describe('Health Component', () => {
|
||||
let health: HealthComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosStub.get.resolves({});
|
||||
const wrapper = shallowMount(Health, {
|
||||
global: {
|
||||
stubs: {
|
||||
bModal: true,
|
||||
'font-awesome-icon': true,
|
||||
'health-modal': true,
|
||||
},
|
||||
directives: {
|
||||
'b-modal': {},
|
||||
},
|
||||
provide: {
|
||||
healthService: new HealthService(),
|
||||
},
|
||||
},
|
||||
});
|
||||
health = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('baseName and subSystemName', () => {
|
||||
it('should return the basename when it has no sub system', () => {
|
||||
expect(health.baseName('base')).toBe('base');
|
||||
});
|
||||
|
||||
it('should return the basename when it has sub systems', () => {
|
||||
expect(health.baseName('base.subsystem.system')).toBe('base');
|
||||
});
|
||||
|
||||
it('should return the sub system name', () => {
|
||||
expect(health.subSystemName('subsystem')).toBe('');
|
||||
});
|
||||
|
||||
it('should return the subsystem when it has multiple keys', () => {
|
||||
expect(health.subSystemName('subsystem.subsystem.system')).toBe(' - subsystem.system');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBadgeClass', () => {
|
||||
it('should get badge class', () => {
|
||||
const upBadgeClass = health.getBadgeClass('UP');
|
||||
const downBadgeClass = health.getBadgeClass('DOWN');
|
||||
expect(upBadgeClass).toEqual('badge-success');
|
||||
expect(downBadgeClass).toEqual('badge-danger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should call refresh on init', async () => {
|
||||
// GIVEN
|
||||
axiosStub.get.resolves({});
|
||||
|
||||
// WHEN
|
||||
health.refresh();
|
||||
await health.$nextTick();
|
||||
|
||||
// THEN
|
||||
expect(axiosStub.get.calledWith('management/health')).toBeTruthy();
|
||||
await health.$nextTick();
|
||||
expect(health.updatingHealth).toEqual(false);
|
||||
});
|
||||
it('should handle a 503 on refreshing health data', async () => {
|
||||
// GIVEN
|
||||
axiosStub.get.rejects({});
|
||||
|
||||
// WHEN
|
||||
health.refresh();
|
||||
await health.$nextTick();
|
||||
|
||||
// THEN
|
||||
expect(axiosStub.get.calledWith('management/health')).toBeTruthy();
|
||||
await health.$nextTick();
|
||||
expect(health.updatingHealth).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { type Ref, defineComponent, inject, ref } from 'vue';
|
||||
|
||||
import HealthService from './health.service';
|
||||
import JhiHealthModal from './health-modal.vue';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiHealth',
|
||||
components: {
|
||||
'health-modal': JhiHealthModal,
|
||||
},
|
||||
setup() {
|
||||
const healthService = inject('healthService', () => new HealthService(), true);
|
||||
|
||||
const healthData: Ref<any> = ref(null);
|
||||
const currentHealth: Ref<any> = ref(null);
|
||||
const updatingHealth = ref(false);
|
||||
|
||||
return {
|
||||
healthService,
|
||||
healthData,
|
||||
currentHealth,
|
||||
updatingHealth,
|
||||
};
|
||||
},
|
||||
mounted(): void {
|
||||
this.refresh();
|
||||
},
|
||||
methods: {
|
||||
baseName(name: any): any {
|
||||
return this.healthService.getBaseName(name);
|
||||
},
|
||||
getBadgeClass(statusState: any): string {
|
||||
if (statusState === 'UP') {
|
||||
return 'badge-success';
|
||||
}
|
||||
return 'badge-danger';
|
||||
},
|
||||
refresh(): void {
|
||||
this.updatingHealth = true;
|
||||
this.healthService
|
||||
.checkHealth()
|
||||
.then(res => {
|
||||
this.healthData = this.healthService.transformHealthData(res.data);
|
||||
this.updatingHealth = false;
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.status === 503) {
|
||||
this.healthData = this.healthService.transformHealthData(error.error);
|
||||
}
|
||||
this.updatingHealth = false;
|
||||
});
|
||||
},
|
||||
showHealth(health: any): void {
|
||||
this.currentHealth = health;
|
||||
(<any>this.$refs.healthModal).show();
|
||||
},
|
||||
subSystemName(name: string): string {
|
||||
return this.healthService.getSubSystemName(name);
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,244 @@
|
||||
import HealthService from './health.service';
|
||||
|
||||
describe('Health Service', () => {
|
||||
let healthService: HealthService;
|
||||
|
||||
beforeEach(() => {
|
||||
healthService = new HealthService();
|
||||
});
|
||||
|
||||
describe('transformHealthData', () => {
|
||||
it('should flatten empty health data', () => {
|
||||
const data = {};
|
||||
const expected = [];
|
||||
expect(healthService.transformHealthData(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should flatten health data with no subsystems', () => {
|
||||
const data = {
|
||||
components: {
|
||||
status: 'UP',
|
||||
db: {
|
||||
status: 'UP',
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
mail: {
|
||||
status: 'UP',
|
||||
error: 'mail.a.b.c',
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [
|
||||
{
|
||||
name: 'db',
|
||||
status: 'UP',
|
||||
details: {
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mail',
|
||||
error: 'mail.a.b.c',
|
||||
status: 'UP',
|
||||
},
|
||||
];
|
||||
expect(healthService.transformHealthData(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should flatten health data with subsystems at level 1, main system has no additional information', () => {
|
||||
const data = {
|
||||
components: {
|
||||
status: 'UP',
|
||||
db: {
|
||||
status: 'UP',
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
mail: {
|
||||
status: 'UP',
|
||||
error: 'mail.a.b.c',
|
||||
},
|
||||
system: {
|
||||
status: 'DOWN',
|
||||
subsystem1: {
|
||||
status: 'UP',
|
||||
property1: 'system.subsystem1.property1',
|
||||
},
|
||||
subsystem2: {
|
||||
status: 'DOWN',
|
||||
error: 'system.subsystem1.error',
|
||||
property2: 'system.subsystem2.property2',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [
|
||||
{
|
||||
name: 'db',
|
||||
status: 'UP',
|
||||
details: {
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mail',
|
||||
error: 'mail.a.b.c',
|
||||
status: 'UP',
|
||||
},
|
||||
{
|
||||
name: 'system.subsystem1',
|
||||
status: 'UP',
|
||||
details: {
|
||||
property1: 'system.subsystem1.property1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'system.subsystem2',
|
||||
error: 'system.subsystem1.error',
|
||||
status: 'DOWN',
|
||||
details: {
|
||||
property2: 'system.subsystem2.property2',
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(healthService.transformHealthData(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should flatten health data with subsystems at level 1, main system has additional information', () => {
|
||||
const data = {
|
||||
components: {
|
||||
status: 'UP',
|
||||
db: {
|
||||
status: 'UP',
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
mail: {
|
||||
status: 'UP',
|
||||
error: 'mail.a.b.c',
|
||||
},
|
||||
system: {
|
||||
status: 'DOWN',
|
||||
property1: 'system.property1',
|
||||
subsystem1: {
|
||||
status: 'UP',
|
||||
property1: 'system.subsystem1.property1',
|
||||
},
|
||||
subsystem2: {
|
||||
status: 'DOWN',
|
||||
error: 'system.subsystem1.error',
|
||||
property2: 'system.subsystem2.property2',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [
|
||||
{
|
||||
name: 'db',
|
||||
status: 'UP',
|
||||
details: {
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mail',
|
||||
error: 'mail.a.b.c',
|
||||
status: 'UP',
|
||||
},
|
||||
{
|
||||
name: 'system',
|
||||
status: 'DOWN',
|
||||
details: {
|
||||
property1: 'system.property1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'system.subsystem1',
|
||||
status: 'UP',
|
||||
details: {
|
||||
property1: 'system.subsystem1.property1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'system.subsystem2',
|
||||
error: 'system.subsystem1.error',
|
||||
status: 'DOWN',
|
||||
details: {
|
||||
property2: 'system.subsystem2.property2',
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(healthService.transformHealthData(data)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should flatten health data with subsystems at level 1, main system has additional error', () => {
|
||||
const data = {
|
||||
components: {
|
||||
status: 'UP',
|
||||
db: {
|
||||
status: 'UP',
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
mail: {
|
||||
status: 'UP',
|
||||
error: 'mail.a.b.c',
|
||||
},
|
||||
system: {
|
||||
status: 'DOWN',
|
||||
error: 'show me',
|
||||
subsystem1: {
|
||||
status: 'UP',
|
||||
property1: 'system.subsystem1.property1',
|
||||
},
|
||||
subsystem2: {
|
||||
status: 'DOWN',
|
||||
error: 'system.subsystem1.error',
|
||||
property2: 'system.subsystem2.property2',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const expected = [
|
||||
{
|
||||
name: 'db',
|
||||
status: 'UP',
|
||||
details: {
|
||||
database: 'H2',
|
||||
hello: '1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'mail',
|
||||
error: 'mail.a.b.c',
|
||||
status: 'UP',
|
||||
},
|
||||
{
|
||||
name: 'system',
|
||||
error: 'show me',
|
||||
status: 'DOWN',
|
||||
},
|
||||
{
|
||||
name: 'system.subsystem1',
|
||||
status: 'UP',
|
||||
details: {
|
||||
property1: 'system.subsystem1.property1',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'system.subsystem2',
|
||||
error: 'system.subsystem1.error',
|
||||
status: 'DOWN',
|
||||
details: {
|
||||
property2: 'system.subsystem2.property2',
|
||||
},
|
||||
},
|
||||
];
|
||||
expect(healthService.transformHealthData(data)).toEqual(expected);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
import axios, { type AxiosPromise } from 'axios';
|
||||
|
||||
export default class HealthService {
|
||||
public separator: string;
|
||||
|
||||
constructor() {
|
||||
this.separator = '.';
|
||||
}
|
||||
|
||||
public checkHealth(): AxiosPromise<any> {
|
||||
return axios.get('management/health');
|
||||
}
|
||||
|
||||
public transformHealthData(data: any): any {
|
||||
const response = [];
|
||||
this.flattenHealthData(response, null, data.components);
|
||||
return response;
|
||||
}
|
||||
|
||||
public getBaseName(name: string): string {
|
||||
if (name) {
|
||||
const split = name.split('.');
|
||||
return split[0];
|
||||
}
|
||||
}
|
||||
|
||||
public getSubSystemName(name: string): string {
|
||||
if (name) {
|
||||
const split = name.split('.');
|
||||
split.splice(0, 1);
|
||||
const remainder = split.join('.');
|
||||
return remainder ? ` - ${remainder}` : '';
|
||||
}
|
||||
}
|
||||
|
||||
public addHealthObject(result: any, isLeaf: boolean, healthObject: any, name: string) {
|
||||
const healthData = {
|
||||
name,
|
||||
details: undefined,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const details = {};
|
||||
let hasDetails = false;
|
||||
|
||||
for (const key in healthObject) {
|
||||
if (Object.hasOwn(healthObject, key)) {
|
||||
const value = healthObject[key];
|
||||
if (key === 'status' || key === 'error') {
|
||||
healthData[key] = value;
|
||||
} else {
|
||||
if (!this.isHealthObject(value)) {
|
||||
details[key] = value;
|
||||
hasDetails = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the details
|
||||
if (hasDetails) {
|
||||
healthData.details = details;
|
||||
}
|
||||
|
||||
// Only add nodes if they provide additional information
|
||||
if (isLeaf || hasDetails || healthData.error) {
|
||||
result.push(healthData);
|
||||
}
|
||||
return healthData;
|
||||
}
|
||||
|
||||
public flattenHealthData(result: any, path: any, data: any): any {
|
||||
for (const key in data) {
|
||||
if (Object.hasOwn(data, key)) {
|
||||
const value = data[key];
|
||||
if (this.isHealthObject(value)) {
|
||||
if (this.hasSubSystem(value)) {
|
||||
this.addHealthObject(result, false, value, this.getModuleName(path, key));
|
||||
this.flattenHealthData(result, this.getModuleName(path, key), value);
|
||||
} else {
|
||||
this.addHealthObject(result, true, value, this.getModuleName(path, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public getModuleName(path: any, name: string) {
|
||||
if (path && name) {
|
||||
return path + this.separator + name;
|
||||
} else if (path) {
|
||||
return path;
|
||||
} else if (name) {
|
||||
return name;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
public hasSubSystem(healthObject: any): any {
|
||||
let result = false;
|
||||
|
||||
for (const key in healthObject) {
|
||||
if (Object.hasOwn(healthObject, key)) {
|
||||
const value = healthObject[key];
|
||||
if (value && value.status) {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public isHealthObject(healthObject: any): any {
|
||||
let result = false;
|
||||
|
||||
for (const key in healthObject) {
|
||||
if (Object.hasOwn(healthObject, key)) {
|
||||
if (key === 'status') {
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>
|
||||
<span id="health-page-heading" data-cy="healthPageHeading">Health Checks</span>
|
||||
<button class="btn btn-primary float-right" @click="refresh()" :disabled="updatingHealth">
|
||||
<font-awesome-icon icon="sync"></font-awesome-icon> <span>Refresh</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div class="table-responsive">
|
||||
<table id="healthCheck" class="table table-striped" aria-describedby="Health check">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Service name</th>
|
||||
<th class="text-center" scope="col">Status</th>
|
||||
<th class="text-center" scope="col">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="health of healthData" :key="health.name">
|
||||
<td>
|
||||
<span class="text-capitalize">{{ baseName(health.name) }}</span> {{ subSystemName(health.name) }}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge" :class="getBadgeClass(health.status)">
|
||||
{{ health.status }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<a class="hand" @click="showHealth(health)" v-if="health.details || health.error">
|
||||
<font-awesome-icon icon="eye"></font-awesome-icon>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<b-modal ref="healthModal">
|
||||
<template #modal-title>
|
||||
<h4 v-if="currentHealth" class="modal-title" id="showHealthLabel">
|
||||
<span class="text-capitalize">{{ baseName(currentHealth.name) }}</span>
|
||||
{{ subSystemName(currentHealth.name) }}
|
||||
</h4>
|
||||
</template>
|
||||
<health-modal :current-health="currentHealth"></health-modal>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./health.component.ts"></script>
|
||||
@@ -0,0 +1,63 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
import Logs from './logs.vue';
|
||||
|
||||
type LogsComponentType = InstanceType<typeof Logs>;
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
post: sinon.stub(axios, 'post'),
|
||||
};
|
||||
|
||||
describe('Logs Component', () => {
|
||||
let logs: LogsComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosStub.get.resolves({});
|
||||
const wrapper = shallowMount(Logs);
|
||||
logs = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('OnInit', () => {
|
||||
it('should set all default values correctly', () => {
|
||||
expect(logs.filtered).toBe('');
|
||||
expect(logs.orderProp).toBe('name');
|
||||
expect(logs.reverse).toBe(false);
|
||||
});
|
||||
|
||||
it('Should call load all on init', async () => {
|
||||
// WHEN
|
||||
logs.init();
|
||||
await logs.$nextTick();
|
||||
|
||||
// THEN
|
||||
expect(axiosStub.get.calledWith('management/loggers')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('change log level', () => {
|
||||
it('should change log level correctly', async () => {
|
||||
axiosStub.post.resolves({});
|
||||
|
||||
// WHEN
|
||||
logs.updateLevel('main', 'ERROR');
|
||||
await logs.$nextTick();
|
||||
|
||||
// THEN
|
||||
expect(axiosStub.post.calledWith('management/loggers/main', { configuredLevel: 'ERROR' })).toBeTruthy();
|
||||
expect(axiosStub.get.calledWith('management/loggers')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('change order', () => {
|
||||
it('should change order and invert reverse', () => {
|
||||
// WHEN
|
||||
logs.changeOrder('dummy-order');
|
||||
|
||||
// THEN
|
||||
expect(logs.orderProp).toEqual('dummy-order');
|
||||
expect(logs.reverse).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { type Ref, computed, defineComponent, inject, ref } from 'vue';
|
||||
|
||||
import LogsService from './logs.service';
|
||||
import { orderAndFilterBy } from '@/shared/computables';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiLogs',
|
||||
setup() {
|
||||
const logsService = inject('logsService', () => new LogsService(), true);
|
||||
|
||||
const loggers: Ref<any[]> = ref([]);
|
||||
const filtered = ref('');
|
||||
const orderProp = ref('name');
|
||||
const reverse = ref(false);
|
||||
const filteredLoggers = computed(() =>
|
||||
orderAndFilterBy(loggers.value, {
|
||||
filterByTerm: filtered.value,
|
||||
orderByProp: orderProp.value,
|
||||
reverse: reverse.value,
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
logsService,
|
||||
loggers,
|
||||
filtered,
|
||||
orderProp,
|
||||
reverse,
|
||||
filteredLoggers,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.init();
|
||||
},
|
||||
methods: {
|
||||
init(): void {
|
||||
this.logsService.findAll().then(response => {
|
||||
this.extractLoggers(response);
|
||||
});
|
||||
},
|
||||
updateLevel(name: string, level: string): void {
|
||||
this.logsService.changeLevel(name, level).then(() => {
|
||||
this.init();
|
||||
});
|
||||
},
|
||||
changeOrder(orderProp: string): void {
|
||||
this.orderProp = orderProp;
|
||||
this.reverse = !this.reverse;
|
||||
},
|
||||
extractLoggers(response) {
|
||||
this.loggers = [];
|
||||
if (response.data) {
|
||||
for (const key of Object.keys(response.data.loggers)) {
|
||||
const logger = response.data.loggers[key];
|
||||
this.loggers.push({ name: key, level: logger.effectiveLevel });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import axios, { type AxiosPromise } from 'axios';
|
||||
|
||||
export default class LogsService {
|
||||
public changeLevel(name: string, configuredLevel: string): AxiosPromise<any> {
|
||||
return axios.post(`management/loggers/${name}`, { configuredLevel });
|
||||
}
|
||||
|
||||
public findAll(): AxiosPromise<any> {
|
||||
return axios.get('management/loggers');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="table-responsive">
|
||||
<h2 id="logs-page-heading" data-cy="logsPageHeading">Logs</h2>
|
||||
|
||||
<div v-if="loggers">
|
||||
<p>There are {{ loggers.length }} loggers.</p>
|
||||
|
||||
<span>Filter</span> <input type="text" v-model="filtered" class="form-control" />
|
||||
|
||||
<table class="table table-sm table-striped table-bordered" aria-describedby="Logs">
|
||||
<thead>
|
||||
<tr title="click to order">
|
||||
<th @click="changeOrder('name')" scope="col"><span>Name</span></th>
|
||||
<th @click="changeOrder('level')" scope="col"><span>Level</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tr v-for="logger in filteredLoggers" :key="logger.name">
|
||||
<td>
|
||||
<small>{{ logger.name }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
@click="updateLevel(logger.name, 'TRACE')"
|
||||
:class="logger.level === 'TRACE' ? 'btn-primary' : 'btn-light'"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
TRACE
|
||||
</button>
|
||||
<button
|
||||
@click="updateLevel(logger.name, 'DEBUG')"
|
||||
:class="logger.level === 'DEBUG' ? 'btn-success' : 'btn-light'"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
DEBUG
|
||||
</button>
|
||||
<button
|
||||
@click="updateLevel(logger.name, 'INFO')"
|
||||
:class="logger.level === 'INFO' ? 'btn-info' : 'btn-light'"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
INFO
|
||||
</button>
|
||||
<button
|
||||
@click="updateLevel(logger.name, 'WARN')"
|
||||
:class="logger.level === 'WARN' ? 'btn-warning' : 'btn-light'"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
WARN
|
||||
</button>
|
||||
<button
|
||||
@click="updateLevel(logger.name, 'ERROR')"
|
||||
:class="logger.level === 'ERROR' ? 'btn-danger' : 'btn-light'"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
ERROR
|
||||
</button>
|
||||
<button
|
||||
@click="updateLevel(logger.name, 'OFF')"
|
||||
:class="logger.level === 'OFF' ? 'btn-secondary' : 'btn-light'"
|
||||
class="btn btn-sm"
|
||||
>
|
||||
OFF
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./logs.component.ts"></script>
|
||||
@@ -0,0 +1,57 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
|
||||
import MetricsModal from './metrics-modal.vue';
|
||||
|
||||
type MetricsModalComponentType = InstanceType<typeof MetricsModal>;
|
||||
|
||||
describe('Metrics Component', () => {
|
||||
let metricsModal: MetricsModalComponentType;
|
||||
|
||||
beforeEach(() => {
|
||||
const wrapper = shallowMount(MetricsModal, {
|
||||
propsData: {
|
||||
threadDump: [
|
||||
{ name: 'test1', threadState: 'RUNNABLE' },
|
||||
{ name: 'test2', threadState: 'WAITING' },
|
||||
{ name: 'test3', threadState: 'TIMED_WAITING' },
|
||||
{ name: 'test4', threadState: 'BLOCKED' },
|
||||
{ name: 'test5', threadState: 'BLOCKED' },
|
||||
{ name: 'test5', threadState: 'NONE' },
|
||||
],
|
||||
},
|
||||
});
|
||||
metricsModal = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('init', () => {
|
||||
it('should count the numbers of each thread type', async () => {
|
||||
expect(metricsModal.threadDumpData.threadDumpRunnable).toBe(1);
|
||||
expect(metricsModal.threadDumpData.threadDumpWaiting).toBe(1);
|
||||
expect(metricsModal.threadDumpData.threadDumpTimedWaiting).toBe(1);
|
||||
expect(metricsModal.threadDumpData.threadDumpBlocked).toBe(2);
|
||||
expect(metricsModal.threadDumpData.threadDumpAll).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBadgeClass', () => {
|
||||
it('should return badge-success for RUNNABLE', () => {
|
||||
expect(metricsModal.getBadgeClass('RUNNABLE')).toBe('badge-success');
|
||||
});
|
||||
|
||||
it('should return badge-info for WAITING', () => {
|
||||
expect(metricsModal.getBadgeClass('WAITING')).toBe('badge-info');
|
||||
});
|
||||
|
||||
it('should return badge-warning for TIMED_WAITING', () => {
|
||||
expect(metricsModal.getBadgeClass('TIMED_WAITING')).toBe('badge-warning');
|
||||
});
|
||||
|
||||
it('should return badge-danger for BLOCKED', () => {
|
||||
expect(metricsModal.getBadgeClass('BLOCKED')).toBe('badge-danger');
|
||||
});
|
||||
|
||||
it('should return undefined for anything else', () => {
|
||||
expect(metricsModal.getBadgeClass('')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import { type PropType, type Ref, computed, defineComponent, ref } from 'vue';
|
||||
|
||||
import { filterBy } from '@/shared/computables';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiMetricsModal',
|
||||
props: {
|
||||
threadDump: {
|
||||
type: Array as PropType<any[]>,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const threadDumpFilter: Ref<any> = ref('');
|
||||
const filteredThreadDump = computed(() => filterBy(props.threadDump, { filterByTerm: threadDumpFilter.value }));
|
||||
|
||||
const threadDumpData = computed(() => {
|
||||
const data = {
|
||||
threadDumpAll: 0,
|
||||
threadDumpBlocked: 0,
|
||||
threadDumpRunnable: 0,
|
||||
threadDumpTimedWaiting: 0,
|
||||
threadDumpWaiting: 0,
|
||||
};
|
||||
if (props.threadDump) {
|
||||
props.threadDump.forEach(value => {
|
||||
if (value.threadState === 'RUNNABLE') {
|
||||
data.threadDumpRunnable += 1;
|
||||
} else if (value.threadState === 'WAITING') {
|
||||
data.threadDumpWaiting += 1;
|
||||
} else if (value.threadState === 'TIMED_WAITING') {
|
||||
data.threadDumpTimedWaiting += 1;
|
||||
} else if (value.threadState === 'BLOCKED') {
|
||||
data.threadDumpBlocked += 1;
|
||||
}
|
||||
});
|
||||
data.threadDumpAll = data.threadDumpRunnable + data.threadDumpWaiting + data.threadDumpTimedWaiting + data.threadDumpBlocked;
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
return {
|
||||
threadDumpFilter,
|
||||
threadDumpData,
|
||||
filteredThreadDump,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
getBadgeClass(threadState: string): string {
|
||||
if (threadState === 'RUNNABLE') {
|
||||
return 'badge-success';
|
||||
} else if (threadState === 'WAITING') {
|
||||
return 'badge-info';
|
||||
} else if (threadState === 'TIMED_WAITING') {
|
||||
return 'badge-warning';
|
||||
} else if (threadState === 'BLOCKED') {
|
||||
return 'badge-danger';
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="modal-body">
|
||||
<span class="badge badge-primary" @click="threadDumpFilter = ''"
|
||||
>All <span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpAll }}</span></span
|
||||
>
|
||||
<span class="badge badge-success" @click="threadDumpFilter = 'RUNNABLE'"
|
||||
>Runnable <span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpRunnable }}</span></span
|
||||
>
|
||||
<span class="badge badge-info" @click="threadDumpFilter = 'WAITING'"
|
||||
>Waiting <span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpWaiting }}</span></span
|
||||
>
|
||||
<span class="badge badge-warning" @click="threadDumpFilter = 'TIMED_WAITING'"
|
||||
>Timed Waiting <span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpTimedWaiting }}</span></span
|
||||
>
|
||||
<span class="badge badge-danger" @click="threadDumpFilter = 'BLOCKED'"
|
||||
>Blocked <span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpBlocked }}</span></span
|
||||
>
|
||||
<div class="mt-2"> </div>
|
||||
Filter
|
||||
<input type="text" v-model="threadDumpFilter" class="form-control" />
|
||||
<div class="pad" v-for="(entry, key) of filteredThreadDump" :key="key">
|
||||
<h6>
|
||||
<span class="badge" :class="getBadgeClass(entry.threadState)">{{ entry.threadState }}</span
|
||||
> {{ entry.threadName }} (ID {{ entry.threadId }})
|
||||
<a @click="entry.show = !entry.show" href="javascript:void(0);">
|
||||
<span :hidden="entry.show">Show Stacktrace</span>
|
||||
<span :hidden="!entry.show">Hide Stacktrace</span>
|
||||
</a>
|
||||
</h6>
|
||||
<div class="card" :hidden="!entry.show">
|
||||
<div class="card-body">
|
||||
<div v-for="(st, key) of entry.stackTrace" :key="key" class="break">
|
||||
<samp
|
||||
>{{ st.className }}.{{ st.methodName }}(<code>{{ st.fileName }}:{{ st.lineNumber }}</code
|
||||
>)</samp
|
||||
>
|
||||
<span class="mt-1"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table table-sm table-responsive" aria-describedby="Metrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Blocked Time</th>
|
||||
<th scope="col">Blocked Count</th>
|
||||
<th scope="col">Waited Time</th>
|
||||
<th scope="col">Waited Count</th>
|
||||
<th scope="col">Lock name</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ entry.blockedTime }}</td>
|
||||
<td>{{ entry.blockedCount }}</td>
|
||||
<td>{{ entry.waitedTime }}</td>
|
||||
<td>{{ entry.waitedCount }}</td>
|
||||
<td class="thread-dump-modal-lock" :title="entry.lockName">
|
||||
<code>{{ entry.lockName }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./metrics-modal.component.ts"></script>
|
||||
@@ -0,0 +1,271 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import axios from 'axios';
|
||||
import sinon from 'sinon';
|
||||
|
||||
import Metrics from './metrics.vue';
|
||||
import MetricsService from './metrics.service';
|
||||
|
||||
type MetricsComponentType = InstanceType<typeof Metrics>;
|
||||
|
||||
const axiosStub = {
|
||||
get: sinon.stub(axios, 'get'),
|
||||
};
|
||||
|
||||
describe('Metrics Component', () => {
|
||||
let metricsComponent: MetricsComponentType;
|
||||
const response = {
|
||||
jvm: {
|
||||
'PS Eden Space': {
|
||||
committed: 5.57842432e8,
|
||||
max: 6.49592832e8,
|
||||
used: 4.20828184e8,
|
||||
},
|
||||
'Code Cache': {
|
||||
committed: 2.3461888e7,
|
||||
max: 2.5165824e8,
|
||||
used: 2.2594368e7,
|
||||
},
|
||||
'Compressed Class Space': {
|
||||
committed: 1.2320768e7,
|
||||
max: 1.073741824e9,
|
||||
used: 1.1514008e7,
|
||||
},
|
||||
'PS Survivor Space': {
|
||||
committed: 1.5204352e7,
|
||||
max: 1.5204352e7,
|
||||
used: 1.2244376e7,
|
||||
},
|
||||
'PS Old Gen': {
|
||||
committed: 1.10624768e8,
|
||||
max: 1.37887744e9,
|
||||
used: 4.1390776e7,
|
||||
},
|
||||
Metaspace: {
|
||||
committed: 9.170944e7,
|
||||
max: -1.0,
|
||||
used: 8.7377552e7,
|
||||
},
|
||||
},
|
||||
databases: {
|
||||
min: {
|
||||
value: 10.0,
|
||||
},
|
||||
max: {
|
||||
value: 10.0,
|
||||
},
|
||||
idle: {
|
||||
value: 10.0,
|
||||
},
|
||||
usage: {
|
||||
'0.0': 0.0,
|
||||
'1.0': 0.0,
|
||||
max: 0.0,
|
||||
totalTime: 4210.0,
|
||||
mean: 701.6666666666666,
|
||||
'0.5': 0.0,
|
||||
count: 6,
|
||||
'0.99': 0.0,
|
||||
'0.75': 0.0,
|
||||
'0.95': 0.0,
|
||||
},
|
||||
pending: {
|
||||
value: 0.0,
|
||||
},
|
||||
active: {
|
||||
value: 0.0,
|
||||
},
|
||||
acquire: {
|
||||
'0.0': 0.0,
|
||||
'1.0': 0.0,
|
||||
max: 0.0,
|
||||
totalTime: 0.884426,
|
||||
mean: 0.14740433333333333,
|
||||
'0.5': 0.0,
|
||||
count: 6,
|
||||
'0.99': 0.0,
|
||||
'0.75': 0.0,
|
||||
'0.95': 0.0,
|
||||
},
|
||||
creation: {
|
||||
'0.0': 0.0,
|
||||
'1.0': 0.0,
|
||||
max: 0.0,
|
||||
totalTime: 27.0,
|
||||
mean: 3.0,
|
||||
'0.5': 0.0,
|
||||
count: 9,
|
||||
'0.99': 0.0,
|
||||
'0.75': 0.0,
|
||||
'0.95': 0.0,
|
||||
},
|
||||
connections: {
|
||||
value: 10.0,
|
||||
},
|
||||
},
|
||||
'http.server.requests': {
|
||||
all: {
|
||||
count: 5,
|
||||
},
|
||||
percode: {
|
||||
'200': {
|
||||
max: 0.0,
|
||||
mean: 298.9012628,
|
||||
count: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
cache: {
|
||||
usersByEmail: {
|
||||
'cache.gets.miss': 0.0,
|
||||
'cache.puts': 0.0,
|
||||
'cache.gets.hit': 0.0,
|
||||
'cache.removals': 0.0,
|
||||
'cache.evictions': 0.0,
|
||||
},
|
||||
usersByLogin: {
|
||||
'cache.gets.miss': 1.0,
|
||||
'cache.puts': 1.0,
|
||||
'cache.gets.hit': 1.0,
|
||||
'cache.removals': 0.0,
|
||||
'cache.evictions': 0.0,
|
||||
},
|
||||
'tech.jhipster.domain.Authority': {
|
||||
'cache.gets.miss': 0.0,
|
||||
'cache.puts': 2.0,
|
||||
'cache.gets.hit': 0.0,
|
||||
'cache.removals': 0.0,
|
||||
'cache.evictions': 0.0,
|
||||
},
|
||||
'tech.jhipster.domain.User.authorities': {
|
||||
'cache.gets.miss': 0.0,
|
||||
'cache.puts': 1.0,
|
||||
'cache.gets.hit': 0.0,
|
||||
'cache.removals': 0.0,
|
||||
'cache.evictions': 0.0,
|
||||
},
|
||||
'tech.jhipster.domain.User': {
|
||||
'cache.gets.miss': 0.0,
|
||||
'cache.puts': 1.0,
|
||||
'cache.gets.hit': 0.0,
|
||||
'cache.removals': 0.0,
|
||||
'cache.evictions': 0.0,
|
||||
},
|
||||
},
|
||||
garbageCollector: {
|
||||
'jvm.gc.max.data.size': 1.37887744e9,
|
||||
'jvm.gc.pause': {
|
||||
'0.0': 0.0,
|
||||
'1.0': 0.0,
|
||||
max: 0.0,
|
||||
totalTime: 242.0,
|
||||
mean: 242.0,
|
||||
'0.5': 0.0,
|
||||
count: 1,
|
||||
'0.99': 0.0,
|
||||
'0.75': 0.0,
|
||||
'0.95': 0.0,
|
||||
},
|
||||
'jvm.gc.memory.promoted': 2.992732e7,
|
||||
'jvm.gc.memory.allocated': 1.26362872e9,
|
||||
classesLoaded: 17393.0,
|
||||
'jvm.gc.live.data.size': 3.1554408e7,
|
||||
classesUnloaded: 0.0,
|
||||
},
|
||||
services: {
|
||||
'/management/info': {
|
||||
GET: {
|
||||
max: 0.0,
|
||||
mean: 104.952893,
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
'/api/authenticate': {
|
||||
POST: {
|
||||
max: 0.0,
|
||||
mean: 909.53003,
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
'/api/account': {
|
||||
GET: {
|
||||
max: 0.0,
|
||||
mean: 141.209628,
|
||||
count: 1,
|
||||
},
|
||||
},
|
||||
'/**': {
|
||||
GET: {
|
||||
max: 0.0,
|
||||
mean: 169.4068815,
|
||||
count: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
processMetrics: {
|
||||
'system.load.average.1m': 3.63,
|
||||
'system.cpu.usage': 0.5724934148485453,
|
||||
'system.cpu.count': 4.0,
|
||||
'process.start.time': 1.548140811306e12,
|
||||
'process.files.open': 205.0,
|
||||
'process.cpu.usage': 0.003456347568026252,
|
||||
'process.uptime': 88404.0,
|
||||
'process.files.max': 1048576.0,
|
||||
},
|
||||
threads: [{ name: 'test1', threadState: 'RUNNABLE' }],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
axiosStub.get.resolves({ data: { timers: [], gauges: [] } });
|
||||
const wrapper = shallowMount(Metrics, {
|
||||
global: {
|
||||
stubs: {
|
||||
bModal: true,
|
||||
bProgress: true,
|
||||
bProgressBar: true,
|
||||
'font-awesome-icon': true,
|
||||
'metrics-modal': true,
|
||||
},
|
||||
directives: {
|
||||
'b-modal': {},
|
||||
'b-progress': {},
|
||||
'b-progress-bar': {},
|
||||
},
|
||||
provide: {
|
||||
metricsService: new MetricsService(),
|
||||
},
|
||||
},
|
||||
});
|
||||
metricsComponent = wrapper.vm;
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should call refresh on init', async () => {
|
||||
// GIVEN
|
||||
axiosStub.get.resolves({ data: response });
|
||||
|
||||
// WHEN
|
||||
await metricsComponent.refresh();
|
||||
await metricsComponent.$nextTick();
|
||||
|
||||
// THEN
|
||||
expect(axiosStub.get.calledWith('management/jhimetrics')).toBeTruthy();
|
||||
expect(axiosStub.get.calledWith('management/threaddump')).toBeTruthy();
|
||||
expect(metricsComponent.metrics).toHaveProperty('jvm');
|
||||
expect(metricsComponent.metrics).toEqual(response);
|
||||
expect(metricsComponent.threadStats).toEqual({
|
||||
threadDumpRunnable: 1,
|
||||
threadDumpWaiting: 0,
|
||||
threadDumpTimedWaiting: 0,
|
||||
threadDumpBlocked: 0,
|
||||
threadDumpAll: 1,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isNan', () => {
|
||||
it('should return if a variable is NaN', () => {
|
||||
expect(metricsComponent.filterNaN(1)).toBe(1);
|
||||
expect(metricsComponent.filterNaN('test')).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import { type Ref, defineComponent, inject, ref } from 'vue';
|
||||
import numeral from 'numeral';
|
||||
|
||||
import JhiMetricsModal from './metrics-modal.vue';
|
||||
import MetricsService from './metrics.service';
|
||||
import { useDateFormat } from '@/shared/composables';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiMetrics',
|
||||
components: {
|
||||
'metrics-modal': JhiMetricsModal,
|
||||
},
|
||||
setup() {
|
||||
const { formatDate } = useDateFormat();
|
||||
const metricsService = inject('metricsService', () => new MetricsService(), true);
|
||||
|
||||
const metrics: Ref<any> = ref({});
|
||||
const threadData: Ref<any> = ref(null);
|
||||
const threadStats: Ref<any> = ref({});
|
||||
const updatingMetrics = ref(true);
|
||||
|
||||
return {
|
||||
metricsService,
|
||||
metrics,
|
||||
threadData,
|
||||
threadStats,
|
||||
updatingMetrics,
|
||||
formatDate,
|
||||
};
|
||||
},
|
||||
mounted(): void {
|
||||
this.refresh();
|
||||
},
|
||||
methods: {
|
||||
refresh() {
|
||||
return this.metricsService
|
||||
.getMetrics()
|
||||
.then(resultsMetrics => {
|
||||
this.metrics = resultsMetrics.data;
|
||||
this.metricsService
|
||||
.retrieveThreadDump()
|
||||
.then(res => {
|
||||
this.updatingMetrics = true;
|
||||
this.threadData = res.data.threads;
|
||||
|
||||
this.threadStats = {
|
||||
threadDumpRunnable: 0,
|
||||
threadDumpWaiting: 0,
|
||||
threadDumpTimedWaiting: 0,
|
||||
threadDumpBlocked: 0,
|
||||
threadDumpAll: 0,
|
||||
};
|
||||
|
||||
this.threadData.forEach(value => {
|
||||
if (value.threadState === 'RUNNABLE') {
|
||||
this.threadStats.threadDumpRunnable += 1;
|
||||
} else if (value.threadState === 'WAITING') {
|
||||
this.threadStats.threadDumpWaiting += 1;
|
||||
} else if (value.threadState === 'TIMED_WAITING') {
|
||||
this.threadStats.threadDumpTimedWaiting += 1;
|
||||
} else if (value.threadState === 'BLOCKED') {
|
||||
this.threadStats.threadDumpBlocked += 1;
|
||||
}
|
||||
});
|
||||
|
||||
this.threadStats.threadDumpAll =
|
||||
this.threadStats.threadDumpRunnable +
|
||||
this.threadStats.threadDumpWaiting +
|
||||
this.threadStats.threadDumpTimedWaiting +
|
||||
this.threadStats.threadDumpBlocked;
|
||||
|
||||
this.updatingMetrics = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.updatingMetrics = true;
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
this.updatingMetrics = true;
|
||||
});
|
||||
},
|
||||
openModal(): void {
|
||||
if ((<any>this.$refs.metricsModal).show) {
|
||||
(<any>this.$refs.metricsModal).show();
|
||||
}
|
||||
},
|
||||
filterNaN(input: any): any {
|
||||
if (isNaN(input)) {
|
||||
return 0;
|
||||
}
|
||||
return input;
|
||||
},
|
||||
formatNumber1(value: any): any {
|
||||
return numeral(value).format('0,0');
|
||||
},
|
||||
formatNumber2(value: any): any {
|
||||
return numeral(value).format('0,00');
|
||||
},
|
||||
convertMillisecondsToDuration(ms) {
|
||||
const times = {
|
||||
year: 31557600000,
|
||||
month: 2629746000,
|
||||
day: 86400000,
|
||||
hour: 3600000,
|
||||
minute: 60000,
|
||||
second: 1000,
|
||||
};
|
||||
let time_string = '';
|
||||
let plural = '';
|
||||
for (const key in times) {
|
||||
if (Math.floor(ms / times[key]) > 0) {
|
||||
if (Math.floor(ms / times[key]) > 1) {
|
||||
plural = 's';
|
||||
} else {
|
||||
plural = '';
|
||||
}
|
||||
time_string += `${Math.floor(ms / times[key])} ${key}${plural} `;
|
||||
ms = ms - times[key] * Math.floor(ms / times[key]);
|
||||
}
|
||||
}
|
||||
return time_string;
|
||||
},
|
||||
isObjectExisting(metrics: any, key: string): boolean {
|
||||
return metrics && metrics[key];
|
||||
},
|
||||
isObjectExistingAndNotEmpty(metrics: any, key: string): boolean {
|
||||
return this.isObjectExisting(metrics, key) && JSON.stringify(metrics[key]) !== '{}';
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import axios, { type AxiosPromise } from 'axios';
|
||||
|
||||
export default class MetricsService {
|
||||
public getMetrics(): AxiosPromise<any> {
|
||||
return axios.get('management/jhimetrics');
|
||||
}
|
||||
|
||||
public retrieveThreadDump(): AxiosPromise<any> {
|
||||
return axios.get('management/threaddump');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>
|
||||
<span id="metrics-page-heading" data-cy="metricsPageHeading">Application Metrics</span>
|
||||
<button class="btn btn-primary float-right" @click="refresh()">
|
||||
<font-awesome-icon icon="sync"></font-awesome-icon> <span>Refresh</span>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<h3>JVM Metrics</h3>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-4">
|
||||
<h4>Memory</h4>
|
||||
<div>
|
||||
<div v-for="(entry, key) of metrics.jvm" :key="key">
|
||||
<span v-if="entry.max !== -1">
|
||||
<span>{{ key }}</span> ({{ formatNumber1(entry.used / 1048576) }}M / {{ formatNumber1(entry.max / 1048576) }}M)
|
||||
</span>
|
||||
<span v-else>
|
||||
<span>{{ key }}</span> {{ formatNumber1(entry.used / 1048576) }}M
|
||||
</span>
|
||||
<div>Committed : {{ formatNumber1(entry.committed / 1048576) }}M</div>
|
||||
<b-progress v-if="entry.max !== -1" variant="success" animated :max="entry.max" striped>
|
||||
<b-progress-bar :value="entry.used" :label="formatNumber1((entry.used * 100) / entry.max) + '%'"> </b-progress-bar>
|
||||
</b-progress>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>Threads</h4>
|
||||
<span><span>Runnable</span> {{ threadStats.threadDumpRunnable }}</span>
|
||||
<b-progress variant="success" :max="threadStats.threadDumpAll" striped>
|
||||
<b-progress-bar
|
||||
:value="threadStats.threadDumpRunnable"
|
||||
:label="formatNumber1((threadStats.threadDumpRunnable * 100) / threadStats.threadDumpAll) + '%'"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
<span><span>Timed waiting</span> ({{ threadStats.threadDumpTimedWaiting }})</span>
|
||||
<b-progress variant="success" :max="threadStats.threadDumpAll" striped>
|
||||
<b-progress-bar
|
||||
:value="threadStats.threadDumpTimedWaiting"
|
||||
:label="formatNumber1((threadStats.threadDumpTimedWaiting * 100) / threadStats.threadDumpAll) + '%'"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
<span><span>Waiting</span> ({{ threadStats.threadDumpWaiting }})</span>
|
||||
<b-progress variant="success" :max="threadStats.threadDumpAll" striped>
|
||||
<b-progress-bar
|
||||
:value="threadStats.threadDumpWaiting"
|
||||
:label="formatNumber1((threadStats.threadDumpWaiting * 100) / threadStats.threadDumpAll) + '%'"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
<span><span>Blocked</span> ({{ threadStats.threadDumpBlocked }})</span>
|
||||
<b-progress variant="success" :max="threadStats.threadDumpAll" striped>
|
||||
<b-progress-bar
|
||||
:value="threadStats.threadDumpBlocked"
|
||||
:label="formatNumber1((threadStats.threadDumpBlocked * 100) / threadStats.threadDumpAll) + '%'"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
|
||||
<span
|
||||
>Total: {{ threadStats.threadDumpAll }}
|
||||
<a class="hand" v-b-modal.metricsModal data-toggle="modal" @click="openModal()" data-target="#threadDump">
|
||||
<font-awesome-icon icon="eye"></font-awesome-icon>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4>System</h4>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-4">Uptime</div>
|
||||
<div class="col-md-8 text-right">{{ convertMillisecondsToDuration(metrics.processMetrics['process.uptime']) }}</div>
|
||||
</div>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-4">Start time</div>
|
||||
<div class="col-md-8 text-right">{{ formatDate(metrics.processMetrics['process.start.time']) }}</div>
|
||||
</div>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-9">Process CPU usage</div>
|
||||
<div class="col-md-3 text-right">{{ formatNumber2(100 * metrics.processMetrics['process.cpu.usage']) }} %</div>
|
||||
</div>
|
||||
<b-progress variant="success" :max="100" striped>
|
||||
<b-progress-bar
|
||||
:value="100 * metrics.processMetrics['process.cpu.usage']"
|
||||
:label="formatNumber1(100 * metrics.processMetrics['process.cpu.usage']) + '%'"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-9">System CPU usage</div>
|
||||
<div class="col-md-3 text-right">{{ formatNumber2(100 * metrics.processMetrics['system.cpu.usage']) }} %</div>
|
||||
</div>
|
||||
<b-progress variant="success" :max="100" striped>
|
||||
<b-progress-bar
|
||||
:value="100 * metrics.processMetrics['system.cpu.usage']"
|
||||
:label="formatNumber1(100 * metrics.processMetrics['system.cpu.usage']) + '%'"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-9">System CPU count</div>
|
||||
<div class="col-md-3 text-right">{{ metrics.processMetrics['system.cpu.count'] }}</div>
|
||||
</div>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-9">System 1m Load average</div>
|
||||
<div class="col-md-3 text-right">{{ formatNumber2(metrics.processMetrics['system.load.average.1m']) }}</div>
|
||||
</div>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-9">Process files max</div>
|
||||
<div class="col-md-3 text-right">{{ formatNumber1(metrics.processMetrics['process.files.max']) }}</div>
|
||||
</div>
|
||||
<div class="row" v-if="!updatingMetrics">
|
||||
<div class="col-md-9">Process files open</div>
|
||||
<div class="col-md-3 text-right">{{ formatNumber1(metrics.processMetrics['process.files.open']) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Garbage collections</h3>
|
||||
<div class="row" v-if="!updatingMetrics && isObjectExisting(metrics, 'garbageCollector')">
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<span>
|
||||
GC Live Data Size/GC Max Data Size ({{ formatNumber1(metrics.garbageCollector['jvm.gc.live.data.size'] / 1048576) }}M /
|
||||
{{ formatNumber1(metrics.garbageCollector['jvm.gc.max.data.size'] / 1048576) }}M)
|
||||
</span>
|
||||
<b-progress variant="success" :max="metrics.garbageCollector['jvm.gc.max.data.size']" striped>
|
||||
<b-progress-bar
|
||||
:value="metrics.garbageCollector['jvm.gc.live.data.size']"
|
||||
:label="
|
||||
formatNumber2(
|
||||
(100 * metrics.garbageCollector['jvm.gc.live.data.size']) / metrics.garbageCollector['jvm.gc.max.data.size'],
|
||||
) + '%'
|
||||
"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<span>
|
||||
GC Memory Promoted/GC Memory Allocated ({{ formatNumber1(metrics.garbageCollector['jvm.gc.memory.promoted'] / 1048576) }}M /
|
||||
{{ formatNumber1(metrics.garbageCollector['jvm.gc.memory.allocated'] / 1048576) }}M)
|
||||
</span>
|
||||
<b-progress variant="success" :max="metrics.garbageCollector['jvm.gc.memory.allocated']" striped>
|
||||
<b-progress-bar
|
||||
:value="metrics.garbageCollector['jvm.gc.memory.promoted']"
|
||||
:label="
|
||||
formatNumber2(
|
||||
(100 * metrics.garbageCollector['jvm.gc.memory.promoted']) / metrics.garbageCollector['jvm.gc.memory.allocated'],
|
||||
) + '%'
|
||||
"
|
||||
>
|
||||
</b-progress-bar>
|
||||
</b-progress>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="row">
|
||||
<div class="col-md-9">Classes loaded</div>
|
||||
<div class="col-md-3 text-right">{{ metrics.garbageCollector.classesLoaded }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-9">Classes unloaded</div>
|
||||
<div class="col-md-3 text-right">{{ metrics.garbageCollector.classesUnloaded }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="Jvm gc">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col" class="text-right">Count</th>
|
||||
<th scope="col" class="text-right">Mean</th>
|
||||
<th scope="col" class="text-right">Min</th>
|
||||
<th scope="col" class="text-right">p50</th>
|
||||
<th scope="col" class="text-right">p75</th>
|
||||
<th scope="col" class="text-right">p95</th>
|
||||
<th scope="col" class="text-right">p99</th>
|
||||
<th scope="col" class="text-right">Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>jvm.gc.pause</td>
|
||||
<td class="text-right">{{ metrics.garbageCollector['jvm.gc.pause'].count }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause'].mean) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause']['0.0']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause']['0.5']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause']['0.75']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause']['0.95']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause']['0.99']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.garbageCollector['jvm.gc.pause'].max) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>HTTP requests (time in millisecond)</h3>
|
||||
<table
|
||||
class="table table-striped"
|
||||
v-if="!updatingMetrics && isObjectExisting(metrics, 'http.server.requests')"
|
||||
aria-describedby="Jvm http"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Code</th>
|
||||
<th scope="col">Count</th>
|
||||
<th scope="col" class="text-right">Mean</th>
|
||||
<th scope="col" class="text-right">Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, key) of metrics['http.server.requests']['percode']" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
<td>
|
||||
<b-progress variant="success" animated :max="metrics['http.server.requests']['all'].count" striped>
|
||||
<b-progress-bar :value="entry.count" :label="formatNumber1(entry.count)"></b-progress-bar>
|
||||
</b-progress>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumber2(filterNaN(entry.mean)) }}
|
||||
</td>
|
||||
<td class="text-right">{{ formatNumber2(entry.max) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>Endpoints requests (time in millisecond)</h3>
|
||||
<div class="table-responsive" v-if="!updatingMetrics">
|
||||
<table class="table table-striped" aria-describedby="Endpoint">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Method</th>
|
||||
<th scope="col">Endpoint url</th>
|
||||
<th scope="col" class="text-right">Count</th>
|
||||
<th scope="col" class="text-right">Mean</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(entry, entryKey) of metrics.services">
|
||||
<tr v-for="(method, methodKey) of entry" :key="entryKey + '-' + methodKey">
|
||||
<td>{{ methodKey }}</td>
|
||||
<td>{{ entryKey }}</td>
|
||||
<td class="text-right">{{ method.count }}</td>
|
||||
<td class="text-right">{{ formatNumber2(method.mean) }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>Cache statistics</h3>
|
||||
<div class="table-responsive" v-if="!updatingMetrics && isObjectExisting(metrics, 'cache')">
|
||||
<table class="table table-striped" aria-describedby="Cache">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Cache name</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.hits">Cache Hits</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.misses">Cache Misses</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.gets">Cache Gets</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.puts">Cache Puts</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.removals">Cache Removals</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.evictions">Cache Evictions</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.hitPercent">Cache Hit %</th>
|
||||
<th scope="col" class="text-right" data-translate="metrics.cache.missPercent">Cache Miss %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(entry, key) of metrics.cache" :key="key">
|
||||
<td>{{ key }}</td>
|
||||
<td class="text-right">{{ entry['cache.gets.hit'] }}</td>
|
||||
<td class="text-right">{{ entry['cache.gets.miss'] }}</td>
|
||||
<td class="text-right">{{ entry['cache.gets.hit'] + entry['cache.gets.miss'] }}</td>
|
||||
<td class="text-right">{{ entry['cache.puts'] }}</td>
|
||||
<td class="text-right">{{ entry['cache.removals'] }}</td>
|
||||
<td class="text-right">{{ entry['cache.evictions'] }}</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumber2(filterNaN((100 * entry['cache.gets.hit']) / (entry['cache.gets.hit'] + entry['cache.gets.miss']))) }}
|
||||
</td>
|
||||
<td class="text-right">
|
||||
{{ formatNumber2(filterNaN((100 * entry['cache.gets.miss']) / (entry['cache.gets.hit'] + entry['cache.gets.miss']))) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h3>DataSource statistics (time in millisecond)</h3>
|
||||
<div class="table-responsive" v-if="!updatingMetrics && isObjectExistingAndNotEmpty(metrics, 'databases')">
|
||||
<table class="table table-striped" aria-describedby="Connection pool">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<span>Connection Pool Usage</span> (active: {{ metrics.databases.active.value }}, min: {{ metrics.databases.min.value }}, max:
|
||||
{{ metrics.databases.max.value }}, idle: {{ metrics.databases.idle.value }})
|
||||
</th>
|
||||
<th scope="col" class="text-right">Count</th>
|
||||
<th scope="col" class="text-right">Mean</th>
|
||||
<th scope="col" class="text-right">Min</th>
|
||||
<th scope="col" class="text-right">p50</th>
|
||||
<th scope="col" class="text-right">p75</th>
|
||||
<th scope="col" class="text-right">p95</th>
|
||||
<th scope="col" class="text-right">p99</th>
|
||||
<th scope="col" class="text-right">Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Acquire</td>
|
||||
<td class="text-right">{{ metrics.databases.acquire.count }}</td>
|
||||
<td class="text-right">{{ formatNumber2(filterNaN(metrics.databases.acquire.mean)) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.acquire['0.0']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.acquire['0.5']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.acquire['0.75']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.acquire['0.95']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.acquire['0.99']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(filterNaN(metrics.databases.acquire.max)) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td class="text-right">{{ metrics.databases.creation.count }}</td>
|
||||
<td class="text-right">{{ formatNumber2(filterNaN(metrics.databases.creation.mean)) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.creation['0.0']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.creation['0.5']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.creation['0.75']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.creation['0.95']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.creation['0.99']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(filterNaN(metrics.databases.creation.max)) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Usage</td>
|
||||
<td class="text-right">{{ metrics.databases.usage.count }}</td>
|
||||
<td class="text-right">{{ formatNumber2(filterNaN(metrics.databases.usage.mean)) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.usage['0.0']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.usage['0.5']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.usage['0.75']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.usage['0.95']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(metrics.databases.usage['0.99']) }}</td>
|
||||
<td class="text-right">{{ formatNumber2(filterNaN(metrics.databases.usage.max)) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<b-modal ref="metricsModal" size="lg">
|
||||
<template #modal-title>
|
||||
<h4 class="modal-title" id="showMetricsLabel">Threads dump</h4>
|
||||
</template>
|
||||
<metrics-modal :thread-dump="threadData"></metrics-modal>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./metrics.component.ts"></script>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { defineComponent, provide } from 'vue';
|
||||
import Ribbon from '@/core/ribbon/ribbon.vue';
|
||||
import JhiFooter from '@/core/jhi-footer/jhi-footer.vue';
|
||||
import JhiNavbar from '@/core/jhi-navbar/jhi-navbar.vue';
|
||||
|
||||
import { useAlertService } from '@/shared/alert/alert.service';
|
||||
|
||||
import '@/shared/config/dayjs';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'App',
|
||||
components: {
|
||||
ribbon: Ribbon,
|
||||
'jhi-navbar': JhiNavbar,
|
||||
'jhi-footer': JhiFooter,
|
||||
},
|
||||
setup() {
|
||||
provide('alertService', useAlertService());
|
||||
|
||||
return {};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<ribbon></ribbon>
|
||||
<div id="app-header">
|
||||
<jhi-navbar></jhi-navbar>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="card jh-card">
|
||||
<router-view></router-view>
|
||||
</div>
|
||||
|
||||
<jhi-footer></jhi-footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./app.component.ts"></script>
|
||||
@@ -0,0 +1,4 @@
|
||||
// Errors
|
||||
export const PROBLEM_BASE_URL = 'https://www.jhipster.tech/problem';
|
||||
export const EMAIL_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/email-already-used`;
|
||||
export const LOGIN_ALREADY_USED_TYPE = `${PROBLEM_BASE_URL}/login-already-used`;
|
||||
@@ -0,0 +1,109 @@
|
||||
import { vitest } from 'vitest';
|
||||
import { type Ref, ref } from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { type RouteLocation } from 'vue-router';
|
||||
|
||||
import Error from './error.vue';
|
||||
|
||||
import type LoginService from '@/account/login.service';
|
||||
|
||||
type ErrorComponentType = InstanceType<typeof Error>;
|
||||
|
||||
let route: Partial<RouteLocation>;
|
||||
|
||||
vitest.mock('vue-router', () => ({
|
||||
useRoute: () => route,
|
||||
}));
|
||||
|
||||
const customErrorMsg = 'An error occurred.';
|
||||
|
||||
describe('Error component', () => {
|
||||
let error: ErrorComponentType;
|
||||
let loginService: LoginService;
|
||||
let authenticated: Ref<boolean>;
|
||||
|
||||
beforeEach(() => {
|
||||
route = {};
|
||||
authenticated = ref(false);
|
||||
loginService = { login: vitest.fn(), logout: vitest.fn() };
|
||||
});
|
||||
|
||||
it('should have retrieve custom error on routing', () => {
|
||||
route = {
|
||||
path: '/custom-error',
|
||||
name: 'CustomMessage',
|
||||
meta: { errorMessage: customErrorMsg },
|
||||
};
|
||||
const wrapper = shallowMount(Error, {
|
||||
global: {
|
||||
provide: {
|
||||
loginService,
|
||||
authenticated,
|
||||
},
|
||||
},
|
||||
});
|
||||
error = wrapper.vm;
|
||||
|
||||
expect(error.errorMessage).toBe(customErrorMsg);
|
||||
expect(error.error403).toBeFalsy();
|
||||
expect(error.error404).toBeFalsy();
|
||||
expect(loginService.login).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should have set forbidden error on routing', () => {
|
||||
route = {
|
||||
meta: { error403: true },
|
||||
};
|
||||
const wrapper = shallowMount(Error, {
|
||||
global: {
|
||||
provide: {
|
||||
loginService,
|
||||
authenticated,
|
||||
},
|
||||
},
|
||||
});
|
||||
error = wrapper.vm;
|
||||
|
||||
expect(error.errorMessage).toBeNull();
|
||||
expect(error.error403).toBeTruthy();
|
||||
expect(error.error404).toBeFalsy();
|
||||
expect(loginService.login).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have set not found error on routing', () => {
|
||||
route = {
|
||||
meta: { error404: true },
|
||||
};
|
||||
const wrapper = shallowMount(Error, {
|
||||
global: {
|
||||
provide: {
|
||||
loginService,
|
||||
authenticated,
|
||||
},
|
||||
},
|
||||
});
|
||||
error = wrapper.vm;
|
||||
|
||||
expect(error.errorMessage).toBeNull();
|
||||
expect(error.error403).toBeFalsy();
|
||||
expect(error.error404).toBeTruthy();
|
||||
expect(loginService.login).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('should have set default on no error', () => {
|
||||
const wrapper = shallowMount(Error, {
|
||||
global: {
|
||||
provide: {
|
||||
loginService,
|
||||
authenticated,
|
||||
},
|
||||
},
|
||||
});
|
||||
error = wrapper.vm;
|
||||
|
||||
expect(error.errorMessage).toBeNull();
|
||||
expect(error.error403).toBeFalsy();
|
||||
expect(error.error404).toBeFalsy();
|
||||
expect(loginService.login).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { type ComputedRef, type Ref, defineComponent, inject, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import type LoginService from '@/account/login.service';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'Error',
|
||||
setup() {
|
||||
const loginService = inject<LoginService>('loginService');
|
||||
const authenticated = inject<ComputedRef<boolean>>('authenticated');
|
||||
const errorMessage: Ref<string> = ref(null);
|
||||
const error403: Ref<boolean> = ref(false);
|
||||
const error404: Ref<boolean> = ref(false);
|
||||
const route = useRoute();
|
||||
|
||||
if (route.meta) {
|
||||
errorMessage.value = route.meta.errorMessage ?? null;
|
||||
error403.value = route.meta.error403 ?? false;
|
||||
error404.value = route.meta.error404 ?? false;
|
||||
if (!authenticated.value && error403.value) {
|
||||
loginService.login();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorMessage,
|
||||
error403,
|
||||
error404,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<span class="hipster img-fluid rounded"></span>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<h1>Error page!</h1>
|
||||
|
||||
<div v-if="errorMessage">
|
||||
<div class="alert alert-danger">{{ errorMessage }}</div>
|
||||
</div>
|
||||
<div v-if="error403" class="alert alert-danger">You are not authorized to access this page.</div>
|
||||
<div v-if="error404" class="alert alert-warning">The page does not exist.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./error.component.ts"></script>
|
||||
@@ -0,0 +1,50 @@
|
||||
import { vitest } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import Home from './home.vue';
|
||||
|
||||
type HomeComponentType = InstanceType<typeof Home>;
|
||||
|
||||
describe('Home', () => {
|
||||
let home: HomeComponentType;
|
||||
let authenticated;
|
||||
let currentUsername;
|
||||
const loginService = { login: vitest.fn(), logout: vitest.fn() };
|
||||
|
||||
beforeEach(() => {
|
||||
authenticated = ref(false);
|
||||
currentUsername = ref('');
|
||||
const wrapper = shallowMount(Home, {
|
||||
global: {
|
||||
stubs: {
|
||||
'router-link': true,
|
||||
},
|
||||
provide: {
|
||||
loginService,
|
||||
authenticated,
|
||||
currentUsername,
|
||||
},
|
||||
},
|
||||
});
|
||||
home = wrapper.vm;
|
||||
});
|
||||
|
||||
it('should not have user data set', () => {
|
||||
expect(home.authenticated).toBeFalsy();
|
||||
expect(home.username).toBe('');
|
||||
});
|
||||
|
||||
it('should have user data set after authentication', () => {
|
||||
authenticated.value = true;
|
||||
currentUsername.value = 'test';
|
||||
|
||||
expect(home.authenticated).toBeTruthy();
|
||||
expect(home.username).toBe('test');
|
||||
});
|
||||
|
||||
it('should use login service', () => {
|
||||
home.openLogin();
|
||||
|
||||
expect(loginService.login).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { type ComputedRef, defineComponent, inject } from 'vue';
|
||||
|
||||
import type LoginService from '@/account/login.service';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
setup() {
|
||||
const loginService = inject<LoginService>('loginService');
|
||||
|
||||
const authenticated = inject<ComputedRef<boolean>>('authenticated');
|
||||
const username = inject<ComputedRef<string>>('currentUsername');
|
||||
|
||||
const openLogin = () => {
|
||||
loginService.login();
|
||||
};
|
||||
|
||||
return {
|
||||
authenticated,
|
||||
username,
|
||||
openLogin,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div class="home row">
|
||||
<div class="col-md-3">
|
||||
<span class="hipster img-fluid rounded"></span>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<h1 class="display-4">Welcome, Java Hipster!</h1>
|
||||
<p class="lead">This is your homepage</p>
|
||||
|
||||
<div>
|
||||
<div class="alert alert-success" v-if="authenticated">
|
||||
<span v-if="username">You are logged in as user "{{ username }}".</span>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning" v-if="!authenticated">
|
||||
<span>If you want to </span>
|
||||
<a class="alert-link" @click="openLogin()">sign in</a
|
||||
><span
|
||||
>, you can try the default accounts:<br />- Administrator (login="admin" and password="admin") <br />- User (login="user" and
|
||||
password="user").</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>If you have any question on JHipster:</p>
|
||||
|
||||
<ul>
|
||||
<li><a href="https://www.jhipster.tech/" target="_blank" rel="noopener noreferrer">JHipster homepage</a></li>
|
||||
<li>
|
||||
<a href="https://stackoverflow.com/tags/jhipster/info" target="_blank" rel="noopener noreferrer">JHipster on Stack Overflow</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/jhipster/generator-jhipster/issues?state=open" target="_blank" rel="noopener noreferrer"
|
||||
>JHipster bug tracker</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://gitter.im/jhipster/generator-jhipster" target="_blank" rel="noopener noreferrer">JHipster public chat room</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://twitter.com/jhipster" target="_blank" rel="noopener noreferrer">follow @jhipster on Twitter</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<span>If you like JHipster, don't forget to give us a star on</span>
|
||||
<a href="https://github.com/jhipster/generator-jhipster" target="_blank" rel="noopener noreferrer">GitHub</a>!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./home.component.ts"></script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiFooter',
|
||||
setup() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div id="footer" class="footer">
|
||||
<p>This is your footer</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./jhi-footer.component.ts"></script>
|
||||
@@ -0,0 +1,97 @@
|
||||
import { vitest } from 'vitest';
|
||||
import { computed } from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { type Router } from 'vue-router';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import JhiNavbar from './jhi-navbar.vue';
|
||||
|
||||
import { useStore } from '@/store';
|
||||
import { createRouter } from '@/router';
|
||||
import type LoginService from '@/account/login.service';
|
||||
|
||||
type JhiNavbarComponentType = InstanceType<typeof JhiNavbar>;
|
||||
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
const store = useStore();
|
||||
|
||||
describe('JhiNavbar', () => {
|
||||
let jhiNavbar: JhiNavbarComponentType;
|
||||
let loginService: LoginService;
|
||||
const accountService = { hasAnyAuthorityAndCheckAuth: vitest.fn().mockImplementation(() => Promise.resolve(true)) };
|
||||
let router: Router;
|
||||
|
||||
beforeEach(() => {
|
||||
router = createRouter();
|
||||
loginService = { login: vitest.fn(), logout: vitest.fn() };
|
||||
const wrapper = shallowMount(JhiNavbar, {
|
||||
global: {
|
||||
plugins: [pinia, router],
|
||||
stubs: {
|
||||
'font-awesome-icon': true,
|
||||
'b-navbar': true,
|
||||
'b-navbar-nav': true,
|
||||
'b-dropdown-item': true,
|
||||
'b-collapse': true,
|
||||
'b-nav-item': true,
|
||||
'b-nav-item-dropdown': true,
|
||||
'b-navbar-toggle': true,
|
||||
'b-navbar-brand': true,
|
||||
},
|
||||
provide: {
|
||||
loginService,
|
||||
currentLanguage: computed(() => 'foo'),
|
||||
accountService,
|
||||
},
|
||||
},
|
||||
});
|
||||
jhiNavbar = wrapper.vm;
|
||||
});
|
||||
|
||||
it('should not have user data set', () => {
|
||||
expect(jhiNavbar.authenticated).toBeFalsy();
|
||||
expect(jhiNavbar.openAPIEnabled).toBeFalsy();
|
||||
expect(jhiNavbar.inProduction).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should have user data set after authentication', () => {
|
||||
store.setAuthentication({ login: 'test' });
|
||||
|
||||
expect(jhiNavbar.authenticated).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have profile info set after info retrieved', () => {
|
||||
store.setActiveProfiles(['prod', 'api-docs']);
|
||||
|
||||
expect(jhiNavbar.openAPIEnabled).toBeTruthy();
|
||||
expect(jhiNavbar.inProduction).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should use login service', () => {
|
||||
jhiNavbar.openLogin();
|
||||
expect(loginService.login).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should use account service', () => {
|
||||
jhiNavbar.hasAnyAuthority('auth');
|
||||
|
||||
expect(accountService.hasAnyAuthorityAndCheckAuth).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('logout should clear credentials', async () => {
|
||||
store.setAuthentication({ login: 'test' });
|
||||
const logoutUrl = '/to-match';
|
||||
(loginService.logout as any).mockReturnValue(Promise.resolve({ data: { logoutUrl } }));
|
||||
await jhiNavbar.logout();
|
||||
|
||||
expect(loginService.logout).toHaveBeenCalled();
|
||||
expect(router.currentRoute.value.path).toBe(logoutUrl);
|
||||
});
|
||||
|
||||
it('should determine active route', async () => {
|
||||
await router.push('/toto');
|
||||
|
||||
expect(jhiNavbar.subIsActive('/titi')).toBeFalsy();
|
||||
expect(jhiNavbar.subIsActive('/toto')).toBeTruthy();
|
||||
expect(jhiNavbar.subIsActive(['/toto', 'toto'])).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,74 @@
|
||||
import { type Ref, computed, defineComponent, inject, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import type LoginService from '@/account/login.service';
|
||||
import type AccountService from '@/account/account.service';
|
||||
import EntitiesMenu from '@/entities/entities-menu.vue';
|
||||
|
||||
import { useStore } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiNavbar',
|
||||
components: {
|
||||
'entities-menu': EntitiesMenu,
|
||||
},
|
||||
setup() {
|
||||
const loginService = inject<LoginService>('loginService');
|
||||
const accountService = inject<AccountService>('accountService');
|
||||
const currentLanguage = inject('currentLanguage', () => computed(() => navigator.language ?? 'en'), true);
|
||||
|
||||
const router = useRouter();
|
||||
const store = useStore();
|
||||
|
||||
const version = `v${APP_VERSION}`;
|
||||
const hasAnyAuthorityValues: Ref<any> = ref({});
|
||||
|
||||
const openAPIEnabled = computed(() => store.activeProfiles.indexOf('api-docs') > -1);
|
||||
const inProduction = computed(() => store.activeProfiles.indexOf('prod') > -1);
|
||||
const authenticated = computed(() => store.authenticated);
|
||||
|
||||
const openLogin = () => {
|
||||
loginService.login();
|
||||
};
|
||||
|
||||
const subIsActive = (input: string | string[]) => {
|
||||
const paths = Array.isArray(input) ? input : [input];
|
||||
return paths.some(path => {
|
||||
return router.currentRoute.value.path.indexOf(path) === 0; // current path starts with this path string
|
||||
});
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
const response = await loginService.logout();
|
||||
store.logout();
|
||||
window.location.href = response.data.logoutUrl;
|
||||
const next = response.data?.logoutUrl ?? '/';
|
||||
if (router.currentRoute.value.path !== next) {
|
||||
await router.push(next);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
logout,
|
||||
subIsActive,
|
||||
accountService,
|
||||
openLogin,
|
||||
version,
|
||||
currentLanguage,
|
||||
hasAnyAuthorityValues,
|
||||
openAPIEnabled,
|
||||
inProduction,
|
||||
authenticated,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hasAnyAuthority(authorities: any): boolean {
|
||||
this.accountService.hasAnyAuthorityAndCheckAuth(authorities).then(value => {
|
||||
if (this.hasAnyAuthorityValues[authorities] !== value) {
|
||||
this.hasAnyAuthorityValues = { ...this.hasAnyAuthorityValues, [authorities]: value };
|
||||
}
|
||||
});
|
||||
return this.hasAnyAuthorityValues[authorities] ?? false;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<b-navbar data-cy="navbar" toggleable="md" type="dark" class="jh-navbar">
|
||||
<b-navbar-brand class="logo" b-link to="/">
|
||||
<span class="logo-img"></span>
|
||||
<span class="navbar-title">Sasiedzi</span> <span class="navbar-version">{{ version }}</span>
|
||||
</b-navbar-brand>
|
||||
<b-navbar-toggle
|
||||
right
|
||||
class="jh-navbar-toggler d-lg-none"
|
||||
href="javascript:void(0);"
|
||||
data-toggle="collapse"
|
||||
target="header-tabs"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<font-awesome-icon icon="bars" />
|
||||
</b-navbar-toggle>
|
||||
|
||||
<b-collapse is-nav id="header-tabs">
|
||||
<b-navbar-nav class="ml-auto">
|
||||
<b-nav-item to="/" exact>
|
||||
<span>
|
||||
<font-awesome-icon icon="home" />
|
||||
<span>Home</span>
|
||||
</span>
|
||||
</b-nav-item>
|
||||
<b-nav-item-dropdown right id="entity-menu" v-if="authenticated" active-class="active" class="pointer" data-cy="entity">
|
||||
<template #button-content>
|
||||
<span class="navbar-dropdown-menu">
|
||||
<font-awesome-icon icon="th-list" />
|
||||
<span class="no-bold">Entities</span>
|
||||
</span>
|
||||
</template>
|
||||
<entities-menu></entities-menu>
|
||||
<!-- jhipster-needle-add-entity-to-menu - JHipster will add entities to the menu here -->
|
||||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown
|
||||
right
|
||||
id="admin-menu"
|
||||
v-if="hasAnyAuthority('ROLE_ADMIN') && authenticated"
|
||||
:class="{ 'router-link-active': subIsActive('/admin') }"
|
||||
active-class="active"
|
||||
class="pointer"
|
||||
data-cy="adminMenu"
|
||||
>
|
||||
<template #button-content>
|
||||
<span class="navbar-dropdown-menu">
|
||||
<font-awesome-icon icon="users-cog" />
|
||||
<span class="no-bold">Administration</span>
|
||||
</span>
|
||||
</template>
|
||||
<b-dropdown-item to="/admin/metrics" active-class="active">
|
||||
<font-awesome-icon icon="tachometer-alt" />
|
||||
<span>Metrics</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item to="/admin/health" active-class="active">
|
||||
<font-awesome-icon icon="heart" />
|
||||
<span>Health</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item to="/admin/configuration" active-class="active">
|
||||
<font-awesome-icon icon="cogs" />
|
||||
<span>Configuration</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item to="/admin/logs" active-class="active">
|
||||
<font-awesome-icon icon="tasks" />
|
||||
<span>Logs</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item v-if="openAPIEnabled" to="/admin/docs" active-class="active">
|
||||
<font-awesome-icon icon="book" />
|
||||
<span>API</span>
|
||||
</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
<b-nav-item-dropdown
|
||||
right
|
||||
href="javascript:void(0);"
|
||||
id="account-menu"
|
||||
:class="{ 'router-link-active': subIsActive('/account') }"
|
||||
active-class="active"
|
||||
class="pointer"
|
||||
data-cy="accountMenu"
|
||||
>
|
||||
<template #button-content>
|
||||
<span class="navbar-dropdown-menu">
|
||||
<font-awesome-icon icon="user" />
|
||||
<span class="no-bold">Account</span>
|
||||
</span>
|
||||
</template>
|
||||
<b-dropdown-item data-cy="logout" v-if="authenticated" @click="logout()" id="logout" active-class="active">
|
||||
<font-awesome-icon icon="sign-out-alt" />
|
||||
<span>Sign out</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item data-cy="login" v-if="!authenticated" @click="openLogin()" id="login" active-class="active">
|
||||
<font-awesome-icon icon="sign-in-alt" />
|
||||
<span>Sign in</span>
|
||||
</b-dropdown-item>
|
||||
</b-nav-item-dropdown>
|
||||
</b-navbar-nav>
|
||||
</b-collapse>
|
||||
</b-navbar>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./jhi-navbar.component.ts"></script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
/* ==========================================================================
|
||||
Navbar
|
||||
========================================================================== */
|
||||
.navbar-version {
|
||||
font-size: 0.65em;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.jh-navbar {
|
||||
background-color: #353d47;
|
||||
padding: 0.2em 1em;
|
||||
}
|
||||
|
||||
.jh-navbar .profile-image {
|
||||
margin: -10px 0px;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.jh-navbar .dropdown-item.active,
|
||||
.jh-navbar .dropdown-item.active:focus,
|
||||
.jh-navbar .dropdown-item.active:hover {
|
||||
background-color: #353d47;
|
||||
}
|
||||
|
||||
.jh-navbar .dropdown-toggle::after {
|
||||
margin-left: 0.15em;
|
||||
}
|
||||
|
||||
.jh-navbar ul.navbar-nav {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.jh-navbar .navbar-nav .nav-item {
|
||||
margin-left: 1.5rem;
|
||||
}
|
||||
|
||||
.jh-navbar a.nav-link,
|
||||
.jh-navbar .no-bold {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.jh-navbar .jh-navbar-toggler {
|
||||
color: #ccc;
|
||||
font-size: 1.5em;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.jh-navbar .jh-navbar-toggler:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.jh-navbar-toggler {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) and (max-width: 1150px) {
|
||||
span span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-title {
|
||||
display: inline-block;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Logo styles
|
||||
========================================================================== */
|
||||
.navbar-brand.logo {
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.logo .logo-img {
|
||||
height: 45px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
width: 45px;
|
||||
}
|
||||
|
||||
.logo-img {
|
||||
height: 100%;
|
||||
background: url('/content/images/logo-jhipster.png') no-repeat center center;
|
||||
background-size: contain;
|
||||
width: 100%;
|
||||
filter: drop-shadow(0 0 0.05rem white);
|
||||
margin: 0 5px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import Ribbon from './ribbon.vue';
|
||||
|
||||
import { type AccountStore, useStore } from '@/store';
|
||||
|
||||
type RibbonComponentType = InstanceType<typeof Ribbon>;
|
||||
|
||||
const pinia = createTestingPinia({ stubActions: false });
|
||||
|
||||
describe('Ribbon', () => {
|
||||
let ribbon: RibbonComponentType;
|
||||
let store: AccountStore;
|
||||
|
||||
beforeEach(async () => {
|
||||
const wrapper = shallowMount(Ribbon, {
|
||||
global: {
|
||||
plugins: [pinia],
|
||||
},
|
||||
});
|
||||
ribbon = wrapper.vm;
|
||||
await ribbon.$nextTick();
|
||||
store = useStore();
|
||||
store.setRibbonOnProfiles(null);
|
||||
});
|
||||
|
||||
it('should not have ribbonEnabled when no data', () => {
|
||||
expect(ribbon.ribbonEnabled).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should have ribbonEnabled set to value in store', async () => {
|
||||
const profile = 'dev';
|
||||
store.setActiveProfiles(['foo', profile, 'bar']);
|
||||
store.setRibbonOnProfiles(profile);
|
||||
expect(ribbon.ribbonEnabled).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not have ribbonEnabled when profile not activated', async () => {
|
||||
const profile = 'dev';
|
||||
store.setActiveProfiles(['foo', 'bar']);
|
||||
store.setRibbonOnProfiles(profile);
|
||||
expect(ribbon.ribbonEnabled).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { computed, defineComponent } from 'vue';
|
||||
import { useStore } from '@/store';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'Ribbon',
|
||||
setup(prop) {
|
||||
const store = useStore();
|
||||
const ribbonEnv = computed(() => store.ribbonOnProfiles);
|
||||
const ribbonEnabled = computed(() => store.ribbonOnProfiles && store.activeProfiles.indexOf(store.ribbonOnProfiles) > -1);
|
||||
|
||||
return {
|
||||
ribbonEnv,
|
||||
ribbonEnabled,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div class="ribbon" v-if="ribbonEnabled">
|
||||
<a href="">{{ ribbonEnv }}</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./ribbon.component.ts"></script>
|
||||
|
||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
||||
<style scoped>
|
||||
/* ==========================================================================
|
||||
Development Ribbon
|
||||
========================================================================== */
|
||||
.ribbon {
|
||||
background-color: rgba(170, 0, 0, 0.5);
|
||||
left: -3.5em;
|
||||
-moz-transform: rotate(-45deg);
|
||||
-ms-transform: rotate(-45deg);
|
||||
-o-transform: rotate(-45deg);
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
white-space: nowrap;
|
||||
width: 15em;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.ribbon a {
|
||||
color: #fff;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
margin: 1px 0;
|
||||
padding: 10px 50px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
text-shadow: 0 0 5px #444;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,6 @@
|
||||
// These constants are injected via webpack environment variables.
|
||||
// You can add more variables in webpack.common.js or in profile specific webpack.<dev|prod>.js files.
|
||||
// If you change the values in the webpack config files, you need to re run webpack to update the application
|
||||
|
||||
declare const SERVER_API_URL: string;
|
||||
declare const APP_VERSION: string;
|
||||
@@ -0,0 +1,6 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'EntitiesMenu',
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- jhipster-needle-add-entity-to-menu - JHipster will add entities to the menu here -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./entities-menu.component.ts"></script>
|
||||
@@ -0,0 +1,13 @@
|
||||
import { defineComponent, provide } from 'vue';
|
||||
|
||||
import UserService from '@/entities/user/user.service';
|
||||
// jhipster-needle-add-entity-service-to-entities-component-import - JHipster will import entities services here
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'Entities',
|
||||
setup() {
|
||||
provide('userService', () => new UserService());
|
||||
// jhipster-needle-add-entity-service-to-entities-component - JHipster will import entities services here
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<router-view></router-view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./entities.component.ts"></script>
|
||||
@@ -0,0 +1,18 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const baseApiUrl = 'api/users';
|
||||
|
||||
export default class UserService {
|
||||
public retrieve(): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
axios
|
||||
.get(baseApiUrl)
|
||||
.then(res => {
|
||||
resolve(res);
|
||||
})
|
||||
.catch(err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// The Vue build version to load with the `import` command
|
||||
// (runtime-only or standalone) has been set in webpack.common with an alias.
|
||||
import Vue, { computed, createApp, provide } from 'vue';
|
||||
import { createPinia, storeToRefs } from 'pinia';
|
||||
|
||||
import App from './app.vue';
|
||||
import router from './router';
|
||||
import { initFortAwesome } from './shared/config/config';
|
||||
import { initBootstrapVue } from './shared/config/config-bootstrap-vue';
|
||||
import JhiItemCountComponent from './shared/jhi-item-count.vue';
|
||||
import JhiSortIndicatorComponent from './shared/sort/jhi-sort-indicator.vue';
|
||||
import LoginService from './account/login.service';
|
||||
import AccountService from './account/account.service';
|
||||
import { setupAxiosInterceptors } from '@/shared/config/axios-interceptor';
|
||||
import { useStore } from '@/store';
|
||||
|
||||
import '../content/scss/global.scss';
|
||||
import '../content/scss/vendor.scss';
|
||||
|
||||
const pinia = createPinia();
|
||||
|
||||
// jhipster-needle-add-entity-service-to-main-import - JHipster will import entities services here
|
||||
|
||||
initBootstrapVue(Vue);
|
||||
|
||||
Vue.configureCompat({
|
||||
MODE: 2,
|
||||
ATTR_FALSE_VALUE: 'suppress-warning',
|
||||
COMPONENT_FUNCTIONAL: 'suppress-warning',
|
||||
COMPONENT_V_MODEL: 'suppress-warning',
|
||||
CONFIG_OPTION_MERGE_STRATS: 'suppress-warning',
|
||||
CONFIG_WHITESPACE: 'suppress-warning',
|
||||
CUSTOM_DIR: 'suppress-warning',
|
||||
GLOBAL_EXTEND: 'suppress-warning',
|
||||
GLOBAL_MOUNT: 'suppress-warning',
|
||||
GLOBAL_PRIVATE_UTIL: 'suppress-warning',
|
||||
GLOBAL_PROTOTYPE: 'suppress-warning',
|
||||
GLOBAL_SET: 'suppress-warning',
|
||||
INSTANCE_ATTRS_CLASS_STYLE: 'suppress-warning',
|
||||
INSTANCE_CHILDREN: 'suppress-warning',
|
||||
INSTANCE_DELETE: 'suppress-warning',
|
||||
INSTANCE_DESTROY: 'suppress-warning',
|
||||
INSTANCE_EVENT_EMITTER: 'suppress-warning',
|
||||
INSTANCE_EVENT_HOOKS: 'suppress-warning',
|
||||
INSTANCE_LISTENERS: 'suppress-warning',
|
||||
INSTANCE_SCOPED_SLOTS: 'suppress-warning',
|
||||
INSTANCE_SET: 'suppress-warning',
|
||||
OPTIONS_BEFORE_DESTROY: 'suppress-warning',
|
||||
OPTIONS_DATA_MERGE: 'suppress-warning',
|
||||
OPTIONS_DESTROYED: 'suppress-warning',
|
||||
RENDER_FUNCTION: 'suppress-warning',
|
||||
WATCH_ARRAY: 'suppress-warning',
|
||||
PRIVATE_APIS: 'suppress-warning',
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
compatConfig: { MODE: 3 },
|
||||
components: { App },
|
||||
template: '<App/>',
|
||||
setup() {
|
||||
provide('loginService', new LoginService());
|
||||
const store = useStore();
|
||||
const accountService = new AccountService(store);
|
||||
provide(
|
||||
'currentLanguage',
|
||||
computed(() => store.account?.langKey ?? navigator.language ?? 'en'),
|
||||
);
|
||||
|
||||
router.beforeResolve(async (to, from, next) => {
|
||||
if (!store.authenticated) {
|
||||
await accountService.update();
|
||||
}
|
||||
if (to.meta?.authorities && to.meta.authorities.length > 0) {
|
||||
const value = await accountService.hasAnyAuthorityAndCheckAuth(to.meta.authorities);
|
||||
if (!value) {
|
||||
if (from.path !== '/forbidden') {
|
||||
next({ path: '/forbidden' });
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
setupAxiosInterceptors(
|
||||
error => {
|
||||
const url = error.response?.config?.url;
|
||||
const status = error.status || error.response.status;
|
||||
if (status === 401) {
|
||||
// Store logged out state.
|
||||
store.logout();
|
||||
if (!url.endsWith('api/account') && !url.endsWith('api/authenticate')) {
|
||||
// Ask for a new authentication
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
const { authenticated } = storeToRefs(store);
|
||||
provide('authenticated', authenticated);
|
||||
provide(
|
||||
'currentUsername',
|
||||
computed(() => store.account?.login),
|
||||
);
|
||||
|
||||
provide('accountService', accountService);
|
||||
// jhipster-needle-add-entity-service-to-main - JHipster will import entities services here
|
||||
},
|
||||
});
|
||||
|
||||
initFortAwesome(app);
|
||||
|
||||
app
|
||||
.component('jhi-item-count', JhiItemCountComponent)
|
||||
.component('jhi-sort-indicator', JhiSortIndicatorComponent)
|
||||
.use(router)
|
||||
.use(pinia)
|
||||
.mount('#app');
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Authority } from '@/shared/security/authority';
|
||||
|
||||
const JhiDocsComponent = () => import('@/admin/docs/docs.vue');
|
||||
const JhiConfigurationComponent = () => import('@/admin/configuration/configuration.vue');
|
||||
const JhiHealthComponent = () => import('@/admin/health/health.vue');
|
||||
const JhiLogsComponent = () => import('@/admin/logs/logs.vue');
|
||||
const JhiMetricsComponent = () => import('@/admin/metrics/metrics.vue');
|
||||
|
||||
export default [
|
||||
{
|
||||
path: '/admin/docs',
|
||||
name: 'JhiDocsComponent',
|
||||
component: JhiDocsComponent,
|
||||
meta: { authorities: [Authority.ADMIN] },
|
||||
},
|
||||
{
|
||||
path: '/admin/health',
|
||||
name: 'JhiHealthComponent',
|
||||
component: JhiHealthComponent,
|
||||
meta: { authorities: [Authority.ADMIN] },
|
||||
},
|
||||
{
|
||||
path: '/admin/logs',
|
||||
name: 'JhiLogsComponent',
|
||||
component: JhiLogsComponent,
|
||||
meta: { authorities: [Authority.ADMIN] },
|
||||
},
|
||||
{
|
||||
path: '/admin/metrics',
|
||||
name: 'JhiMetricsComponent',
|
||||
component: JhiMetricsComponent,
|
||||
meta: { authorities: [Authority.ADMIN] },
|
||||
},
|
||||
{
|
||||
path: '/admin/configuration',
|
||||
name: 'JhiConfigurationComponent',
|
||||
component: JhiConfigurationComponent,
|
||||
meta: { authorities: [Authority.ADMIN] },
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,13 @@
|
||||
/* tslint:disable */
|
||||
// prettier-ignore
|
||||
const Entities = () => import('@/entities/entities.vue');
|
||||
|
||||
// jhipster-needle-add-entity-to-router-import - JHipster will import entities to the router here
|
||||
|
||||
export default {
|
||||
path: '/',
|
||||
component: Entities,
|
||||
children: [
|
||||
// jhipster-needle-add-entity-to-router - JHipster will add entities to the router here
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import { createRouter as createVueRouter, createWebHistory } from 'vue-router';
|
||||
|
||||
const Home = () => import('@/core/home/home.vue');
|
||||
const Error = () => import('@/core/error/error.vue');
|
||||
import admin from '@/router/admin';
|
||||
import entities from '@/router/entities';
|
||||
import pages from '@/router/pages';
|
||||
|
||||
export const createRouter = () =>
|
||||
createVueRouter({
|
||||
history: createWebHistory(),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: Home,
|
||||
},
|
||||
{
|
||||
path: '/forbidden',
|
||||
name: 'Forbidden',
|
||||
component: Error,
|
||||
meta: { error403: true },
|
||||
},
|
||||
{
|
||||
path: '/not-found',
|
||||
name: 'NotFound',
|
||||
component: Error,
|
||||
meta: { error404: true },
|
||||
},
|
||||
...admin,
|
||||
entities,
|
||||
...pages,
|
||||
],
|
||||
});
|
||||
|
||||
const router = createRouter();
|
||||
|
||||
router.beforeResolve(async (to, from, next) => {
|
||||
if (!to.matched.length) {
|
||||
next({ path: '/not-found' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,8 @@
|
||||
/* tslint:disable */
|
||||
// prettier-ignore
|
||||
|
||||
// jhipster-needle-add-entity-to-router-import - JHipster will import entities to the router here
|
||||
|
||||
export default [
|
||||
// jhipster-needle-add-entity-to-router - JHipster will add entities to the router here
|
||||
]
|
||||
@@ -0,0 +1,149 @@
|
||||
import { vitest } from 'vitest';
|
||||
import AlertService from './alert.service';
|
||||
|
||||
describe('Alert Service test suite', () => {
|
||||
let toastStub: vitest.Mock;
|
||||
let alertService: AlertService;
|
||||
|
||||
beforeEach(() => {
|
||||
toastStub = vitest.fn();
|
||||
alertService = new AlertService({
|
||||
bvToast: {
|
||||
toast: toastStub,
|
||||
} as any,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast with translation/message', async () => {
|
||||
const message = 'translatedMessage';
|
||||
|
||||
// WHEN
|
||||
alertService.showError(message);
|
||||
|
||||
// THEN
|
||||
expect(toastStub).toBeCalledTimes(1);
|
||||
expect(toastStub).toHaveBeenCalledWith(message, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show not reachable toast when http status = 0', async () => {
|
||||
const message = 'Server not reachable';
|
||||
const httpErrorResponse = {
|
||||
status: 0,
|
||||
};
|
||||
|
||||
// WHEN
|
||||
alertService.showHttpError(httpErrorResponse);
|
||||
|
||||
// THEN
|
||||
expect(toastStub).toBeCalledTimes(1);
|
||||
expect(toastStub).toHaveBeenCalledWith(message, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show parameterized error toast when http status = 400 and entity headers', async () => {
|
||||
const message = 'Updation Error';
|
||||
const httpErrorResponse = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'x-jhipsterapp-error': message,
|
||||
'x-jhipsterapp-params': 'dummyEntity',
|
||||
},
|
||||
};
|
||||
|
||||
// WHEN
|
||||
alertService.showHttpError(httpErrorResponse);
|
||||
|
||||
// THEN
|
||||
expect(toastStub).toHaveBeenCalledWith(message, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast with data.message when http status = 400 and entity headers', async () => {
|
||||
const message = 'Validation error';
|
||||
const httpErrorResponse = {
|
||||
status: 400,
|
||||
headers: {
|
||||
'x-jhipsterapp-error400': 'error',
|
||||
'x-jhipsterapp-params400': 'dummyEntity',
|
||||
},
|
||||
data: {
|
||||
message,
|
||||
fieldErrors: {
|
||||
field1: 'error1',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// WHEN
|
||||
alertService.showHttpError(httpErrorResponse);
|
||||
|
||||
// THEN
|
||||
expect(toastStub).toBeCalledTimes(1);
|
||||
expect(toastStub).toHaveBeenCalledWith(message, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast when http status = 404', async () => {
|
||||
const message = 'The page does not exist.';
|
||||
const httpErrorResponse = {
|
||||
status: 404,
|
||||
};
|
||||
|
||||
// WHEN
|
||||
alertService.showHttpError(httpErrorResponse);
|
||||
|
||||
// THEN
|
||||
expect(toastStub).toBeCalledTimes(1);
|
||||
expect(toastStub).toHaveBeenCalledWith(message, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it('should show error toast when http status != 400,404', async () => {
|
||||
const message = 'Error 500';
|
||||
const httpErrorResponse = {
|
||||
status: 500,
|
||||
data: {
|
||||
message,
|
||||
},
|
||||
};
|
||||
|
||||
// WHEN
|
||||
alertService.showHttpError(httpErrorResponse);
|
||||
|
||||
// THEN
|
||||
expect(toastStub).toBeCalledTimes(1);
|
||||
expect(toastStub).toHaveBeenCalledWith(message, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { BvToast } from 'bootstrap-vue';
|
||||
import { getCurrentInstance } from 'vue';
|
||||
|
||||
export const useAlertService = () => {
|
||||
const bvToast = getCurrentInstance().root.proxy._bv__toast;
|
||||
if (!bvToast) {
|
||||
throw new Error('BootstrapVue toast component was not found');
|
||||
}
|
||||
return new AlertService({
|
||||
bvToast,
|
||||
});
|
||||
};
|
||||
|
||||
export default class AlertService {
|
||||
private bvToast: BvToast;
|
||||
|
||||
constructor({ bvToast }: { bvToast: BvToast }) {
|
||||
this.bvToast = bvToast;
|
||||
}
|
||||
|
||||
public showInfo(toastMessage: string, toastOptions?: any) {
|
||||
this.bvToast.toast(toastMessage, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Info',
|
||||
variant: 'info',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
...toastOptions,
|
||||
});
|
||||
}
|
||||
|
||||
public showSuccess(toastMessage: string) {
|
||||
this.bvToast.toast(toastMessage, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Success',
|
||||
variant: 'success',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
public showError(toastMessage: string) {
|
||||
this.bvToast.toast(toastMessage, {
|
||||
toaster: 'b-toaster-top-center',
|
||||
title: 'Error',
|
||||
variant: 'danger',
|
||||
solid: true,
|
||||
autoHideDelay: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
public showHttpError(httpErrorResponse: any) {
|
||||
let errorMessage: string | null = null;
|
||||
switch (httpErrorResponse.status) {
|
||||
case 0:
|
||||
errorMessage = 'Server not reachable';
|
||||
break;
|
||||
|
||||
case 400: {
|
||||
const arr = Object.keys(httpErrorResponse.headers);
|
||||
for (const entry of arr) {
|
||||
if (entry.toLowerCase().endsWith('app-error')) {
|
||||
errorMessage = httpErrorResponse.headers[entry];
|
||||
}
|
||||
}
|
||||
if (!errorMessage && httpErrorResponse.data?.fieldErrors) {
|
||||
errorMessage = 'Validation error';
|
||||
} else if (!errorMessage) {
|
||||
errorMessage = httpErrorResponse.data.message;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 404:
|
||||
errorMessage = 'The page does not exist.';
|
||||
break;
|
||||
|
||||
default:
|
||||
errorMessage = httpErrorResponse.data.message;
|
||||
}
|
||||
this.showError(errorMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { type Ref } from 'vue';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
export const DATE_FORMAT = 'YYYY-MM-DD';
|
||||
export const DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm';
|
||||
|
||||
export const DATE_TIME_LONG_FORMAT = 'YYYY-MM-DDTHH:mm';
|
||||
|
||||
export const useDateFormat = ({ entityRef }: { entityRef?: Ref<Record<string, any>> } = {}) => {
|
||||
const formatDate = value => (value ? dayjs(value).format(DATE_TIME_FORMAT) : '');
|
||||
const dateFormatUtils = {
|
||||
convertDateTimeFromServer: (date: Date): string => (date && dayjs(date).isValid() ? dayjs(date).format(DATE_TIME_LONG_FORMAT) : null),
|
||||
formatDate,
|
||||
formatDuration: value => (value ? (dayjs.duration(value).humanize() ?? value) : ''),
|
||||
formatDateLong: formatDate,
|
||||
formatDateShort: formatDate,
|
||||
};
|
||||
const entityUtils = entityRef
|
||||
? {
|
||||
...dateFormatUtils,
|
||||
updateInstantField: (field: string, event: any) => {
|
||||
if (event.target?.value) {
|
||||
entityRef.value[field] = dayjs(event.target.value, DATE_TIME_LONG_FORMAT);
|
||||
} else {
|
||||
entityRef.value[field] = null;
|
||||
}
|
||||
},
|
||||
updateZonedDateTimeField: (field: string, event: any) => {
|
||||
if (event.target?.value) {
|
||||
entityRef.value[field] = dayjs(event.target.value, DATE_TIME_LONG_FORMAT);
|
||||
} else {
|
||||
entityRef.value[field] = null;
|
||||
}
|
||||
},
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
...dateFormatUtils,
|
||||
...entityUtils,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { useDateFormat } from './date-format';
|
||||
export { useValidation } from './validation';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { decimal, helpers, integer, maxLength, maxValue, minLength, minValue, required, sameAs } from '@vuelidate/validators';
|
||||
|
||||
export const useValidation = () => {
|
||||
return {
|
||||
required: (message: string) => helpers.withMessage(message, required),
|
||||
decimal: (message: string) => helpers.withMessage(message, decimal),
|
||||
integer: (message: string) => helpers.withMessage(message, integer),
|
||||
sameAs: (message: string, ...args: Parameters<typeof sameAs>) => helpers.withMessage(message, sameAs(...args)),
|
||||
minLength: (message: string, ...args: Parameters<typeof minLength>) => helpers.withMessage(message, minLength(...args)),
|
||||
maxLength: (message: string, ...args: Parameters<typeof maxLength>) => helpers.withMessage(message, maxLength(...args)),
|
||||
minValue: (message: string, ...args: Parameters<typeof minValue>) => helpers.withMessage(message, minValue(...args)),
|
||||
maxValue: (message: string, ...args: Parameters<typeof maxValue>) => helpers.withMessage(message, maxValue(...args)),
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
const compareString = (a: string, b: string): number => {
|
||||
if (b == null) return 1;
|
||||
if (a == null) return -1;
|
||||
|
||||
return a.localeCompare(b);
|
||||
};
|
||||
|
||||
const asString = (val): string => {
|
||||
return typeof val === 'string' ? val : `${val}`;
|
||||
};
|
||||
|
||||
const compareAny = (a, b): number => {
|
||||
if (b == null) return 1;
|
||||
if (a == null) return -1;
|
||||
|
||||
return a - b;
|
||||
};
|
||||
|
||||
export type OrderByOptions = { orderByProp: string; reverse?: boolean };
|
||||
|
||||
export const orderBy = (array: any[], opts: OrderByOptions) => {
|
||||
if (!Array.isArray(array)) return array;
|
||||
|
||||
const { orderByProp, reverse = false } = opts;
|
||||
let sorted: any[];
|
||||
if (array.some(el => typeof el[orderByProp] === 'string')) {
|
||||
sorted = array.sort((a, b) => compareString(asString(a), asString(b)));
|
||||
} else {
|
||||
sorted = array.sort((a, b) => compareAny(a, b));
|
||||
}
|
||||
if (reverse) {
|
||||
return sorted.slice().reverse();
|
||||
}
|
||||
return sorted;
|
||||
};
|
||||
|
||||
export type FilterByOptions = { filterByTerm: string; filterMaxDepth?: number };
|
||||
|
||||
const filterObject = (val: any, opts: FilterByOptions): boolean => {
|
||||
const { filterByTerm, filterMaxDepth = 2 } = opts;
|
||||
if (typeof val === 'string') {
|
||||
return val.toLocaleLowerCase().startsWith(filterByTerm);
|
||||
}
|
||||
if (typeof val === 'object') {
|
||||
if (filterMaxDepth < 0) return false;
|
||||
for (const value of Object.values(val)) {
|
||||
if (filterObject(value, { filterByTerm, filterMaxDepth: filterMaxDepth - 1 })) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return `${val}`.toLocaleLowerCase().startsWith(filterByTerm);
|
||||
};
|
||||
|
||||
export const filterBy = (array: any, opts: FilterByOptions) => {
|
||||
return Array.isArray(array) && opts.filterByTerm
|
||||
? array.filter(el => filterObject(el, { ...opts, filterByTerm: opts.filterByTerm.toLocaleLowerCase() }))
|
||||
: array;
|
||||
};
|
||||
|
||||
export const orderAndFilterBy = (array: any, opts: FilterByOptions & OrderByOptions) => orderBy(filterBy(array, opts), opts);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './arrays';
|
||||
@@ -0,0 +1,64 @@
|
||||
import * as sinon from 'sinon';
|
||||
import axios from 'axios';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import * as setupAxiosConfig from './axios-interceptor';
|
||||
|
||||
const mock = new MockAdapter(axios);
|
||||
|
||||
describe('Axios interceptor', () => {
|
||||
beforeEach(() => {
|
||||
axios.interceptors.request.clear();
|
||||
axios.interceptors.response.clear();
|
||||
});
|
||||
|
||||
it('should use localStorage to provide bearer', () => {
|
||||
const result = setupAxiosConfig.onRequestSuccess(() => console.log('A problem occurred'));
|
||||
|
||||
expect(result.url.indexOf(SERVER_API_URL)).toBeGreaterThan(-1);
|
||||
});
|
||||
|
||||
it('should use sessionStorage to provide bearer', () => {
|
||||
const result = setupAxiosConfig.onRequestSuccess(() => console.log('A problem occured'));
|
||||
|
||||
expect(result.url.indexOf(SERVER_API_URL)).toBeGreaterThan(-1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Axios errors interceptor', () => {
|
||||
it('should use callback on 401, 403 errors', async () => {
|
||||
const callback = sinon.spy();
|
||||
|
||||
setupAxiosConfig.setupAxiosInterceptors(callback, () => {});
|
||||
|
||||
try {
|
||||
mock.onGet().reply(401);
|
||||
await axios('/api/test');
|
||||
} catch {
|
||||
expect(callback.called).toBeTruthy();
|
||||
}
|
||||
});
|
||||
it('should use callback 50x errors', async () => {
|
||||
const callback = sinon.spy();
|
||||
|
||||
setupAxiosConfig.setupAxiosInterceptors(() => {}, callback);
|
||||
|
||||
try {
|
||||
mock.onGet().reply(500);
|
||||
await axios('/api/test');
|
||||
} catch {
|
||||
expect(callback.called).toBeTruthy();
|
||||
}
|
||||
});
|
||||
it('should not use callback for errors different 50x, 401, 403', async () => {
|
||||
const callback = sinon.spy();
|
||||
|
||||
setupAxiosConfig.setupAxiosInterceptors(() => {}, callback);
|
||||
|
||||
try {
|
||||
mock.onGet().reply(402);
|
||||
await axios('/api/test');
|
||||
} catch {
|
||||
expect(callback.called).toBeFalsy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const TIMEOUT = 1000000;
|
||||
const onRequestSuccess = config => {
|
||||
config.timeout = TIMEOUT;
|
||||
config.url = `${SERVER_API_URL}${config.url}`;
|
||||
return config;
|
||||
};
|
||||
const setupAxiosInterceptors = (onUnauthenticated, onServerError) => {
|
||||
const onResponseError = err => {
|
||||
const status = err.status || err.response.status;
|
||||
if (status === 403 || status === 401) {
|
||||
return onUnauthenticated(err);
|
||||
}
|
||||
if (status >= 500) {
|
||||
return onServerError(err);
|
||||
}
|
||||
return Promise.reject(err);
|
||||
};
|
||||
|
||||
if (axios.interceptors) {
|
||||
axios.interceptors.request.use(onRequestSuccess);
|
||||
axios.interceptors.response.use(res => res, onResponseError);
|
||||
}
|
||||
};
|
||||
|
||||
export { onRequestSuccess, setupAxiosInterceptors };
|
||||
@@ -0,0 +1,58 @@
|
||||
import {
|
||||
BAlert,
|
||||
BBadge,
|
||||
BButton,
|
||||
BCollapse,
|
||||
BDropdown,
|
||||
BDropdownItem,
|
||||
BForm,
|
||||
BFormCheckbox,
|
||||
BFormDatepicker,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
BInputGroup,
|
||||
BInputGroupPrepend,
|
||||
BLink,
|
||||
BModal,
|
||||
BNavItem,
|
||||
BNavItemDropdown,
|
||||
BNavbar,
|
||||
BNavbarBrand,
|
||||
BNavbarNav,
|
||||
BNavbarToggle,
|
||||
BPagination,
|
||||
BProgress,
|
||||
BProgressBar,
|
||||
ToastPlugin,
|
||||
VBModal,
|
||||
} from 'bootstrap-vue';
|
||||
|
||||
export function initBootstrapVue(vue) {
|
||||
vue.use(ToastPlugin);
|
||||
|
||||
vue.component('b-badge', BBadge);
|
||||
vue.component('b-dropdown', BDropdown);
|
||||
vue.component('b-dropdown-item', BDropdownItem);
|
||||
vue.component('b-link', BLink);
|
||||
vue.component('b-alert', BAlert);
|
||||
vue.component('b-button', BButton);
|
||||
vue.component('b-navbar', BNavbar);
|
||||
vue.component('b-navbar-nav', BNavbarNav);
|
||||
vue.component('b-navbar-brand', BNavbarBrand);
|
||||
vue.component('b-navbar-toggle', BNavbarToggle);
|
||||
vue.component('b-pagination', BPagination);
|
||||
vue.component('b-progress', BProgress);
|
||||
vue.component('b-progress-bar', BProgressBar);
|
||||
vue.component('b-form', BForm);
|
||||
vue.component('b-form-input', BFormInput);
|
||||
vue.component('b-form-group', BFormGroup);
|
||||
vue.component('b-form-checkbox', BFormCheckbox);
|
||||
vue.component('b-collapse', BCollapse);
|
||||
vue.component('b-nav-item', BNavItem);
|
||||
vue.component('b-nav-item-dropdown', BNavItemDropdown);
|
||||
vue.component('b-modal', BModal);
|
||||
vue.directive('b-modal', VBModal);
|
||||
vue.component('b-form-datepicker', BFormDatepicker);
|
||||
vue.component('b-input-group', BInputGroup);
|
||||
vue.component('b-input-group-prepend', BInputGroupPrepend);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||
|
||||
import { library } from '@fortawesome/fontawesome-svg-core';
|
||||
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft';
|
||||
import { faAsterisk } from '@fortawesome/free-solid-svg-icons/faAsterisk';
|
||||
import { faBan } from '@fortawesome/free-solid-svg-icons/faBan';
|
||||
import { faBars } from '@fortawesome/free-solid-svg-icons/faBars';
|
||||
import { faBell } from '@fortawesome/free-solid-svg-icons/faBell';
|
||||
import { faBook } from '@fortawesome/free-solid-svg-icons/faBook';
|
||||
import { faCloud } from '@fortawesome/free-solid-svg-icons/faCloud';
|
||||
import { faCogs } from '@fortawesome/free-solid-svg-icons/faCogs';
|
||||
import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase';
|
||||
import { faEye } from '@fortawesome/free-solid-svg-icons/faEye';
|
||||
import { faFlag } from '@fortawesome/free-solid-svg-icons/faFlag';
|
||||
import { faHeart } from '@fortawesome/free-solid-svg-icons/faHeart';
|
||||
import { faHome } from '@fortawesome/free-solid-svg-icons/faHome';
|
||||
import { faList } from '@fortawesome/free-solid-svg-icons/faList';
|
||||
import { faLock } from '@fortawesome/free-solid-svg-icons/faLock';
|
||||
import { faPencilAlt } from '@fortawesome/free-solid-svg-icons/faPencilAlt';
|
||||
import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus';
|
||||
import { faRoad } from '@fortawesome/free-solid-svg-icons/faRoad';
|
||||
import { faSave } from '@fortawesome/free-solid-svg-icons/faSave';
|
||||
import { faSearch } from '@fortawesome/free-solid-svg-icons/faSearch';
|
||||
import { faSignInAlt } from '@fortawesome/free-solid-svg-icons/faSignInAlt';
|
||||
import { faSignOutAlt } from '@fortawesome/free-solid-svg-icons/faSignOutAlt';
|
||||
import { faSort } from '@fortawesome/free-solid-svg-icons/faSort';
|
||||
import { faSortDown } from '@fortawesome/free-solid-svg-icons/faSortDown';
|
||||
import { faSortUp } from '@fortawesome/free-solid-svg-icons/faSortUp';
|
||||
import { faSync } from '@fortawesome/free-solid-svg-icons/faSync';
|
||||
import { faTachometerAlt } from '@fortawesome/free-solid-svg-icons/faTachometerAlt';
|
||||
import { faTasks } from '@fortawesome/free-solid-svg-icons/faTasks';
|
||||
import { faThList } from '@fortawesome/free-solid-svg-icons/faThList';
|
||||
import { faTimesCircle } from '@fortawesome/free-solid-svg-icons/faTimesCircle';
|
||||
import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes';
|
||||
import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash';
|
||||
import { faUser } from '@fortawesome/free-solid-svg-icons/faUser';
|
||||
import { faUserPlus } from '@fortawesome/free-solid-svg-icons/faUserPlus';
|
||||
import { faUsers } from '@fortawesome/free-solid-svg-icons/faUsers';
|
||||
import { faUsersCog } from '@fortawesome/free-solid-svg-icons/faUsersCog';
|
||||
import { faWrench } from '@fortawesome/free-solid-svg-icons/faWrench';
|
||||
|
||||
export function initFortAwesome(vue) {
|
||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||
|
||||
library.add(
|
||||
faArrowLeft,
|
||||
faAsterisk,
|
||||
faBan,
|
||||
faBars,
|
||||
faBell,
|
||||
faBook,
|
||||
faCloud,
|
||||
faCogs,
|
||||
faDatabase,
|
||||
faEye,
|
||||
faFlag,
|
||||
faHeart,
|
||||
faHome,
|
||||
faList,
|
||||
faLock,
|
||||
faPencilAlt,
|
||||
faPlus,
|
||||
faRoad,
|
||||
faSave,
|
||||
faSearch,
|
||||
faSignInAlt,
|
||||
faSignOutAlt,
|
||||
faSort,
|
||||
faSortDown,
|
||||
faSortUp,
|
||||
faSync,
|
||||
faTachometerAlt,
|
||||
faTasks,
|
||||
faThList,
|
||||
faTimes,
|
||||
faTimesCircle,
|
||||
faTrash,
|
||||
faUser,
|
||||
faUserPlus,
|
||||
faUsers,
|
||||
faUsersCog,
|
||||
faWrench,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import dayjs from 'dayjs';
|
||||
import customParseFormat from 'dayjs/plugin/customParseFormat';
|
||||
import duration from 'dayjs/plugin/duration';
|
||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||
|
||||
// jhipster-needle-i18n-language-dayjs-imports - JHipster will import languages from dayjs here
|
||||
|
||||
// DAYJS CONFIGURATION
|
||||
dayjs.extend(customParseFormat);
|
||||
dayjs.extend(duration);
|
||||
dayjs.extend(relativeTime);
|
||||
@@ -0,0 +1,50 @@
|
||||
import { defineStore } from 'pinia';
|
||||
|
||||
export interface AccountStateStorable {
|
||||
logon: boolean;
|
||||
userIdentity: null | any;
|
||||
authenticated: boolean;
|
||||
profilesLoaded: boolean;
|
||||
ribbonOnProfiles: string;
|
||||
activeProfiles: string;
|
||||
}
|
||||
|
||||
export const defaultAccountState: AccountStateStorable = {
|
||||
logon: null,
|
||||
userIdentity: null,
|
||||
authenticated: false,
|
||||
profilesLoaded: false,
|
||||
ribbonOnProfiles: '',
|
||||
activeProfiles: '',
|
||||
};
|
||||
|
||||
export const useAccountStore = defineStore('main', {
|
||||
state: (): AccountStateStorable => ({ ...defaultAccountState }),
|
||||
getters: {
|
||||
account: state => state.userIdentity,
|
||||
},
|
||||
actions: {
|
||||
authenticate(promise) {
|
||||
this.logon = promise;
|
||||
},
|
||||
setAuthentication(identity) {
|
||||
this.userIdentity = identity;
|
||||
this.authenticated = true;
|
||||
this.logon = null;
|
||||
},
|
||||
logout() {
|
||||
this.userIdentity = null;
|
||||
this.authenticated = false;
|
||||
this.logon = null;
|
||||
},
|
||||
setProfilesLoaded() {
|
||||
this.profilesLoaded = true;
|
||||
},
|
||||
setActiveProfiles(profile) {
|
||||
this.activeProfiles = profile;
|
||||
},
|
||||
setRibbonOnProfiles(ribbon) {
|
||||
this.ribbonOnProfiles = ribbon;
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,117 @@
|
||||
import { vitest } from 'vitest';
|
||||
import useDataUtils from './data-utils.service';
|
||||
|
||||
describe('Formatter i18n', () => {
|
||||
let dataUtilsService: ReturnType<typeof useDataUtils>;
|
||||
|
||||
beforeEach(() => {
|
||||
dataUtilsService = useDataUtils();
|
||||
});
|
||||
|
||||
it('should not abbreviate text shorter than 30 characters', () => {
|
||||
const result = dataUtilsService.abbreviate('JHipster JHipster');
|
||||
|
||||
expect(result).toBe('JHipster JHipster');
|
||||
});
|
||||
|
||||
it('should abbreviate text longer than 30 characters', () => {
|
||||
const result = dataUtilsService.abbreviate('JHipster JHipster JHipster JHipster JHipster');
|
||||
|
||||
expect(result).toBe('JHipster JHipst...r JHipster');
|
||||
});
|
||||
|
||||
it('should retrieve byteSize', () => {
|
||||
const result = dataUtilsService.byteSize('JHipster rocks!');
|
||||
|
||||
expect(result).toBe('11.25 bytes');
|
||||
});
|
||||
|
||||
it('should clear input entity', () => {
|
||||
const entity = { field: 'key', value: 'value' };
|
||||
dataUtilsService.clearInputImage(entity, null, 'field', 'value', 1);
|
||||
|
||||
expect(entity.field).toBeNull();
|
||||
expect(entity.value).toBeNull();
|
||||
});
|
||||
|
||||
it('should open file', () => {
|
||||
window.open = vitest.fn().mockReturnValue({});
|
||||
const objectURL = 'blob:http://localhost:9000/xxx';
|
||||
URL.createObjectURL = vitest.fn().mockImplementationOnce(() => {
|
||||
return objectURL;
|
||||
});
|
||||
|
||||
dataUtilsService.openFile('text', 'data');
|
||||
|
||||
expect(window.open).toHaveBeenCalledWith(objectURL);
|
||||
});
|
||||
|
||||
it('should check text ends with suffix', () => {
|
||||
const result = dataUtilsService.endsWith('rocky', 'JHipster rocks!');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('should paddingSize to 0', () => {
|
||||
const result = dataUtilsService.paddingSize('toto');
|
||||
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
it('should paddingSize to 1', () => {
|
||||
const result = dataUtilsService.paddingSize('toto=');
|
||||
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
it('should paddingSize to 2', () => {
|
||||
const result = dataUtilsService.paddingSize('toto==');
|
||||
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
|
||||
it('should parse links', () => {
|
||||
const result = dataUtilsService.parseLinks(
|
||||
'<http://localhost/api/entities?' +
|
||||
'sort=date%2Cdesc&sort=id&page=1&size=12>; rel="next",<http://localhost/api' +
|
||||
'/entities?sort=date%2Cdesc&sort=id&page=2&size=12>; rel="last",<http://localhost' +
|
||||
'/api/entities?sort=date%2Cdesc&sort=id&page=0&size=12>; rel="first"',
|
||||
);
|
||||
|
||||
expect(result.last).toBe(2);
|
||||
});
|
||||
|
||||
it('should return empty JSON object for empty string', () => {
|
||||
const result = dataUtilsService.parseLinks('');
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should return empty JSON object for text with no link header', () => {
|
||||
const result = dataUtilsService.parseLinks('JHipster rocks!');
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should return empty JSON object for text without >;', () => {
|
||||
const result = dataUtilsService.parseLinks(
|
||||
'<http://localhost/api/entities?' +
|
||||
'sort=date%2Cdesc&sort=id&page=1&size=12> rel="next",<http://localhost/api' +
|
||||
'/entities?sort=date%2Cdesc&sort=id&page=2&size=12> rel="last",<http://localhost' +
|
||||
'/api/entities?sort=date%2Cdesc&sort=id&page=0&size=12> rel="first"',
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
});
|
||||
|
||||
it('should return empty JSON object for text with no comma separated link header', () => {
|
||||
const result = dataUtilsService.parseLinks(
|
||||
'<http://localhost/api/entities?' +
|
||||
'sort=id%2Cdesc&sort=id&page=1&size=12>; rel="next"<http://localhost/api' +
|
||||
'/entities?sort=id%2Cdesc&sort=id&page=2&size=12>; rel="last"<http://localhost' +
|
||||
'/api/entities?sort=id%2Cdesc&sort=id&page=0&size=12>; rel="first"',
|
||||
);
|
||||
|
||||
expect(result).toStrictEqual({});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* An composable utility for data.
|
||||
*/
|
||||
const useDataUtils = () => ({
|
||||
/**
|
||||
* Method to abbreviate the text given
|
||||
*/
|
||||
abbreviate(text, append = '...') {
|
||||
if (text.length < 30) {
|
||||
return text;
|
||||
}
|
||||
return text ? text.substring(0, 15) + append + text.slice(-10) : '';
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to find the byte size of the string provides
|
||||
*/
|
||||
byteSize(base64String) {
|
||||
return this.formatAsBytes(this.size(base64String));
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to open file
|
||||
*/
|
||||
openFile(contentType, data) {
|
||||
const byteCharacters = atob(data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], {
|
||||
type: contentType,
|
||||
});
|
||||
const objectURL = URL.createObjectURL(blob);
|
||||
const win = window.open(objectURL);
|
||||
if (win) {
|
||||
win.onload = () => URL.revokeObjectURL(objectURL);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to convert the file to base64
|
||||
*/
|
||||
toBase64(file, cb) {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.readAsDataURL(file);
|
||||
fileReader.onload = (e: any) => {
|
||||
const base64Data = e.target.result.substring(e.target.result.indexOf('base64,') + 'base64,'.length);
|
||||
cb(base64Data);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to clear the input
|
||||
*/
|
||||
clearInputImage(entity, elementRef, field, fieldContentType, idInput) {
|
||||
if (entity && field && fieldContentType) {
|
||||
if (Object.hasOwn(entity, field)) {
|
||||
entity[field] = null;
|
||||
}
|
||||
if (Object.hasOwn(entity, fieldContentType)) {
|
||||
entity[fieldContentType] = null;
|
||||
}
|
||||
if (elementRef && idInput && elementRef.nativeElement.querySelector(`#${idInput}`)) {
|
||||
elementRef.nativeElement.querySelector(`#${idInput}`).value = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
endsWith(suffix, str) {
|
||||
return str.indexOf(suffix, str.length - suffix.length) !== -1;
|
||||
},
|
||||
|
||||
paddingSize(value) {
|
||||
if (this.endsWith('==', value)) {
|
||||
return 2;
|
||||
}
|
||||
if (this.endsWith('=', value)) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
|
||||
size(value) {
|
||||
return (value.length / 4) * 3 - this.paddingSize(value);
|
||||
},
|
||||
|
||||
formatAsBytes(size) {
|
||||
return `${size.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')} bytes`;
|
||||
},
|
||||
|
||||
setFileData(event, entity, field, isImage) {
|
||||
if (event && event.target.files && event.target.files[0]) {
|
||||
const file = event.target.files[0];
|
||||
if (isImage && !/^image\//.test(file.type)) {
|
||||
return;
|
||||
}
|
||||
this.toBase64(file, base64Data => {
|
||||
entity[field] = base64Data;
|
||||
entity[`${field}ContentType`] = file.type;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to download file
|
||||
*/
|
||||
downloadFile(contentType, data, fileName) {
|
||||
const byteCharacters = atob(data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i);
|
||||
}
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], {
|
||||
type: contentType,
|
||||
});
|
||||
const tempLink = document.createElement('a');
|
||||
tempLink.href = window.URL.createObjectURL(blob);
|
||||
tempLink.download = fileName;
|
||||
tempLink.target = '_blank';
|
||||
tempLink.click();
|
||||
},
|
||||
|
||||
/**
|
||||
* Method to parse header links
|
||||
*/
|
||||
parseLinks(header) {
|
||||
const links = {};
|
||||
|
||||
if ((header?.indexOf(',') ?? -1) === -1) {
|
||||
return links;
|
||||
}
|
||||
// Split parts by comma
|
||||
const parts = header.split(',');
|
||||
|
||||
// Parse each part into a named link
|
||||
parts.forEach(p => {
|
||||
if (p.indexOf('>;') === -1) {
|
||||
return;
|
||||
}
|
||||
const section = p.split('>;');
|
||||
const url = section[0].replace(/<(.*)/, '$1').trim();
|
||||
const queryString = { page: null };
|
||||
url.replace(new RegExp(/([^?=&]+)(=([^&]*))?/g), ($0, $1, $2, $3) => {
|
||||
queryString[$1] = $3;
|
||||
});
|
||||
let page = queryString.page;
|
||||
if (typeof page === 'string') {
|
||||
page = parseInt(page, 10);
|
||||
}
|
||||
const name = section[1].replace(/rel="(.*)"/, '$1').trim();
|
||||
links[name] = page;
|
||||
});
|
||||
return links;
|
||||
},
|
||||
});
|
||||
|
||||
export default useDataUtils;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { computed, defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
props: {
|
||||
page: Number,
|
||||
total: Number,
|
||||
itemsPerPage: Number,
|
||||
},
|
||||
setup(props) {
|
||||
const first = computed(() => ((props.page - 1) * props.itemsPerPage === 0 ? 1 : (props.page - 1) * props.itemsPerPage + 1));
|
||||
const second = computed(() => (props.page * props.itemsPerPage < props.total ? props.page * props.itemsPerPage : props.total));
|
||||
|
||||
return {
|
||||
first,
|
||||
second,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<div class="info jhi-item-count">
|
||||
<span>Showing {{ first }} - {{ second }} of {{ total }} items.</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./jhi-item-count.component.ts"></script>
|
||||
@@ -0,0 +1,33 @@
|
||||
export interface IUser {
|
||||
id?: any;
|
||||
login?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
email?: string;
|
||||
activated?: boolean;
|
||||
langKey?: string;
|
||||
authorities?: any[];
|
||||
createdBy?: string;
|
||||
createdDate?: Date;
|
||||
lastModifiedBy?: string;
|
||||
lastModifiedDate?: Date;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class User implements IUser {
|
||||
constructor(
|
||||
public id?: any,
|
||||
public login?: string,
|
||||
public firstName?: string,
|
||||
public lastName?: string,
|
||||
public email?: string,
|
||||
public activated?: boolean,
|
||||
public langKey?: string,
|
||||
public authorities?: any[],
|
||||
public createdBy?: string,
|
||||
public createdDate?: Date,
|
||||
public lastModifiedBy?: string,
|
||||
public lastModifiedDate?: Date,
|
||||
public password?: string,
|
||||
) {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum Authority {
|
||||
ADMIN = 'ROLE_ADMIN',
|
||||
USER = 'ROLE_USER',
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
export default defineComponent({
|
||||
compatConfig: { MODE: 3 },
|
||||
name: 'JhiSortIndicatorComponent',
|
||||
props: {
|
||||
currentOrder: String,
|
||||
fieldName: String,
|
||||
reverse: Boolean,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<font-awesome-icon :icon="currentOrder === fieldName ? (reverse ? 'sort-down' : 'sort-up') : 'sort'"> </font-awesome-icon>
|
||||
</template>
|
||||
|
||||
<script lang="ts" src="./jhi-sort-indicator.component.ts"></script>
|
||||
@@ -0,0 +1,9 @@
|
||||
import buildPaginationQueryOpts from './sorts';
|
||||
|
||||
describe('Sort', () => {
|
||||
it('should return an empty string if there is no pagination', () => {
|
||||
const result = buildPaginationQueryOpts(undefined);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
export default function buildPaginationQueryOpts(paginationQuery) {
|
||||
if (paginationQuery) {
|
||||
return Object.entries(paginationQuery)
|
||||
.map(([paramName, paramValue]) => {
|
||||
if (Array.isArray(paramValue)) {
|
||||
return paramValue.map(eachValue => `${paramName}=${eachValue}`).join('&');
|
||||
}
|
||||
return `${paramName}=${paramValue}`;
|
||||
})
|
||||
.join('&');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
declare module '*.vue' {
|
||||
import { type DefineComponent } from 'vue';
|
||||
const component: DefineComponent & any;
|
||||
export default component;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { useAccountStore as useStore } from '@/shared/config/store/account-store';
|
||||
export type AccountStore = ReturnType<typeof useStore>;
|
||||
export { useStore };
|
||||
@@ -0,0 +1,11 @@
|
||||
import { beforeAll } from 'vitest';
|
||||
import axios from 'axios';
|
||||
|
||||
beforeAll(() => {
|
||||
window.location.href = 'https://jhipster.tech/';
|
||||
|
||||
// Make sure axios is never executed.
|
||||
axios.interceptors.request.use(request => {
|
||||
throw new Error(`Error axios should be mocked ${request.url}`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,152 @@
|
||||
@keyframes lds-pacman-1 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes lds-pacman-1 {
|
||||
0% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: rotate(-45deg);
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(0deg);
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
}
|
||||
@keyframes lds-pacman-2 {
|
||||
0% {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: rotate(225deg);
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes lds-pacman-2 {
|
||||
0% {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
50% {
|
||||
-webkit-transform: rotate(225deg);
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: rotate(180deg);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
@keyframes lds-pacman-3 {
|
||||
0% {
|
||||
-webkit-transform: translate(190px, 0);
|
||||
transform: translate(190px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translate(70px, 0);
|
||||
transform: translate(70px, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@-webkit-keyframes lds-pacman-3 {
|
||||
0% {
|
||||
-webkit-transform: translate(190px, 0);
|
||||
transform: translate(190px, 0);
|
||||
opacity: 0;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
-webkit-transform: translate(70px, 0);
|
||||
transform: translate(70px, 0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.app-loading {
|
||||
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 10em;
|
||||
}
|
||||
.app-loading p {
|
||||
display: block;
|
||||
font-size: 1.17em;
|
||||
margin-inline-start: 0px;
|
||||
margin-inline-end: 0px;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.app-loading .lds-pacman {
|
||||
position: relative;
|
||||
margin: auto;
|
||||
width: 200px !important;
|
||||
height: 200px !important;
|
||||
-webkit-transform: translate(-100px, -100px) scale(1) translate(100px, 100px);
|
||||
transform: translate(-100px, -100px) scale(1) translate(100px, 100px);
|
||||
}
|
||||
.app-loading .lds-pacman > div:nth-child(2) div {
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
width: 120px;
|
||||
height: 60px;
|
||||
border-radius: 120px 120px 0 0;
|
||||
background: #bbcedd;
|
||||
-webkit-animation: lds-pacman-1 1s linear infinite;
|
||||
animation: lds-pacman-1 1s linear infinite;
|
||||
-webkit-transform-origin: 60px 60px;
|
||||
transform-origin: 60px 60px;
|
||||
}
|
||||
.app-loading .lds-pacman > div:nth-child(2) div:nth-child(2) {
|
||||
-webkit-animation: lds-pacman-2 1s linear infinite;
|
||||
animation: lds-pacman-2 1s linear infinite;
|
||||
}
|
||||
.app-loading .lds-pacman > div:nth-child(1) div {
|
||||
position: absolute;
|
||||
top: 97px;
|
||||
left: -8px;
|
||||
width: 24px;
|
||||
height: 10px;
|
||||
background-image: url('/content/images/logo-jhipster.png');
|
||||
background-size: contain;
|
||||
-webkit-animation: lds-pacman-3 1s linear infinite;
|
||||
animation: lds-pacman-3 1.5s linear infinite;
|
||||
}
|
||||
.app-loading .lds-pacman > div:nth-child(1) div:nth-child(1) {
|
||||
-webkit-animation-delay: -0.67s;
|
||||
animation-delay: -1s;
|
||||
}
|
||||
.app-loading .lds-pacman > div:nth-child(1) div:nth-child(2) {
|
||||
-webkit-animation-delay: -0.33s;
|
||||
animation-delay: -0.5s;
|
||||
}
|
||||
.app-loading .lds-pacman > div:nth-child(1) div:nth-child(3) {
|
||||
-webkit-animation-delay: 0s;
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 6.9 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 9.5 KiB |