Initial version of sasiedzi generated by generator-jhipster@8.7.2

This commit is contained in:
2024-10-29 19:16:15 +01:00
commit adea956b48
271 changed files with 35183 additions and 0 deletions
+58
View File
@@ -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 -->
+13
View File
@@ -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',
});
+14
View File
@@ -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');
}
}
+72
View File
@@ -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&nbsp;<span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpAll }}</span></span
>&nbsp;
<span class="badge badge-success" @click="threadDumpFilter = 'RUNNABLE'"
>Runnable&nbsp;<span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpRunnable }}</span></span
>&nbsp;
<span class="badge badge-info" @click="threadDumpFilter = 'WAITING'"
>Waiting&nbsp;<span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpWaiting }}</span></span
>&nbsp;
<span class="badge badge-warning" @click="threadDumpFilter = 'TIMED_WAITING'"
>Timed Waiting&nbsp;<span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpTimedWaiting }}</span></span
>&nbsp;
<span class="badge badge-danger" @click="threadDumpFilter = 'BLOCKED'"
>Blocked&nbsp;<span class="badge badge-pill badge-default">{{ threadDumpData.threadDumpBlocked }}</span></span
>&nbsp;
<div class="mt-2">&nbsp;</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
>&nbsp;{{ 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>
+23
View File
@@ -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 {};
},
});
+17
View File
@@ -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>
+4
View File
@@ -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,
};
},
});
+20
View File
@@ -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,
};
},
});
+53
View File
@@ -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>
+6
View File
@@ -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);
});
});
}
}
+124
View File
@@ -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');
+40
View File
@@ -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] },
},
];
+13
View File
@@ -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
],
};
+46
View File
@@ -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;
+8
View File
@@ -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('');
});
});
+13
View File
@@ -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 '';
}
+5
View File
@@ -0,0 +1,5 @@
declare module '*.vue' {
import { type DefineComponent } from 'vue';
const component: DefineComponent & any;
export default component;
}
+3
View File
@@ -0,0 +1,3 @@
import { useAccountStore as useStore } from '@/shared/config/store/account-store';
export type AccountStore = ReturnType<typeof useStore>;
export { useStore };
+11
View File
@@ -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}`);
});
});
+152
View File
@@ -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;
}
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Some files were not shown because too many files have changed in this diff Show More