Initial Project Commit
This commit is contained in:
commit
a6dea9c888
2148 changed files with 173870 additions and 0 deletions
21
src/main/webapp/app/account/account.route.ts
Normal file
21
src/main/webapp/app/account/account.route.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import { Routes } from '@angular/router';
|
||||
|
||||
import activateRoute from './activate/activate.route';
|
||||
import passwordRoute from './password/password.route';
|
||||
import passwordResetFinishRoute from './password-reset/finish/password-reset-finish.route';
|
||||
import passwordResetInitRoute from './password-reset/init/password-reset-init.route';
|
||||
import registerRoute from './register/register.route';
|
||||
import sessionsRoute from './sessions/sessions.route';
|
||||
import settingsRoute from './settings/settings.route';
|
||||
|
||||
const accountRoutes: Routes = [
|
||||
activateRoute,
|
||||
passwordRoute,
|
||||
passwordResetFinishRoute,
|
||||
passwordResetInitRoute,
|
||||
/* registerRoute, DISABLED Users are created by admin, not requested */
|
||||
sessionsRoute,
|
||||
settingsRoute,
|
||||
];
|
||||
|
||||
export default accountRoutes;
|
18
src/main/webapp/app/account/activate/activate.component.html
Normal file
18
src/main/webapp/app/account/activate/activate.component.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<h1 jhiTranslate="activate.title">Ativação</h1>
|
||||
@if (success()) {
|
||||
<div class="alert alert-success">
|
||||
<span jhiTranslate="activate.messages.success"><strong>Utilizador ativado com sucesso.</strong> Por favor </span>
|
||||
<a class="alert-link" routerLink="/login" jhiTranslate="global.messages.info.authenticated.link">entrar</a>.
|
||||
</div>
|
||||
}
|
||||
@if (error()) {
|
||||
<div class="alert alert-danger" jhiTranslate="activate.messages.error">
|
||||
<strong>O utilizador não pode ser ativado.</strong> Por favor utilize o formulário de registo para criar uma nova conta.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,68 @@
|
|||
import { TestBed, waitForAsync, tick, fakeAsync, inject } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { ActivateService } from './activate.service';
|
||||
import ActivateComponent from './activate.component';
|
||||
|
||||
describe('ActivateComponent', () => {
|
||||
let comp: ActivateComponent;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, ActivateComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { queryParams: of({ key: 'ABC123' }) },
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideTemplate(ActivateComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
const fixture = TestBed.createComponent(ActivateComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('calls activate.get with the key from params', inject(
|
||||
[ActivateService],
|
||||
fakeAsync((service: ActivateService) => {
|
||||
jest.spyOn(service, 'get').mockReturnValue(of());
|
||||
|
||||
comp.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(service.get).toHaveBeenCalledWith('ABC123');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should set set success to true upon successful activation', inject(
|
||||
[ActivateService],
|
||||
fakeAsync((service: ActivateService) => {
|
||||
jest.spyOn(service, 'get').mockReturnValue(of({}));
|
||||
|
||||
comp.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(comp.error()).toBe(false);
|
||||
expect(comp.success()).toBe(true);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should set set error to true upon activation failure', inject(
|
||||
[ActivateService],
|
||||
fakeAsync((service: ActivateService) => {
|
||||
jest.spyOn(service, 'get').mockReturnValue(throwError('ERROR'));
|
||||
|
||||
comp.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(comp.error()).toBe(true);
|
||||
expect(comp.success()).toBe(false);
|
||||
}),
|
||||
));
|
||||
});
|
27
src/main/webapp/app/account/activate/activate.component.ts
Normal file
27
src/main/webapp/app/account/activate/activate.component.ts
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import { mergeMap } from 'rxjs/operators';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { ActivateService } from './activate.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-activate',
|
||||
imports: [SharedModule, RouterModule],
|
||||
templateUrl: './activate.component.html',
|
||||
})
|
||||
export default class ActivateComponent implements OnInit {
|
||||
error = signal(false);
|
||||
success = signal(false);
|
||||
|
||||
private activateService = inject(ActivateService);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.pipe(mergeMap(params => this.activateService.get(params.key))).subscribe({
|
||||
next: () => this.success.set(true),
|
||||
error: () => this.error.set(true),
|
||||
});
|
||||
}
|
||||
}
|
11
src/main/webapp/app/account/activate/activate.route.ts
Normal file
11
src/main/webapp/app/account/activate/activate.route.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import ActivateComponent from './activate.component';
|
||||
|
||||
const activateRoute: Route = {
|
||||
path: 'activate',
|
||||
component: ActivateComponent,
|
||||
title: 'activate.title',
|
||||
};
|
||||
|
||||
export default activateRoute;
|
|
@ -0,0 +1,47 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { ActivateService } from './activate.service';
|
||||
|
||||
describe('ActivateService Service', () => {
|
||||
let service: ActivateService;
|
||||
let httpMock: HttpTestingController;
|
||||
let applicationConfigService: ApplicationConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(ActivateService);
|
||||
applicationConfigService = TestBed.inject(ApplicationConfigService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should call api/activate endpoint with correct values', () => {
|
||||
// GIVEN
|
||||
let expectedResult;
|
||||
const key = 'key';
|
||||
const value = true;
|
||||
|
||||
// WHEN
|
||||
service.get(key).subscribe(received => {
|
||||
expectedResult = received;
|
||||
});
|
||||
const testRequest = httpMock.expectOne({
|
||||
method: 'GET',
|
||||
url: applicationConfigService.getEndpointFor(`api/activate?key=${key}`),
|
||||
});
|
||||
testRequest.flush(value);
|
||||
|
||||
// THEN
|
||||
expect(expectedResult).toEqual(value);
|
||||
});
|
||||
});
|
||||
});
|
17
src/main/webapp/app/account/activate/activate.service.ts
Normal file
17
src/main/webapp/app/account/activate/activate.service.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ActivateService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
get(key: string): Observable<{}> {
|
||||
return this.http.get(this.applicationConfigService.getEndpointFor('api/activate'), {
|
||||
params: new HttpParams().set('key', key),
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-4">
|
||||
<h1 jhiTranslate="reset.finish.title">Reposição da palavra-passe</h1>
|
||||
|
||||
@if (initialized() && !key()) {
|
||||
<div class="alert alert-danger" jhiTranslate="reset.finish.messages.keymissing">Chave de reposição em falta.</div>
|
||||
}
|
||||
|
||||
@if (key() && !success()) {
|
||||
<div class="alert alert-warning">
|
||||
<span jhiTranslate="reset.finish.messages.info">Escolha uma nova palavra-passe</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="alert alert-danger">
|
||||
<span jhiTranslate="reset.finish.messages.error"
|
||||
>A sua palavra-passe não pode ser reposta. O pedido de reposição da palavra-passe é válido apenas por 24 horas.</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (success()) {
|
||||
<div class="alert alert-success">
|
||||
<span jhiTranslate="reset.finish.messages.success"><strong>A sua palavra-passe foi reposta.</strong> Por favor </span>
|
||||
<a class="alert-link" routerLink="/login" jhiTranslate="global.messages.info.authenticated.link">entrar</a>.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (doNotMatch()) {
|
||||
<div class="alert alert-danger" jhiTranslate="global.messages.error.dontmatch">
|
||||
A palavra-passe e a sua confirmação devem ser iguais!
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (key() && !success()) {
|
||||
<div>
|
||||
<form name="form" (ngSubmit)="finishReset()" [formGroup]="passwordForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="newPassword" jhiTranslate="global.form.newpassword.label">Nova palavra-passe</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
placeholder="{{ 'global.form.newpassword.placeholder' | translate }}"
|
||||
formControlName="newPassword"
|
||||
data-cy="resetPassword"
|
||||
#newPassword
|
||||
/>
|
||||
|
||||
@if (
|
||||
passwordForm.get('newPassword')!.invalid &&
|
||||
(passwordForm.get('newPassword')!.dirty || passwordForm.get('newPassword')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (passwordForm.get('newPassword')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.required"
|
||||
>A palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('newPassword')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.minlength"
|
||||
>A palavra-passe deve ter pelo menos 4 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('newPassword')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.maxlength"
|
||||
>A palavra-passe não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<jhi-password-strength-bar [passwordToCheck]="passwordForm.get('newPassword')!.value"></jhi-password-strength-bar>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="confirmPassword" jhiTranslate="global.form.confirmpassword.label"
|
||||
>Confirmação de nova palavra-passe</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
placeholder="{{ 'global.form.confirmpassword.placeholder' | translate }}"
|
||||
formControlName="confirmPassword"
|
||||
data-cy="confirmResetPassword"
|
||||
/>
|
||||
|
||||
@if (
|
||||
passwordForm.get('confirmPassword')!.invalid &&
|
||||
(passwordForm.get('confirmPassword')!.dirty || passwordForm.get('confirmPassword')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (passwordForm.get('confirmPassword')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.required"
|
||||
>A confirmação da palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('confirmPassword')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.minlength"
|
||||
>A confirmação da palavra-passe deve ter pelo menos 4 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('confirmPassword')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.maxlength"
|
||||
>A confirmação da palavra-passe não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="passwordForm.invalid"
|
||||
class="btn btn-primary"
|
||||
data-cy="submit"
|
||||
jhiTranslate="reset.finish.form.button"
|
||||
>
|
||||
Repor a palavra-passe
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,97 @@
|
|||
import { ElementRef, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import PasswordResetFinishComponent from './password-reset-finish.component';
|
||||
import { PasswordResetFinishService } from './password-reset-finish.service';
|
||||
|
||||
describe('PasswordResetFinishComponent', () => {
|
||||
let fixture: ComponentFixture<PasswordResetFinishComponent>;
|
||||
let comp: PasswordResetFinishComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, PasswordResetFinishComponent],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: { queryParams: of({ key: 'XYZPDQ' }) },
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideTemplate(PasswordResetFinishComponent, '')
|
||||
.createComponent(PasswordResetFinishComponent);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordResetFinishComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.ngOnInit();
|
||||
});
|
||||
|
||||
it('should define its initial state', () => {
|
||||
expect(comp.initialized()).toBe(true);
|
||||
expect(comp.key()).toEqual('XYZPDQ');
|
||||
});
|
||||
|
||||
it('sets focus after the view has been initialized', () => {
|
||||
const node = {
|
||||
focus: jest.fn(),
|
||||
};
|
||||
comp.newPassword = signal<ElementRef>(new ElementRef(node));
|
||||
|
||||
comp.ngAfterViewInit();
|
||||
|
||||
expect(node.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should ensure the two passwords entered match', () => {
|
||||
comp.passwordForm.patchValue({
|
||||
newPassword: 'password',
|
||||
confirmPassword: 'non-matching',
|
||||
});
|
||||
|
||||
comp.finishReset();
|
||||
|
||||
expect(comp.doNotMatch()).toBe(true);
|
||||
});
|
||||
|
||||
it('should update success to true after resetting password', inject(
|
||||
[PasswordResetFinishService],
|
||||
fakeAsync((service: PasswordResetFinishService) => {
|
||||
jest.spyOn(service, 'save').mockReturnValue(of({}));
|
||||
comp.passwordForm.patchValue({
|
||||
newPassword: 'password',
|
||||
confirmPassword: 'password',
|
||||
});
|
||||
|
||||
comp.finishReset();
|
||||
tick();
|
||||
|
||||
expect(service.save).toHaveBeenCalledWith('XYZPDQ', 'password');
|
||||
expect(comp.success()).toBe(true);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should notify of generic error', inject(
|
||||
[PasswordResetFinishService],
|
||||
fakeAsync((service: PasswordResetFinishService) => {
|
||||
jest.spyOn(service, 'save').mockReturnValue(throwError('ERROR'));
|
||||
comp.passwordForm.patchValue({
|
||||
newPassword: 'password',
|
||||
confirmPassword: 'password',
|
||||
});
|
||||
|
||||
comp.finishReset();
|
||||
tick();
|
||||
|
||||
expect(service.save).toHaveBeenCalledWith('XYZPDQ', 'password');
|
||||
expect(comp.success()).toBe(false);
|
||||
expect(comp.error()).toBe(true);
|
||||
}),
|
||||
));
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import { Component, inject, OnInit, AfterViewInit, ElementRef, signal, viewChild } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, RouterModule } from '@angular/router';
|
||||
import PasswordStrengthBarComponent from 'app/account/password/password-strength-bar/password-strength-bar.component';
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
|
||||
import { PasswordResetFinishService } from './password-reset-finish.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-password-reset-finish',
|
||||
imports: [SharedModule, RouterModule, FormsModule, ReactiveFormsModule, PasswordStrengthBarComponent],
|
||||
templateUrl: './password-reset-finish.component.html',
|
||||
})
|
||||
export default class PasswordResetFinishComponent implements OnInit, AfterViewInit {
|
||||
newPassword = viewChild.required<ElementRef>('newPassword');
|
||||
|
||||
initialized = signal(false);
|
||||
doNotMatch = signal(false);
|
||||
error = signal(false);
|
||||
success = signal(false);
|
||||
key = signal('');
|
||||
|
||||
passwordForm = new FormGroup({
|
||||
newPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
|
||||
}),
|
||||
confirmPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
|
||||
}),
|
||||
});
|
||||
|
||||
private passwordResetFinishService = inject(PasswordResetFinishService);
|
||||
private route = inject(ActivatedRoute);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.queryParams.subscribe(params => {
|
||||
if (params['key']) {
|
||||
this.key.set(params['key']);
|
||||
}
|
||||
this.initialized.set(true);
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.newPassword().nativeElement.focus();
|
||||
}
|
||||
|
||||
finishReset(): void {
|
||||
this.doNotMatch.set(false);
|
||||
this.error.set(false);
|
||||
|
||||
const { newPassword, confirmPassword } = this.passwordForm.getRawValue();
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
this.doNotMatch.set(true);
|
||||
} else {
|
||||
this.passwordResetFinishService.save(this.key(), newPassword).subscribe({
|
||||
next: () => this.success.set(true),
|
||||
error: () => this.error.set(true),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import PasswordResetFinishComponent from './password-reset-finish.component';
|
||||
|
||||
const passwordResetFinishRoute: Route = {
|
||||
path: 'reset/finish',
|
||||
component: PasswordResetFinishComponent,
|
||||
title: 'global.menu.account.password',
|
||||
};
|
||||
|
||||
export default passwordResetFinishRoute;
|
|
@ -0,0 +1,44 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { PasswordResetFinishService } from './password-reset-finish.service';
|
||||
|
||||
describe('PasswordResetFinish Service', () => {
|
||||
let service: PasswordResetFinishService;
|
||||
let httpMock: HttpTestingController;
|
||||
let applicationConfigService: ApplicationConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(PasswordResetFinishService);
|
||||
applicationConfigService = TestBed.inject(ApplicationConfigService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should call reset-password/finish endpoint with correct values', () => {
|
||||
// GIVEN
|
||||
const key = 'abc';
|
||||
const newPassword = 'password';
|
||||
|
||||
// WHEN
|
||||
service.save(key, newPassword).subscribe();
|
||||
|
||||
const testRequest = httpMock.expectOne({
|
||||
method: 'POST',
|
||||
url: applicationConfigService.getEndpointFor('api/account/reset-password/finish'),
|
||||
});
|
||||
|
||||
// THEN
|
||||
expect(testRequest.request.body).toEqual({ key, newPassword });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PasswordResetFinishService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
save(key: string, newPassword: string): Observable<{}> {
|
||||
return this.http.post(this.applicationConfigService.getEndpointFor('api/account/reset-password/finish'), { key, newPassword });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<h1 jhiTranslate="reset.request.title">Reponha a palavra-passe</h1>
|
||||
|
||||
<jhi-alert-error></jhi-alert-error>
|
||||
|
||||
@if (!success()) {
|
||||
<div class="alert alert-warning">
|
||||
<span jhiTranslate="reset.request.messages.info">Introduza o endereço de email utilizado para registar</span>
|
||||
</div>
|
||||
<form name="form" (ngSubmit)="requestReset()" [formGroup]="resetRequestForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="email" jhiTranslate="global.form.email.label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="{{ 'global.form.email.placeholder' | translate }}"
|
||||
formControlName="email"
|
||||
data-cy="emailResetPassword"
|
||||
#email
|
||||
/>
|
||||
|
||||
@if (
|
||||
resetRequestForm.get('email')!.invalid && (resetRequestForm.get('email')!.dirty || resetRequestForm.get('email')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (resetRequestForm.get('email')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.required">O email é obrigatório.</small>
|
||||
}
|
||||
@if (resetRequestForm.get('email')?.errors?.email) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.invalid">Email inválido.</small>
|
||||
}
|
||||
|
||||
@if (resetRequestForm.get('email')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.minlength"
|
||||
>O email deve ter pelo menos 5 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (resetRequestForm.get('email')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.maxlength"
|
||||
>O email não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="resetRequestForm.invalid"
|
||||
class="btn btn-primary"
|
||||
data-cy="submit"
|
||||
jhiTranslate="reset.request.form.button"
|
||||
>
|
||||
Reposição da palavra-passe
|
||||
</button>
|
||||
</form>
|
||||
} @else {
|
||||
<div class="alert alert-success">
|
||||
<span jhiTranslate="reset.request.messages.success"
|
||||
>Verifique o seu email para mais detalhes sobre como repor a palavra-passe.</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,58 @@
|
|||
import { ElementRef, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, inject } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import PasswordResetInitComponent from './password-reset-init.component';
|
||||
import { PasswordResetInitService } from './password-reset-init.service';
|
||||
|
||||
describe('PasswordResetInitComponent', () => {
|
||||
let fixture: ComponentFixture<PasswordResetInitComponent>;
|
||||
let comp: PasswordResetInitComponent;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, PasswordResetInitComponent],
|
||||
providers: [FormBuilder],
|
||||
})
|
||||
.overrideTemplate(PasswordResetInitComponent, '')
|
||||
.createComponent(PasswordResetInitComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('sets focus after the view has been initialized', () => {
|
||||
const node = {
|
||||
focus: jest.fn(),
|
||||
};
|
||||
comp.email = signal<ElementRef>(new ElementRef(node));
|
||||
|
||||
comp.ngAfterViewInit();
|
||||
|
||||
expect(node.focus).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('notifies of success upon successful requestReset', inject([PasswordResetInitService], (service: PasswordResetInitService) => {
|
||||
jest.spyOn(service, 'save').mockReturnValue(of({}));
|
||||
comp.resetRequestForm.patchValue({
|
||||
email: 'user@domain.com',
|
||||
});
|
||||
|
||||
comp.requestReset();
|
||||
|
||||
expect(service.save).toHaveBeenCalledWith('user@domain.com');
|
||||
expect(comp.success()).toBe(true);
|
||||
}));
|
||||
|
||||
it('no notification of success upon error response', inject([PasswordResetInitService], (service: PasswordResetInitService) => {
|
||||
const err = { status: 503, data: 'something else' };
|
||||
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
|
||||
comp.resetRequestForm.patchValue({
|
||||
email: 'user@domain.com',
|
||||
});
|
||||
comp.requestReset();
|
||||
|
||||
expect(service.save).toHaveBeenCalledWith('user@domain.com');
|
||||
expect(comp.success()).toBe(false);
|
||||
}));
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import { Component, AfterViewInit, ElementRef, inject, signal, viewChild } from '@angular/core';
|
||||
import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
|
||||
import { PasswordResetInitService } from './password-reset-init.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-password-reset-init',
|
||||
imports: [SharedModule, FormsModule, ReactiveFormsModule],
|
||||
templateUrl: './password-reset-init.component.html',
|
||||
})
|
||||
export default class PasswordResetInitComponent implements AfterViewInit {
|
||||
email = viewChild.required<ElementRef>('email');
|
||||
|
||||
success = signal(false);
|
||||
resetRequestForm;
|
||||
|
||||
private passwordResetInitService = inject(PasswordResetInitService);
|
||||
private fb = inject(FormBuilder);
|
||||
|
||||
constructor() {
|
||||
this.resetRequestForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email]],
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.email().nativeElement.focus();
|
||||
}
|
||||
|
||||
requestReset(): void {
|
||||
this.passwordResetInitService.save(this.resetRequestForm.get(['email'])!.value).subscribe(() => this.success.set(true));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import PasswordResetInitComponent from './password-reset-init.component';
|
||||
|
||||
const passwordResetInitRoute: Route = {
|
||||
path: 'reset/request',
|
||||
component: PasswordResetInitComponent,
|
||||
title: 'global.menu.account.password',
|
||||
};
|
||||
|
||||
export default passwordResetInitRoute;
|
|
@ -0,0 +1,43 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { PasswordResetInitService } from './password-reset-init.service';
|
||||
|
||||
describe('PasswordResetInit Service', () => {
|
||||
let service: PasswordResetInitService;
|
||||
let httpMock: HttpTestingController;
|
||||
let applicationConfigService: ApplicationConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(PasswordResetInitService);
|
||||
applicationConfigService = TestBed.inject(ApplicationConfigService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should call reset-password/init endpoint with correct values', () => {
|
||||
// GIVEN
|
||||
const mail = 'test@test.com';
|
||||
|
||||
// WHEN
|
||||
service.save(mail).subscribe();
|
||||
|
||||
const testRequest = httpMock.expectOne({
|
||||
method: 'POST',
|
||||
url: applicationConfigService.getEndpointFor('api/account/reset-password/init'),
|
||||
});
|
||||
|
||||
// THEN
|
||||
expect(testRequest.request.body).toEqual(mail);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PasswordResetInitService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
save(mail: string): Observable<{}> {
|
||||
return this.http.post(this.applicationConfigService.getEndpointFor('api/account/reset-password/init'), mail);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
<div id="strength">
|
||||
<small jhiTranslate="global.messages.validate.newpassword.strength">Nível de dificuldade da palavra-passe:</small>
|
||||
<ul id="strengthBar">
|
||||
<li class="point"></li>
|
||||
<li class="point"></li>
|
||||
<li class="point"></li>
|
||||
<li class="point"></li>
|
||||
<li class="point"></li>
|
||||
</ul>
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
/* ==========================================================================
|
||||
start Password strength bar style
|
||||
========================================================================== */
|
||||
ul#strength {
|
||||
display: inline;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
margin-left: 15px;
|
||||
padding: 0;
|
||||
vertical-align: 2px;
|
||||
}
|
||||
|
||||
.point {
|
||||
background: #ddd;
|
||||
border-radius: 2px;
|
||||
display: inline-block;
|
||||
height: 5px;
|
||||
margin-right: 1px;
|
||||
width: 20px;
|
||||
&:last-child {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import PasswordStrengthBarComponent from './password-strength-bar.component';
|
||||
|
||||
describe('PasswordStrengthBarComponent', () => {
|
||||
let comp: PasswordStrengthBarComponent;
|
||||
let fixture: ComponentFixture<PasswordStrengthBarComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [PasswordStrengthBarComponent],
|
||||
})
|
||||
.overrideTemplate(PasswordStrengthBarComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordStrengthBarComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
describe('PasswordStrengthBarComponents', () => {
|
||||
it('should initialize with default values', () => {
|
||||
expect(comp.measureStrength('')).toBe(0);
|
||||
expect(comp.colors).toEqual(['#F00', '#F90', '#FF0', '#9F0', '#0F0']);
|
||||
expect(comp.getColor(0).idx).toBe(1);
|
||||
expect(comp.getColor(0).color).toBe(comp.colors[0]);
|
||||
});
|
||||
|
||||
it('should increase strength upon password value change', () => {
|
||||
expect(comp.measureStrength('')).toBe(0);
|
||||
expect(comp.measureStrength('aa')).toBeGreaterThanOrEqual(comp.measureStrength(''));
|
||||
expect(comp.measureStrength('aa^6')).toBeGreaterThanOrEqual(comp.measureStrength('aa'));
|
||||
expect(comp.measureStrength('Aa090(**)')).toBeGreaterThanOrEqual(comp.measureStrength('aa^6'));
|
||||
expect(comp.measureStrength('Aa090(**)+-07365')).toBeGreaterThanOrEqual(comp.measureStrength('Aa090(**)'));
|
||||
});
|
||||
|
||||
it('should change the color based on strength', () => {
|
||||
expect(comp.getColor(0).color).toBe(comp.colors[0]);
|
||||
expect(comp.getColor(11).color).toBe(comp.colors[1]);
|
||||
expect(comp.getColor(22).color).toBe(comp.colors[2]);
|
||||
expect(comp.getColor(33).color).toBe(comp.colors[3]);
|
||||
expect(comp.getColor(44).color).toBe(comp.colors[4]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,77 @@
|
|||
import { Component, ElementRef, inject, Input, Renderer2 } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-password-strength-bar',
|
||||
imports: [SharedModule],
|
||||
templateUrl: './password-strength-bar.component.html',
|
||||
styleUrl: './password-strength-bar.component.scss',
|
||||
})
|
||||
export default class PasswordStrengthBarComponent {
|
||||
colors = ['#F00', '#F90', '#FF0', '#9F0', '#0F0'];
|
||||
|
||||
private renderer = inject(Renderer2);
|
||||
private elementRef = inject(ElementRef);
|
||||
|
||||
measureStrength(p: string): number {
|
||||
let force = 0;
|
||||
const regex = /[$-/:-?{-~!"^_`[\]]/g; // "
|
||||
const lowerLetters = /[a-z]+/.test(p);
|
||||
const upperLetters = /[A-Z]+/.test(p);
|
||||
const numbers = /\d+/.test(p);
|
||||
const symbols = regex.test(p);
|
||||
|
||||
const flags = [lowerLetters, upperLetters, numbers, symbols];
|
||||
const passedMatches = flags.filter((isMatchedFlag: boolean) => isMatchedFlag === true).length;
|
||||
|
||||
force += 2 * p.length + (p.length >= 10 ? 1 : 0);
|
||||
force += passedMatches * 10;
|
||||
|
||||
// penalty (short password)
|
||||
force = p.length <= 6 ? Math.min(force, 10) : force;
|
||||
|
||||
// penalty (poor variety of characters)
|
||||
force = passedMatches === 1 ? Math.min(force, 10) : force;
|
||||
force = passedMatches === 2 ? Math.min(force, 20) : force;
|
||||
force = passedMatches === 3 ? Math.min(force, 40) : force;
|
||||
|
||||
return force;
|
||||
}
|
||||
|
||||
getColor(s: number): { idx: number; color: string } {
|
||||
let idx = 0;
|
||||
if (s > 10) {
|
||||
if (s <= 20) {
|
||||
idx = 1;
|
||||
} else if (s <= 30) {
|
||||
idx = 2;
|
||||
} else if (s <= 40) {
|
||||
idx = 3;
|
||||
} else {
|
||||
idx = 4;
|
||||
}
|
||||
}
|
||||
return { idx: idx + 1, color: this.colors[idx] };
|
||||
}
|
||||
|
||||
@Input()
|
||||
set passwordToCheck(password: string) {
|
||||
if (password) {
|
||||
const c = this.getColor(this.measureStrength(password));
|
||||
const element = this.elementRef.nativeElement;
|
||||
if (element.className) {
|
||||
this.renderer.removeClass(element, element.className);
|
||||
}
|
||||
const lis = element.getElementsByTagName('li');
|
||||
for (let i = 0; i < lis.length; i++) {
|
||||
if (i < c.idx) {
|
||||
this.renderer.setStyle(lis[i], 'backgroundColor', c.color);
|
||||
} else {
|
||||
this.renderer.setStyle(lis[i], 'backgroundColor', '#DDD');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
146
src/main/webapp/app/account/password/password.component.html
Normal file
146
src/main/webapp/app/account/password/password.component.html
Normal file
|
@ -0,0 +1,146 @@
|
|||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
@if (account$ | async; as account) {
|
||||
<div class="col-md-8">
|
||||
<h2 jhiTranslate="password.title" [translateValues]="{ username: account.login }">
|
||||
Palavra-passe para [<strong>{{ account.login }}</strong
|
||||
>]
|
||||
</h2>
|
||||
|
||||
@if (success()) {
|
||||
<div class="alert alert-success" jhiTranslate="password.messages.success">
|
||||
<strong>Palavra-passe alterada com sucesso!</strong>
|
||||
</div>
|
||||
}
|
||||
@if (error()) {
|
||||
<div class="alert alert-danger" jhiTranslate="password.messages.error">
|
||||
<strong>Ocorreu um erro!</strong> A palavra-passe não pode ser alterada.
|
||||
</div>
|
||||
}
|
||||
@if (doNotMatch()) {
|
||||
<div class="alert alert-danger" jhiTranslate="global.messages.error.dontmatch">
|
||||
A palavra-passe e a sua confirmação devem ser iguais!
|
||||
</div>
|
||||
}
|
||||
|
||||
<form name="form" (ngSubmit)="changePassword()" [formGroup]="passwordForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="currentPassword" jhiTranslate="global.form.currentpassword.label">Palavra-passe actual</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="currentPassword"
|
||||
name="currentPassword"
|
||||
placeholder="{{ 'global.form.currentpassword.placeholder' | translate }}"
|
||||
formControlName="currentPassword"
|
||||
data-cy="currentPassword"
|
||||
/>
|
||||
|
||||
@if (
|
||||
passwordForm.get('currentPassword')!.invalid &&
|
||||
(passwordForm.get('currentPassword')!.dirty || passwordForm.get('currentPassword')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (passwordForm.get('currentPassword')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.required"
|
||||
>A palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="newPassword" jhiTranslate="global.form.newpassword.label">Nova palavra-passe</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
placeholder="{{ 'global.form.newpassword.placeholder' | translate }}"
|
||||
formControlName="newPassword"
|
||||
data-cy="newPassword"
|
||||
/>
|
||||
|
||||
@if (
|
||||
passwordForm.get('newPassword')!.invalid &&
|
||||
(passwordForm.get('newPassword')!.dirty || passwordForm.get('newPassword')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (passwordForm.get('newPassword')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.required"
|
||||
>A palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('newPassword')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.minlength"
|
||||
>A palavra-passe deve ter pelo menos 4 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('newPassword')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.maxlength"
|
||||
>A palavra-passe não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<jhi-password-strength-bar [passwordToCheck]="passwordForm.get('newPassword')!.value"></jhi-password-strength-bar>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="confirmPassword" jhiTranslate="global.form.confirmpassword.label"
|
||||
>Confirmação de nova palavra-passe</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
placeholder="{{ 'global.form.confirmpassword.placeholder' | translate }}"
|
||||
formControlName="confirmPassword"
|
||||
data-cy="confirmPassword"
|
||||
/>
|
||||
|
||||
@if (
|
||||
passwordForm.get('confirmPassword')!.invalid &&
|
||||
(passwordForm.get('confirmPassword')!.dirty || passwordForm.get('confirmPassword')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (passwordForm.get('confirmPassword')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.required"
|
||||
>A confirmação da palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('confirmPassword')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.minlength"
|
||||
>A confirmação da palavra-passe deve ter pelo menos 4 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (passwordForm.get('confirmPassword')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.maxlength"
|
||||
>A confirmação da palavra-passe não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="passwordForm.invalid"
|
||||
class="btn btn-primary"
|
||||
data-cy="submit"
|
||||
jhiTranslate="password.form.button"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
103
src/main/webapp/app/account/password/password.component.spec.ts
Normal file
103
src/main/webapp/app/account/password/password.component.spec.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
jest.mock('app/core/auth/account.service');
|
||||
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpResponse } from '@angular/common/http';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
|
||||
import PasswordComponent from './password.component';
|
||||
import { PasswordService } from './password.service';
|
||||
|
||||
describe('PasswordComponent', () => {
|
||||
let comp: PasswordComponent;
|
||||
let fixture: ComponentFixture<PasswordComponent>;
|
||||
let service: PasswordService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, PasswordComponent],
|
||||
providers: [FormBuilder, AccountService],
|
||||
})
|
||||
.overrideTemplate(PasswordComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(PasswordComponent);
|
||||
comp = fixture.componentInstance;
|
||||
service = TestBed.inject(PasswordService);
|
||||
});
|
||||
|
||||
it('should show error if passwords do not match', () => {
|
||||
// GIVEN
|
||||
comp.passwordForm.patchValue({
|
||||
newPassword: 'password1',
|
||||
confirmPassword: 'password2',
|
||||
});
|
||||
// WHEN
|
||||
comp.changePassword();
|
||||
// THEN
|
||||
expect(comp.doNotMatch()).toBe(true);
|
||||
expect(comp.error()).toBe(false);
|
||||
expect(comp.success()).toBe(false);
|
||||
});
|
||||
|
||||
it('should call Auth.changePassword when passwords match', () => {
|
||||
// GIVEN
|
||||
const passwordValues = {
|
||||
currentPassword: 'oldPassword',
|
||||
newPassword: 'myPassword',
|
||||
};
|
||||
|
||||
jest.spyOn(service, 'save').mockReturnValue(of(new HttpResponse({ body: true })));
|
||||
|
||||
comp.passwordForm.patchValue({
|
||||
currentPassword: passwordValues.currentPassword,
|
||||
newPassword: passwordValues.newPassword,
|
||||
confirmPassword: passwordValues.newPassword,
|
||||
});
|
||||
|
||||
// WHEN
|
||||
comp.changePassword();
|
||||
|
||||
// THEN
|
||||
expect(service.save).toHaveBeenCalledWith(passwordValues.newPassword, passwordValues.currentPassword);
|
||||
});
|
||||
|
||||
it('should set success to true upon success', () => {
|
||||
// GIVEN
|
||||
jest.spyOn(service, 'save').mockReturnValue(of(new HttpResponse({ body: true })));
|
||||
comp.passwordForm.patchValue({
|
||||
newPassword: 'myPassword',
|
||||
confirmPassword: 'myPassword',
|
||||
});
|
||||
|
||||
// WHEN
|
||||
comp.changePassword();
|
||||
|
||||
// THEN
|
||||
expect(comp.doNotMatch()).toBe(false);
|
||||
expect(comp.error()).toBe(false);
|
||||
expect(comp.success()).toBe(true);
|
||||
});
|
||||
|
||||
it('should notify of error if change password fails', () => {
|
||||
// GIVEN
|
||||
jest.spyOn(service, 'save').mockReturnValue(throwError('ERROR'));
|
||||
comp.passwordForm.patchValue({
|
||||
newPassword: 'myPassword',
|
||||
confirmPassword: 'myPassword',
|
||||
});
|
||||
|
||||
// WHEN
|
||||
comp.changePassword();
|
||||
|
||||
// THEN
|
||||
expect(comp.doNotMatch()).toBe(false);
|
||||
expect(comp.success()).toBe(false);
|
||||
expect(comp.error()).toBe(true);
|
||||
});
|
||||
});
|
56
src/main/webapp/app/account/password/password.component.ts
Normal file
56
src/main/webapp/app/account/password/password.component.ts
Normal file
|
@ -0,0 +1,56 @@
|
|||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { Account } from 'app/core/auth/account.model';
|
||||
import { PasswordService } from './password.service';
|
||||
import PasswordStrengthBarComponent from './password-strength-bar/password-strength-bar.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-password',
|
||||
imports: [SharedModule, FormsModule, ReactiveFormsModule, PasswordStrengthBarComponent],
|
||||
templateUrl: './password.component.html',
|
||||
})
|
||||
export default class PasswordComponent implements OnInit {
|
||||
doNotMatch = signal(false);
|
||||
error = signal(false);
|
||||
success = signal(false);
|
||||
account$?: Observable<Account | null>;
|
||||
passwordForm = new FormGroup({
|
||||
currentPassword: new FormControl('', { nonNullable: true, validators: Validators.required }),
|
||||
newPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
|
||||
}),
|
||||
confirmPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
|
||||
}),
|
||||
});
|
||||
|
||||
private passwordService = inject(PasswordService);
|
||||
private accountService = inject(AccountService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.account$ = this.accountService.identity();
|
||||
}
|
||||
|
||||
changePassword(): void {
|
||||
this.error.set(false);
|
||||
this.success.set(false);
|
||||
this.doNotMatch.set(false);
|
||||
|
||||
const { newPassword, confirmPassword, currentPassword } = this.passwordForm.getRawValue();
|
||||
if (newPassword !== confirmPassword) {
|
||||
this.doNotMatch.set(true);
|
||||
} else {
|
||||
this.passwordService.save(newPassword, currentPassword).subscribe({
|
||||
next: () => this.success.set(true),
|
||||
error: () => this.error.set(true),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
13
src/main/webapp/app/account/password/password.route.ts
Normal file
13
src/main/webapp/app/account/password/password.route.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import { UserRouteAccessService } from 'app/core/auth/user-route-access.service';
|
||||
import PasswordComponent from './password.component';
|
||||
|
||||
const passwordRoute: Route = {
|
||||
path: 'password',
|
||||
component: PasswordComponent,
|
||||
title: 'global.menu.account.password',
|
||||
canActivate: [UserRouteAccessService],
|
||||
};
|
||||
|
||||
export default passwordRoute;
|
|
@ -0,0 +1,44 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { PasswordService } from './password.service';
|
||||
|
||||
describe('Password Service', () => {
|
||||
let service: PasswordService;
|
||||
let httpMock: HttpTestingController;
|
||||
let applicationConfigService: ApplicationConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(PasswordService);
|
||||
applicationConfigService = TestBed.inject(ApplicationConfigService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should call change-password endpoint with correct values', () => {
|
||||
// GIVEN
|
||||
const password1 = 'password1';
|
||||
const password2 = 'password2';
|
||||
|
||||
// WHEN
|
||||
service.save(password2, password1).subscribe();
|
||||
|
||||
const testRequest = httpMock.expectOne({
|
||||
method: 'POST',
|
||||
url: applicationConfigService.getEndpointFor('api/account/change-password'),
|
||||
});
|
||||
|
||||
// THEN
|
||||
expect(testRequest.request.body).toEqual({ currentPassword: password1, newPassword: password2 });
|
||||
});
|
||||
});
|
||||
});
|
15
src/main/webapp/app/account/password/password.service.ts
Normal file
15
src/main/webapp/app/account/password/password.service.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PasswordService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
save(newPassword: string, currentPassword: string): Observable<{}> {
|
||||
return this.http.post(this.applicationConfigService.getEndpointFor('api/account/change-password'), { currentPassword, newPassword });
|
||||
}
|
||||
}
|
220
src/main/webapp/app/account/register/register.component.html
Normal file
220
src/main/webapp/app/account/register/register.component.html
Normal file
|
@ -0,0 +1,220 @@
|
|||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<h1 data-cy="registerTitle" jhiTranslate="register.title">Registo</h1>
|
||||
|
||||
@if (success()) {
|
||||
<div class="alert alert-success" jhiTranslate="register.messages.success">
|
||||
<strong>Registo efetuado com sucesso!</strong> Por favor verifique o seu email para confirmar a conta.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error()) {
|
||||
<div class="alert alert-danger" jhiTranslate="register.messages.error.fail">
|
||||
<strong>Erro ao realizar o registo!</strong> Por favor tente novamente mais tarde.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorUserExists()) {
|
||||
<div class="alert alert-danger" jhiTranslate="register.messages.error.userexists">
|
||||
<strong>Utilizador já registado!</strong> Por favor escolha outro.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorEmailExists()) {
|
||||
<div class="alert alert-danger" jhiTranslate="register.messages.error.emailexists">
|
||||
<strong>O email já está em uso!</strong> Por favor escolha outro.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (doNotMatch()) {
|
||||
<div class="alert alert-danger" jhiTranslate="global.messages.error.dontmatch">
|
||||
A palavra-passe e a sua confirmação devem ser iguais!
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-8">
|
||||
@if (!success()) {
|
||||
<form name="form" (ngSubmit)="register()" [formGroup]="registerForm">
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="login" jhiTranslate="global.form.username.label">Nome de utilizador</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="login"
|
||||
name="login"
|
||||
placeholder="{{ 'global.form.username.placeholder' | translate }}"
|
||||
formControlName="login"
|
||||
data-cy="username"
|
||||
#login
|
||||
/>
|
||||
|
||||
@if (registerForm.get('login')!.invalid && (registerForm.get('login')!.dirty || registerForm.get('login')!.touched)) {
|
||||
<div>
|
||||
@if (registerForm.get('login')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="register.messages.validate.login.required"
|
||||
>O utilizador é obrigatório.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('login')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="register.messages.validate.login.minlength"
|
||||
>O utilizador deve ter pelo menos 1 caracter</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('login')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="register.messages.validate.login.maxlength"
|
||||
>O utilizador não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('login')?.errors?.pattern) {
|
||||
<small class="form-text text-danger" jhiTranslate="register.messages.validate.login.pattern"
|
||||
>Your username is invalid.</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="email" jhiTranslate="global.form.email.label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="{{ 'global.form.email.placeholder' | translate }}"
|
||||
formControlName="email"
|
||||
data-cy="email"
|
||||
/>
|
||||
|
||||
@if (registerForm.get('email')!.invalid && (registerForm.get('email')!.dirty || registerForm.get('email')!.touched)) {
|
||||
<div>
|
||||
@if (registerForm.get('email')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.required">O email é obrigatório.</small>
|
||||
}
|
||||
|
||||
@if (registerForm.get('email')?.errors?.invalid) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.invalid">Email inválido.</small>
|
||||
}
|
||||
|
||||
@if (registerForm.get('email')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.minlength"
|
||||
>O email deve ter pelo menos 5 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('email')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.maxlength"
|
||||
>O email não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="password" jhiTranslate="global.form.newpassword.label">Nova palavra-passe</label>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="password"
|
||||
name="password"
|
||||
placeholder="{{ 'global.form.newpassword.placeholder' | translate }}"
|
||||
formControlName="password"
|
||||
data-cy="firstPassword"
|
||||
/>
|
||||
|
||||
@if (registerForm.get('password')!.invalid && (registerForm.get('password')!.dirty || registerForm.get('password')!.touched)) {
|
||||
<div>
|
||||
@if (registerForm.get('password')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.required"
|
||||
>A palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('password')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.minlength"
|
||||
>A palavra-passe deve ter pelo menos 4 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('password')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.newpassword.maxlength"
|
||||
>A palavra-passe não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<jhi-password-strength-bar [passwordToCheck]="registerForm.get('password')!.value"></jhi-password-strength-bar>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="confirmPassword" jhiTranslate="global.form.confirmpassword.label"
|
||||
>Confirmação de nova palavra-passe</label
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
class="form-control"
|
||||
id="confirmPassword"
|
||||
name="confirmPassword"
|
||||
placeholder="{{ 'global.form.confirmpassword.placeholder' | translate }}"
|
||||
formControlName="confirmPassword"
|
||||
data-cy="secondPassword"
|
||||
/>
|
||||
|
||||
@if (
|
||||
registerForm.get('confirmPassword')!.invalid &&
|
||||
(registerForm.get('confirmPassword')!.dirty || registerForm.get('confirmPassword')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (registerForm.get('confirmPassword')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.required"
|
||||
>A confirmação da palavra-passe é obrigatória.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('confirmPassword')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.minlength"
|
||||
>A confirmação da palavra-passe deve ter pelo menos 4 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (registerForm.get('confirmPassword')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.confirmpassword.maxlength"
|
||||
>A confirmação da palavra-passe não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="registerForm.invalid"
|
||||
class="btn btn-primary"
|
||||
data-cy="submit"
|
||||
jhiTranslate="register.form.button"
|
||||
>
|
||||
Registar
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
|
||||
<div class="mt-3 alert alert-warning">
|
||||
<span jhiTranslate="global.messages.info.authenticated.prefix">Para </span>
|
||||
<a class="alert-link" routerLink="/login" jhiTranslate="global.messages.info.authenticated.link">entrar</a
|
||||
><span jhiTranslate="global.messages.info.authenticated.suffix"
|
||||
>, utilize as seguintes contas:<br />- Administrador (utilizador="admin" e palavra-passe="admin") <br />-
|
||||
Utilizador (utilizador="user" e palavra-passe="user").</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
123
src/main/webapp/app/account/register/register.component.spec.ts
Normal file
123
src/main/webapp/app/account/register/register.component.spec.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync, inject, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { of, throwError } from 'rxjs';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { EMAIL_ALREADY_USED_TYPE, LOGIN_ALREADY_USED_TYPE } from 'app/config/error.constants';
|
||||
|
||||
import { RegisterService } from './register.service';
|
||||
import RegisterComponent from './register.component';
|
||||
|
||||
describe('RegisterComponent', () => {
|
||||
let fixture: ComponentFixture<RegisterComponent>;
|
||||
let comp: RegisterComponent;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), HttpClientTestingModule, RegisterComponent],
|
||||
providers: [FormBuilder],
|
||||
})
|
||||
.overrideTemplate(RegisterComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RegisterComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should ensure the two passwords entered match', () => {
|
||||
comp.registerForm.patchValue({
|
||||
password: 'password',
|
||||
confirmPassword: 'non-matching',
|
||||
});
|
||||
|
||||
comp.register();
|
||||
|
||||
expect(comp.doNotMatch()).toBe(true);
|
||||
});
|
||||
|
||||
it('should update success to true after creating an account', inject(
|
||||
[RegisterService, TranslateService],
|
||||
fakeAsync((service: RegisterService, mockTranslateService: TranslateService) => {
|
||||
jest.spyOn(service, 'save').mockReturnValue(of({}));
|
||||
mockTranslateService.currentLang = 'pt-pt';
|
||||
comp.registerForm.patchValue({
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
});
|
||||
|
||||
comp.register();
|
||||
tick();
|
||||
|
||||
expect(service.save).toHaveBeenCalledWith({
|
||||
email: '',
|
||||
password: 'password',
|
||||
login: '',
|
||||
langKey: 'pt-pt',
|
||||
});
|
||||
expect(comp.success()).toBe(true);
|
||||
expect(comp.errorUserExists()).toBe(false);
|
||||
expect(comp.errorEmailExists()).toBe(false);
|
||||
expect(comp.error()).toBe(false);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should notify of user existence upon 400/login already in use', inject(
|
||||
[RegisterService],
|
||||
fakeAsync((service: RegisterService) => {
|
||||
const err = { status: 400, error: { type: LOGIN_ALREADY_USED_TYPE } };
|
||||
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
|
||||
comp.registerForm.patchValue({
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
});
|
||||
|
||||
comp.register();
|
||||
tick();
|
||||
|
||||
expect(comp.errorUserExists()).toBe(true);
|
||||
expect(comp.errorEmailExists()).toBe(false);
|
||||
expect(comp.error()).toBe(false);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should notify of email existence upon 400/email address already in use', inject(
|
||||
[RegisterService],
|
||||
fakeAsync((service: RegisterService) => {
|
||||
const err = { status: 400, error: { type: EMAIL_ALREADY_USED_TYPE } };
|
||||
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
|
||||
comp.registerForm.patchValue({
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
});
|
||||
|
||||
comp.register();
|
||||
tick();
|
||||
|
||||
expect(comp.errorEmailExists()).toBe(true);
|
||||
expect(comp.errorUserExists()).toBe(false);
|
||||
expect(comp.error()).toBe(false);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should notify of generic error', inject(
|
||||
[RegisterService],
|
||||
fakeAsync((service: RegisterService) => {
|
||||
const err = { status: 503 };
|
||||
jest.spyOn(service, 'save').mockReturnValue(throwError(() => err));
|
||||
comp.registerForm.patchValue({
|
||||
password: 'password',
|
||||
confirmPassword: 'password',
|
||||
});
|
||||
|
||||
comp.register();
|
||||
tick();
|
||||
|
||||
expect(comp.errorUserExists()).toBe(false);
|
||||
expect(comp.errorEmailExists()).toBe(false);
|
||||
expect(comp.error()).toBe(true);
|
||||
}),
|
||||
));
|
||||
});
|
84
src/main/webapp/app/account/register/register.component.ts
Normal file
84
src/main/webapp/app/account/register/register.component.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { Component, AfterViewInit, ElementRef, inject, signal, viewChild } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { EMAIL_ALREADY_USED_TYPE, LOGIN_ALREADY_USED_TYPE } from 'app/config/error.constants';
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import PasswordStrengthBarComponent from '../password/password-strength-bar/password-strength-bar.component';
|
||||
import { RegisterService } from './register.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-register',
|
||||
imports: [SharedModule, RouterModule, FormsModule, ReactiveFormsModule, PasswordStrengthBarComponent],
|
||||
templateUrl: './register.component.html',
|
||||
})
|
||||
export default class RegisterComponent implements AfterViewInit {
|
||||
login = viewChild.required<ElementRef>('login');
|
||||
|
||||
doNotMatch = signal(false);
|
||||
error = signal(false);
|
||||
errorEmailExists = signal(false);
|
||||
errorUserExists = signal(false);
|
||||
success = signal(false);
|
||||
|
||||
registerForm = new FormGroup({
|
||||
login: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [
|
||||
Validators.required,
|
||||
Validators.minLength(1),
|
||||
Validators.maxLength(50),
|
||||
Validators.pattern('^[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$|^[_.@A-Za-z0-9-]+$'),
|
||||
],
|
||||
}),
|
||||
email: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email],
|
||||
}),
|
||||
password: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
|
||||
}),
|
||||
confirmPassword: new FormControl('', {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(4), Validators.maxLength(50)],
|
||||
}),
|
||||
});
|
||||
|
||||
private translateService = inject(TranslateService);
|
||||
private registerService = inject(RegisterService);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
this.login().nativeElement.focus();
|
||||
}
|
||||
|
||||
register(): void {
|
||||
this.doNotMatch.set(false);
|
||||
this.error.set(false);
|
||||
this.errorEmailExists.set(false);
|
||||
this.errorUserExists.set(false);
|
||||
|
||||
const { password, confirmPassword } = this.registerForm.getRawValue();
|
||||
if (password !== confirmPassword) {
|
||||
this.doNotMatch.set(true);
|
||||
} else {
|
||||
const { login, email } = this.registerForm.getRawValue();
|
||||
this.registerService
|
||||
.save({ login, email, password, langKey: this.translateService.currentLang })
|
||||
.subscribe({ next: () => this.success.set(true), error: response => this.processError(response) });
|
||||
}
|
||||
}
|
||||
|
||||
private processError(response: HttpErrorResponse): void {
|
||||
if (response.status === 400 && response.error.type === LOGIN_ALREADY_USED_TYPE) {
|
||||
this.errorUserExists.set(true);
|
||||
} else if (response.status === 400 && response.error.type === EMAIL_ALREADY_USED_TYPE) {
|
||||
this.errorEmailExists.set(true);
|
||||
} else {
|
||||
this.error.set(true);
|
||||
}
|
||||
}
|
||||
}
|
8
src/main/webapp/app/account/register/register.model.ts
Normal file
8
src/main/webapp/app/account/register/register.model.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export class Registration {
|
||||
constructor(
|
||||
public login: string,
|
||||
public email: string,
|
||||
public password: string,
|
||||
public langKey: string,
|
||||
) {}
|
||||
}
|
11
src/main/webapp/app/account/register/register.route.ts
Normal file
11
src/main/webapp/app/account/register/register.route.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import RegisterComponent from './register.component';
|
||||
|
||||
const registerRoute: Route = {
|
||||
path: 'register',
|
||||
component: RegisterComponent,
|
||||
title: 'register.title',
|
||||
};
|
||||
|
||||
export default registerRoute;
|
|
@ -0,0 +1,48 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { RegisterService } from './register.service';
|
||||
import { Registration } from './register.model';
|
||||
|
||||
describe('RegisterService Service', () => {
|
||||
let service: RegisterService;
|
||||
let httpMock: HttpTestingController;
|
||||
let applicationConfigService: ApplicationConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(RegisterService);
|
||||
applicationConfigService = TestBed.inject(ApplicationConfigService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should call register endpoint with correct values', () => {
|
||||
// GIVEN
|
||||
const login = 'abc';
|
||||
const email = 'test@test.com';
|
||||
const password = 'pass';
|
||||
const langKey = 'FR';
|
||||
const registration = new Registration(login, email, password, langKey);
|
||||
|
||||
// WHEN
|
||||
service.save(registration).subscribe();
|
||||
|
||||
const testRequest = httpMock.expectOne({
|
||||
method: 'POST',
|
||||
url: applicationConfigService.getEndpointFor('api/register'),
|
||||
});
|
||||
|
||||
// THEN
|
||||
expect(testRequest.request.body).toEqual({ email, langKey, login, password });
|
||||
});
|
||||
});
|
||||
});
|
16
src/main/webapp/app/account/register/register.service.ts
Normal file
16
src/main/webapp/app/account/register/register.service.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { Registration } from './register.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class RegisterService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
save(registration: Registration): Observable<{}> {
|
||||
return this.http.post(this.applicationConfigService.getEndpointFor('api/register'), registration);
|
||||
}
|
||||
}
|
8
src/main/webapp/app/account/sessions/session.model.ts
Normal file
8
src/main/webapp/app/account/sessions/session.model.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export class Session {
|
||||
constructor(
|
||||
public series: string,
|
||||
public tokenDate: Date,
|
||||
public ipAddress: string,
|
||||
public userAgent: string,
|
||||
) {}
|
||||
}
|
45
src/main/webapp/app/account/sessions/sessions.component.html
Normal file
45
src/main/webapp/app/account/sessions/sessions.component.html
Normal file
|
@ -0,0 +1,45 @@
|
|||
<div>
|
||||
@if (account) {
|
||||
<h2 id="session-page-heading" jhiTranslate="sessions.title" [translateValues]="{ username: account.login }">
|
||||
Sessões ativas para [<strong>{{ account.login }}</strong
|
||||
>]
|
||||
</h2>
|
||||
}
|
||||
|
||||
@if (success) {
|
||||
<div class="alert alert-success" jhiTranslate="sessions.messages.success"><strong>Sessão terminada com sucesso!</strong></div>
|
||||
}
|
||||
|
||||
@if (error) {
|
||||
<div class="alert alert-danger" jhiTranslate="sessions.messages.error">
|
||||
<strong>Ocorreu um erro!</strong> A sessão não pode ser terminada.
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="session-page-heading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" jhiTranslate="sessions.table.ipaddress">IP</th>
|
||||
<th scope="col" jhiTranslate="sessions.table.useragent">Agente</th>
|
||||
<th scope="col" jhiTranslate="sessions.table.date">Data</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (session of sessions; track $index) {
|
||||
<tr>
|
||||
<td>{{ session.ipAddress }}</td>
|
||||
<td>{{ session.userAgent }}</td>
|
||||
<td>{{ session.tokenDate | date: 'longDate' }}</td>
|
||||
<td>
|
||||
<button type="submit" class="btn btn-primary" (click)="invalidate(session.series)" jhiTranslate="sessions.table.button">
|
||||
Terminar sessão
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
103
src/main/webapp/app/account/sessions/sessions.component.spec.ts
Normal file
103
src/main/webapp/app/account/sessions/sessions.component.spec.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
jest.mock('app/core/auth/account.service');
|
||||
|
||||
import { ComponentFixture, TestBed, inject, tick, fakeAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { Account } from 'app/core/auth/account.model';
|
||||
|
||||
import { Session } from './session.model';
|
||||
import SessionsComponent from './sessions.component';
|
||||
import { SessionsService } from './sessions.service';
|
||||
|
||||
describe('SessionsComponent', () => {
|
||||
let fixture: ComponentFixture<SessionsComponent>;
|
||||
let comp: SessionsComponent;
|
||||
const sessions: Session[] = [new Session('xxxxxx==', new Date(2015, 10, 15), '0:0:0:0:0:0:0:1', 'Mozilla/5.0')];
|
||||
const account: Account = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
activated: true,
|
||||
email: 'john.doe@mail.com',
|
||||
langKey: 'pt-pt',
|
||||
login: 'john',
|
||||
authorities: [],
|
||||
imageUrl: '',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, SessionsComponent],
|
||||
providers: [AccountService],
|
||||
})
|
||||
.overrideTemplate(SessionsComponent, '')
|
||||
.createComponent(SessionsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('should define its initial state', inject(
|
||||
[AccountService, SessionsService],
|
||||
fakeAsync((mockAccountService: AccountService, service: SessionsService) => {
|
||||
mockAccountService.identity = jest.fn(() => of(account));
|
||||
jest.spyOn(service, 'findAll').mockReturnValue(of(sessions));
|
||||
|
||||
comp.ngOnInit();
|
||||
tick();
|
||||
|
||||
expect(mockAccountService.identity).toHaveBeenCalled();
|
||||
expect(service.findAll).toHaveBeenCalled();
|
||||
expect(comp.success).toBe(false);
|
||||
expect(comp.error).toBe(false);
|
||||
expect(comp.account).toEqual(account);
|
||||
expect(comp.sessions).toEqual(sessions);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should call delete on Sessions to invalidate a session', inject(
|
||||
[AccountService, SessionsService],
|
||||
fakeAsync((mockAccountService: AccountService, service: SessionsService) => {
|
||||
mockAccountService.identity = jest.fn(() => of(account));
|
||||
jest.spyOn(service, 'findAll').mockReturnValue(of(sessions));
|
||||
jest.spyOn(service, 'delete').mockReturnValue(of({}));
|
||||
|
||||
comp.ngOnInit();
|
||||
comp.invalidate('xyz');
|
||||
tick();
|
||||
|
||||
expect(service.delete).toHaveBeenCalledWith('xyz');
|
||||
}),
|
||||
));
|
||||
|
||||
it('should call delete on Sessions and notify of error', inject(
|
||||
[AccountService, SessionsService],
|
||||
fakeAsync((mockAccountService: AccountService, service: SessionsService) => {
|
||||
mockAccountService.identity = jest.fn(() => of(account));
|
||||
jest.spyOn(service, 'findAll').mockReturnValue(of(sessions));
|
||||
jest.spyOn(service, 'delete').mockReturnValue(throwError(() => {}));
|
||||
|
||||
comp.ngOnInit();
|
||||
comp.invalidate('xyz');
|
||||
tick();
|
||||
|
||||
expect(comp.success).toBe(false);
|
||||
expect(comp.error).toBe(true);
|
||||
}),
|
||||
));
|
||||
|
||||
it('should call notify of success upon session invalidation', inject(
|
||||
[AccountService, SessionsService],
|
||||
fakeAsync((mockAccountService: AccountService, service: SessionsService) => {
|
||||
mockAccountService.identity = jest.fn(() => of(account));
|
||||
jest.spyOn(service, 'findAll').mockReturnValue(of(sessions));
|
||||
jest.spyOn(service, 'delete').mockReturnValue(of({}));
|
||||
|
||||
comp.ngOnInit();
|
||||
comp.invalidate('xyz');
|
||||
tick();
|
||||
|
||||
expect(comp.error).toBe(false);
|
||||
expect(comp.success).toBe(true);
|
||||
}),
|
||||
));
|
||||
});
|
42
src/main/webapp/app/account/sessions/sessions.component.ts
Normal file
42
src/main/webapp/app/account/sessions/sessions.component.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { Component, inject, OnInit } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { Account } from 'app/core/auth/account.model';
|
||||
import { Session } from './session.model';
|
||||
import { SessionsService } from './sessions.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-sessions',
|
||||
imports: [SharedModule],
|
||||
templateUrl: './sessions.component.html',
|
||||
})
|
||||
export default class SessionsComponent implements OnInit {
|
||||
account: Account | null = null;
|
||||
error = false;
|
||||
success = false;
|
||||
sessions: Session[] = [];
|
||||
|
||||
private sessionsService = inject(SessionsService);
|
||||
private accountService = inject(AccountService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sessionsService.findAll().subscribe(sessions => (this.sessions = sessions));
|
||||
|
||||
this.accountService.identity().subscribe(account => (this.account = account));
|
||||
}
|
||||
|
||||
invalidate(series: string): void {
|
||||
this.error = false;
|
||||
this.success = false;
|
||||
|
||||
this.sessionsService.delete(encodeURIComponent(series)).subscribe(
|
||||
() => {
|
||||
this.success = true;
|
||||
this.sessionsService.findAll().subscribe(sessions => (this.sessions = sessions));
|
||||
},
|
||||
() => (this.error = true),
|
||||
);
|
||||
}
|
||||
}
|
13
src/main/webapp/app/account/sessions/sessions.route.ts
Normal file
13
src/main/webapp/app/account/sessions/sessions.route.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import { UserRouteAccessService } from 'app/core/auth/user-route-access.service';
|
||||
import SessionsComponent from './sessions.component';
|
||||
|
||||
const sessionsRoute: Route = {
|
||||
path: 'sessions',
|
||||
component: SessionsComponent,
|
||||
title: 'global.menu.account.sessions',
|
||||
canActivate: [UserRouteAccessService],
|
||||
};
|
||||
|
||||
export default sessionsRoute;
|
22
src/main/webapp/app/account/sessions/sessions.service.ts
Normal file
22
src/main/webapp/app/account/sessions/sessions.service.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { Session } from './session.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class SessionsService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
private resourceUrl = this.applicationConfigService.getEndpointFor('api/account/sessions');
|
||||
|
||||
findAll(): Observable<Session[]> {
|
||||
return this.http.get<Session[]>(this.resourceUrl);
|
||||
}
|
||||
|
||||
delete(series: string): Observable<{}> {
|
||||
return this.http.delete(`${this.resourceUrl}/${series}`);
|
||||
}
|
||||
}
|
154
src/main/webapp/app/account/settings/settings.component.html
Normal file
154
src/main/webapp/app/account/settings/settings.component.html
Normal file
|
@ -0,0 +1,154 @@
|
|||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<div class="col-md-8">
|
||||
@if (settingsForm.value.login) {
|
||||
<h2 jhiTranslate="settings.title" [translateValues]="{ username: settingsForm.value.login }">
|
||||
Configurações para o utilizador [<strong>{{ settingsForm.value.login }}</strong
|
||||
>]
|
||||
</h2>
|
||||
}
|
||||
|
||||
@if (success()) {
|
||||
<div class="alert alert-success" jhiTranslate="settings.messages.success">
|
||||
<strong>Configurações guardadas com sucesso!</strong>
|
||||
</div>
|
||||
}
|
||||
|
||||
<jhi-alert-error></jhi-alert-error>
|
||||
|
||||
@if (settingsForm.value.login) {
|
||||
<form name="form" (ngSubmit)="save()" [formGroup]="settingsForm" novalidate>
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="firstName" jhiTranslate="settings.form.firstname">Nome</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
placeholder="{{ 'settings.form.firstname.placeholder' | translate }}"
|
||||
formControlName="firstName"
|
||||
data-cy="firstname"
|
||||
/>
|
||||
|
||||
@if (
|
||||
settingsForm.get('firstName')!.invalid && (settingsForm.get('firstName')!.dirty || settingsForm.get('firstName')!.touched)
|
||||
) {
|
||||
<div>
|
||||
@if (settingsForm.get('firstName')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="settings.messages.validate.firstname.required"
|
||||
>O nome é obrigatório.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('firstName')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="settings.messages.validate.firstname.minlength"
|
||||
>O nome deve ter pelo menos 1 caracter</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('firstName')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="settings.messages.validate.firstname.maxlength"
|
||||
>O nome não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="lastName" jhiTranslate="settings.form.lastname">Apelido</label>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
placeholder="{{ 'settings.form.lastname.placeholder' | translate }}"
|
||||
formControlName="lastName"
|
||||
data-cy="lastname"
|
||||
/>
|
||||
|
||||
@if (settingsForm.get('lastName')!.invalid && (settingsForm.get('lastName')!.dirty || settingsForm.get('lastName')!.touched)) {
|
||||
<div>
|
||||
@if (settingsForm.get('lastName')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="settings.messages.validate.lastname.required"
|
||||
>O apelido é obrigatório.</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('lastName')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="settings.messages.validate.lastname.minlength"
|
||||
>O apelido deve ter pelo menos 1 caracter</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('lastName')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="settings.messages.validate.lastname.maxlength"
|
||||
>O apelido não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label" for="email" jhiTranslate="global.form.email.label">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
class="form-control"
|
||||
id="email"
|
||||
name="email"
|
||||
placeholder="{{ 'global.form.email.placeholder' | translate }}"
|
||||
formControlName="email"
|
||||
data-cy="email"
|
||||
/>
|
||||
|
||||
@if (settingsForm.get('email')!.invalid && (settingsForm.get('email')!.dirty || settingsForm.get('email')!.touched)) {
|
||||
<div>
|
||||
@if (settingsForm.get('email')?.errors?.required) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.required">O email é obrigatório.</small>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('email')?.errors?.email) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.invalid">Email inválido.</small>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('email')?.errors?.minlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.minlength"
|
||||
>O email deve ter pelo menos 5 caracteres</small
|
||||
>
|
||||
}
|
||||
|
||||
@if (settingsForm.get('email')?.errors?.maxlength) {
|
||||
<small class="form-text text-danger" jhiTranslate="global.messages.validate.email.maxlength"
|
||||
>O email não pode ter mais de 50 caracteres</small
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (languages && languages.length > 0) {
|
||||
<div class="mb-3">
|
||||
<label for="langKey" jhiTranslate="settings.form.language">Idioma</label>
|
||||
<select class="form-control" id="langKey" name="langKey" formControlName="langKey" data-cy="langKey">
|
||||
@for (language of languages; track $index) {
|
||||
<option [value]="language">{{ language | findLanguageFromKey }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[disabled]="settingsForm.invalid"
|
||||
class="btn btn-primary"
|
||||
data-cy="submit"
|
||||
jhiTranslate="settings.form.button"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,90 @@
|
|||
jest.mock('app/core/auth/account.service');
|
||||
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { throwError, of } from 'rxjs';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { Account } from 'app/core/auth/account.model';
|
||||
|
||||
import SettingsComponent from './settings.component';
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
let comp: SettingsComponent;
|
||||
let fixture: ComponentFixture<SettingsComponent>;
|
||||
let mockAccountService: AccountService;
|
||||
const account: Account = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
activated: true,
|
||||
email: 'john.doe@mail.com',
|
||||
langKey: 'pt-pt',
|
||||
login: 'john',
|
||||
authorities: [],
|
||||
imageUrl: '',
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), HttpClientTestingModule, SettingsComponent],
|
||||
providers: [FormBuilder, AccountService],
|
||||
})
|
||||
.overrideTemplate(SettingsComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SettingsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
mockAccountService = TestBed.inject(AccountService);
|
||||
mockAccountService.identity = jest.fn(() => of(account));
|
||||
mockAccountService.getAuthenticationState = jest.fn(() => of(account));
|
||||
});
|
||||
|
||||
it('should send the current identity upon save', () => {
|
||||
// GIVEN
|
||||
mockAccountService.save = jest.fn(() => of({}));
|
||||
const settingsFormValues = {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
email: 'john.doe@mail.com',
|
||||
langKey: 'pt-pt',
|
||||
};
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
comp.save();
|
||||
|
||||
// THEN
|
||||
expect(mockAccountService.identity).toHaveBeenCalled();
|
||||
expect(mockAccountService.save).toHaveBeenCalledWith(account);
|
||||
expect(mockAccountService.authenticate).toHaveBeenCalledWith(account);
|
||||
expect(comp.settingsForm.value).toMatchObject(expect.objectContaining(settingsFormValues));
|
||||
});
|
||||
|
||||
it('should notify of success upon successful save', () => {
|
||||
// GIVEN
|
||||
mockAccountService.save = jest.fn(() => of({}));
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
comp.save();
|
||||
|
||||
// THEN
|
||||
expect(comp.success()).toBe(true);
|
||||
});
|
||||
|
||||
it('should notify of error upon failed save', () => {
|
||||
// GIVEN
|
||||
mockAccountService.save = jest.fn(() => throwError('ERROR'));
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
comp.save();
|
||||
|
||||
// THEN
|
||||
expect(comp.success()).toBe(false);
|
||||
});
|
||||
});
|
74
src/main/webapp/app/account/settings/settings.component.ts
Normal file
74
src/main/webapp/app/account/settings/settings.component.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { FormGroup, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { Account } from 'app/core/auth/account.model';
|
||||
import { LANGUAGES } from 'app/config/language.constants';
|
||||
|
||||
const initialAccount: Account = {} as Account;
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-settings',
|
||||
imports: [SharedModule, FormsModule, ReactiveFormsModule],
|
||||
templateUrl: './settings.component.html',
|
||||
})
|
||||
export default class SettingsComponent implements OnInit {
|
||||
success = signal(false);
|
||||
languages = LANGUAGES;
|
||||
|
||||
settingsForm = new FormGroup({
|
||||
firstName: new FormControl(initialAccount.firstName, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(1), Validators.maxLength(50)],
|
||||
}),
|
||||
lastName: new FormControl(initialAccount.lastName, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(1), Validators.maxLength(50)],
|
||||
}),
|
||||
email: new FormControl(initialAccount.email, {
|
||||
nonNullable: true,
|
||||
validators: [Validators.required, Validators.minLength(5), Validators.maxLength(254), Validators.email],
|
||||
}),
|
||||
langKey: new FormControl(initialAccount.langKey, { nonNullable: true }),
|
||||
|
||||
activated: new FormControl(initialAccount.activated, { nonNullable: true }),
|
||||
authorities: new FormControl(initialAccount.authorities, { nonNullable: true }),
|
||||
imageUrl: new FormControl(initialAccount.imageUrl, { nonNullable: true }),
|
||||
login: new FormControl(initialAccount.login, { nonNullable: true }),
|
||||
});
|
||||
|
||||
private accountService = inject(AccountService);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.accountService.identity().subscribe(account => {
|
||||
if (account) {
|
||||
this.settingsForm.patchValue(account);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save(): void {
|
||||
this.success.set(false);
|
||||
|
||||
// const account = this.settingsForm.getRawValue();
|
||||
const account: Account = {
|
||||
...this.settingsForm.getRawValue(),
|
||||
securityGroup: null,
|
||||
parentOrganization: null,
|
||||
};
|
||||
|
||||
this.accountService.save(account).subscribe(() => {
|
||||
this.success.set(true);
|
||||
|
||||
this.accountService.authenticate(account);
|
||||
|
||||
if (account.langKey !== this.translateService.currentLang) {
|
||||
this.translateService.use(account.langKey);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
13
src/main/webapp/app/account/settings/settings.route.ts
Normal file
13
src/main/webapp/app/account/settings/settings.route.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Route } from '@angular/router';
|
||||
|
||||
import { UserRouteAccessService } from 'app/core/auth/user-route-access.service';
|
||||
import SettingsComponent from './settings.component';
|
||||
|
||||
const settingsRoute: Route = {
|
||||
path: 'settings',
|
||||
component: SettingsComponent,
|
||||
title: 'global.menu.account.settings',
|
||||
canActivate: [UserRouteAccessService],
|
||||
};
|
||||
|
||||
export default settingsRoute;
|
38
src/main/webapp/app/admin/admin.routes.ts
Normal file
38
src/main/webapp/app/admin/admin.routes.ts
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { Routes } from '@angular/router';
|
||||
/* jhipster-needle-add-admin-module-import - JHipster will add admin modules imports here */
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: 'user-management',
|
||||
loadChildren: () => import('./user-management/user-management.route'),
|
||||
title: 'userManagement.home.title',
|
||||
},
|
||||
{
|
||||
path: 'docs',
|
||||
loadComponent: () => import('./docs/docs.component'),
|
||||
title: 'global.menu.admin.apidocs',
|
||||
},
|
||||
{
|
||||
path: 'configuration',
|
||||
loadComponent: () => import('./configuration/configuration.component'),
|
||||
title: 'configuration.title',
|
||||
},
|
||||
{
|
||||
path: 'health',
|
||||
loadComponent: () => import('./health/health.component'),
|
||||
title: 'health.title',
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadComponent: () => import('./logs/logs.component'),
|
||||
title: 'logs.title',
|
||||
},
|
||||
{
|
||||
path: 'metrics',
|
||||
loadComponent: () => import('./metrics/metrics.component'),
|
||||
title: 'metrics.title',
|
||||
},
|
||||
/* jhipster-needle-add-admin-route - JHipster will add admin routes here */
|
||||
];
|
||||
|
||||
export default routes;
|
|
@ -0,0 +1,70 @@
|
|||
@if (allBeans()) {
|
||||
<div>
|
||||
<h2 id="configuration-page-heading" data-cy="configurationPageHeading" jhiTranslate="configuration.title">Configuração</h2>
|
||||
|
||||
<span jhiTranslate="configuration.filter">Filtrar (por prefixo)</span>
|
||||
<input type="text" [ngModel]="beansFilter()" (ngModelChange)="beansFilter.set($event)" class="form-control" />
|
||||
|
||||
<h3 id="spring-configuration">Spring configuration</h3>
|
||||
|
||||
<table class="table table-striped table-bordered table-responsive d-table" aria-describedby="spring-configuration">
|
||||
<thead>
|
||||
<tr jhiSort [sortState]="sortState" (sortChange)="sortState.set($event)">
|
||||
<th jhiSortBy="prefix" scope="col" class="w-40">
|
||||
<span jhiTranslate="configuration.table.prefix">Prefixo</span> <fa-icon icon="sort"></fa-icon>
|
||||
</th>
|
||||
<th scope="col" class="w-60"><span jhiTranslate="configuration.table.properties">Propriedades</span></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (bean of beans(); track $index) {
|
||||
<tr>
|
||||
<td>
|
||||
<span>{{ bean.prefix }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@for (property of bean.properties | keyvalue; track property.key) {
|
||||
<div class="row">
|
||||
<div class="col-md-4">{{ property.key }}</div>
|
||||
<div class="col-md-8">
|
||||
<span class="float-end bg-secondary break">{{ property.value | json }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@for (propertySource of propertySources(); track i; let i = $index) {
|
||||
<div>
|
||||
<h4 [id]="'property-source-' + i">
|
||||
<span>{{ propertySource.name }}</span>
|
||||
</h4>
|
||||
|
||||
<table
|
||||
class="table table-sm table-striped table-bordered table-responsive d-table"
|
||||
[attr.aria-describedby]="'property-source-' + i"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="w-40">Property</th>
|
||||
<th scope="col" class="w-60">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (property of propertySource.properties | keyvalue; track property.key) {
|
||||
<tr>
|
||||
<td class="break">{{ property.key }}</td>
|
||||
<td class="break">
|
||||
<span class="float-end bg-secondary break">{{ property.value.value }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import ConfigurationComponent from './configuration.component';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { Bean, PropertySource } from './configuration.model';
|
||||
|
||||
describe('ConfigurationComponent', () => {
|
||||
let comp: ConfigurationComponent;
|
||||
let fixture: ComponentFixture<ConfigurationComponent>;
|
||||
let service: ConfigurationService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, ConfigurationComponent],
|
||||
providers: [ConfigurationService],
|
||||
})
|
||||
.overrideTemplate(ConfigurationComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ConfigurationComponent);
|
||||
comp = fixture.componentInstance;
|
||||
service = TestBed.inject(ConfigurationService);
|
||||
});
|
||||
|
||||
describe('OnInit', () => {
|
||||
it('Should call load all on init', () => {
|
||||
// GIVEN
|
||||
const beans: Bean[] = [
|
||||
{
|
||||
prefix: 'jhipster',
|
||||
properties: {
|
||||
clientApp: {
|
||||
name: 'jhipsterApp',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const propertySources: PropertySource[] = [
|
||||
{
|
||||
name: 'server.ports',
|
||||
properties: {
|
||||
'local.server.port': {
|
||||
value: '8080',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
jest.spyOn(service, 'getBeans').mockReturnValue(of(beans));
|
||||
jest.spyOn(service, 'getPropertySources').mockReturnValue(of(propertySources));
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
|
||||
// THEN
|
||||
expect(service.getBeans).toHaveBeenCalled();
|
||||
expect(service.getPropertySources).toHaveBeenCalled();
|
||||
expect(comp.allBeans()).toEqual(beans);
|
||||
expect(comp.beans()).toEqual(beans);
|
||||
expect(comp.propertySources()).toEqual(propertySources);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import { Component, computed, inject, OnInit, signal } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { SortDirective, SortByDirective, sortStateSignal, SortService } from 'app/shared/sort';
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { Bean, PropertySource } from './configuration.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-configuration',
|
||||
templateUrl: './configuration.component.html',
|
||||
imports: [SharedModule, FormsModule, SortDirective, SortByDirective],
|
||||
})
|
||||
export default class ConfigurationComponent implements OnInit {
|
||||
allBeans = signal<Bean[] | undefined>(undefined);
|
||||
beansFilter = signal<string>('');
|
||||
propertySources = signal<PropertySource[]>([]);
|
||||
sortState = sortStateSignal({ predicate: 'prefix', order: 'asc' });
|
||||
beans = computed(() => {
|
||||
let data = this.allBeans() ?? [];
|
||||
const beansFilter = this.beansFilter();
|
||||
if (beansFilter) {
|
||||
data = data.filter(bean => bean.prefix.toLowerCase().includes(beansFilter.toLowerCase()));
|
||||
}
|
||||
|
||||
const { order, predicate } = this.sortState();
|
||||
if (predicate && order) {
|
||||
data = data.sort(this.sortService.startSort({ predicate, order }));
|
||||
}
|
||||
return data;
|
||||
});
|
||||
|
||||
private sortService = inject(SortService);
|
||||
private configurationService = inject(ConfigurationService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.configurationService.getBeans().subscribe(beans => {
|
||||
this.allBeans.set(beans);
|
||||
});
|
||||
|
||||
this.configurationService.getPropertySources().subscribe(propertySources => this.propertySources.set(propertySources));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
export interface ConfigProps {
|
||||
contexts: Contexts;
|
||||
}
|
||||
|
||||
export interface Contexts {
|
||||
[key: string]: Context;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
beans: Beans;
|
||||
parentId?: any;
|
||||
}
|
||||
|
||||
export interface Beans {
|
||||
[key: string]: Bean;
|
||||
}
|
||||
|
||||
export interface Bean {
|
||||
prefix: string;
|
||||
properties: any;
|
||||
}
|
||||
|
||||
export interface Env {
|
||||
activeProfiles?: string[];
|
||||
propertySources: PropertySource[];
|
||||
}
|
||||
|
||||
export interface PropertySource {
|
||||
name: string;
|
||||
properties: Properties;
|
||||
}
|
||||
|
||||
export interface Properties {
|
||||
[key: string]: Property;
|
||||
}
|
||||
|
||||
export interface Property {
|
||||
value: string;
|
||||
origin?: string;
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ConfigurationService } from './configuration.service';
|
||||
import { Bean, ConfigProps, Env, PropertySource } from './configuration.model';
|
||||
|
||||
describe('Logs Service', () => {
|
||||
let service: ConfigurationService;
|
||||
let httpMock: HttpTestingController;
|
||||
let expectedResult: Bean[] | PropertySource[] | null;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
expectedResult = null;
|
||||
service = TestBed.inject(ConfigurationService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should get the config', () => {
|
||||
const bean: Bean = {
|
||||
prefix: 'jhipster',
|
||||
properties: {
|
||||
clientApp: {
|
||||
name: 'jhipsterApp',
|
||||
},
|
||||
},
|
||||
};
|
||||
const configProps: ConfigProps = {
|
||||
contexts: {
|
||||
jhipster: {
|
||||
beans: {
|
||||
'tech.jhipster.config.JHipsterProperties': bean,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
service.getBeans().subscribe(received => (expectedResult = received));
|
||||
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
req.flush(configProps);
|
||||
expect(expectedResult).toEqual([bean]);
|
||||
});
|
||||
|
||||
it('should get the env', () => {
|
||||
const propertySources: PropertySource[] = [
|
||||
{
|
||||
name: 'server.ports',
|
||||
properties: {
|
||||
'local.server.port': {
|
||||
value: '8080',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
const env: Env = { propertySources };
|
||||
service.getPropertySources().subscribe(received => (expectedResult = received));
|
||||
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
req.flush(env);
|
||||
expect(expectedResult).toEqual(propertySources);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { Bean, Beans, ConfigProps, Env, PropertySource } from './configuration.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ConfigurationService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
getBeans(): Observable<Bean[]> {
|
||||
return this.http.get<ConfigProps>(this.applicationConfigService.getEndpointFor('management/configprops')).pipe(
|
||||
map(configProps =>
|
||||
Object.values(
|
||||
Object.values(configProps.contexts)
|
||||
.map(context => context.beans)
|
||||
.reduce((allBeans: Beans, contextBeans: Beans) => ({ ...allBeans, ...contextBeans }), {}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getPropertySources(): Observable<PropertySource[]> {
|
||||
return this.http.get<Env>(this.applicationConfigService.getEndpointFor('management/env')).pipe(map(env => env.propertySources));
|
||||
}
|
||||
}
|
10
src/main/webapp/app/admin/docs/docs.component.html
Normal file
10
src/main/webapp/app/admin/docs/docs.component.html
Normal file
|
@ -0,0 +1,10 @@
|
|||
<iframe
|
||||
src="swagger-ui/index.html"
|
||||
width="100%"
|
||||
height="900"
|
||||
seamless
|
||||
target="_top"
|
||||
title="Swagger UI"
|
||||
class="border-0"
|
||||
data-cy="swagger-frame"
|
||||
></iframe>
|
6
src/main/webapp/app/admin/docs/docs.component.scss
Normal file
6
src/main/webapp/app/admin/docs/docs.component.scss
Normal file
|
@ -0,0 +1,6 @@
|
|||
@import 'bootstrap/scss/functions';
|
||||
@import 'bootstrap/scss/variables';
|
||||
|
||||
iframe {
|
||||
background: white;
|
||||
}
|
9
src/main/webapp/app/admin/docs/docs.component.ts
Normal file
9
src/main/webapp/app/admin/docs/docs.component.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-docs',
|
||||
templateUrl: './docs.component.html',
|
||||
styleUrl: './docs.component.scss',
|
||||
})
|
||||
export default class DocsComponent {}
|
61
src/main/webapp/app/admin/health/health.component.html
Normal file
61
src/main/webapp/app/admin/health/health.component.html
Normal file
|
@ -0,0 +1,61 @@
|
|||
<div>
|
||||
<h2>
|
||||
<span id="health-page-heading" data-cy="healthPageHeading" jhiTranslate="health.title">Estado do Sistema</span>
|
||||
|
||||
<button class="btn btn-primary float-end" (click)="refresh()">
|
||||
<fa-icon icon="sync"></fa-icon> <span jhiTranslate="health.refresh.button">Atualizar</span>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="healthCheck" class="table table-striped" aria-describedby="health-page-heading">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" jhiTranslate="health.table.service">Nome do Serviço</th>
|
||||
<th scope="col" class="text-center" jhiTranslate="health.table.status">Estado</th>
|
||||
<th scope="col" class="text-center" jhiTranslate="health.details.details">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@if (health) {
|
||||
<tbody>
|
||||
@for (componentHealth of health.components | keyvalue; track componentHealth.key) {
|
||||
<tr>
|
||||
<td [jhiTranslate]="'health.indicator.' + componentHealth.key">
|
||||
{{
|
||||
{
|
||||
diskSpace: 'Espaço em disco',
|
||||
mail: 'Email',
|
||||
livenessState: 'Liveness state',
|
||||
readinessState: 'Readiness state',
|
||||
ping: 'Application',
|
||||
db: 'Base de dados'
|
||||
}[componentHealth.key] || componentHealth.key
|
||||
}}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span
|
||||
class="badge"
|
||||
[ngClass]="getBadgeClass(componentHealth.value!.status)"
|
||||
[jhiTranslate]="'health.status.' + (componentHealth.value?.status ?? 'UNKNOWN')"
|
||||
>
|
||||
{{
|
||||
{ UNKNOWN: 'UNKNOWN', UP: 'UP', OUT_OF_SERVICE: 'OUT_OF_SERVICE', DOWN: 'DOWN' }[
|
||||
componentHealth.value?.status ?? 'UNKNOWN'
|
||||
]
|
||||
}}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
@if (componentHealth.value!.details) {
|
||||
<a class="hand" (click)="showHealth({ key: componentHealth.key, value: componentHealth.value! })">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
65
src/main/webapp/app/admin/health/health.component.spec.ts
Normal file
65
src/main/webapp/app/admin/health/health.component.spec.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
import HealthComponent from './health.component';
|
||||
import { HealthService } from './health.service';
|
||||
import { Health } from './health.model';
|
||||
|
||||
describe('HealthComponent', () => {
|
||||
let comp: HealthComponent;
|
||||
let fixture: ComponentFixture<HealthComponent>;
|
||||
let service: HealthService;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, HealthComponent],
|
||||
})
|
||||
.overrideTemplate(HealthComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HealthComponent);
|
||||
comp = fixture.componentInstance;
|
||||
service = TestBed.inject(HealthService);
|
||||
});
|
||||
|
||||
describe('getBadgeClass', () => {
|
||||
it('should get badge class', () => {
|
||||
const upBadgeClass = comp.getBadgeClass('UP');
|
||||
const downBadgeClass = comp.getBadgeClass('DOWN');
|
||||
expect(upBadgeClass).toEqual('bg-success');
|
||||
expect(downBadgeClass).toEqual('bg-danger');
|
||||
});
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should call refresh on init', () => {
|
||||
// GIVEN
|
||||
const health: Health = { status: 'UP', components: { mail: { status: 'UP', details: { mailDetail: 'mail' } } } };
|
||||
jest.spyOn(service, 'checkHealth').mockReturnValue(of(health));
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
|
||||
// THEN
|
||||
expect(service.checkHealth).toHaveBeenCalled();
|
||||
expect(comp.health).toEqual(health);
|
||||
});
|
||||
|
||||
it('should handle a 503 on refreshing health data', () => {
|
||||
// GIVEN
|
||||
const health: Health = { status: 'DOWN', components: { mail: { status: 'DOWN' } } };
|
||||
jest.spyOn(service, 'checkHealth').mockReturnValue(throwError(new HttpErrorResponse({ status: 503, error: health })));
|
||||
|
||||
// WHEN
|
||||
comp.refresh();
|
||||
|
||||
// THEN
|
||||
expect(service.checkHealth).toHaveBeenCalled();
|
||||
expect(comp.health).toEqual(health);
|
||||
});
|
||||
});
|
||||
});
|
48
src/main/webapp/app/admin/health/health.component.ts
Normal file
48
src/main/webapp/app/admin/health/health.component.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { HealthService } from './health.service';
|
||||
import { Health, HealthDetails, HealthStatus } from './health.model';
|
||||
import HealthModalComponent from './modal/health-modal.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-health',
|
||||
templateUrl: './health.component.html',
|
||||
imports: [SharedModule, HealthModalComponent],
|
||||
})
|
||||
export default class HealthComponent implements OnInit {
|
||||
health?: Health;
|
||||
|
||||
private modalService = inject(NgbModal);
|
||||
private healthService = inject(HealthService);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getBadgeClass(statusState: HealthStatus): string {
|
||||
if (statusState === 'UP') {
|
||||
return 'bg-success';
|
||||
}
|
||||
return 'bg-danger';
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.healthService.checkHealth().subscribe({
|
||||
next: health => (this.health = health),
|
||||
error: (error: HttpErrorResponse) => {
|
||||
if (error.status === 503) {
|
||||
this.health = error.error;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showHealth(health: { key: string; value: HealthDetails }): void {
|
||||
const modalRef = this.modalService.open(HealthModalComponent);
|
||||
modalRef.componentInstance.health = health;
|
||||
}
|
||||
}
|
15
src/main/webapp/app/admin/health/health.model.ts
Normal file
15
src/main/webapp/app/admin/health/health.model.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export type HealthStatus = 'UP' | 'DOWN' | 'UNKNOWN' | 'OUT_OF_SERVICE';
|
||||
|
||||
export type HealthKey = 'diskSpace' | 'mail' | 'ping' | 'livenessState' | 'readinessState' | 'db';
|
||||
|
||||
export interface Health {
|
||||
status: HealthStatus;
|
||||
components: {
|
||||
[key in HealthKey]?: HealthDetails;
|
||||
};
|
||||
}
|
||||
|
||||
export interface HealthDetails {
|
||||
status: HealthStatus;
|
||||
details?: { [key: string]: unknown };
|
||||
}
|
48
src/main/webapp/app/admin/health/health.service.spec.ts
Normal file
48
src/main/webapp/app/admin/health/health.service.spec.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { HealthService } from './health.service';
|
||||
|
||||
describe('HealthService Service', () => {
|
||||
let service: HealthService;
|
||||
let httpMock: HttpTestingController;
|
||||
let applicationConfigService: ApplicationConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(HealthService);
|
||||
applicationConfigService = TestBed.inject(ApplicationConfigService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should call management/health endpoint with correct values', () => {
|
||||
// GIVEN
|
||||
let expectedResult;
|
||||
const checkHealth = {
|
||||
components: [],
|
||||
};
|
||||
|
||||
// WHEN
|
||||
service.checkHealth().subscribe(received => {
|
||||
expectedResult = received;
|
||||
});
|
||||
const testRequest = httpMock.expectOne({
|
||||
method: 'GET',
|
||||
url: applicationConfigService.getEndpointFor('management/health'),
|
||||
});
|
||||
testRequest.flush(checkHealth);
|
||||
|
||||
// THEN
|
||||
expect(expectedResult).toEqual(checkHealth);
|
||||
});
|
||||
});
|
||||
});
|
16
src/main/webapp/app/admin/health/health.service.ts
Normal file
16
src/main/webapp/app/admin/health/health.service.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { Health } from './health.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class HealthService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
checkHealth(): Observable<Health> {
|
||||
return this.http.get<Health>(this.applicationConfigService.getEndpointFor('management/health'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<div class="modal-header">
|
||||
@if (health) {
|
||||
<h4 class="modal-title" id="showHealthLabel" [jhiTranslate]="'health.indicator.' + health.key">
|
||||
{{
|
||||
{
|
||||
diskSpace: 'Espaço em disco',
|
||||
mail: 'Email',
|
||||
livenessState: 'Liveness state',
|
||||
readinessState: 'Readiness state',
|
||||
ping: 'Application',
|
||||
db: 'Base de dados'
|
||||
}[health.key] || health.key
|
||||
}}
|
||||
</h4>
|
||||
}
|
||||
|
||||
<button aria-label="Close" data-dismiss="modal" class="btn-close" type="button" (click)="dismiss()">
|
||||
<span aria-hidden="true"> </span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body pad">
|
||||
@if (health) {
|
||||
<div>
|
||||
<h5 jhiTranslate="health.details.properties">Properties</h5>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="showHealthLabel">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="text-start" jhiTranslate="health.details.name">Name</th>
|
||||
<th scope="col" class="text-start" jhiTranslate="health.details.value">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (healthDetail of health.value.details! | keyvalue; track healthDetail.key) {
|
||||
<tr>
|
||||
<td class="text-start">{{ healthDetail.key }}</td>
|
||||
<td class="text-start">{{ readableValue(healthDetail.value) }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button data-dismiss="modal" class="btn btn-secondary float-start" type="button" (click)="dismiss()">Done</button>
|
||||
</div>
|
|
@ -0,0 +1,111 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import HealthModalComponent from './health-modal.component';
|
||||
|
||||
describe('HealthModalComponent', () => {
|
||||
let comp: HealthModalComponent;
|
||||
let fixture: ComponentFixture<HealthModalComponent>;
|
||||
let mockActiveModal: NgbActiveModal;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, HealthModalComponent],
|
||||
providers: [NgbActiveModal],
|
||||
})
|
||||
.overrideTemplate(HealthModalComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(HealthModalComponent);
|
||||
comp = fixture.componentInstance;
|
||||
mockActiveModal = TestBed.inject(NgbActiveModal);
|
||||
});
|
||||
|
||||
describe('readableValue', () => {
|
||||
it('should return stringify value', () => {
|
||||
// GIVEN
|
||||
comp.health = undefined;
|
||||
|
||||
// WHEN
|
||||
const result = comp.readableValue({ name: 'jhipster' });
|
||||
|
||||
// THEN
|
||||
expect(result).toEqual('{"name":"jhipster"}');
|
||||
});
|
||||
|
||||
it('should return string value', () => {
|
||||
// GIVEN
|
||||
comp.health = undefined;
|
||||
|
||||
// WHEN
|
||||
const result = comp.readableValue('jhipster');
|
||||
|
||||
// THEN
|
||||
expect(result).toEqual('jhipster');
|
||||
});
|
||||
|
||||
it('should return storage space in an human readable unit (GB)', () => {
|
||||
// GIVEN
|
||||
comp.health = {
|
||||
key: 'diskSpace',
|
||||
value: {
|
||||
status: 'UP',
|
||||
},
|
||||
};
|
||||
|
||||
// WHEN
|
||||
const result = comp.readableValue(1073741825);
|
||||
|
||||
// THEN
|
||||
expect(result).toEqual('1.00 GB');
|
||||
});
|
||||
|
||||
it('should return storage space in an human readable unit (MB)', () => {
|
||||
// GIVEN
|
||||
comp.health = {
|
||||
key: 'diskSpace',
|
||||
value: {
|
||||
status: 'UP',
|
||||
},
|
||||
};
|
||||
|
||||
// WHEN
|
||||
const result = comp.readableValue(1073741824);
|
||||
|
||||
// THEN
|
||||
expect(result).toEqual('1024.00 MB');
|
||||
});
|
||||
|
||||
it('should return string value', () => {
|
||||
// GIVEN
|
||||
comp.health = {
|
||||
key: 'mail',
|
||||
value: {
|
||||
status: 'UP',
|
||||
},
|
||||
};
|
||||
|
||||
// WHEN
|
||||
const result = comp.readableValue(1234);
|
||||
|
||||
// THEN
|
||||
expect(result).toEqual('1234');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismiss', () => {
|
||||
it('should call dismiss when dismiss modal is called', () => {
|
||||
// GIVEN
|
||||
const spy = jest.spyOn(mockActiveModal, 'dismiss');
|
||||
|
||||
// WHEN
|
||||
comp.dismiss();
|
||||
|
||||
// THEN
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import { Component, inject } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { HealthKey, HealthDetails } from '../health.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-health-modal',
|
||||
templateUrl: './health-modal.component.html',
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export default class HealthModalComponent {
|
||||
health?: { key: HealthKey; value: HealthDetails };
|
||||
|
||||
private activeModal = inject(NgbActiveModal);
|
||||
|
||||
readableValue(value: any): string {
|
||||
if (this.health?.key === 'diskSpace') {
|
||||
// Should display storage space in an human readable unit
|
||||
const val = value / 1073741824;
|
||||
if (val > 1) {
|
||||
return `${val.toFixed(2)} GB`;
|
||||
}
|
||||
return `${(value / 1048576).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
dismiss(): void {
|
||||
this.activeModal.dismiss();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<h4 jhiTranslate="metrics.jvm.memory.title">Memória</h4>
|
||||
|
||||
@if (!updating() && jvmMemoryMetrics()) {
|
||||
<div>
|
||||
@for (entry of jvmMemoryMetrics() | keyvalue; track $index) {
|
||||
<div>
|
||||
@if (entry.value.max !== -1) {
|
||||
<span>
|
||||
<span>{{ entry.key }}</span>
|
||||
({{ entry.value.used / 1048576 | number: '1.0-0' }}M / {{ entry.value.max / 1048576 | number: '1.0-0' }}M)
|
||||
</span>
|
||||
|
||||
<div>Committed : {{ entry.value.committed / 1048576 | number: '1.0-0' }}M</div>
|
||||
<ngb-progressbar type="success" [value]="(100 * entry.value.used) / entry.value.max" [striped]="true" [animated]="false">
|
||||
<span>{{ (entry.value.used * 100) / entry.value.max | number: '1.0-0' }}%</span>
|
||||
</ngb-progressbar>
|
||||
} @else {
|
||||
<span
|
||||
><span>{{ entry.key }}</span> {{ entry.value.used / 1048576 | number: '1.0-0' }}M</span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { JvmMetrics } from 'app/admin/metrics/metrics.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-jvm-memory',
|
||||
templateUrl: './jvm-memory.component.html',
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class JvmMemoryComponent {
|
||||
/**
|
||||
* object containing all jvm memory metrics
|
||||
*/
|
||||
jvmMemoryMetrics = input<{ [key: string]: JvmMetrics }>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
<h4 jhiTranslate="metrics.jvm.threads.title">Threads</h4>
|
||||
|
||||
<span><span jhiTranslate="metrics.jvm.threads.runnable">Runnable</span> {{ threadStats.threadDumpRunnable }}</span>
|
||||
|
||||
<ngb-progressbar
|
||||
[value]="threadStats.threadDumpRunnable"
|
||||
[max]="threadStats.threadDumpAll"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="success"
|
||||
>
|
||||
<span>{{ (threadStats.threadDumpRunnable * 100) / threadStats.threadDumpAll | number: '1.0-0' }}%</span>
|
||||
</ngb-progressbar>
|
||||
|
||||
<span><span jhiTranslate="metrics.jvm.threads.timedwaiting">Tempo de espera</span> ({{ threadStats.threadDumpTimedWaiting }})</span>
|
||||
|
||||
<ngb-progressbar
|
||||
[value]="threadStats.threadDumpTimedWaiting"
|
||||
[max]="threadStats.threadDumpAll"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="warning"
|
||||
>
|
||||
<span>{{ (threadStats.threadDumpTimedWaiting * 100) / threadStats.threadDumpAll | number: '1.0-0' }}%</span>
|
||||
</ngb-progressbar>
|
||||
|
||||
<span><span jhiTranslate="metrics.jvm.threads.waiting">Aguardando</span> ({{ threadStats.threadDumpWaiting }})</span>
|
||||
|
||||
<ngb-progressbar
|
||||
[value]="threadStats.threadDumpWaiting"
|
||||
[max]="threadStats.threadDumpAll"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="warning"
|
||||
>
|
||||
<span>{{ (threadStats.threadDumpWaiting * 100) / threadStats.threadDumpAll | number: '1.0-0' }}%</span>
|
||||
</ngb-progressbar>
|
||||
|
||||
<span><span jhiTranslate="metrics.jvm.threads.blocked">Bloqueado</span> ({{ threadStats.threadDumpBlocked }})</span>
|
||||
|
||||
<ngb-progressbar
|
||||
[value]="threadStats.threadDumpBlocked"
|
||||
[max]="threadStats.threadDumpAll"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="success"
|
||||
>
|
||||
<span>{{ (threadStats.threadDumpBlocked * 100) / threadStats.threadDumpAll | number: '1.0-0' }}%</span>
|
||||
</ngb-progressbar>
|
||||
|
||||
<div>Total: {{ threadStats.threadDumpAll }}</div>
|
||||
|
||||
<button class="hand btn btn-primary btn-sm" (click)="open()" data-toggle="modal" data-target="#threadDump">
|
||||
<span>Expand</span>
|
||||
</button>
|
|
@ -0,0 +1,58 @@
|
|||
import { Component, inject, Input } from '@angular/core';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { Thread, ThreadState } from 'app/admin/metrics/metrics.model';
|
||||
import { MetricsModalThreadsComponent } from '../metrics-modal-threads/metrics-modal-threads.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-jvm-threads',
|
||||
templateUrl: './jvm-threads.component.html',
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class JvmThreadsComponent {
|
||||
threadStats = {
|
||||
threadDumpAll: 0,
|
||||
threadDumpRunnable: 0,
|
||||
threadDumpTimedWaiting: 0,
|
||||
threadDumpWaiting: 0,
|
||||
threadDumpBlocked: 0,
|
||||
};
|
||||
|
||||
@Input()
|
||||
set threads(threads: Thread[] | undefined) {
|
||||
this._threads = threads;
|
||||
|
||||
threads?.forEach(thread => {
|
||||
if (thread.threadState === ThreadState.Runnable) {
|
||||
this.threadStats.threadDumpRunnable += 1;
|
||||
} else if (thread.threadState === ThreadState.Waiting) {
|
||||
this.threadStats.threadDumpWaiting += 1;
|
||||
} else if (thread.threadState === ThreadState.TimedWaiting) {
|
||||
this.threadStats.threadDumpTimedWaiting += 1;
|
||||
} else if (thread.threadState === ThreadState.Blocked) {
|
||||
this.threadStats.threadDumpBlocked += 1;
|
||||
}
|
||||
});
|
||||
|
||||
this.threadStats.threadDumpAll =
|
||||
this.threadStats.threadDumpRunnable +
|
||||
this.threadStats.threadDumpWaiting +
|
||||
this.threadStats.threadDumpTimedWaiting +
|
||||
this.threadStats.threadDumpBlocked;
|
||||
}
|
||||
|
||||
get threads(): Thread[] | undefined {
|
||||
return this._threads;
|
||||
}
|
||||
|
||||
private _threads: Thread[] | undefined;
|
||||
|
||||
private modalService = inject(NgbModal);
|
||||
|
||||
open(): void {
|
||||
const modalRef = this.modalService.open(MetricsModalThreadsComponent);
|
||||
modalRef.componentInstance.threads = this.threads;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<h3 id="cacheMetrics()" jhiTranslate="metrics.cache.title">Estatísticas da cache</h3>
|
||||
|
||||
@if (!updating() && cacheMetrics()) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="cacheMetrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" jhiTranslate="metrics.cache.cachename">Nome da cache</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.hits">Hits</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.misses">Misses</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.gets">Cache Gets</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.puts">Cache Puts</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.removals">Cache Removals</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.evictions">Despejos</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.hitPercent">Cache Hit %</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.cache.missPercent">Cache Miss %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of cacheMetrics() | keyvalue; track entry.key) {
|
||||
<tr>
|
||||
<td>{{ entry.key }}</td>
|
||||
<td class="text-end">{{ entry.value['cache.gets.hit'] }}</td>
|
||||
<td class="text-end">{{ entry.value['cache.gets.miss'] }}</td>
|
||||
<td class="text-end">{{ entry.value['cache.gets.hit'] + entry.value['cache.gets.miss'] }}</td>
|
||||
<td class="text-end">{{ entry.value['cache.puts'] }}</td>
|
||||
<td class="text-end">{{ entry.value['cache.removals'] }}</td>
|
||||
<td class="text-end">{{ entry.value['cache.evictions'] }}</td>
|
||||
<td class="text-end">
|
||||
{{
|
||||
filterNaN((100 * entry.value['cache.gets.hit']) / (entry.value['cache.gets.hit'] + entry.value['cache.gets.miss']))
|
||||
| number: '1.0-4'
|
||||
}}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{{
|
||||
filterNaN((100 * entry.value['cache.gets.miss']) / (entry.value['cache.gets.hit'] + entry.value['cache.gets.miss']))
|
||||
| number: '1.0-4'
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { CacheMetrics } from 'app/admin/metrics/metrics.model';
|
||||
import { filterNaN } from 'app/core/util/operators';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics-cache',
|
||||
templateUrl: './metrics-cache.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsCacheComponent {
|
||||
/**
|
||||
* object containing all cache related metrics
|
||||
*/
|
||||
cacheMetrics = input<{ [key: string]: CacheMetrics }>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
|
||||
filterNaN = (n: number): number => filterNaN(n);
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
<h3 id="datasourceMetrics" jhiTranslate="metrics.datasource.title">Estatísticas da DataSource (tempo em milisegundos)</h3>
|
||||
|
||||
@if (!updating() && datasourceMetrics()) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="datasourceMetrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<span jhiTranslate="metrics.datasource.usage">Utilização</span> (active: {{ datasourceMetrics()!.active.value }}, min:
|
||||
{{ datasourceMetrics()!.min.value }}, max: {{ datasourceMetrics()!.max.value }}, idle: {{ datasourceMetrics()!.idle.value }})
|
||||
</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.datasource.count">Contagem</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.datasource.mean">Mediana</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.min">Min</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p50">p50</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p75">p75</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p95">p95</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p99">p99</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.datasource.max">Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Acquire</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.acquire.count }}</td>
|
||||
<td class="text-end">{{ filterNaN(datasourceMetrics()!.acquire.mean) | number: '1.0-2' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.acquire['0.0'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.acquire['0.5'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.acquire['0.75'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.acquire['0.95'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.acquire['0.99'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ filterNaN(datasourceMetrics()!.acquire.max) | number: '1.0-2' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Creation</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.creation.count }}</td>
|
||||
<td class="text-end">{{ filterNaN(datasourceMetrics()!.creation.mean) | number: '1.0-2' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.creation['0.0'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.creation['0.5'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.creation['0.75'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.creation['0.95'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.creation['0.99'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ filterNaN(datasourceMetrics()!.creation.max) | number: '1.0-2' }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Usage</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.usage.count }}</td>
|
||||
<td class="text-end">{{ filterNaN(datasourceMetrics()!.usage.mean) | number: '1.0-2' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.usage['0.0'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.usage['0.5'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.usage['0.75'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.usage['0.95'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ datasourceMetrics()!.usage['0.99'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ filterNaN(datasourceMetrics()!.usage.max) | number: '1.0-2' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { Databases } from 'app/admin/metrics/metrics.model';
|
||||
import { filterNaN } from 'app/core/util/operators';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics-datasource',
|
||||
templateUrl: './metrics-datasource.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsDatasourceComponent {
|
||||
/**
|
||||
* object containing all datasource related metrics
|
||||
*/
|
||||
datasourceMetrics = input<Databases>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
|
||||
filterNaN = (n: number): number => filterNaN(n);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
<h3 id="endpointsRequestsMetrics">Endpoints requests (time in millisecond)</h3>
|
||||
|
||||
@if (!updating() && endpointsRequestsMetrics()) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="endpointsRequestsMetrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Method</th>
|
||||
<th scope="col">Endpoint url</th>
|
||||
<th scope="col" class="text-end">Count</th>
|
||||
<th scope="col" class="text-end">Mean</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of endpointsRequestsMetrics() | keyvalue; track entry.key) {
|
||||
@for (method of entry.value | keyvalue; track method.key) {
|
||||
<tr>
|
||||
<td>{{ method.key }}</td>
|
||||
<td>{{ entry.key }}</td>
|
||||
<td class="text-end">{{ method.value!.count }}</td>
|
||||
<td class="text-end">{{ method.value!.mean | number: '1.0-3' }}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { Services } from 'app/admin/metrics/metrics.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics-endpoints-requests',
|
||||
templateUrl: './metrics-endpoints-requests.component.html',
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsEndpointsRequestsComponent {
|
||||
/**
|
||||
* object containing service related metrics
|
||||
*/
|
||||
endpointsRequestsMetrics = input<Services>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
<h3 id="garbageCollectorMetrics" jhiTranslate="metrics.jvm.gc.title">Garbage collections</h3>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
@if (garbageCollectorMetrics()) {
|
||||
<div>
|
||||
<span>
|
||||
GC Live Data Size/GC Max Data Size ({{ garbageCollectorMetrics()!['jvm.gc.live.data.size'] / 1048576 | number: '1.0-0' }}M /
|
||||
{{ garbageCollectorMetrics()!['jvm.gc.max.data.size'] / 1048576 | number: '1.0-0' }}M)
|
||||
</span>
|
||||
|
||||
<ngb-progressbar
|
||||
[max]="garbageCollectorMetrics()!['jvm.gc.max.data.size']"
|
||||
[value]="garbageCollectorMetrics()!['jvm.gc.live.data.size']"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="success"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
(100 * garbageCollectorMetrics()!['jvm.gc.live.data.size']) / garbageCollectorMetrics()!['jvm.gc.max.data.size']
|
||||
| number: '1.0-2'
|
||||
}}%
|
||||
</span>
|
||||
</ngb-progressbar>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
@if (garbageCollectorMetrics()) {
|
||||
<div>
|
||||
<span>
|
||||
GC Memory Promoted/GC Memory Allocated ({{ garbageCollectorMetrics()!['jvm.gc.memory.promoted'] / 1048576 | number: '1.0-0' }}M /
|
||||
{{ garbageCollectorMetrics()!['jvm.gc.memory.allocated'] / 1048576 | number: '1.0-0' }}M)
|
||||
</span>
|
||||
|
||||
<ngb-progressbar
|
||||
[max]="garbageCollectorMetrics()!['jvm.gc.memory.allocated']"
|
||||
[value]="garbageCollectorMetrics()!['jvm.gc.memory.promoted']"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="success"
|
||||
>
|
||||
<span>
|
||||
{{
|
||||
(100 * garbageCollectorMetrics()!['jvm.gc.memory.promoted']) / garbageCollectorMetrics()!['jvm.gc.memory.allocated']
|
||||
| number: '1.0-2'
|
||||
}}%
|
||||
</span>
|
||||
</ngb-progressbar>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div id="garbageCollectorMetrics" class="col-md-4">
|
||||
@if (garbageCollectorMetrics()) {
|
||||
<div class="row">
|
||||
<div class="col-md-9">Classes loaded</div>
|
||||
<div class="col-md-3 text-end">{{ garbageCollectorMetrics()!.classesLoaded }}</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-9">Classes unloaded</div>
|
||||
<div class="col-md-3 text-end">{{ garbageCollectorMetrics()!.classesUnloaded }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (!updating() && garbageCollectorMetrics()) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="garbageCollectorMetrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"></th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.count">Contagem</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.mean">Mediana</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.min">Min</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p50">p50</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p75">p75</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p95">p95</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.p99">p99</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.servicesstats.table.max">Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>jvm.gc.pause</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause'].count }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause'].mean | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.0'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.5'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.75'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.95'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause']['0.99'] | number: '1.0-3' }}</td>
|
||||
<td class="text-end">{{ garbageCollectorMetrics()!['jvm.gc.pause'].max | number: '1.0-3' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
import { Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { GarbageCollector } from 'app/admin/metrics/metrics.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics-garbagecollector',
|
||||
templateUrl: './metrics-garbagecollector.component.html',
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsGarbageCollectorComponent {
|
||||
/**
|
||||
* object containing garbage collector related metrics
|
||||
*/
|
||||
garbageCollectorMetrics = input<GarbageCollector>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
<div class="modal-header">
|
||||
<h4 class="modal-title" jhiTranslate="metrics.jvm.threads.dump.title">Threads dump</h4>
|
||||
|
||||
<button type="button" class="btn-close" (click)="dismiss()"> </button>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<button class="badge bg-primary hand" (click)="threadStateFilter = undefined" (keydown.enter)="threadStateFilter = undefined">
|
||||
@if (threadStateFilter === undefined) {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
}
|
||||
All <span class="badge rounded-pill bg-default">{{ threadDumpAll }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="badge bg-success hand"
|
||||
(click)="threadStateFilter = ThreadState.Runnable"
|
||||
(keydown.enter)="threadStateFilter = ThreadState.Runnable"
|
||||
>
|
||||
@if (threadStateFilter === ThreadState.Runnable) {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
}
|
||||
Runnable <span class="badge rounded-pill bg-default">{{ threadDumpRunnable }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="badge bg-info hand"
|
||||
(click)="threadStateFilter = ThreadState.Waiting"
|
||||
(keydown.enter)="threadStateFilter = ThreadState.Waiting"
|
||||
>
|
||||
@if (threadStateFilter === ThreadState.Waiting) {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
}
|
||||
Waiting <span class="badge rounded-pill bg-default">{{ threadDumpWaiting }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="badge bg-warning hand"
|
||||
(click)="threadStateFilter = ThreadState.TimedWaiting"
|
||||
(keydown.enter)="threadStateFilter = ThreadState.TimedWaiting"
|
||||
>
|
||||
@if (threadStateFilter === ThreadState.TimedWaiting) {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
}
|
||||
Timed Waiting <span class="badge rounded-pill bg-default">{{ threadDumpTimedWaiting }}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="badge bg-danger hand"
|
||||
(click)="threadStateFilter = ThreadState.Blocked"
|
||||
(keydown.enter)="threadStateFilter = ThreadState.Blocked"
|
||||
>
|
||||
@if (threadStateFilter === ThreadState.Blocked) {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
}
|
||||
Blocked <span class="badge rounded-pill bg-default">{{ threadDumpBlocked }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@for (thread of getThreads(); track $index) {
|
||||
<div class="pad">
|
||||
<h6>
|
||||
<span class="badge" [ngClass]="getBadgeClass(thread.threadState)">{{ thread.threadState }}</span>
|
||||
|
||||
{{ thread.threadName }} (ID {{ thread.threadId }})
|
||||
|
||||
<a (click)="thread.showThreadDump = !thread.showThreadDump" href="javascript:void(0);">
|
||||
<span [hidden]="thread.showThreadDump" jhiTranslate="metrics.jvm.threads.dump.show">Mostrar</span>
|
||||
<span [hidden]="!thread.showThreadDump" jhiTranslate="metrics.jvm.threads.dump.hide">Esconder</span>
|
||||
</a>
|
||||
</h6>
|
||||
|
||||
<div class="card" [hidden]="!thread.showThreadDump">
|
||||
<div class="card-body">
|
||||
@for (st of thread.stackTrace; track $index) {
|
||||
<div 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">
|
||||
<caption>
|
||||
Threads dump:
|
||||
{{
|
||||
thread.threadName
|
||||
}}
|
||||
</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.threads.dump.blockedtime">Tempo bloqueado</th>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.threads.dump.blockedcount">Número de bloqueios</th>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.threads.dump.waitedtime">Tempo de espera</th>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.threads.dump.waitedcount">Número de esperas</th>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.threads.dump.lockname">Nome do bloqueio</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ thread.blockedTime }}</td>
|
||||
<td>{{ thread.blockedCount }}</td>
|
||||
<td>{{ thread.waitedTime }}</td>
|
||||
<td>{{ thread.waitedCount }}</td>
|
||||
<td class="thread-dump-modal-lock" title="{{ thread.lockName }}">
|
||||
<code>{{ thread.lockName }}</code>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary float-start" data-dismiss="modal" (click)="dismiss()">Done</button>
|
||||
</div>
|
|
@ -0,0 +1,325 @@
|
|||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { ThreadState } from '../../metrics.model';
|
||||
import { MetricsModalThreadsComponent } from './metrics-modal-threads.component';
|
||||
|
||||
describe('MetricsModalThreadsComponent', () => {
|
||||
let comp: MetricsModalThreadsComponent;
|
||||
let fixture: ComponentFixture<MetricsModalThreadsComponent>;
|
||||
let mockActiveModal: NgbActiveModal;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, MetricsModalThreadsComponent],
|
||||
providers: [NgbActiveModal],
|
||||
})
|
||||
.overrideTemplate(MetricsModalThreadsComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetricsModalThreadsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
mockActiveModal = TestBed.inject(NgbActiveModal);
|
||||
});
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should count threads on init', () => {
|
||||
// GIVEN
|
||||
comp.threads = [
|
||||
{
|
||||
threadName: '',
|
||||
threadId: 1,
|
||||
blockedTime: 1,
|
||||
blockedCount: 1,
|
||||
waitedTime: 1,
|
||||
waitedCount: 1,
|
||||
lockName: 'lock1',
|
||||
lockOwnerId: 1,
|
||||
lockOwnerName: 'lock1',
|
||||
daemon: true,
|
||||
inNative: true,
|
||||
suspended: true,
|
||||
threadState: ThreadState.Blocked,
|
||||
priority: 1,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
},
|
||||
{
|
||||
threadName: '',
|
||||
threadId: 2,
|
||||
blockedTime: 2,
|
||||
blockedCount: 2,
|
||||
waitedTime: 2,
|
||||
waitedCount: 2,
|
||||
lockName: 'lock2',
|
||||
lockOwnerId: 2,
|
||||
lockOwnerName: 'lock2',
|
||||
daemon: false,
|
||||
inNative: false,
|
||||
suspended: false,
|
||||
threadState: ThreadState.Runnable,
|
||||
priority: 2,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
},
|
||||
{
|
||||
threadName: '',
|
||||
threadId: 3,
|
||||
blockedTime: 3,
|
||||
blockedCount: 3,
|
||||
waitedTime: 3,
|
||||
waitedCount: 3,
|
||||
lockName: 'lock3',
|
||||
lockOwnerId: 3,
|
||||
lockOwnerName: 'lock3',
|
||||
daemon: false,
|
||||
inNative: false,
|
||||
suspended: false,
|
||||
threadState: ThreadState.TimedWaiting,
|
||||
priority: 3,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
},
|
||||
{
|
||||
threadName: '',
|
||||
threadId: 4,
|
||||
blockedTime: 4,
|
||||
blockedCount: 4,
|
||||
waitedTime: 4,
|
||||
waitedCount: 4,
|
||||
lockName: 'lock4',
|
||||
lockOwnerId: 4,
|
||||
lockOwnerName: 'lock4',
|
||||
daemon: false,
|
||||
inNative: false,
|
||||
suspended: false,
|
||||
threadState: ThreadState.Waiting,
|
||||
priority: 4,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
},
|
||||
];
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
|
||||
// THEN
|
||||
expect(comp.threadDumpRunnable).toEqual(1);
|
||||
expect(comp.threadDumpWaiting).toEqual(1);
|
||||
expect(comp.threadDumpTimedWaiting).toEqual(1);
|
||||
expect(comp.threadDumpBlocked).toEqual(1);
|
||||
expect(comp.threadDumpAll).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBadgeClass', () => {
|
||||
it('should return a success badge class for runnable thread state', () => {
|
||||
// GIVEN
|
||||
const threadState = ThreadState.Runnable;
|
||||
|
||||
// WHEN
|
||||
const badgeClass = comp.getBadgeClass(threadState);
|
||||
|
||||
// THEN
|
||||
expect(badgeClass).toEqual('bg-success');
|
||||
});
|
||||
|
||||
it('should return an info badge class for waiting thread state', () => {
|
||||
// GIVEN
|
||||
const threadState = ThreadState.Waiting;
|
||||
|
||||
// WHEN
|
||||
const badgeClass = comp.getBadgeClass(threadState);
|
||||
|
||||
// THEN
|
||||
expect(badgeClass).toEqual('bg-info');
|
||||
});
|
||||
|
||||
it('should return a warning badge class for time waiting thread state', () => {
|
||||
// GIVEN
|
||||
const threadState = ThreadState.TimedWaiting;
|
||||
|
||||
// WHEN
|
||||
const badgeClass = comp.getBadgeClass(threadState);
|
||||
|
||||
// THEN
|
||||
expect(badgeClass).toEqual('bg-warning');
|
||||
});
|
||||
|
||||
it('should return a danger badge class for blocked thread state', () => {
|
||||
// GIVEN
|
||||
const threadState = ThreadState.Blocked;
|
||||
|
||||
// WHEN
|
||||
const badgeClass = comp.getBadgeClass(threadState);
|
||||
|
||||
// THEN
|
||||
expect(badgeClass).toEqual('bg-danger');
|
||||
});
|
||||
|
||||
it('should return an empty string for others threads', () => {
|
||||
// GIVEN
|
||||
const threadState = ThreadState.New;
|
||||
|
||||
// WHEN
|
||||
const badgeClass = comp.getBadgeClass(threadState);
|
||||
|
||||
// THEN
|
||||
expect(badgeClass).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getThreads', () => {
|
||||
it('should return blocked threads', () => {
|
||||
// GIVEN
|
||||
const thread1 = {
|
||||
threadName: '',
|
||||
threadId: 1,
|
||||
blockedTime: 1,
|
||||
blockedCount: 1,
|
||||
waitedTime: 1,
|
||||
waitedCount: 1,
|
||||
lockName: 'lock1',
|
||||
lockOwnerId: 1,
|
||||
lockOwnerName: 'lock1',
|
||||
daemon: true,
|
||||
inNative: true,
|
||||
suspended: true,
|
||||
threadState: ThreadState.Blocked,
|
||||
priority: 1,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
};
|
||||
const thread2 = {
|
||||
threadName: '',
|
||||
threadId: 2,
|
||||
blockedTime: 2,
|
||||
blockedCount: 2,
|
||||
waitedTime: 2,
|
||||
waitedCount: 2,
|
||||
lockName: 'lock2',
|
||||
lockOwnerId: 1,
|
||||
lockOwnerName: 'lock2',
|
||||
daemon: false,
|
||||
inNative: false,
|
||||
suspended: false,
|
||||
threadState: ThreadState.Runnable,
|
||||
priority: 2,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
};
|
||||
comp.threads = [thread1, thread2];
|
||||
comp.threadStateFilter = ThreadState.Blocked;
|
||||
|
||||
// WHEN
|
||||
const threadsFiltered = comp.getThreads();
|
||||
|
||||
// THEN
|
||||
expect(threadsFiltered).toEqual([thread1]);
|
||||
});
|
||||
|
||||
it('should return an empty array of threads', () => {
|
||||
// GIVEN
|
||||
comp.threads = [];
|
||||
comp.threadStateFilter = ThreadState.Blocked;
|
||||
|
||||
// WHEN
|
||||
const threadsFiltered = comp.getThreads();
|
||||
|
||||
// THEN
|
||||
expect(threadsFiltered).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return all threads if there is no filter', () => {
|
||||
// GIVEN
|
||||
const thread1 = {
|
||||
threadName: '',
|
||||
threadId: 1,
|
||||
blockedTime: 1,
|
||||
blockedCount: 1,
|
||||
waitedTime: 1,
|
||||
waitedCount: 1,
|
||||
lockName: 'lock1',
|
||||
lockOwnerId: 1,
|
||||
lockOwnerName: 'lock1',
|
||||
daemon: true,
|
||||
inNative: true,
|
||||
suspended: true,
|
||||
threadState: ThreadState.Blocked,
|
||||
priority: 1,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
};
|
||||
const thread2 = {
|
||||
threadName: '',
|
||||
threadId: 2,
|
||||
blockedTime: 2,
|
||||
blockedCount: 2,
|
||||
waitedTime: 2,
|
||||
waitedCount: 2,
|
||||
lockName: 'lock2',
|
||||
lockOwnerId: 1,
|
||||
lockOwnerName: 'lock2',
|
||||
daemon: false,
|
||||
inNative: false,
|
||||
suspended: false,
|
||||
threadState: ThreadState.Runnable,
|
||||
priority: 2,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
};
|
||||
comp.threads = [thread1, thread2];
|
||||
comp.threadStateFilter = undefined;
|
||||
|
||||
// WHEN
|
||||
const threadsFiltered = comp.getThreads();
|
||||
|
||||
// THEN
|
||||
expect(threadsFiltered).toEqual(comp.threads);
|
||||
});
|
||||
|
||||
it('should return an empty array if there are no threads to filter', () => {
|
||||
// GIVEN
|
||||
comp.threads = undefined;
|
||||
comp.threadStateFilter = ThreadState.Blocked;
|
||||
|
||||
// WHEN
|
||||
const threadsFiltered = comp.getThreads();
|
||||
|
||||
// THEN
|
||||
expect(threadsFiltered).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dismiss', () => {
|
||||
it('should call dismiss function for modal on dismiss', () => {
|
||||
// GIVEN
|
||||
jest.spyOn(mockActiveModal, 'dismiss').mockReturnValue(undefined);
|
||||
|
||||
// WHEN
|
||||
comp.dismiss();
|
||||
|
||||
// THEN
|
||||
expect(mockActiveModal.dismiss).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,62 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { Thread, ThreadState } from 'app/admin/metrics/metrics.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-thread-modal',
|
||||
templateUrl: './metrics-modal-threads.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsModalThreadsComponent implements OnInit {
|
||||
ThreadState = ThreadState;
|
||||
threadStateFilter?: ThreadState;
|
||||
threads?: Thread[];
|
||||
threadDumpAll = 0;
|
||||
threadDumpBlocked = 0;
|
||||
threadDumpRunnable = 0;
|
||||
threadDumpTimedWaiting = 0;
|
||||
threadDumpWaiting = 0;
|
||||
|
||||
private activeModal = inject(NgbActiveModal);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.threads?.forEach(thread => {
|
||||
if (thread.threadState === ThreadState.Runnable) {
|
||||
this.threadDumpRunnable += 1;
|
||||
} else if (thread.threadState === ThreadState.Waiting) {
|
||||
this.threadDumpWaiting += 1;
|
||||
} else if (thread.threadState === ThreadState.TimedWaiting) {
|
||||
this.threadDumpTimedWaiting += 1;
|
||||
} else if (thread.threadState === ThreadState.Blocked) {
|
||||
this.threadDumpBlocked += 1;
|
||||
}
|
||||
});
|
||||
|
||||
this.threadDumpAll = this.threadDumpRunnable + this.threadDumpWaiting + this.threadDumpTimedWaiting + this.threadDumpBlocked;
|
||||
}
|
||||
|
||||
getBadgeClass(threadState: ThreadState): string {
|
||||
if (threadState === ThreadState.Runnable) {
|
||||
return 'bg-success';
|
||||
} else if (threadState === ThreadState.Waiting) {
|
||||
return 'bg-info';
|
||||
} else if (threadState === ThreadState.TimedWaiting) {
|
||||
return 'bg-warning';
|
||||
} else if (threadState === ThreadState.Blocked) {
|
||||
return 'bg-danger';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
getThreads(): Thread[] {
|
||||
return this.threads?.filter(thread => !this.threadStateFilter || thread.threadState === this.threadStateFilter) ?? [];
|
||||
}
|
||||
|
||||
dismiss(): void {
|
||||
this.activeModal.dismiss();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
<h3 id="requestMetrics" jhiTranslate="metrics.jvm.http.title">Pedidos HTTP (eventos por segundo)</h3>
|
||||
|
||||
@if (!updating() && requestMetrics()) {
|
||||
<table class="table table-striped" aria-describedby="requestMetrics">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.http.table.code">Código</th>
|
||||
<th scope="col" jhiTranslate="metrics.jvm.http.table.count">Contagem</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.jvm.http.table.mean">Mediana</th>
|
||||
<th scope="col" class="text-end" jhiTranslate="metrics.jvm.http.table.max">Max</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of requestMetrics()!['percode'] | keyvalue; track entry.key) {
|
||||
<tr>
|
||||
<td>{{ entry.key }}</td>
|
||||
<td>
|
||||
<ngb-progressbar
|
||||
[max]="requestMetrics()!['all'].count"
|
||||
[value]="entry.value.count"
|
||||
[striped]="true"
|
||||
[animated]="false"
|
||||
type="success"
|
||||
>
|
||||
<span>{{ entry.value.count }}</span>
|
||||
</ngb-progressbar>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
{{ filterNaN(entry.value.mean) | number: '1.0-2' }}
|
||||
</td>
|
||||
<td class="text-end">{{ entry.value.max | number: '1.0-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { HttpServerRequests } from 'app/admin/metrics/metrics.model';
|
||||
import { filterNaN } from 'app/core/util/operators';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics-request',
|
||||
templateUrl: './metrics-request.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsRequestComponent {
|
||||
/**
|
||||
* object containing http request related metrics
|
||||
*/
|
||||
requestMetrics = input<HttpServerRequests>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
|
||||
filterNaN = (n: number): number => filterNaN(n);
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<h4>System</h4>
|
||||
|
||||
@if (!updating() && systemMetrics()) {
|
||||
<ng-container>
|
||||
<div class="row">
|
||||
<div class="col-md-4">Uptime</div>
|
||||
<div class="col-md-8 text-end">{{ convertMillisecondsToDuration(systemMetrics()!['process.uptime']) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4">Start time</div>
|
||||
<div class="col-md-8 text-end">{{ systemMetrics()!['process.start.time'] | date: 'full' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">Process CPU usage</div>
|
||||
<div class="col-md-3 text-end">{{ 100 * systemMetrics()!['process.cpu.usage'] | number: '1.0-2' }} %</div>
|
||||
</div>
|
||||
|
||||
<ngb-progressbar [value]="100 * systemMetrics()!['process.cpu.usage']" [striped]="true" [animated]="false" type="success">
|
||||
<span>{{ 100 * systemMetrics()!['process.cpu.usage'] | number: '1.0-2' }} %</span>
|
||||
</ngb-progressbar>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">System CPU usage</div>
|
||||
<div class="col-md-3 text-end">{{ 100 * systemMetrics()!['system.cpu.usage'] | number: '1.0-2' }} %</div>
|
||||
</div>
|
||||
|
||||
<ngb-progressbar [value]="100 * systemMetrics()!['system.cpu.usage']" [striped]="true" [animated]="false" type="success">
|
||||
<span>{{ 100 * systemMetrics()!['system.cpu.usage'] | number: '1.0-2' }} %</span>
|
||||
</ngb-progressbar>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">System CPU count</div>
|
||||
<div class="col-md-3 text-end">{{ systemMetrics()!['system.cpu.count'] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">System 1m Load average</div>
|
||||
<div class="col-md-3 text-end">{{ systemMetrics()!['system.load.average.1m'] | number: '1.0-2' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">Process files max</div>
|
||||
<div class="col-md-3 text-end">{{ systemMetrics()!['process.files.max'] | number: '1.0-0' }}</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">Process files open</div>
|
||||
<div class="col-md-3 text-end">{{ systemMetrics()!['process.files.open'] | number: '1.0-0' }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { ProcessMetrics } from 'app/admin/metrics/metrics.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics-system',
|
||||
templateUrl: './metrics-system.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [SharedModule],
|
||||
})
|
||||
export class MetricsSystemComponent {
|
||||
/**
|
||||
* object containing thread related metrics
|
||||
*/
|
||||
systemMetrics = input<ProcessMetrics>();
|
||||
|
||||
/**
|
||||
* boolean field saying if the metrics are in the process of being updated
|
||||
*/
|
||||
updating = input<boolean>();
|
||||
|
||||
convertMillisecondsToDuration(ms: number): string {
|
||||
const times = {
|
||||
year: 31557600000,
|
||||
month: 2629746000,
|
||||
day: 86400000,
|
||||
hour: 3600000,
|
||||
minute: 60000,
|
||||
second: 1000,
|
||||
};
|
||||
let timeString = '';
|
||||
for (const [key, value] of Object.entries(times)) {
|
||||
if (Math.floor(ms / value) > 0) {
|
||||
let plural = '';
|
||||
if (Math.floor(ms / value) > 1) {
|
||||
plural = 's';
|
||||
}
|
||||
timeString += `${Math.floor(ms / value).toString()} ${key.toString()}${plural} `;
|
||||
ms = ms - value * Math.floor(ms / value);
|
||||
}
|
||||
}
|
||||
return timeString;
|
||||
}
|
||||
}
|
51
src/main/webapp/app/admin/metrics/metrics.component.html
Normal file
51
src/main/webapp/app/admin/metrics/metrics.component.html
Normal file
|
@ -0,0 +1,51 @@
|
|||
<div>
|
||||
<h2>
|
||||
<span id="metrics-page-heading" data-cy="metricsPageHeading" jhiTranslate="metrics.title">Métricas da aplicação</span>
|
||||
|
||||
<button class="btn btn-primary float-end" (click)="refresh()">
|
||||
<fa-icon icon="sync"></fa-icon> <span jhiTranslate="metrics.refresh.button">Atualizar</span>
|
||||
</button>
|
||||
</h2>
|
||||
|
||||
<h3 jhiTranslate="metrics.jvm.title">Métricas da JVM</h3>
|
||||
|
||||
@if (metrics() && !updatingMetrics()) {
|
||||
<div class="row">
|
||||
<jhi-jvm-memory class="col-md-4" [updating]="updatingMetrics()" [jvmMemoryMetrics]="metrics()?.jvm"></jhi-jvm-memory>
|
||||
|
||||
<jhi-jvm-threads class="col-md-4" [threads]="threads()"></jhi-jvm-threads>
|
||||
|
||||
<jhi-metrics-system class="col-md-4" [updating]="updatingMetrics()" [systemMetrics]="metrics()?.processMetrics"></jhi-metrics-system>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (metrics() && metricsKeyExists('garbageCollector')) {
|
||||
<jhi-metrics-garbagecollector
|
||||
[updating]="updatingMetrics()"
|
||||
[garbageCollectorMetrics]="metrics()?.garbageCollector"
|
||||
></jhi-metrics-garbagecollector>
|
||||
}
|
||||
|
||||
@if (updatingMetrics()) {
|
||||
<div class="well well-lg" jhiTranslate="metrics.updating">Atualização...</div>
|
||||
}
|
||||
|
||||
@if (metrics() && metricsKeyExists('http.server.requests')) {
|
||||
<jhi-metrics-request [updating]="updatingMetrics()" [requestMetrics]="metrics()?.['http.server.requests']"></jhi-metrics-request>
|
||||
}
|
||||
|
||||
@if (metrics() && metricsKeyExists('services')) {
|
||||
<jhi-metrics-endpoints-requests
|
||||
[updating]="updatingMetrics()"
|
||||
[endpointsRequestsMetrics]="metrics()?.services"
|
||||
></jhi-metrics-endpoints-requests>
|
||||
}
|
||||
|
||||
@if (metrics() && metricsKeyExists('cache')) {
|
||||
<jhi-metrics-cache [updating]="updatingMetrics()" [cacheMetrics]="metrics()?.cache"></jhi-metrics-cache>
|
||||
}
|
||||
|
||||
@if (metrics() && metricsKeyExistsAndObjectNotEmpty('databases')) {
|
||||
<jhi-metrics-datasource [updating]="updatingMetrics()" [datasourceMetrics]="metrics()?.databases"></jhi-metrics-datasource>
|
||||
}
|
||||
</div>
|
146
src/main/webapp/app/admin/metrics/metrics.component.spec.ts
Normal file
146
src/main/webapp/app/admin/metrics/metrics.component.spec.ts
Normal file
|
@ -0,0 +1,146 @@
|
|||
import { ChangeDetectorRef } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import MetricsComponent from './metrics.component';
|
||||
import { MetricsService } from './metrics.service';
|
||||
import { Metrics, Thread, ThreadDump } from './metrics.model';
|
||||
|
||||
describe('MetricsComponent', () => {
|
||||
let comp: MetricsComponent;
|
||||
let fixture: ComponentFixture<MetricsComponent>;
|
||||
let service: MetricsService;
|
||||
let changeDetector: ChangeDetectorRef;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, MetricsComponent],
|
||||
})
|
||||
.overrideTemplate(MetricsComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetricsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
service = TestBed.inject(MetricsService);
|
||||
changeDetector = fixture.debugElement.injector.get(ChangeDetectorRef);
|
||||
});
|
||||
|
||||
describe('refresh', () => {
|
||||
it('should call refresh on init', () => {
|
||||
// GIVEN
|
||||
const metrics = {
|
||||
garbageCollector: {
|
||||
'PS Scavenge': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
'PS MarkSweep': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
},
|
||||
} as unknown as Metrics;
|
||||
const threadDump = { threads: [{ threadName: 'thread 1' } as Thread] } as ThreadDump;
|
||||
|
||||
jest.spyOn(service, 'getMetrics').mockReturnValue(of(metrics));
|
||||
jest.spyOn(service, 'threadDump').mockReturnValue(of(threadDump));
|
||||
jest.spyOn(changeDetector.constructor.prototype, 'markForCheck');
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
|
||||
// THEN
|
||||
expect(service.getMetrics).toHaveBeenCalled();
|
||||
expect(comp.metrics()).toEqual(metrics);
|
||||
expect(comp.threads()).toEqual(threadDump.threads);
|
||||
expect(comp.updatingMetrics()).toBeFalsy();
|
||||
expect(changeDetector.constructor.prototype.markForCheck).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('metricsKeyExists', () => {
|
||||
it('should check that metrics key exists', () => {
|
||||
// GIVEN
|
||||
comp.metrics.set({
|
||||
garbageCollector: {
|
||||
'PS Scavenge': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
'PS MarkSweep': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
},
|
||||
} as unknown as Metrics);
|
||||
|
||||
// WHEN
|
||||
const garbageCollectorKeyExists = comp.metricsKeyExists('garbageCollector');
|
||||
|
||||
// THEN
|
||||
expect(garbageCollectorKeyExists).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should check that metrics key does not exist', () => {
|
||||
// GIVEN
|
||||
comp.metrics.set({
|
||||
garbageCollector: {
|
||||
'PS Scavenge': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
'PS MarkSweep': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
},
|
||||
} as unknown as Metrics);
|
||||
|
||||
// WHEN
|
||||
const databasesCollectorKeyExists = comp.metricsKeyExists('databases');
|
||||
|
||||
// THEN
|
||||
expect(databasesCollectorKeyExists).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('metricsKeyExistsAndObjectNotEmpty', () => {
|
||||
it('should check that metrics key exists and is not empty', () => {
|
||||
// GIVEN
|
||||
comp.metrics.set({
|
||||
garbageCollector: {
|
||||
'PS Scavenge': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
'PS MarkSweep': {
|
||||
collectionCount: 0,
|
||||
collectionTime: 0,
|
||||
},
|
||||
},
|
||||
} as unknown as Metrics);
|
||||
|
||||
// WHEN
|
||||
const garbageCollectorKeyExistsAndNotEmpty = comp.metricsKeyExistsAndObjectNotEmpty('garbageCollector');
|
||||
|
||||
// THEN
|
||||
expect(garbageCollectorKeyExistsAndNotEmpty).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should check that metrics key is empty', () => {
|
||||
// GIVEN
|
||||
comp.metrics.set({
|
||||
garbageCollector: {},
|
||||
} as Metrics);
|
||||
|
||||
// WHEN
|
||||
const garbageCollectorKeyEmpty = comp.metricsKeyExistsAndObjectNotEmpty('garbageCollector');
|
||||
|
||||
// THEN
|
||||
expect(garbageCollectorKeyEmpty).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
64
src/main/webapp/app/admin/metrics/metrics.component.ts
Normal file
64
src/main/webapp/app/admin/metrics/metrics.component.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import { Component, OnInit, ChangeDetectionStrategy, ChangeDetectorRef, inject, signal } from '@angular/core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { MetricsService } from './metrics.service';
|
||||
import { Metrics, Thread } from './metrics.model';
|
||||
import { JvmMemoryComponent } from './blocks/jvm-memory/jvm-memory.component';
|
||||
import { JvmThreadsComponent } from './blocks/jvm-threads/jvm-threads.component';
|
||||
import { MetricsCacheComponent } from './blocks/metrics-cache/metrics-cache.component';
|
||||
import { MetricsDatasourceComponent } from './blocks/metrics-datasource/metrics-datasource.component';
|
||||
import { MetricsEndpointsRequestsComponent } from './blocks/metrics-endpoints-requests/metrics-endpoints-requests.component';
|
||||
import { MetricsGarbageCollectorComponent } from './blocks/metrics-garbagecollector/metrics-garbagecollector.component';
|
||||
import { MetricsModalThreadsComponent } from './blocks/metrics-modal-threads/metrics-modal-threads.component';
|
||||
import { MetricsRequestComponent } from './blocks/metrics-request/metrics-request.component';
|
||||
import { MetricsSystemComponent } from './blocks/metrics-system/metrics-system.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-metrics',
|
||||
templateUrl: './metrics.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
SharedModule,
|
||||
JvmMemoryComponent,
|
||||
JvmThreadsComponent,
|
||||
MetricsCacheComponent,
|
||||
MetricsDatasourceComponent,
|
||||
MetricsEndpointsRequestsComponent,
|
||||
MetricsGarbageCollectorComponent,
|
||||
MetricsModalThreadsComponent,
|
||||
MetricsRequestComponent,
|
||||
MetricsSystemComponent,
|
||||
],
|
||||
})
|
||||
export default class MetricsComponent implements OnInit {
|
||||
metrics = signal<Metrics | undefined>(undefined);
|
||||
threads = signal<Thread[] | undefined>(undefined);
|
||||
updatingMetrics = signal(true);
|
||||
|
||||
private metricsService = inject(MetricsService);
|
||||
private changeDetector = inject(ChangeDetectorRef);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh(): void {
|
||||
this.updatingMetrics.set(true);
|
||||
combineLatest([this.metricsService.getMetrics(), this.metricsService.threadDump()]).subscribe(([metrics, threadDump]) => {
|
||||
this.metrics.set(metrics);
|
||||
this.threads.set(threadDump.threads);
|
||||
this.updatingMetrics.set(false);
|
||||
this.changeDetector.markForCheck();
|
||||
});
|
||||
}
|
||||
|
||||
metricsKeyExists(key: keyof Metrics): boolean {
|
||||
return Boolean(this.metrics()?.[key]);
|
||||
}
|
||||
|
||||
metricsKeyExistsAndObjectNotEmpty(key: keyof Metrics): boolean {
|
||||
return Boolean(this.metrics()?.[key] && JSON.stringify(this.metrics()?.[key]) !== '{}');
|
||||
}
|
||||
}
|
158
src/main/webapp/app/admin/metrics/metrics.model.ts
Normal file
158
src/main/webapp/app/admin/metrics/metrics.model.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
export interface Metrics {
|
||||
jvm: { [key: string]: JvmMetrics };
|
||||
databases: Databases;
|
||||
'http.server.requests': HttpServerRequests;
|
||||
cache: { [key: string]: CacheMetrics };
|
||||
garbageCollector: GarbageCollector;
|
||||
services: Services;
|
||||
processMetrics: ProcessMetrics;
|
||||
}
|
||||
|
||||
export interface JvmMetrics {
|
||||
committed: number;
|
||||
max: number;
|
||||
used: number;
|
||||
}
|
||||
|
||||
export interface Databases {
|
||||
min: Value;
|
||||
idle: Value;
|
||||
max: Value;
|
||||
usage: MetricsWithPercentile;
|
||||
pending: Value;
|
||||
active: Value;
|
||||
acquire: MetricsWithPercentile;
|
||||
creation: MetricsWithPercentile;
|
||||
connections: Value;
|
||||
}
|
||||
|
||||
export interface Value {
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface MetricsWithPercentile {
|
||||
'0.0': number;
|
||||
'1.0': number;
|
||||
max: number;
|
||||
totalTime: number;
|
||||
mean: number;
|
||||
'0.5': number;
|
||||
count: number;
|
||||
'0.99': number;
|
||||
'0.75': number;
|
||||
'0.95': number;
|
||||
}
|
||||
|
||||
export interface HttpServerRequests {
|
||||
all: {
|
||||
count: number;
|
||||
};
|
||||
percode: { [key: string]: MaxMeanCount };
|
||||
}
|
||||
|
||||
export interface MaxMeanCount {
|
||||
max: number;
|
||||
mean: number;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface CacheMetrics {
|
||||
'cache.gets.miss': number;
|
||||
'cache.puts': number;
|
||||
'cache.gets.hit': number;
|
||||
'cache.removals': number;
|
||||
'cache.evictions': number;
|
||||
}
|
||||
|
||||
export interface GarbageCollector {
|
||||
'jvm.gc.max.data.size': number;
|
||||
'jvm.gc.pause': MetricsWithPercentile;
|
||||
'jvm.gc.memory.promoted': number;
|
||||
'jvm.gc.memory.allocated': number;
|
||||
classesLoaded: number;
|
||||
'jvm.gc.live.data.size': number;
|
||||
classesUnloaded: number;
|
||||
}
|
||||
|
||||
export interface Services {
|
||||
[key: string]: {
|
||||
[key in HttpMethod]?: MaxMeanCount;
|
||||
};
|
||||
}
|
||||
|
||||
export enum HttpMethod {
|
||||
Post = 'POST',
|
||||
Get = 'GET',
|
||||
Put = 'PUT',
|
||||
Patch = 'PATCH',
|
||||
Delete = 'DELETE',
|
||||
}
|
||||
|
||||
export interface ProcessMetrics {
|
||||
'system.cpu.usage': number;
|
||||
'system.cpu.count': number;
|
||||
'system.load.average.1m'?: number;
|
||||
'process.cpu.usage': number;
|
||||
'process.files.max'?: number;
|
||||
'process.files.open'?: number;
|
||||
'process.start.time': number;
|
||||
'process.uptime': number;
|
||||
}
|
||||
|
||||
export interface ThreadDump {
|
||||
threads: Thread[];
|
||||
}
|
||||
|
||||
export interface Thread {
|
||||
threadName: string;
|
||||
threadId: number;
|
||||
blockedTime: number;
|
||||
blockedCount: number;
|
||||
waitedTime: number;
|
||||
waitedCount: number;
|
||||
lockName: string | null;
|
||||
lockOwnerId: number;
|
||||
lockOwnerName: string | null;
|
||||
daemon: boolean;
|
||||
inNative: boolean;
|
||||
suspended: boolean;
|
||||
threadState: ThreadState;
|
||||
priority: number;
|
||||
stackTrace: StackTrace[];
|
||||
lockedMonitors: LockedMonitor[];
|
||||
lockedSynchronizers: string[];
|
||||
lockInfo: LockInfo | null;
|
||||
// custom field for showing-hiding thread dump
|
||||
showThreadDump?: boolean;
|
||||
}
|
||||
|
||||
export interface LockInfo {
|
||||
className: string;
|
||||
identityHashCode: number;
|
||||
}
|
||||
|
||||
export interface LockedMonitor {
|
||||
className: string;
|
||||
identityHashCode: number;
|
||||
lockedStackDepth: number;
|
||||
lockedStackFrame: StackTrace;
|
||||
}
|
||||
|
||||
export interface StackTrace {
|
||||
classLoaderName: string | null;
|
||||
moduleName: string | null;
|
||||
moduleVersion: string | null;
|
||||
methodName: string;
|
||||
fileName: string;
|
||||
lineNumber: number;
|
||||
className: string;
|
||||
nativeMethod: boolean;
|
||||
}
|
||||
|
||||
export enum ThreadState {
|
||||
Runnable = 'RUNNABLE',
|
||||
TimedWaiting = 'TIMED_WAITING',
|
||||
Waiting = 'WAITING',
|
||||
Blocked = 'BLOCKED',
|
||||
New = 'NEW',
|
||||
}
|
81
src/main/webapp/app/admin/metrics/metrics.service.spec.ts
Normal file
81
src/main/webapp/app/admin/metrics/metrics.service.spec.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { MetricsService } from './metrics.service';
|
||||
import { ThreadDump, ThreadState } from './metrics.model';
|
||||
|
||||
describe('Logs Service', () => {
|
||||
let service: MetricsService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
service = TestBed.inject(MetricsService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should return Metrics', () => {
|
||||
let expectedResult;
|
||||
const metrics = {
|
||||
jvm: {},
|
||||
'http.server.requests': {},
|
||||
cache: {},
|
||||
services: {},
|
||||
databases: {},
|
||||
garbageCollector: {},
|
||||
processMetrics: {},
|
||||
};
|
||||
|
||||
service.getMetrics().subscribe(received => {
|
||||
expectedResult = received;
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
req.flush(metrics);
|
||||
expect(expectedResult).toEqual(metrics);
|
||||
});
|
||||
|
||||
it('should return Thread Dump', () => {
|
||||
let expectedResult: ThreadDump | null = null;
|
||||
const dump: ThreadDump = {
|
||||
threads: [
|
||||
{
|
||||
threadName: 'Reference Handler',
|
||||
threadId: 2,
|
||||
blockedTime: -1,
|
||||
blockedCount: 7,
|
||||
waitedTime: -1,
|
||||
waitedCount: 0,
|
||||
lockName: null,
|
||||
lockOwnerId: -1,
|
||||
lockOwnerName: null,
|
||||
daemon: true,
|
||||
inNative: false,
|
||||
suspended: false,
|
||||
threadState: ThreadState.Runnable,
|
||||
priority: 10,
|
||||
stackTrace: [],
|
||||
lockedMonitors: [],
|
||||
lockedSynchronizers: [],
|
||||
lockInfo: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
service.threadDump().subscribe(received => {
|
||||
expectedResult = received;
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
req.flush(dump);
|
||||
expect(expectedResult).toEqual(dump);
|
||||
});
|
||||
});
|
||||
});
|
20
src/main/webapp/app/admin/metrics/metrics.service.ts
Normal file
20
src/main/webapp/app/admin/metrics/metrics.service.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { inject, Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
import { ApplicationConfigService } from 'app/core/config/application-config.service';
|
||||
import { Metrics, ThreadDump } from './metrics.model';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class MetricsService {
|
||||
private http = inject(HttpClient);
|
||||
private applicationConfigService = inject(ApplicationConfigService);
|
||||
|
||||
getMetrics(): Observable<Metrics> {
|
||||
return this.http.get<Metrics>(this.applicationConfigService.getEndpointFor('management/jhimetrics'));
|
||||
}
|
||||
|
||||
threadDump(): Observable<ThreadDump> {
|
||||
return this.http.get<ThreadDump>(this.applicationConfigService.getEndpointFor('management/threaddump'));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
@if (user) {
|
||||
<form name="deleteForm" (ngSubmit)="confirmDelete(user.login!)">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" jhiTranslate="entity.delete.title">Confirme a eliminação</h4>
|
||||
</div>
|
||||
|
||||
<div class="modal-body">
|
||||
<jhi-alert-error></jhi-alert-error>
|
||||
|
||||
<p jhiTranslate="userManagement.delete.question" [translateValues]="{ login: user.login }">
|
||||
Tem a certeza que deseja eliminar o utilizador {{ user.login }}?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" (click)="cancel()">
|
||||
<fa-icon icon="ban"></fa-icon> <span jhiTranslate="entity.action.cancel">Cancelar</span>
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn btn-danger">
|
||||
<fa-icon icon="times"></fa-icon> <span jhiTranslate="entity.action.delete">Eliminar</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
jest.mock('@ng-bootstrap/ng-bootstrap');
|
||||
|
||||
import { ComponentFixture, TestBed, waitForAsync, inject, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { UserManagementService } from '../service/user-management.service';
|
||||
|
||||
import UserManagementDeleteDialogComponent from './user-management-delete-dialog.component';
|
||||
|
||||
describe('User Management Delete Component', () => {
|
||||
let comp: UserManagementDeleteDialogComponent;
|
||||
let fixture: ComponentFixture<UserManagementDeleteDialogComponent>;
|
||||
let service: UserManagementService;
|
||||
let mockActiveModal: NgbActiveModal;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, UserManagementDeleteDialogComponent],
|
||||
providers: [NgbActiveModal],
|
||||
})
|
||||
.overrideTemplate(UserManagementDeleteDialogComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserManagementDeleteDialogComponent);
|
||||
comp = fixture.componentInstance;
|
||||
service = TestBed.inject(UserManagementService);
|
||||
mockActiveModal = TestBed.inject(NgbActiveModal);
|
||||
});
|
||||
|
||||
describe('confirmDelete', () => {
|
||||
it('Should call delete service on confirmDelete', inject(
|
||||
[],
|
||||
fakeAsync(() => {
|
||||
// GIVEN
|
||||
jest.spyOn(service, 'delete').mockReturnValue(of({}));
|
||||
|
||||
// WHEN
|
||||
comp.confirmDelete('user');
|
||||
tick();
|
||||
|
||||
// THEN
|
||||
expect(service.delete).toHaveBeenCalledWith('user');
|
||||
expect(mockActiveModal.close).toHaveBeenCalledWith('deleted');
|
||||
}),
|
||||
));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,30 @@
|
|||
import { Component, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { User } from '../user-management.model';
|
||||
import { UserManagementService } from '../service/user-management.service';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-user-mgmt-delete-dialog',
|
||||
templateUrl: './user-management-delete-dialog.component.html',
|
||||
imports: [SharedModule, FormsModule],
|
||||
})
|
||||
export default class UserManagementDeleteDialogComponent {
|
||||
user?: User;
|
||||
|
||||
private userService = inject(UserManagementService);
|
||||
private activeModal = inject(NgbActiveModal);
|
||||
|
||||
cancel(): void {
|
||||
this.activeModal.dismiss();
|
||||
}
|
||||
|
||||
confirmDelete(login: string): void {
|
||||
this.userService.delete(login).subscribe(() => {
|
||||
this.activeModal.close('deleted');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<div class="d-flex justify-content-center">
|
||||
<div class="col-8">
|
||||
@if (user()) {
|
||||
<div>
|
||||
<h2>
|
||||
<span jhiTranslate="userManagement.detail.title">Utilizador</span> [<strong>{{ user()!.login }}</strong
|
||||
>]
|
||||
</h2>
|
||||
|
||||
<dl class="row-md jh-entity-details">
|
||||
<dt><span jhiTranslate="userManagement.login">Login</span></dt>
|
||||
<dd>
|
||||
<span>{{ user()!.login }}</span>
|
||||
@if (user()!.activated) {
|
||||
<span class="badge bg-success" jhiTranslate="userManagement.activated">Ativo</span>
|
||||
} @else {
|
||||
<span class="badge bg-danger" jhiTranslate="userManagement.deactivated">Inativo</span>
|
||||
}
|
||||
</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.firstName">Primeiro nome</span></dt>
|
||||
<dd>{{ user()!.firstName }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.lastName">Último nome</span></dt>
|
||||
<dd>{{ user()!.lastName }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.email">Email</span></dt>
|
||||
<dd>{{ user()!.email }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.langKey">Linguagem</span></dt>
|
||||
<dd>{{ user()!.langKey }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.createdBy">Criado por</span></dt>
|
||||
<dd>{{ user()!.createdBy }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.createdDate">Criado em</span></dt>
|
||||
<dd>{{ user()!.createdDate | date: 'dd/MM/yy HH:mm' }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.lastModifiedBy">Alterado por</span></dt>
|
||||
<dd>{{ user()!.lastModifiedBy }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.lastModifiedDate">Alterado em</span></dt>
|
||||
<dd>{{ user()!.lastModifiedDate | date: 'dd/MM/yy HH:mm' }}</dd>
|
||||
|
||||
<dt><span jhiTranslate="userManagement.profiles">Perfis</span></dt>
|
||||
<dd>
|
||||
<ul class="list-unstyled">
|
||||
@for (authority of user()!.authorities; track $index) {
|
||||
<li>
|
||||
<span class="badge bg-info">{{ authority }}</span>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<button type="submit" routerLink="../../" class="btn btn-info">
|
||||
<fa-icon icon="arrow-left"></fa-icon> <span jhiTranslate="entity.action.back">Voltar</span>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,66 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||
import { RouterTestingHarness } from '@angular/router/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { Authority } from 'app/config/authority.constants';
|
||||
|
||||
import UserManagementDetailComponent from './user-management-detail.component';
|
||||
|
||||
describe('User Management Detail Component', () => {
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [UserManagementDetailComponent],
|
||||
providers: [
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: '**',
|
||||
component: UserManagementDetailComponent,
|
||||
resolve: {
|
||||
user: () =>
|
||||
of({
|
||||
id: 123,
|
||||
login: 'user',
|
||||
firstName: 'first',
|
||||
lastName: 'last',
|
||||
email: 'first@last.com',
|
||||
activated: true,
|
||||
langKey: 'en',
|
||||
authorities: [Authority.USER],
|
||||
createdBy: 'admin',
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
],
|
||||
})
|
||||
.overrideTemplate(UserManagementDetailComponent, '')
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
describe('Construct', () => {
|
||||
it('Should call load all on construct', async () => {
|
||||
// WHEN
|
||||
const harness = await RouterTestingHarness.create();
|
||||
const instance = await harness.navigateByUrl('/', UserManagementDetailComponent);
|
||||
|
||||
// THEN
|
||||
expect(instance.user()).toEqual(
|
||||
expect.objectContaining({
|
||||
id: 123,
|
||||
login: 'user',
|
||||
firstName: 'first',
|
||||
lastName: 'last',
|
||||
email: 'first@last.com',
|
||||
activated: true,
|
||||
langKey: 'en',
|
||||
authorities: [Authority.USER],
|
||||
createdBy: 'admin',
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
import { Component, input } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
|
||||
import { User } from '../user-management.model';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-user-mgmt-detail',
|
||||
templateUrl: './user-management-detail.component.html',
|
||||
imports: [RouterModule, SharedModule],
|
||||
})
|
||||
export default class UserManagementDetailComponent {
|
||||
user = input<User | null>(null);
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
<div>
|
||||
<h2>
|
||||
<span id="user-management-page-heading" data-cy="userManagementPageHeading" jhiTranslate="userManagement.home.title">Utilizadores</span>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-info me-2" (click)="loadAll()" [disabled]="isLoading()">
|
||||
<fa-icon icon="sync" [spin]="isLoading()"></fa-icon>
|
||||
<span jhiTranslate="userManagement.home.refreshListLabel">Refresh list</span>
|
||||
</button>
|
||||
<button class="btn btn-primary jh-create-entity" [routerLink]="['./new']">
|
||||
<fa-icon icon="plus"></fa-icon> <span jhiTranslate="userManagement.home.createLabel">Adicionar</span>
|
||||
</button>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<jhi-alert-error></jhi-alert-error>
|
||||
|
||||
<jhi-alert></jhi-alert>
|
||||
|
||||
@if (users()) {
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped" aria-describedby="user-management-page-heading">
|
||||
<thead>
|
||||
<tr jhiSort [sortState]="sortState" (sortChange)="transition($event)">
|
||||
<th scope="col" jhiSortBy="id"><span jhiTranslate="global.field.id">Código</span> <fa-icon icon="sort"></fa-icon></th>
|
||||
<th scope="col" jhiSortBy="login"><span jhiTranslate="userManagement.login">Login</span> <fa-icon icon="sort"></fa-icon></th>
|
||||
<th scope="col" jhiSortBy="email"><span jhiTranslate="userManagement.email">Email</span> <fa-icon icon="sort"></fa-icon></th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col" jhiSortBy="langKey">
|
||||
<span jhiTranslate="userManagement.langKey">Linguagem</span> <fa-icon icon="sort"></fa-icon>
|
||||
</th>
|
||||
<th scope="col"><span jhiTranslate="userManagement.profiles">Perfis</span></th>
|
||||
<th scope="col" jhiSortBy="createdDate">
|
||||
<span jhiTranslate="userManagement.createdDate">Criado em</span> <fa-icon icon="sort"></fa-icon>
|
||||
</th>
|
||||
<th scope="col" jhiSortBy="lastModifiedBy">
|
||||
<span jhiTranslate="userManagement.lastModifiedBy">Alterado por</span> <fa-icon icon="sort"></fa-icon>
|
||||
</th>
|
||||
<th scope="col" jhiSortBy="lastModifiedDate">
|
||||
<span jhiTranslate="userManagement.lastModifiedDate">Alterado em</span> <fa-icon icon="sort"></fa-icon>
|
||||
</th>
|
||||
<th scope="col"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (user of users(); track trackIdentity) {
|
||||
<tr>
|
||||
<td>
|
||||
<a [routerLink]="['./', user.login, 'view']">{{ user.id }}</a>
|
||||
</td>
|
||||
<td>{{ user.login }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
@if (!user.activated) {
|
||||
<button class="btn btn-danger btn-sm" (click)="setActive(user, true)" jhiTranslate="userManagement.deactivated">
|
||||
Inativo
|
||||
</button>
|
||||
} @else {
|
||||
<button
|
||||
class="btn btn-success btn-sm"
|
||||
(click)="setActive(user, false)"
|
||||
[disabled]="!currentAccount() || currentAccount()?.login === user.login"
|
||||
jhiTranslate="userManagement.activated"
|
||||
>
|
||||
Ativo
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
<td>{{ user.langKey }}</td>
|
||||
<td>
|
||||
@for (authority of user.authorities; track $index) {
|
||||
<div>
|
||||
<span class="badge bg-info">{{ authority }}</span>
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
<td>{{ user.createdDate | date: 'dd/MM/yy HH:mm' }}</td>
|
||||
<td>{{ user.lastModifiedBy }}</td>
|
||||
<td>{{ user.lastModifiedDate | date: 'dd/MM/yy HH:mm' }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group">
|
||||
<button type="submit" [routerLink]="['./', user.login, 'view']" class="btn btn-info btn-sm">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
<span class="d-none d-md-inline" jhiTranslate="entity.action.view">Visualizar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
[routerLink]="['./', user.login, 'edit']"
|
||||
queryParamsHandling="merge"
|
||||
class="btn btn-primary btn-sm"
|
||||
>
|
||||
<fa-icon icon="pencil-alt"></fa-icon>
|
||||
<span class="d-none d-md-inline" jhiTranslate="entity.action.edit">Editar</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
(click)="deleteUser(user)"
|
||||
class="btn btn-danger btn-sm"
|
||||
[disabled]="!currentAccount() || currentAccount()?.login === user.login"
|
||||
>
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
<span class="d-none d-md-inline" jhiTranslate="entity.action.delete">Eliminar</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<jhi-item-count [params]="{ page: page, totalItems: totalItems(), itemsPerPage: itemsPerPage }"></jhi-item-count>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center">
|
||||
<ngb-pagination
|
||||
[collectionSize]="totalItems()"
|
||||
[(page)]="page"
|
||||
[pageSize]="itemsPerPage"
|
||||
[maxSize]="5"
|
||||
[rotate]="true"
|
||||
[boundaryLinks]="true"
|
||||
(pageChange)="transition()"
|
||||
></ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
|
@ -0,0 +1,102 @@
|
|||
jest.mock('app/core/auth/account.service');
|
||||
|
||||
import { ComponentFixture, TestBed, waitForAsync, inject, fakeAsync, tick } from '@angular/core/testing';
|
||||
import { HttpHeaders, HttpResponse } from '@angular/common/http';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { UserManagementService } from '../service/user-management.service';
|
||||
import { User } from '../user-management.model';
|
||||
|
||||
import UserManagementComponent from './user-management.component';
|
||||
|
||||
describe('User Management Component', () => {
|
||||
let comp: UserManagementComponent;
|
||||
let fixture: ComponentFixture<UserManagementComponent>;
|
||||
let service: UserManagementService;
|
||||
let mockAccountService: AccountService;
|
||||
const data = of({
|
||||
defaultSort: 'id,asc',
|
||||
});
|
||||
const queryParamMap = of(
|
||||
jest.requireActual('@angular/router').convertToParamMap({
|
||||
page: '1',
|
||||
size: '1',
|
||||
sort: 'id,desc',
|
||||
}),
|
||||
);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule, UserManagementComponent],
|
||||
providers: [{ provide: ActivatedRoute, useValue: { data, queryParamMap } }, AccountService],
|
||||
})
|
||||
.overrideTemplate(UserManagementComponent, '')
|
||||
.compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(UserManagementComponent);
|
||||
comp = fixture.componentInstance;
|
||||
service = TestBed.inject(UserManagementService);
|
||||
mockAccountService = TestBed.inject(AccountService);
|
||||
mockAccountService.identity = jest.fn(() => of(null));
|
||||
});
|
||||
|
||||
describe('OnInit', () => {
|
||||
it('Should call load all on init', inject(
|
||||
[],
|
||||
fakeAsync(() => {
|
||||
// GIVEN
|
||||
const headers = new HttpHeaders().append('link', 'link;link');
|
||||
jest.spyOn(service, 'query').mockReturnValue(
|
||||
of(
|
||||
new HttpResponse({
|
||||
body: [new User(123)],
|
||||
headers,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// WHEN
|
||||
comp.ngOnInit();
|
||||
tick(); // simulate async
|
||||
|
||||
// THEN
|
||||
expect(service.query).toHaveBeenCalled();
|
||||
expect(comp.users()?.[0]).toEqual(expect.objectContaining({ id: 123 }));
|
||||
}),
|
||||
));
|
||||
});
|
||||
|
||||
describe('setActive', () => {
|
||||
it('Should update user and call load all', inject(
|
||||
[],
|
||||
fakeAsync(() => {
|
||||
// GIVEN
|
||||
const headers = new HttpHeaders().append('link', 'link;link');
|
||||
const user = new User(123);
|
||||
jest.spyOn(service, 'query').mockReturnValue(
|
||||
of(
|
||||
new HttpResponse({
|
||||
body: [user],
|
||||
headers,
|
||||
}),
|
||||
),
|
||||
);
|
||||
jest.spyOn(service, 'update').mockReturnValue(of(user));
|
||||
|
||||
// WHEN
|
||||
comp.setActive(user, true);
|
||||
tick(); // simulate async
|
||||
|
||||
// THEN
|
||||
expect(service.update).toHaveBeenCalledWith({ ...user, activated: true });
|
||||
expect(service.query).toHaveBeenCalled();
|
||||
expect(comp.users()?.[0]).toEqual(expect.objectContaining({ id: 123 }));
|
||||
}),
|
||||
));
|
||||
});
|
||||
});
|
|
@ -0,0 +1,101 @@
|
|||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { RouterModule, ActivatedRoute, Router } from '@angular/router';
|
||||
import { HttpResponse, HttpHeaders } from '@angular/common/http';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import SharedModule from 'app/shared/shared.module';
|
||||
import { SortDirective, SortByDirective, sortStateSignal, SortService, SortState } from 'app/shared/sort';
|
||||
import { ITEMS_PER_PAGE } from 'app/config/pagination.constants';
|
||||
import { SORT } from 'app/config/navigation.constants';
|
||||
import { ItemCountComponent } from 'app/shared/pagination';
|
||||
import { AccountService } from 'app/core/auth/account.service';
|
||||
import { UserManagementService } from '../service/user-management.service';
|
||||
import { User } from '../user-management.model';
|
||||
import UserManagementDeleteDialogComponent from '../delete/user-management-delete-dialog.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'jhi-user-mgmt',
|
||||
templateUrl: './user-management.component.html',
|
||||
imports: [RouterModule, SharedModule, UserManagementDeleteDialogComponent, SortDirective, SortByDirective, ItemCountComponent],
|
||||
})
|
||||
export default class UserManagementComponent implements OnInit {
|
||||
currentAccount = inject(AccountService).trackCurrentAccount();
|
||||
users = signal<User[] | null>(null);
|
||||
isLoading = signal(false);
|
||||
totalItems = signal(0);
|
||||
itemsPerPage = ITEMS_PER_PAGE;
|
||||
page!: number;
|
||||
sortState = sortStateSignal({});
|
||||
|
||||
private userService = inject(UserManagementService);
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private sortService = inject(SortService);
|
||||
private modalService = inject(NgbModal);
|
||||
|
||||
ngOnInit(): void {
|
||||
this.handleNavigation();
|
||||
}
|
||||
|
||||
setActive(user: User, isActivated: boolean): void {
|
||||
this.userService.update({ ...user, activated: isActivated }).subscribe(() => this.loadAll());
|
||||
}
|
||||
|
||||
trackIdentity(_index: number, item: User): number {
|
||||
return item.id!;
|
||||
}
|
||||
|
||||
deleteUser(user: User): void {
|
||||
const modalRef = this.modalService.open(UserManagementDeleteDialogComponent, { size: 'lg', backdrop: 'static' });
|
||||
modalRef.componentInstance.user = user;
|
||||
// unsubscribe not needed because closed completes on modal close
|
||||
modalRef.closed.subscribe(reason => {
|
||||
if (reason === 'deleted') {
|
||||
this.loadAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadAll(): void {
|
||||
this.isLoading.set(true);
|
||||
this.userService
|
||||
.query({
|
||||
page: this.page - 1,
|
||||
size: this.itemsPerPage,
|
||||
sort: this.sortService.buildSortParam(this.sortState(), 'id'),
|
||||
})
|
||||
.subscribe({
|
||||
next: (res: HttpResponse<User[]>) => {
|
||||
this.isLoading.set(false);
|
||||
this.onSuccess(res.body, res.headers);
|
||||
},
|
||||
error: () => this.isLoading.set(false),
|
||||
});
|
||||
}
|
||||
|
||||
transition(sortState?: SortState): void {
|
||||
this.router.navigate(['./'], {
|
||||
relativeTo: this.activatedRoute.parent,
|
||||
queryParams: {
|
||||
page: this.page,
|
||||
sort: this.sortService.buildSortParam(sortState ?? this.sortState()),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private handleNavigation(): void {
|
||||
combineLatest([this.activatedRoute.data, this.activatedRoute.queryParamMap]).subscribe(([data, params]) => {
|
||||
const page = params.get('page');
|
||||
this.page = +(page ?? 1);
|
||||
this.sortState.set(this.sortService.parseSortParam(params.get(SORT) ?? data['defaultSort']));
|
||||
this.loadAll();
|
||||
});
|
||||
}
|
||||
|
||||
private onSuccess(users: User[] | null, headers: HttpHeaders): void {
|
||||
this.totalItems.set(Number(headers.get('X-Total-Count')));
|
||||
this.users.set(users);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
|
||||
import { Authority } from 'app/config/authority.constants';
|
||||
import { User } from '../user-management.model';
|
||||
|
||||
import { UserManagementService } from './user-management.service';
|
||||
|
||||
describe('User Service', () => {
|
||||
let service: UserManagementService;
|
||||
let httpMock: HttpTestingController;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [HttpClientTestingModule],
|
||||
});
|
||||
|
||||
service = TestBed.inject(UserManagementService);
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
describe('Service methods', () => {
|
||||
it('should return User', () => {
|
||||
let expectedResult: string | undefined;
|
||||
|
||||
service.find('user').subscribe(received => {
|
||||
expectedResult = received.login;
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
req.flush(new User(123, 'user'));
|
||||
expect(expectedResult).toEqual('user');
|
||||
});
|
||||
|
||||
it('should return Authorities', () => {
|
||||
let expectedResult: string[] = [];
|
||||
|
||||
service.authorities().subscribe(authorities => {
|
||||
expectedResult = authorities;
|
||||
});
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
|
||||
req.flush([{ name: Authority.USER }, { name: Authority.ADMIN }]);
|
||||
expect(expectedResult).toEqual([Authority.USER, Authority.ADMIN]);
|
||||
});
|
||||
|
||||
it('should propagate not found response', () => {
|
||||
let expectedResult = 0;
|
||||
|
||||
service.find('user').subscribe({
|
||||
error: (error: HttpErrorResponse) => (expectedResult = error.status),
|
||||
});
|
||||
|
||||
const req = httpMock.expectOne({ method: 'GET' });
|
||||
req.flush('Invalid request parameters', {
|
||||
status: 404,
|
||||
statusText: 'Bad Request',
|
||||
});
|
||||
expect(expectedResult).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue