refs #2252. Queue actions
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/28/head
Manuel Aranda Rosales 2025-06-26 15:57:04 +02:00
parent 90d969ccd3
commit 212c4f9eec
9 changed files with 1167 additions and 33 deletions

View File

@ -0,0 +1,143 @@
.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;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.header-actions button {
display: flex;
align-items: center;
gap: 5px;
}
.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 0.5rem 0rem;
box-sizing: border-box;
}
.search-boolean {
flex: 1;
padding: 5px;
}
.search-select {
flex: 2;
padding: 5px;
}
.search-date {
flex: 1;
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;
}
/* Ajuste para el botón de cancelar en la barra de progreso */
.progress-container .cancel-button {
margin-left: auto;
flex-shrink: 0;
}
.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

@ -0,0 +1,158 @@
<div class="modal-content">
<div class="header-container">
<div class="header-container-title">
<h2 *ngIf="!data.isOrganizationalUnit">{{ 'colaAcciones' | translate }}</h2>
<h2 *ngIf="data.isOrganizationalUnit">{{ 'colaAcciones' | translate }} - {{ data.client?.name }}</h2>
</div>
<div class="header-actions">
<button mat-raised-button color="warn" (click)="clearAllActions()"
[disabled]="traces.length === 0 || loading"
matTooltip="Cancelar todas las acciones mostradas">
<mat-icon>clear_all</mat-icon>
{{ 'limpiarAcciones' | translate }}
</button>
</div>
</div>
<div class="search-container" joyrideStep="filtersStep" text="{{ 'filtersStepText' | translate }}">
<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-date">
<mat-label>Desde</mat-label>
<input matInput [matDatepicker]="fromPicker" [(ngModel)]="filters['startDate']"
(dateChange)="onDateFilterChange()" [max]="today">
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="fill" class="search-date">
<mat-label>Hasta</mat-label>
<input matInput [matDatepicker]="toPicker" [(ngModel)]="filters['endDate']" (dateChange)="onDateFilterChange()"
[max]="today">
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker>
</mat-form-field>
</div>
<app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading">
<table mat-table [dataSource]="traces" class="mat-elevation-z8">
<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">
<span>{{trace.progress}}%</span>
<button mat-icon-button
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="Cancelar tarea">
<mat-icon>cancel</mat-icon>
</button>
</div>
</ng-container>
<ng-template #statusChip>
<div class="status-progress-flex" joyrideStep="tracesProgressStep"
text="{{ 'tracesProgressStepText' | translate }}">
<mat-chip [ngClass]="{
'chip-pending': trace.status === 'pending',
}">
{{
trace.status === 'pending' ? 'Pendiente' :
trace.status
}}
</mat-chip>
<button *ngIf="trace.status === 'in-progress'" mat-icon-button
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="Cancelar tarea">
<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="information">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'informationLabel' | translate }}</th>
<td mat-cell *matCellDef="let trace" style="text-align: center;" joyrideStep="tracesInfoStep"
text="{{ 'tracesInfoStepText' | translate }}">
<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>
<div mat-dialog-actions align="end" style="padding: 16px 24px;">
<button class="ordinary-button" (click)="close()">{{ 'closeButton' | translate }}</button>
</div>

View File

@ -0,0 +1,328 @@
import { Component, OnInit, Inject, ChangeDetectorRef } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DatePipe } from '@angular/common';
import { ConfigService } from '@services/config.service';
import { ToastrService } from 'ngx-toastr';
import { TranslationService } from '@services/translation.service';
import { OutputDialogComponent } from '../output-dialog/output-dialog.component';
import { InputDialogComponent } from '../input-dialog/input-dialog.component';
import { JoyrideService } from 'ngx-joyride';
import { FormControl } from '@angular/forms';
import { Observable } from 'rxjs';
import { COMMAND_TYPES } from 'src/app/shared/constants/command-types';
import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component';
@Component({
selector: 'app-client-pending-tasks',
templateUrl: './client-pending-tasks.component.html',
styleUrls: ['./client-pending-tasks.component.css']
})
export class ClientPendingTasksComponent implements OnInit {
baseUrl: string;
mercureUrl: string;
traces: 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');
filters: { [key: string]: any } = {};
filteredCommands!: Observable<any[]>;
commandControl = new FormControl();
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), 'information'];
filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({
name: key,
value: key,
label: COMMAND_TYPES[key]
}));
today = new Date();
constructor(
private http: HttpClient,
@Inject(MAT_DIALOG_DATA) public data: { client: any, isOrganizationalUnit?: boolean },
private joyrideService: JoyrideService,
private dialog: MatDialog,
private cdr: ChangeDetectorRef,
private configService: ConfigService,
private toastService: ToastrService,
private translationService: TranslationService,
public dialogRef: MatDialogRef<ClientPendingTasksComponent>
) {
this.baseUrl = this.configService.apiUrl;
this.mercureUrl = this.configService.mercureUrl;
}
ngOnInit(): void {
// Si es una unidad organizativa, agregar columna de cliente
if (this.data.isOrganizationalUnit) {
this.columns.splice(2, 0, {
columnDef: 'client',
header: 'Cliente',
cell: (trace: any) => trace.client?.name || 'N/A'
});
this.displayedColumns = [...this.columns.map(column => column.columnDef), 'information'];
}
this.loadTraces();
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);
}
}
}
private updateTracesStatus(clientUuid: string, newStatus: string): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
if (traceIndex !== -1) {
const updatedTraces = [...this.traces];
updatedTraces[traceIndex] = {
...updatedTraces[traceIndex],
status: newStatus
};
this.traces = updatedTraces;
this.cdr.detectChanges();
}
}
loadTraces(): void {
this.loading = true;
let params = new HttpParams()
.set('status', 'pending')
.set('page', (this.page + 1).toString())
.set('itemsPerPage', this.itemsPerPage.toString());
// Si es una unidad organizativa, obtener las trazas de todos sus clientes
if (this.data.isOrganizationalUnit && this.data.client?.clients) {
const clientIds = this.data.client.clients.map((client: any) => client.id);
if (clientIds.length > 0) {
// Agregar cada ID de cliente como un parámetro separado
clientIds.forEach((id: number) => {
params = params.append('client.id[]', id.toString());
});
} else {
this.traces = [];
this.length = 0;
this.loading = false;
return;
}
} else {
// Cliente individual
const clientId = this.data.client?.id;
if (!clientId) {
this.loading = false;
return;
}
params = params.set('client.id', clientId.toString());
}
const url = `${this.baseUrl}/traces`;
console.log('URL con parámetros:', url, params.toString());
this.http.get<any>(url, { params }).subscribe(
(data) => {
this.traces = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.loading = false;
},
(error) => {
console.error('Error fetching traces', error);
this.loading = false;
}
);
}
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 }
});
}
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;
}
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: trace.jobId },
}).afterClosed().subscribe((result) => {
if (result) {
if (trace.status === 'in-progress') {
this.http.post(`${this.baseUrl}/traces/${trace['@id']}/kill-job`, {
jobId: trace.jobId
}).subscribe({
next: () => {
this.toastService.success('Tarea cancelada correctamente');
this.loadTraces();
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
console.error('Error cancelling in-progress trace:', error);
}
});
} else {
this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
next: () => {
this.toastService.success('Tarea cancelada correctamente');
this.loadTraces();
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
console.error('Error cancelling pending trace:', error);
}
});
}
}
});
}
resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any) {
clientSearchCommandInput.value = '';
clientSearchStatusInput.value = '';
this.loadTraces();
}
openOutputModal(outputData: any): void {
this.dialog.open(OutputDialogComponent, {
width: '500px',
data: { input: outputData }
});
}
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 {
clientSearchCommandInput.value = '';
this.loadTraces();
}
clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
clientSearchStatusInput.value = '';
this.loadTraces();
}
onDateFilterChange(): void {
this.loadTraces();
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'tracesTitleStep',
'resetFiltersStep',
'filtersStep',
'tracesProgressStep',
'tracesInfoStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
close(): void {
this.dialogRef.close();
}
clearAllActions(): void {
if (this.traces.length === 0) {
this.toastService.warning('No hay acciones para limpiar');
return;
}
// Mostrar confirmación antes de proceder
this.dialog.open(DeleteModalComponent, {
width: '400px',
data: {
name: `Todas las acciones mostradas (${this.traces.length} acciones)`,
message: '¿Estás seguro de que quieres cancelar todas las acciones mostradas?'
},
}).afterClosed().subscribe((result) => {
if (result) {
this.loading = true;
// Enviar array de traces en el body
const tracesToCancel = this.traces.map((trace: any) => trace['@id']);
this.http.post(`${this.baseUrl}/traces/cancel-multiple`, {
traces: tracesToCancel
}).subscribe({
next: () => {
this.toastService.success(`Se han cancelado ${this.traces.length} acciones correctamente`);
this.loadTraces(); // Recargar las trazas
},
error: (error) => {
console.error('Error al cancelar las acciones:', error);
this.toastService.error('Error al cancelar las acciones');
this.loadTraces(); // Recargar las trazas para mostrar el estado actual
},
complete: () => {
this.loading = false;
}
});
}
});
}
}

View File

@ -10,6 +10,15 @@
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
background: white;
color: #333;
border-radius: 8px 8px 0 0;
}
.header-right {
display: flex;
gap: 8px;
align-items: center;
}
.header-container-title {
@ -18,6 +27,133 @@
margin-left: 1em;
}
.header-container-title h2 {
margin: 0;
font-weight: 500;
}
.header-actions {
display: flex;
gap: 10px;
align-items: center;
}
.action-button {
display: flex;
align-items: center;
gap: 5px;
padding: 8px 16px;
border-radius: 20px;
border: 1px solid #ddd;
cursor: pointer;
font-weight: 500;
transition: all 0.3s ease;
background: white;
color: #333;
}
.action-button:hover {
background: #f8f9fa;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.action-button.secondary {
background: #f8f9fa;
border-color: #adb5bd;
}
.action-button.secondary:hover {
background: #e9ecef;
}
/* Estadísticas */
.stats-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin: 20px 0;
padding: 0 10px;
}
.stat-card {
background: white;
border-radius: 12px;
padding: 20px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
border-left: 4px solid #667eea;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #667eea;
margin-bottom: 5px;
}
.stat-label {
font-size: 0.9rem;
color: #666;
font-weight: 500;
}
/* Filtros mejorados */
.filters-section {
background: #f8f9fa;
border-radius: 8px;
margin: 20px 0;
padding: 20px;
border: 1px solid #e9ecef;
}
.filters-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.filters-header h3 {
margin: 0;
color: #495057;
font-weight: 500;
}
.search-container {
display: flex;
flex-direction: row;
gap: 15px;
transition: all 0.3s ease;
margin: 1.5rem 0rem 0.5rem 0rem;
box-sizing: border-box;
align-items: center;
justify-content: space-between;
}
.search-container.expanded {
gap: 20px;
}
.filter-row {
display: flex;
gap: 15px;
align-items: center;
width: 100%;
}
.advanced-filters {
border-top: 1px solid #dee2e6;
padding-top: 15px;
margin-top: 10px;
}
.calendar-button-row {
display: flex;
gap: 15px;
@ -40,12 +176,9 @@ table {
width: 100%;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
margin: 1.5rem 0rem 0.5rem 0rem;
box-sizing: border-box;
.search-string {
flex: 1;
padding: 5px;
}
.search-boolean {
@ -64,39 +197,142 @@ table {
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
/* Tabla mejorada */
.table-container {
background: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
background: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.table-info {
color: #6c757d;
font-size: 0.9rem;
}
.table-actions {
display: flex;
gap: 10px;
}
.column-header {
display: flex;
align-items: center;
gap: 5px;
}
.sort-button {
opacity: 0.5;
transition: opacity 0.3s ease;
}
.sort-button:hover {
opacity: 1;
}
.sort-button.active {
opacity: 1;
color: #667eea;
}
/* Celdas mejoradas */
.command-cell, .client-cell, .date-cell {
display: flex;
flex-direction: column;
gap: 2px;
}
.command-name, .client-name {
font-weight: 500;
color: #212529;
}
.command-id, .client-ip {
font-size: 0.75rem;
color: #6c757d;
}
.date-time {
font-size: 0.85rem;
color: #212529;
}
.date-relative {
font-size: 0.7rem;
color: #6c757d;
font-style: italic;
}
.progress-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.progress-text {
font-size: 0.8rem;
font-weight: 500;
color: #667eea;
min-width: 35px;
}
/* Ajuste para el botón de cancelar en la barra de progreso */
.progress-container .cancel-button {
margin-left: auto;
flex-shrink: 0;
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
margin: 20px 0;
padding: 0 10px;
}
/* Chips de estado mejorados */
.chip-failed {
background-color: #e87979 !important;
color: white;
background-color: #ff6b6b !important;
color: white !important;
font-weight: 500;
}
.chip-success {
background-color: #46c446 !important;
color: white;
background-color: #51cf66 !important;
color: white !important;
font-weight: 500;
}
.chip-pending {
background-color: #bebdbd !important;
color: black;
background-color: #74c0fc !important;
color: white !important;
font-weight: 500;
}
.chip-in-progress {
background-color: #f5a623 !important;
color: white;
background-color: #ffd43b !important;
color: #212529 !important;
font-weight: 500;
}
.chip-cancelled {
background-color: #adb5bd !important;
color: white !important;
font-weight: 500;
}
.status-progress-flex {
@ -105,6 +341,48 @@ table {
gap: 8px;
}
/* Opciones de estado */
.status-option {
display: flex;
align-items: center;
gap: 8px;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
}
.status-indicator.failed { background-color: #dc3545; }
.status-indicator.success { background-color: #28a745; }
.status-indicator.pending { background-color: #17a2b8; }
.status-indicator.in-progress { background-color: #ffc107; }
.status-indicator.cancelled { background-color: #6c757d; }
/* Opciones de cliente */
.client-option {
display: flex;
flex-direction: column;
gap: 2px;
}
.client-name {
font-weight: 500;
}
.client-details {
font-size: 0.8rem;
color: #6c757d;
}
/* Botones de acción */
.action-buttons {
display: flex;
gap: 5px;
justify-content: center;
}
button.cancel-button {
display: flex;
align-items: center;
@ -113,12 +391,120 @@ button.cancel-button {
}
.cancel-button {
color: red;
color: #dc3545;
background-color: transparent;
border: none;
padding: 0;
transition: all 0.3s ease;
}
.cancel-button:hover {
background-color: rgba(220, 53, 69, 0.1);
border-radius: 50%;
}
.cancel-button mat-icon {
color: red;
color: #dc3545;
}
/* Filas seleccionadas */
.selected-row {
background-color: rgba(102, 126, 234, 0.1) !important;
}
.mat-row:hover {
background-color: rgba(102, 126, 234, 0.05);
cursor: pointer;
}
/* Responsive */
@media (max-width: 768px) {
.header-container {
flex-direction: column;
gap: 15px;
text-align: center;
background: white;
color: #333;
}
.header-actions {
width: 100%;
justify-content: center;
}
.stats-container {
grid-template-columns: repeat(2, 1fr);
}
.filter-row {
grid-template-columns: 1fr;
}
.table-header {
flex-direction: column;
gap: 10px;
text-align: center;
}
.action-buttons {
flex-direction: column;
gap: 2px;
}
}
/* Animaciones */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.stats-container, .filters-section, .table-container {
animation: fadeIn 0.5s ease-out;
}
/* Estados de carga */
.loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* Botón de cerrar en footer */
.footer-actions {
display: flex;
justify-content: flex-end;
padding: 20px 0;
border-top: 1px solid #e9ecef;
margin-top: 20px;
}
.footer-actions button {
min-width: 120px;
padding: 10px 24px;
font-weight: 500;
border-radius: 8px;
transition: all 0.3s ease;
background-color: white;
color: #333;
border: 1px solid #ddd;
}
.footer-actions button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
background-color: #f8f9fa;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}

View File

@ -1,4 +1,4 @@
<div class="modal-content">
<mat-dialog-content class="modal-content">
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
@ -9,7 +9,7 @@
translate }}</h2>
</div>
<div class="images-button-row">
<div class="header-right">
<button class="action-button" (click)="resetFilters(commandSearchInput, commandStatusInput)"
joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
{{ 'resetFilters' | translate }}
@ -82,6 +82,10 @@
[bufferValue]="bufferValue">
</mat-progress-bar>
<span>{{trace.progress}}%</span>
<button mat-icon-button
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="Cancelar tarea">
<mat-icon>cancel</mat-icon>
</button>
</div>
</ng-container>
<ng-template #statusChip>
@ -103,8 +107,8 @@
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">
<button *ngIf="trace.status === 'in-progress'" mat-icon-button
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="Cancelar tarea">
<mat-icon>cancel</mat-icon>
</button>
</div>
@ -176,4 +180,8 @@
(page)="onPageChange($event)">
</mat-paginator>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions align="end" style="padding: 16px 24px;">
<button class="ordinary-button" (click)="closeDialog()">{{ 'closeButton' | translate }}</button>
</mat-dialog-actions>

View File

@ -175,21 +175,41 @@ export class ClientTaskLogsComponent implements OnInit {
}
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;
}
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']);
}
});
if (trace.status === 'in-progress') {
this.http.post(`${this.baseUrl}/traces/${trace['@id']}/cancel`, {
job_id: trace.jobId
}).subscribe({
next: () => {
this.toastService.success('Tarea cancelada correctamente');
this.loadTraces();
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
console.error('Error cancelling in-progress trace:', error);
}
});
} else {
this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({
next: () => {
this.toastService.success('Tarea cancelada correctamente');
this.loadTraces();
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al cancelar la tarea');
console.error('Error cancelling pending trace:', error);
}
});
}
}
});
}
@ -311,4 +331,8 @@ export class ClientTaskLogsComponent implements OnInit {
themeColor: '#3f51b5'
});
}
closeDialog(): void {
this.dialog.closeAll();
}
}

View File

@ -0,0 +1,54 @@
mat-dialog-content {
font-size: 16px;
margin-bottom: 20px;
color: #555;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 0.5em 1.5em 1.5em 1.5em;
}
.action-button {
background-color: #2196f3;
color: white;
padding: 8px 18px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: transform 0.3s ease;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.action-button:hover:not(:disabled) {
background-color: #3f51b5;
}
.action-button:disabled {
background-color: #ced0df;
cursor: not-allowed;
}
.ordinary-button {
background-color: #f5f5f5;
color: #333;
padding: 8px 18px;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: transform 0.3s ease;
font-family: Roboto, "Helvetica Neue", sans-serif;
}
.ordinary-button:hover:not(:disabled) {
background-color: #e0e0e0;
}
mat-checkbox {
margin-top: 15px;
display: block;
}

View File

@ -0,0 +1,9 @@
<h1 mat-dialog-title>Confirmación</h1>
<div mat-dialog-content>
<p>¿Quieres que se encolen las acciones para cuyos PCs no se pueda ejecutar?</p>
<mat-checkbox [(ngModel)]="shouldQueue">Encolar acciones</mat-checkbox>
</div>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()">Cancelar</button>
<button class="action-button" (click)="onYesClick()">Confirmar</button>
</div>

View File

@ -0,0 +1,24 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-queue-confirmation-modal',
templateUrl: './queue-confirmation-modal.component.html',
styleUrl: './queue-confirmation-modal.component.css'
})
export class QueueConfirmationModalComponent {
shouldQueue: boolean = false;
constructor(
public dialogRef: MatDialogRef<QueueConfirmationModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {}
onNoClick(): void {
this.dialogRef.close(false);
}
onYesClick(): void {
this.dialogRef.close(this.shouldQueue);
}
}