refs 2336. Traces new UX
testing/ogGui-multibranch/pipeline/head There was a failure building this commit
Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit
Details
parent
537a220fc4
commit
d526bb851a
|
@ -4,6 +4,9 @@
|
|||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
background: white;
|
||||
color: #333;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
|
@ -12,6 +15,182 @@
|
|||
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;
|
||||
}
|
||||
|
||||
.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-total {
|
||||
border-left-color: #667eea;
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-total .stat-number,
|
||||
.stat-total .stat-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-today {
|
||||
border-left-color: #17a2b8;
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-today .stat-number,
|
||||
.stat-today .stat-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-success {
|
||||
border-left-color: #28a745;
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-success .stat-number,
|
||||
.stat-success .stat-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-failed {
|
||||
border-left-color: #dc3545;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-failed .stat-number,
|
||||
.stat-failed .stat-label {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stat-in-progress {
|
||||
border-left-color: #ffc107;
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.stat-in-progress .stat-number,
|
||||
.stat-in-progress .stat-label {
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.stat-number {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.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: column;
|
||||
gap: 15px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-container.expanded {
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.filter-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 15px;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.advanced-filters {
|
||||
border-top: 1px solid #dee2e6;
|
||||
padding-top: 15px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.calendar-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
|
@ -34,14 +213,6 @@ 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;
|
||||
|
@ -57,40 +228,145 @@ table {
|
|||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-date {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -99,6 +375,45 @@ table {
|
|||
gap: 8px;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.client-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.client-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.client-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
button.cancel-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -107,12 +422,83 @@ 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;
|
||||
}
|
||||
|
||||
.selected-row {
|
||||
background-color: rgba(102, 126, 234, 0.1) !important;
|
||||
}
|
||||
|
||||
.mat-row:hover {
|
||||
background-color: rgba(102, 126, 234, 0.05);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()" matTooltip="Ayuda">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
|
||||
|
@ -8,91 +8,181 @@
|
|||
translate }}</h2>
|
||||
</div>
|
||||
|
||||
<div class="images-button-row">
|
||||
<div class="header-actions">
|
||||
<button class="action-button secondary" (click)="exportToCSV()" matTooltip="Exportar a CSV">
|
||||
<mat-icon>download</mat-icon>
|
||||
{{ 'exportCSV' | translate }}
|
||||
</button>
|
||||
<button class="action-button" (click)="resetFilters(commandSearchInput, commandStatusInput, commandClientInput)"
|
||||
joyrideStep="resetFiltersStep" text="{{ 'resetFiltersStepText' | translate }}">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
{{ 'resetFilters' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container" joyrideStep="filtersStep" text="{{ 'filtersStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto" #commandClientInput
|
||||
placeholder="{{ 'filterClientPlaceholder' | translate }}">
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
|
||||
<div style="display: flex; flex-direction: column;">
|
||||
<span>{{ client.name }}</span>
|
||||
<span style="font-size: 0.8rem; color: gray;">
|
||||
{{ client.ip }} — {{ client.mac }}
|
||||
</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
<div class="stats-container" *ngIf="!loading">
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-number">{{ totalStats.total }}</div>
|
||||
<div class="stat-label">{{ 'totalTraces' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-today">
|
||||
<div class="stat-number">{{ getStatusCount('today') }}</div>
|
||||
<div class="stat-label">{{ 'todayTraces' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-success">
|
||||
<div class="stat-number">{{ getStatusCount('success') }}</div>
|
||||
<div class="stat-label">{{ 'successful' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-failed">
|
||||
<div class="stat-number">{{ getStatusCount('failed') }}</div>
|
||||
<div class="stat-label">{{ 'failed' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-in-progress">
|
||||
<div class="stat-number">{{ getStatusCount('in-progress') }}</div>
|
||||
<div class="stat-label">{{ 'inProgress' | translate }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</mat-autocomplete>
|
||||
<button *ngIf="commandClientInput.value" mat-icon-button matSuffix aria-label="Clear input search"
|
||||
(click)="clearClientFilter($event, commandClientInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
<div class="filters-section" joyrideStep="filtersStep" text="{{ 'filtersStepText' | translate }}">
|
||||
<div class="filters-header">
|
||||
<h3>{{ 'filters' | translate }}</h3>
|
||||
<button mat-button color="primary" (click)="toggleFilters()">
|
||||
<mat-icon>{{ showAdvancedFilters ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||||
{{ showAdvancedFilters ? 'hideAdvanced' : 'showAdvanced' | translate }}
|
||||
</button>
|
||||
<mat-hint>Por favor, ingrese el nombre del cliente</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<div class="search-container" [class.expanded]="showAdvancedFilters">
|
||||
<div class="filter-row">
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<input type="text" matInput [formControl]="clientControl" [matAutocomplete]="clientAuto" #commandClientInput
|
||||
placeholder="{{ 'filterClientPlaceholder' | translate }}">
|
||||
<mat-autocomplete #clientAuto="matAutocomplete" [displayWith]="displayFnClient"
|
||||
(optionSelected)="onOptionClientSelected($event.option.value)">
|
||||
<mat-option *ngFor="let client of filteredClients | async" [value]="client">
|
||||
<div class="client-option">
|
||||
<span class="client-name">{{ client.name }}</span>
|
||||
<span class="client-details">{{ client.ip }} — {{ client.mac }}</span>
|
||||
</div>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
<button *ngIf="commandClientInput.value" mat-icon-button matSuffix aria-label="Clear input search"
|
||||
(click)="clearClientFilter($event, commandClientInput)">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint>{{ 'enterClientName' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<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>
|
||||
<mat-form-field appearance="fill" class="search-boolean">
|
||||
<mat-label>{{ 'status' | translate }}</mat-label>
|
||||
<mat-select (selectionChange)="onOptionStatusSelected($event.value)" placeholder="Seleccionar opción"
|
||||
#commandStatusInput>
|
||||
<mat-option [value]="'failed'">
|
||||
<div class="status-option">
|
||||
<div class="status-indicator failed"></div>
|
||||
{{ 'failed' | translate }}
|
||||
</div>
|
||||
</mat-option>
|
||||
<mat-option [value]="'pending'">
|
||||
<div class="status-option">
|
||||
<div class="status-indicator pending"></div>
|
||||
{{ 'pending' | translate }}
|
||||
</div>
|
||||
</mat-option>
|
||||
<mat-option [value]="'in-progress'">
|
||||
<div class="status-option">
|
||||
<div class="status-indicator in-progress"></div>
|
||||
{{ 'inProgress' | translate }}
|
||||
</div>
|
||||
</mat-option>
|
||||
<mat-option [value]="'success'">
|
||||
<div class="status-option">
|
||||
<div class="status-indicator success"></div>
|
||||
{{ 'success' | translate }}
|
||||
</div>
|
||||
</mat-option>
|
||||
<mat-option [value]="'cancelled'">
|
||||
<div class="status-option">
|
||||
<div class="status-indicator cancelled"></div>
|
||||
{{ 'cancelled' | translate }}
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
<div class="filter-row advanced-filters" *ngIf="showAdvancedFilters">
|
||||
<mat-form-field appearance="fill" class="search-date">
|
||||
<mat-label>{{ 'fromDate' | translate }}</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>
|
||||
<mat-form-field appearance="fill" class="search-date">
|
||||
<mat-label>{{ 'toDate' | translate }}</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>
|
||||
|
||||
<mat-form-field appearance="fill" class="search-select">
|
||||
<mat-label>{{ 'sortBy' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="sortBy" (selectionChange)="onSortChange()">
|
||||
<mat-option value="executedAt">{{ 'executionDate' | translate }}</mat-option>
|
||||
<mat-option value="status">{{ 'status' | translate }}</mat-option>
|
||||
<mat-option value="command">{{ 'command' | translate }}</mat-option>
|
||||
<mat-option value="client">{{ 'client' | translate }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<div *ngIf="!loading" class="table-container">
|
||||
<div class="table-header">
|
||||
<div class="table-info">
|
||||
<span>{{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }}</span>
|
||||
</div>
|
||||
<div class="table-actions">
|
||||
<button mat-icon-button (click)="refreshData()" matTooltip="{{ 'refresh' | translate }}">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<th mat-header-cell *matHeaderCellDef>
|
||||
<div class="column-header">
|
||||
<span>{{ column.header }}</span>
|
||||
<button *ngIf="column.sortable !== false" mat-icon-button (click)="sortColumn(column.columnDef)" class="sort-button">
|
||||
<mat-icon>{{ getSortIcon(column.columnDef) }}</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
<td mat-cell *matCellDef="let trace">
|
||||
|
||||
<ng-container [ngSwitch]="column.columnDef">
|
||||
|
@ -102,7 +192,11 @@
|
|||
<mat-progress-bar class="example-margin" [mode]="mode" [value]="trace.progress"
|
||||
[bufferValue]="bufferValue">
|
||||
</mat-progress-bar>
|
||||
<span>{{trace.progress}}%</span>
|
||||
<span class="progress-text">{{trace.progress}}%</span>
|
||||
<button mat-icon-button
|
||||
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #statusChip>
|
||||
|
@ -116,16 +210,16 @@
|
|||
'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 === 'failed' ? ('failed' | translate) :
|
||||
trace.status === 'in-progress' ? ('inProgress' | translate) :
|
||||
trace.status === 'success' ? ('successful' | translate) :
|
||||
trace.status === 'pending' ? ('pending' | translate) :
|
||||
trace.status === 'cancelled' ? ('cancelled' | translate) :
|
||||
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="{{ 'cancelTask' | translate }}">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
@ -133,29 +227,30 @@
|
|||
</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 class="command-cell">
|
||||
<span class="command-name">{{ translateCommand(trace.command) }}</span>
|
||||
<span class="command-id">{{ 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 class="client-cell">
|
||||
<span class="client-name">{{ trace.client?.name }}</span>
|
||||
<span class="client-ip">{{ 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 class="date-cell">
|
||||
<span class="date-time">{{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span class="date-relative" *ngIf="getRelativeTime(trace.executedAt)">{{ getRelativeTime(trace.executedAt) }}</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 class="date-cell">
|
||||
<span class="date-time">{{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}}</span>
|
||||
<span class="date-relative" *ngIf="getRelativeTime(trace.finishedAt)">{{ getRelativeTime(trace.finishedAt) }}</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-container *ngSwitchDefault>
|
||||
|
@ -170,26 +265,31 @@
|
|||
<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 || trace.input.length === 0"
|
||||
(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>
|
||||
<div class="action-buttons">
|
||||
<button mat-icon-button color="primary" [disabled]="!trace.input || trace.input.length === 0"
|
||||
(click)="openInputModal(trace.input)" matTooltip="{{ 'viewInput' | translate }}">
|
||||
<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)"
|
||||
matTooltip="{{ 'viewOutput' | translate }}">
|
||||
<mat-icon>
|
||||
<span class="material-symbols-outlined">info</span>
|
||||
</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" *ngIf="trace.status === 'pending'"
|
||||
(click)="cancelTrace(trace)" matTooltip="{{ 'cancelTrace' | translate }}">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
|
||||
[class.selected-row]="selectedTrace?.id === row.id"
|
||||
(click)="selectTrace(row)"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeDetectorRef, Component, OnInit } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Observable, forkJoin } from 'rxjs';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
@ -20,7 +20,7 @@ import { COMMAND_TYPES } from '../../shared/constants/command-types';
|
|||
templateUrl: './task-logs.component.html',
|
||||
styleUrls: ['./task-logs.component.css']
|
||||
})
|
||||
export class TaskLogsComponent implements OnInit {
|
||||
export class TaskLogsComponent implements OnInit, OnDestroy {
|
||||
baseUrl: string;
|
||||
mercureUrl: string;
|
||||
traces: any[] = [];
|
||||
|
@ -38,6 +38,30 @@ export class TaskLogsComponent implements OnInit {
|
|||
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,
|
||||
|
@ -49,31 +73,37 @@ export class TaskLogsComponent implements OnInit {
|
|||
columnDef: 'id',
|
||||
header: 'ID',
|
||||
cell: (trace: any) => `${trace.id}`,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
columnDef: 'command',
|
||||
header: 'Comando',
|
||||
cell: (trace: any) => trace.command
|
||||
cell: (trace: any) => trace.command,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
columnDef: 'client',
|
||||
header: 'Cliente',
|
||||
cell: (trace: any) => trace.client?.name
|
||||
cell: (trace: any) => trace.client?.name,
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
columnDef: 'status',
|
||||
header: 'Estado',
|
||||
cell: (trace: any) => trace.status
|
||||
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'];
|
||||
|
@ -100,6 +130,7 @@ export class TaskLogsComponent implements OnInit {
|
|||
this.loadTraces();
|
||||
this.loadCommands();
|
||||
this.loadClients();
|
||||
this.loadTotalStats();
|
||||
this.filteredCommands = this.commandControl.valueChanges.pipe(
|
||||
startWith(''),
|
||||
map(value => (typeof value === 'string' ? value : value?.name)),
|
||||
|
@ -122,6 +153,169 @@ export class TaskLogsComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -135,14 +329,9 @@ export class TaskLogsComponent implements OnInit {
|
|||
|
||||
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 _filterClients(value: string): any[] {
|
||||
const filterValue = value.toLowerCase();
|
||||
|
||||
|
@ -153,7 +342,6 @@ export class TaskLogsComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
private _filterCommands(name: string): any[] {
|
||||
const filterValue = name.toLowerCase();
|
||||
return this.commands.filter(command => command.name.toLowerCase().includes(filterValue));
|
||||
|
@ -193,30 +381,11 @@ export class TaskLogsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
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 {
|
||||
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'];
|
||||
}
|
||||
|
@ -230,12 +399,19 @@ export class TaskLogsComponent implements OnInit {
|
|||
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);
|
||||
|
@ -272,6 +448,52 @@ export class TaskLogsComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -361,4 +583,16 @@ export class TaskLogsComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
.custom-icon {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Animaciones adicionales */
|
||||
.modal-overlay-blur {
|
||||
animation: fadeIn 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
transform: translateY(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<div *ngIf="isVisible" class="modal-overlay-blur" [ngClass]="variant">
|
||||
<div class="spinner-container">
|
||||
<mat-spinner *ngIf="showSpinner" diameter="40"></mat-spinner>
|
||||
<mat-icon *ngIf="customIcon && !showSpinner" class="custom-icon">{{ customIcon }}</mat-icon>
|
||||
<p>{{ message }}</p>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
import { Component, Input } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-modal-overlay',
|
||||
templateUrl: './modal-overlay.component.html',
|
||||
styleUrls: ['./modal-overlay.component.css']
|
||||
})
|
||||
export class ModalOverlayComponent {
|
||||
@Input() isVisible: boolean = false;
|
||||
@Input() message: string = 'Procesando...';
|
||||
@Input() variant: 'default' | 'success' | 'warning' | 'error' = 'default';
|
||||
@Input() showSpinner: boolean = true;
|
||||
@Input() customIcon?: string;
|
||||
}
|
Loading…
Reference in New Issue