import { ChangeDetectorRef, Component, OnInit, OnDestroy } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, forkJoin } from 'rxjs'; import { FormControl } from '@angular/forms'; import { map, startWith } from 'rxjs/operators'; import { DatePipe } from '@angular/common'; import { JoyrideService } from 'ngx-joyride'; import { MatDialog } from "@angular/material/dialog"; import { InputDialogComponent } from "./input-dialog/input-dialog.component"; import { ProgressBarMode } from '@angular/material/progress-bar'; import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component"; import { ToastrService } from "ngx-toastr"; import { ConfigService } from '@services/config.service'; import { OutputDialogComponent } from "./output-dialog/output-dialog.component"; import { TranslationService } from "@services/translation.service"; import { COMMAND_TYPES } from '../../shared/constants/command-types'; @Component({ selector: 'app-task-logs', templateUrl: './task-logs.component.html', styleUrls: ['./task-logs.component.css'] }) export class TaskLogsComponent implements OnInit, OnDestroy { baseUrl: string; mercureUrl: string; traces: any[] = []; groupedTraces: any[] = []; commands: any[] = []; clients: any[] = []; length: number = 0; itemsPerPage: number = 20; page: number = 0; loading: boolean = true; pageSizeOptions: number[] = [10, 20, 30, 50]; datePipe: DatePipe = new DatePipe('es-ES'); mode: ProgressBarMode = 'buffer'; progress = 0; bufferValue = 0; today = new Date(); showAdvancedFilters: boolean = false; selectedTrace: any = null; sortBy: string = 'executedAt'; sortDirection: 'asc' | 'desc' = 'desc'; currentSortColumn: string = 'executedAt'; totalStats: { total: number; success: number; failed: number; pending: number; inProgress: number; cancelled: number; today: number; } = { total: 0, success: 0, failed: 0, pending: 0, inProgress: 0, cancelled: 0, today: 0 }; 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}`, sortable: true }, { columnDef: 'command', header: 'Comando', cell: (trace: any) => trace.command, sortable: true }, { columnDef: 'client', header: 'Cliente', cell: (trace: any) => trace.client?.name, sortable: true }, { columnDef: 'status', header: 'Estado', cell: (trace: any) => trace.status, sortable: true }, { columnDef: 'executedAt', header: 'Ejecución', cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'), sortable: true }, { columnDef: 'finishedAt', header: 'Finalización', cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'), sortable: true }, ]; displayedColumns = [...this.columns.map(column => column.columnDef), 'information']; filters: { [key: string]: string } = {}; filteredClients!: Observable; clientControl = new FormControl(); filteredCommands!: Observable; commandControl = new FormControl(); constructor(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.loadClients(); this.loadTotalStats(); this.filteredCommands = this.commandControl.valueChanges.pipe( startWith(''), map(value => (typeof value === 'string' ? value : value?.name)), map(name => (name ? this._filterCommands(name) : this.commands.slice())) ); this.filteredClients = this.clientControl.valueChanges.pipe( startWith(''), map(value => (typeof value === 'string' ? value : value?.name)), map(name => (name ? this._filterClients(name) : this.clients.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); } } } ngOnDestroy(): void { } toggleFilters(): void { this.showAdvancedFilters = !this.showAdvancedFilters; } refreshData(): void { this.loadTraces(); this.toastService.success('Datos actualizados', 'Éxito'); } selectTrace(trace: any): void { this.selectedTrace = trace; } sortColumn(columnDef: string): void { if (this.currentSortColumn === columnDef) { this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; } else { this.currentSortColumn = columnDef; this.sortDirection = 'desc'; } this.sortBy = columnDef; this.onSortChange(); } getSortIcon(columnDef: string): string { if (this.currentSortColumn !== columnDef) { return 'unfold_more'; } return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more'; } onSortChange(): void { this.loadTraces(); } getStatusCount(status: string): number { switch(status) { case 'success': return this.totalStats.success; case 'failed': return this.totalStats.failed; case 'pending': return this.totalStats.pending; case 'in-progress': return this.totalStats.inProgress; case 'cancelled': return this.totalStats.cancelled; case 'today': return this.totalStats.today; default: return 0; } } getTodayTracesCount(): number { const today = new Date(); const todayString = this.datePipe.transform(today, 'yyyy-MM-dd'); return this.traces.filter(trace => trace.executedAt && trace.executedAt.startsWith(todayString) ).length; } getRelativeTime(date: string): string { if (!date) return ''; const now = new Date(); const traceDate = new Date(date); const diffInSeconds = Math.floor((now.getTime() - traceDate.getTime()) / 1000); if (diffInSeconds < 60) { return 'hace un momento'; } else if (diffInSeconds < 3600) { const minutes = Math.floor(diffInSeconds / 60); return `hace ${minutes} minuto${minutes > 1 ? 's' : ''}`; } else if (diffInSeconds < 86400) { const hours = Math.floor(diffInSeconds / 3600); return `hace ${hours} hora${hours > 1 ? 's' : ''}`; } else { const days = Math.floor(diffInSeconds / 86400); return `hace ${days} día${days > 1 ? 's' : ''}`; } } exportToCSV(): void { const headers = ['ID', 'Comando', 'Cliente', 'Estado', 'Fecha Ejecución', 'Fecha Finalización', 'Job ID']; const csvData = this.traces.map(trace => [ trace.id, this.translateCommand(trace.command), trace.client?.name || '', trace.status, this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'), this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'), trace.jobId || '' ]); const csvContent = [headers, ...csvData] .map(row => row.map(cell => `"${cell}"`).join(',')) .join('\n'); const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); const link = document.createElement('a'); const url = URL.createObjectURL(blob); link.setAttribute('href', url); link.setAttribute('download', `traces_${new Date().toISOString().split('T')[0]}.csv`); link.style.visibility = 'hidden'; document.body.appendChild(link); link.click(); document.body.removeChild(link); this.toastService.success('Archivo CSV exportado correctamente', 'Éxito'); } cancelTrace(trace: any): void { if (trace.status !== 'pending' && trace.status !== 'in-progress') { this.toastService.warning('Solo se pueden cancelar trazas pendientes o en ejecución', 'Advertencia'); return; } const dialogRef = this.dialog.open(DeleteModalComponent, { width: '400px', data: { title: 'Cancelar Traza', message: `¿Estás seguro de que quieres cancelar la traza #${trace.id}?`, confirmText: 'Cancelar', cancelText: 'No cancelar' } }); dialogRef.afterClosed().subscribe(result => { if (result) { if (trace.status === 'in-progress') { this.http.post(`${this.baseUrl}${trace['@id']}/kill-job`, { jobId: trace.jobId }).subscribe( () => { this.toastService.success('Traza cancelada correctamente', 'Éxito'); this.loadTraces(); this.loadTotalStats(); }, (error) => { this.toastService.error('Error al cancelar la traza', 'Error'); } ); } else { this.http.post(`${this.baseUrl}/traces/cancel-multiple`, {traces: [trace['@id']]}).subscribe( () => { this.toastService.success('Traza cancelada correctamente', 'Éxito'); this.loadTraces(); this.loadTotalStats(); }, (error) => { console.error('Error cancelling pending trace:', error); this.toastService.error('Error al cancelar la traza', 'Error'); } ); } } }); } 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(); } } private _filterClients(value: string): any[] { const filterValue = value.toLowerCase(); return this.clients.filter(client => client.name?.toLowerCase().includes(filterValue) || client.ip?.toLowerCase().includes(filterValue) || client.mac?.toLowerCase().includes(filterValue) ); } private _filterCommands(name: string): any[] { const filterValue = name.toLowerCase(); return this.commands.filter(command => command.name.toLowerCase().includes(filterValue)); } displayFnClient(client: any): string { return client && client.name ? client.name : ''; } onOptionCommandSelected(selectedCommand: any): void { this.filters['command'] = selectedCommand.name; this.loadTraces(); } onOptionStatusSelected(selectedStatus: any): void { this.filters['status'] = selectedStatus; this.loadTraces(); } onOptionClientSelected(selectedClient: any): void { this.filters['client.id'] = selectedClient.id; 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 } }); } loadTraces(): void { this.loading = true; const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`; const params: any = { ...this.filters }; if (params['status'] === undefined) { delete params['status']; } if (params['startDate']) { params['executedAt[after]'] = this.datePipe.transform(params['startDate'], 'yyyy-MM-dd'); delete params['startDate']; } if (params['endDate']) { params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd'); delete params['endDate']; } if (this.sortBy) { params['order[' + this.sortBy + ']'] = this.sortDirection; } this.http.get(url, { params }).subscribe( (data) => { this.traces = data['hydra:member']; this.length = data['hydra:totalItems']; this.groupedTraces = this.groupByCommandId(this.traces); this.loading = false; if (Object.keys(this.filters).length === 0) { this.loadTotalStats(); } }, (error) => { console.error('Error fetching traces', error); this.loading = false; } ); } loadCommands() { this.loading = true; this.http.get(`${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; } ); } loadClients() { this.loading = true; this.http.get(`${this.baseUrl}/clients?page=1&itemsPerPage=10000`).subscribe( response => { this.clients = response['hydra:member']; this.loading = false; }, error => { console.error('Error fetching clients:', error); this.loading = false; } ); } loadTotalStats(): void { this.calculateLocalStats(); } private calculateLocalStats(): void { const statuses = ['success', 'failed', 'pending', 'in-progress', 'cancelled']; const requests = statuses.map(status => this.http.get(`${this.baseUrl}/traces?status=${status}&page=1&itemsPerPage=1`) ); const totalRequest = this.http.get(`${this.baseUrl}/traces?page=1&itemsPerPage=1`); const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd'); const todayRequest = this.http.get(`${this.baseUrl}/traces?executedAt[after]=${todayString}&page=1&itemsPerPage=1`); forkJoin([totalRequest, ...requests, todayRequest]).subscribe( (responses) => { const totalData = responses[0]; const statusData = responses.slice(1, 6); const todayData = responses[6]; this.totalStats = { total: totalData['hydra:totalItems'], success: statusData[0]['hydra:totalItems'], failed: statusData[1]['hydra:totalItems'], pending: statusData[2]['hydra:totalItems'], inProgress: statusData[3]['hydra:totalItems'], cancelled: statusData[4]['hydra:totalItems'], today: todayData['hydra:totalItems'] }; }, error => { console.error('Error fetching stats by status:', error); const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd'); this.totalStats = { total: this.length, success: this.traces.filter(trace => trace.status === 'success').length, failed: this.traces.filter(trace => trace.status === 'failed').length, pending: this.traces.filter(trace => trace.status === 'pending').length, inProgress: this.traces.filter(trace => trace.status === 'in-progress').length, cancelled: this.traces.filter(trace => trace.status === 'cancelled').length, today: this.traces.filter(trace => trace.executedAt && trace.executedAt.startsWith(todayString)).length }; } ); } resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any, clientSearchClientInput: any) { this.loading = true; clientSearchCommandInput.value = null; clientSearchStatusInput.value = null; clientSearchClientInput.value = null; 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] })); } onDateFilterChange(): void { const start = this.filters['startDate']; const end = this.filters['endDate']; if (!start || !end) { return; } if (start && end && start > end) { this.toastService.warning('La fecha de inicio no puede ser mayor que la fecha de fin'); return; } this.loadTraces(); } 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() } clearClientFilter(event: Event, clientSearchClientInput: any): void { event.stopPropagation(); delete this.filters['client.id']; clientSearchClientInput.value = null; this.loadTraces() } iniciarTour(): void { this.joyrideService.startTour({ steps: [ 'tracesTitleStep', 'resetFiltersStep', 'filtersStep', 'tracesProgressStep', 'tracesInfoStep', 'paginationStep' ], showPrevButton: true, themeColor: '#3f51b5' }); } // Métodos para paginación getPaginationFrom(): number { return (this.page * this.itemsPerPage) + 1; } getPaginationTo(): number { return Math.min((this.page + 1) * this.itemsPerPage, this.length); } getPaginationTotal(): number { return this.length; } }