Initial Project Commit

This commit is contained in:
Orlando M Guerreiro 2025-05-22 19:23:40 +01:00
commit a6dea9c888
2148 changed files with 173870 additions and 0 deletions

View 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;

View 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>

View file

@ -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);
}),
));
});

View 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),
});
}
}

View 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;

View file

@ -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);
});
});
});

View 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),
});
}
}

View file

@ -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>

View file

@ -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);
}),
));
});

View file

@ -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),
});
}
}
}

View file

@ -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;

View file

@ -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 });
});
});
});

View 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 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 });
}
}

View file

@ -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>

View file

@ -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);
}));
});

View file

@ -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));
}
}

View file

@ -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;

View file

@ -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);
});
});
});

View 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 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);
}
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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]);
});
});
});

View file

@ -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');
}
}
}
}
}

View 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>

View 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);
});
});

View 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),
});
}
}
}

View 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;

View file

@ -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 });
});
});
});

View 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 });
}
}

View 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=&quot;admin&quot; e palavra-passe=&quot;admin&quot;) <br />-
Utilizador (utilizador=&quot;user&quot; e palavra-passe=&quot;user&quot;).</span
>
</div>
</div>
</div>
</div>

View 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);
}),
));
});

View 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);
}
}
}

View file

@ -0,0 +1,8 @@
export class Registration {
constructor(
public login: string,
public email: string,
public password: string,
public langKey: string,
) {}
}

View 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;

View 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 { 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 });
});
});
});

View 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);
}
}

View file

@ -0,0 +1,8 @@
export class Session {
constructor(
public series: string,
public tokenDate: Date,
public ipAddress: string,
public userAgent: string,
) {}
}

View 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>

View 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);
}),
));
});

View 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),
);
}
}

View 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;

View 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}`);
}
}

View 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>

View file

@ -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);
});
});

View 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);
}
});
}
}

View 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;

View 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;

View file

@ -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>
}

View file

@ -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);
});
});
});

View file

@ -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));
}
}

View file

@ -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;
}

View file

@ -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);
});
});
});

View file

@ -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));
}
}

View 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>

View file

@ -0,0 +1,6 @@
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
iframe {
background: white;
}

View 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 {}

View 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>

View 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);
});
});
});

View 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;
}
}

View 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 };
}

View 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);
});
});
});

View 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'));
}
}

View file

@ -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">&nbsp;</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>

View file

@ -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();
});
});
});

View file

@ -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();
}
}

View file

@ -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>
}

View file

@ -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>();
}

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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>
}

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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>();
}

View file

@ -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>

View file

@ -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>();
}

View file

@ -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()">&nbsp;</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&nbsp;<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&nbsp;<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&nbsp;<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&nbsp;<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&nbsp;<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>
&nbsp;{{ 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>

View file

@ -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();
});
});
});

View file

@ -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();
}
}

View file

@ -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>
}

View file

@ -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);
}

View file

@ -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>
}

View file

@ -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;
}
}

View 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>

View 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();
});
});
});

View 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]) !== '{}');
}
}

View 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',
}

View 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);
});
});
});

View 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'));
}
}

View file

@ -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>&nbsp;<span jhiTranslate="entity.action.cancel">Cancelar</span>
</button>
<button type="submit" class="btn btn-danger">
<fa-icon icon="times"></fa-icon>&nbsp;<span jhiTranslate="entity.action.delete">Eliminar</span>
</button>
</div>
</form>
}

View file

@ -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');
}),
));
});
});

View file

@ -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');
});
}
}

View file

@ -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>&nbsp;<span jhiTranslate="entity.action.back">Voltar</span>
</button>
</div>
}
</div>
</div>

View file

@ -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',
}),
);
});
});
});

View file

@ -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);
}

View file

@ -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>

View file

@ -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 }));
}),
));
});
});

View file

@ -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);
}
}

View file

@ -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