Implement ClientTaskLogs component with enhanced UI and functionality
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/22/head
Lucas Lara García 2025-05-08 13:59:24 +02:00
parent 1ba62b9283
commit 60684d2c50
3 changed files with 518 additions and 9 deletions

View File

@ -0,0 +1,124 @@
.modal-content {
max-height: 85vh;
overflow-y: auto;
padding: 1rem;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
}
.header-container-title {
flex-grow: 1;
text-align: left;
margin-left: 1em;
}
.calendar-button-row {
display: flex;
gap: 15px;
}
.lists-container {
padding: 16px;
}
.imagesLists-container {
flex: 1;
}
.card.unidad-card {
height: 100%;
box-sizing: border-box;
}
table {
width: 100%;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1.5rem 0rem 1.5rem 0rem;
box-sizing: border-box;
}
.search-string {
flex: 1;
padding: 5px;
}
.search-boolean {
flex: 1;
padding: 5px;
}
.search-select {
flex: 2;
padding: 5px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}
.chip-failed {
background-color: #e87979 !important;
color: white;
}
.chip-success {
background-color: #46c446 !important;
color: white;
}
.chip-pending {
background-color: #bebdbd !important;
color: black;
}
.chip-in-progress {
background-color: #f5a623 !important;
color: white;
}
.status-progress-flex {
display: flex;
align-items: center;
gap: 8px;
}
button.cancel-button {
display: flex;
align-items: center;
justify-content: center;
padding: 5px;
}
.cancel-button {
color: red;
background-color: transparent;
border: none;
padding: 0;
}
.cancel-button mat-icon {
color: red;
}

View File

@ -1 +1,161 @@
<p>client-task-logs works!</p>
<div class="modal-content">
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandsTitle' |
translate }}</h2>
</div>
<div class="images-button-row">
<button class="action-button" (click)="resetFilters()" joyrideStep="resetFiltersStep"
text="{{ 'resetFiltersStepText' | translate }}">
{{ 'resetFilters' | translate }}
</button>
</div>
</div>
<div class="search-container">
<mat-form-field appearance="fill" class="search-select">
<mat-label>{{ 'commandSelectStepText' | translate }}</mat-label>
<mat-select (selectionChange)="onOptionCommandSelected($event.value)" #commandSearchInput>
<mat-option *ngFor="let command of filteredCommands2" [value]="command">
{{ translateCommand(command.name) }}
</mat-option>
</mat-select>
<button *ngIf="commandSearchInput.value" mat-icon-button matSuffix aria-label="Clear input search"
(click)="clearCommandFilter($event, commandSearchInput)">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
<mat-form-field appearance="fill" class="search-boolean">
<mat-label i18n="@@searchLabel">Estado</mat-label>
<mat-select (selectionChange)="onOptionStatusSelected($event.value)" placeholder="Seleccionar opción"
#commandStatusInput>
<mat-option [value]="'failed'">Fallido</mat-option>
<mat-option [value]="'pending'">Pendiente de ejecutar</mat-option>
<mat-option [value]="'in-progress'">Ejecutando</mat-option>
<mat-option [value]="'success'">Completado con éxito</mat-option>
<mat-option [value]="'cancelled'">Cancelado</mat-option>
</mat-select>
<button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search"
(click)="clearStatusFilter($event, commandStatusInput)">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>
</div>
<app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading">
<table mat-table [dataSource]="traces" class="mat-elevation-z8" joyrideStep="tableStep"
text="{{ 'tableStepText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let trace">
<ng-container [ngSwitch]="column.columnDef">
<ng-container *ngSwitchCase="'status'">
<ng-container *ngIf="trace.status === 'in-progress' && trace.progress; else statusChip">
<div class="progress-container">
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress"
[bufferValue]="bufferValue">
</mat-progress-bar>
<span>{{trace.progress}}%</span>
</div>
</ng-container>
<ng-template #statusChip>
<div class="status-progress-flex">
<mat-chip [ngClass]="{
'chip-failed': trace.status === 'failed',
'chip-success': trace.status === 'success',
'chip-pending': trace.status === 'pending',
'chip-in-progress': trace.status === 'in-progress',
'chip-cancelled': trace.status === 'cancelled'
}">
{{
trace.status === 'failed' ? 'Error' :
trace.status === 'in-progress' ? 'En ejecución' :
trace.status === 'success' ? 'Completado' :
trace.status === 'pending' ? 'Pendiente' :
trace.status === 'cancelled' ? 'Cancelado' :
trace.status
}}
</mat-chip>
<button *ngIf="trace.status === 'in-progress' && trace.command === 'deploy-image'" mat-icon-button
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="Cancelar transmisión de imagen">
<mat-icon>cancel</mat-icon>
</button>
</div>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'command'">
<div style="display: flex; flex-direction: column;">
<span>{{ translateCommand(trace.command) }}</span>
<span style="font-size: 0.75rem; color: gray;">{{ trace.jobId }}</span>
</div>
</ng-container>
<ng-container *ngSwitchCase="'client'">
<div style="display: flex; flex-direction: column;">
<span>{{ trace.client?.name }}</span>
<span style="font-size: 0.75rem; color: gray;">{{ trace.client?.ip }}</span>
</div>
</ng-container>
<ng-container *ngSwitchCase="'executedAt'">
<div style="display: flex; flex-direction: column;">
<span style="font-size: 0.8rem;"> {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
</div>
</ng-container>
<ng-container *ngSwitchCase="'finishedAt'">
<div style="display: flex; flex-direction: column;">
<span style="font-size: 0.8rem;"> {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
</div>
</ng-container>
<ng-container *ngSwitchDefault>
{{ column.cell(trace) }}
</ng-container>
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<td mat-cell *matCellDef="let trace" style="text-align: center;">
<button mat-icon-button color="primary" [disabled]="!trace.input" (click)="openInputModal(trace.input)">
<mat-icon>
<span class="material-symbols-outlined">
mode_comment
</span>
</mat-icon>
</button>
<button mat-icon-button color="primary" [disabled]="!trace.output" (click)="openOutputModal(trace.output)">
<mat-icon>
<span class="material-symbols-outlined">
info
</span>
</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>

View File

@ -1,7 +1,19 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
import { HttpClient, HttpParams } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { DatePipe } from '@angular/common';
import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component';
import { ToastrService } from 'ngx-toastr';
import { TranslationService } from '@services/translation.service';
import { Observable } from 'rxjs';
import { FormControl } from '@angular/forms';
import { OutputDialogComponent } from '../output-dialog/output-dialog.component';
import { InputDialogComponent } from '../input-dialog/input-dialog.component';
import { ProgressBarMode } from '@angular/material/progress-bar';
import { JoyrideService } from 'ngx-joyride';
import { map, startWith } from 'rxjs/operators';
import { COMMAND_TYPES } from 'src/app/shared/constants/command-types';
@Component({
selector: 'app-client-task-logs',
@ -9,24 +21,161 @@ import { ConfigService } from '@services/config.service';
styleUrls: ['./client-task-logs.component.css']
})
export class ClientTaskLogsComponent implements OnInit {
baseUrl: string;
mercureUrl: string;
traces: any[] = [];
loading = false;
groupedTraces: any[] = [];
commands: any[] = [];
length: number = 0;
itemsPerPage: number = 20;
page: number = 0;
length: number = 0;
baseUrl: string;
loading: boolean = true;
pageSizeOptions: number[] = [10, 20, 30, 50];
datePipe: DatePipe = new DatePipe('es-ES');
mode: ProgressBarMode = 'buffer';
progress = 0;
bufferValue = 0;
constructor(
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
name: key,
value: key,
label: COMMAND_TYPES[key]
}));
columns = [
{
columnDef: 'id',
header: 'ID',
cell: (trace: any) => `${trace.id}`,
},
{
columnDef: 'command',
header: 'Comando',
cell: (trace: any) => trace.command
},
{
columnDef: 'status',
header: 'Estado',
cell: (trace: any) => trace.status
},
{
columnDef: 'executedAt',
header: 'Ejecución',
cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'),
},
{
columnDef: 'finishedAt',
header: 'Finalización',
cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'),
},
];
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
filters: { [key: string]: string } = {};
filteredCommands!: Observable<any[]>;
commandControl = new FormControl();
constructor(private http: HttpClient,
@Inject(MAT_DIALOG_DATA) public data: { client: any },
private http: HttpClient,
private joyrideService: JoyrideService,
private dialog: MatDialog,
private cdr: ChangeDetectorRef,
private configService: ConfigService,
private toastService: ToastrService,
private translationService: TranslationService
) {
this.baseUrl = this.configService.apiUrl;
this.mercureUrl = this.configService.mercureUrl;
}
ngOnInit(): void {
this.loadTraces();
this.loadCommands();
this.filteredCommands = this.commandControl.valueChanges.pipe(
startWith(''),
map(value => (typeof value === 'string' ? value : value?.name)),
map(name => (name ? this._filterCommands(name) : this.commands.slice()))
);
const eventSource = new EventSource(`${this.mercureUrl}?topic=`
+ encodeURIComponent(`traces`));
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data && data['@id']) {
this.updateTracesStatus(data['@id'], data.status, data.progress);
}
}
}
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
if (traceIndex !== -1) {
const updatedTraces = [...this.traces];
updatedTraces[traceIndex] = {
...updatedTraces[traceIndex],
status: newStatus,
progress: progress
};
this.traces = updatedTraces;
this.cdr.detectChanges();
console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`);
} else {
console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`);
}
}
private _filterCommands(name: string): any[] {
const filterValue = name.toLowerCase();
return this.commands.filter(command => command.name.toLowerCase().includes(filterValue));
}
onOptionCommandSelected(selectedCommand: any): void {
this.filters['command'] = selectedCommand.name;
this.loadTraces();
}
onOptionStatusSelected(selectedStatus: any): void {
this.filters['status'] = selectedStatus;
this.loadTraces();
}
openInputModal(inputData: any): void {
this.dialog.open(InputDialogComponent, {
width: '70vw',
height: '60vh',
data: { input: inputData }
});
}
openOutputModal(outputData: any): void {
this.dialog.open(OutputDialogComponent, {
width: '500px',
data: { input: outputData }
});
}
cancelTrace(trace: any): void {
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: trace.jobId },
}).afterClosed().subscribe((result) => {
if (result) {
this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
next: () => {
this.toastService.success('Transmision de imagen cancelada');
this.loadTraces();
},
error: (error) => {
this.toastService.error(error.error['hydra:description']);
console.error(error.error['hydra:description']);
}
});
}
});
}
loadTraces(): void {
@ -55,4 +204,80 @@ export class ClientTaskLogsComponent implements OnInit {
);
}
loadCommands() {
this.loading = true;
this.http.get<any>(`${this.baseUrl}/commands?&page=1&itemsPerPage=10000`).subscribe(
response => {
this.commands = response['hydra:member'];
this.loading = false;
},
error => {
console.error('Error fetching commands:', error);
this.loading = false;
}
);
}
resetFilters() {
this.loading = true;
this.filters = {};
this.loadTraces();
}
groupByCommandId(traces: any[]): any[] {
const grouped: { [key: string]: any[] } = {};
traces.forEach(trace => {
const commandId = trace.command.id;
if (!grouped[commandId]) {
grouped[commandId] = [];
}
grouped[commandId].push(trace);
});
return Object.keys(grouped).map(key => ({
commandId: key,
traces: grouped[key]
}));
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.loadTraces();
}
translateCommand(command: string): string {
return this.translationService.getCommandTranslation(command);
}
clearCommandFilter(event: Event, clientSearchCommandInput: any): void {
event.stopPropagation();
delete this.filters['command'];
clientSearchCommandInput.value = null;
this.loadTraces()
}
clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
event.stopPropagation();
delete this.filters['status'];
clientSearchStatusInput.value = null;
this.loadTraces()
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'resetFiltersStep',
'clientSelectStep',
'commandSelectStep',
'tableStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}