diff --git a/src/main/webapp/app/admin/logs/log.model.ts b/src/main/webapp/app/admin/logs/log.model.ts new file mode 100644 index 0000000..2606a88 --- /dev/null +++ b/src/main/webapp/app/admin/logs/log.model.ts @@ -0,0 +1,18 @@ +export type Level = 'TRACE' | 'DEBUG' | 'INFO' | 'WARN' | 'ERROR' | 'OFF'; + +export interface Logger { + configuredLevel: Level | null; + effectiveLevel: Level; +} + +export interface LoggersResponse { + levels: Level[]; + loggers: { [key: string]: Logger }; +} + +export class Log { + constructor( + public name: string, + public level: Level, + ) {} +} diff --git a/src/main/webapp/app/admin/logs/logs.component.html b/src/main/webapp/app/admin/logs/logs.component.html new file mode 100644 index 0000000..239479f --- /dev/null +++ b/src/main/webapp/app/admin/logs/logs.component.html @@ -0,0 +1,82 @@ +@defer (when loggers() && !isLoading()) { +
+

Logs

+ +

Existem {{ loggers()?.length }} loggers.

+ + Filtro + + + + + + + + + + + + @for (logger of filteredAndOrderedLoggers(); track $index) { + + + + + } + +
Nome Nível
+ {{ logger.name | slice: 0 : 140 }} + + + + + + + + + + + + +
+
+} @loading { +
+
+
+} diff --git a/src/main/webapp/app/admin/logs/logs.component.spec.ts b/src/main/webapp/app/admin/logs/logs.component.spec.ts new file mode 100644 index 0000000..78062ea --- /dev/null +++ b/src/main/webapp/app/admin/logs/logs.component.spec.ts @@ -0,0 +1,82 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; + +import LogsComponent from './logs.component'; +import { LogsService } from './logs.service'; +import { Log, LoggersResponse } from './log.model'; + +describe('LogsComponent', () => { + let comp: LogsComponent; + let fixture: ComponentFixture; + let service: LogsService; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, LogsComponent], + providers: [LogsService], + }) + .overrideTemplate(LogsComponent, '') + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LogsComponent); + comp = fixture.componentInstance; + service = TestBed.inject(LogsService); + }); + + describe('OnInit', () => { + it('should set all default values correctly', () => { + expect(comp.filter()).toBe(''); + expect(comp.sortState().predicate).toBe('name'); + expect(comp.sortState().order).toBe('asc'); + }); + + it('Should call load all on init', () => { + // GIVEN + const log = new Log('main', 'WARN'); + jest.spyOn(service, 'findAll').mockReturnValue( + of({ + loggers: { + main: { + effectiveLevel: 'WARN', + }, + }, + } as unknown as LoggersResponse), + ); + + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.findAll).toHaveBeenCalled(); + expect(comp.loggers()?.[0]).toEqual(expect.objectContaining(log)); + }); + }); + + describe('change log level', () => { + it('should change log level correctly', () => { + // GIVEN + const log = new Log('main', 'ERROR'); + jest.spyOn(service, 'changeLevel').mockReturnValue(of({})); + jest.spyOn(service, 'findAll').mockReturnValue( + of({ + loggers: { + main: { + effectiveLevel: 'ERROR', + }, + }, + } as unknown as LoggersResponse), + ); + + // WHEN + comp.changeLevel('main', 'ERROR'); + + // THEN + expect(service.changeLevel).toHaveBeenCalled(); + expect(service.findAll).toHaveBeenCalled(); + expect(comp.loggers()?.[0]).toEqual(expect.objectContaining(log)); + }); + }); +}); diff --git a/src/main/webapp/app/admin/logs/logs.component.ts b/src/main/webapp/app/admin/logs/logs.component.ts new file mode 100644 index 0000000..ea990a8 --- /dev/null +++ b/src/main/webapp/app/admin/logs/logs.component.ts @@ -0,0 +1,61 @@ +import { Component, computed, inject, OnInit, signal } from '@angular/core'; +import { finalize } from 'rxjs/operators'; + +import SharedModule from 'app/shared/shared.module'; +import { FormsModule } from '@angular/forms'; +import { SortDirective, SortByDirective, sortStateSignal, SortService } from 'app/shared/sort'; +import { Log, LoggersResponse, Level } from './log.model'; +import { LogsService } from './logs.service'; + +@Component({ + standalone: true, + selector: 'jhi-logs', + templateUrl: './logs.component.html', + imports: [SharedModule, FormsModule, SortDirective, SortByDirective], +}) +export default class LogsComponent implements OnInit { + loggers = signal(undefined); + isLoading = signal(false); + filter = signal(''); + sortState = sortStateSignal({ predicate: 'name', order: 'asc' }); + filteredAndOrderedLoggers = computed(() => { + let data = this.loggers() ?? []; + const filter = this.filter(); + if (filter) { + data = data.filter(logger => logger.name.toLowerCase().includes(filter.toLowerCase())); + } + + const { order, predicate } = this.sortState(); + if (order && predicate) { + data = data.sort(this.sortService.startSort({ order, predicate }, { predicate: 'name', order: 'asc' })); + } + return data; + }); + + private logsService = inject(LogsService); + private sortService = inject(SortService); + + ngOnInit(): void { + this.findAndExtractLoggers(); + } + + changeLevel(name: string, level: Level): void { + this.logsService.changeLevel(name, level).subscribe(() => this.findAndExtractLoggers()); + } + + private findAndExtractLoggers(): void { + this.isLoading.set(true); + this.logsService + .findAll() + .pipe( + finalize(() => { + this.isLoading.set(false); + }), + ) + .subscribe({ + next: (response: LoggersResponse) => + this.loggers.set(Object.entries(response.loggers).map(([key, logger]) => new Log(key, logger.effectiveLevel))), + error: () => this.loggers.set([]), + }); + } +} diff --git a/src/main/webapp/app/admin/logs/logs.service.spec.ts b/src/main/webapp/app/admin/logs/logs.service.spec.ts new file mode 100644 index 0000000..cebee2c --- /dev/null +++ b/src/main/webapp/app/admin/logs/logs.service.spec.ts @@ -0,0 +1,31 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { LogsService } from './logs.service'; + +describe('Logs Service', () => { + let service: LogsService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(LogsService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + }); + + describe('Service methods', () => { + it('should change log level', () => { + service.changeLevel('main', 'ERROR').subscribe(); + + const req = httpMock.expectOne({ method: 'POST' }); + expect(req.request.body).toEqual({ configuredLevel: 'ERROR' }); + }); + }); +}); diff --git a/src/main/webapp/app/admin/logs/logs.service.ts b/src/main/webapp/app/admin/logs/logs.service.ts new file mode 100644 index 0000000..79781e9 --- /dev/null +++ b/src/main/webapp/app/admin/logs/logs.service.ts @@ -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 { LoggersResponse, Level } from './log.model'; + +@Injectable({ providedIn: 'root' }) +export class LogsService { + private http = inject(HttpClient); + private applicationConfigService = inject(ApplicationConfigService); + + changeLevel(name: string, configuredLevel: Level): Observable<{}> { + return this.http.post(this.applicationConfigService.getEndpointFor(`management/loggers/${name}`), { configuredLevel }); + } + + findAll(): Observable { + return this.http.get(this.applicationConfigService.getEndpointFor('management/loggers')); + } +} diff --git a/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.css b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.css new file mode 100644 index 0000000..9f71dc3 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.css @@ -0,0 +1,4 @@ +:host .modal-body { + overflow-y: auto; + max-height: 500px; +} \ No newline at end of file diff --git a/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.html b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.html new file mode 100644 index 0000000..9a078e5 --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.html @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file diff --git a/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.spec.ts b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.spec.ts new file mode 100644 index 0000000..5cef11a --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.spec.ts @@ -0,0 +1,64 @@ +jest.mock('@ng-bootstrap/ng-bootstrap'); + +import { ComponentFixture, TestBed, inject, fakeAsync, tick } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { of } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +import { InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +import { InputDataUploadLogLogsDialogComponent } from './input-data-upload-log-logs-dialog.component'; + +describe('InputDataUploadLog Management Logs Component', () => { + let comp: InputDataUploadLogLogsDialogComponent; + let fixture: ComponentFixture; + let service: InputDataUploadLogService; + let mockActiveModal: NgbActiveModal; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, InputDataUploadLoglogsDialogComponent], + providers: [NgbActiveModal], + }) + .overrideTemplate(InputDataUploadLogLogsDialogComponent, '') + .compileComponents(); + fixture = TestBed.createComponent(InputDataUploadLogLogsDialogComponent); + comp = fixture.componentInstance; + service = TestBed.inject(InputDataUploadLogService); + mockActiveModal = TestBed.inject(NgbActiveModal); + }); + + /* + describe('confirmDelete', () => { + it('Should call delete service on confirmDelete', inject( + [], + fakeAsync(() => { + // GIVEN + jest.spyOn(service, 'delete').mockReturnValue(of(new HttpResponse({ body: {} }))); + + // WHEN + comp.confirmDelete(123); + tick(); + + // THEN + expect(service.delete).toHaveBeenCalledWith(123); + expect(mockActiveModal.close).toHaveBeenCalledWith('deleted'); + }), + )); + + it('Should not call delete service on clear', () => { + // GIVEN + jest.spyOn(service, 'delete'); + + // WHEN + comp.cancel(); + + // THEN + expect(service.delete).not.toHaveBeenCalled(); + expect(mockActiveModal.close).not.toHaveBeenCalled(); + expect(mockActiveModal.dismiss).toHaveBeenCalled(); + }); + }); + */ +}); diff --git a/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.ts b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.ts new file mode 100644 index 0000000..f1f6d4e --- /dev/null +++ b/src/main/webapp/app/entities/input-data-upload-log/logs/input-data-upload-log-logs-dialog.component.ts @@ -0,0 +1,83 @@ +import { Component, inject, OnInit } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { sortStateSignal, SortDirective, SortByDirective, type SortState, SortService } from 'app/shared/sort'; +import { combineLatest, filter, Observable, Subscription, tap } from 'rxjs'; + +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { ITEM_DELETED_EVENT } from 'app/config/navigation.constants'; +import { IInputDataUpload } from 'app/entities/input-data-upload/input-data-upload.model'; +import { IInputDataUploadLog } from '../input-data-upload-log.model'; +import { EntityArrayResponseType, InputDataUploadLogService } from '../service/input-data-upload-log.service'; + +@Component({ + standalone: true, + templateUrl: './input-data-upload-log-logs-dialog.component.html', + styleUrl: './input-data-upload-log-logs-dialog.component.css', + imports: [ + SharedModule, + FormsModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + ], +}) +export class InputDataUploadLogLogsDialogComponent implements OnInit { + inputDataUpload?: IInputDataUpload; + inputDataUploadLogs?: IInputDataUploadLog[]; + isLoading = false; + + sortState = sortStateSignal({}); + + protected sortService = inject(SortService); + protected inputDataUploadLogService = inject(InputDataUploadLogService); + protected activeModal = inject(NgbActiveModal); + + trackId = (_index: number, item: IInputDataUploadLog): number => this.inputDataUploadLogService.getEntityIdentifier(item); + + setInputDataUpload(inputDataUpload: IInputDataUpload) { + this.inputDataUpload = inputDataUpload; + } + + ngOnInit(): void { + if (this.inputDataUpload) { + this.load(); + } + } + + cancel(): void { + this.activeModal.dismiss(); + } + + load(): void { + this.queryBackend().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.inputDataUploadLogs = this.refineData(dataFromBody); + } + + protected refineData(data: IInputDataUploadLog[]): IInputDataUploadLog[] { + const { predicate, order } = this.sortState(); + return predicate && order ? data.sort(this.sortService.startSort({ predicate, order })) : data; + } + + protected fillComponentAttributesFromResponseBody(data: IInputDataUploadLog[] | null): IInputDataUploadLog[] { + return data ?? []; + } + + protected queryBackend(): Observable { + this.isLoading = true; + + return this.inputDataUploadLogService.findByOwner(this.inputDataUpload?.id ?? 0).pipe(tap(() => (this.isLoading = false))); + + } +}