599 lines
18 KiB
TypeScript
599 lines
18 KiB
TypeScript
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<any[]>;
|
|
clientControl = new FormControl();
|
|
filteredCommands!: Observable<any[]>;
|
|
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<any>(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<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;
|
|
}
|
|
);
|
|
}
|
|
|
|
loadClients() {
|
|
this.loading = true;
|
|
this.http.get<any>(`${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<any>(`${this.baseUrl}/traces?status=${status}&page=1&itemsPerPage=1`)
|
|
);
|
|
|
|
const totalRequest = this.http.get<any>(`${this.baseUrl}/traces?page=1&itemsPerPage=1`);
|
|
|
|
const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd');
|
|
const todayRequest = this.http.get<any>(`${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;
|
|
}
|
|
}
|