Compare commits
12 Commits
Author | SHA1 | Date |
---|---|---|
|
ff9ea3d1f1 | |
|
d6a092ce75 | |
|
483168146b | |
|
2cc10615a1 | |
|
f16581e4ed | |
|
f2e4f5d081 | |
|
24f45e6ba6 | |
|
c2c5bb68be | |
|
01390a1fab | |
|
642a439f21 | |
|
d11d3f2d75 | |
|
0403385421 |
|
@ -1,4 +1,12 @@
|
|||
# Changelog
|
||||
## [0.16.0] - 2025-06-27
|
||||
### Added
|
||||
- Sistema de logs en tiempo real.
|
||||
|
||||
### Improved
|
||||
- Se ha mejorado el comportamiento de algunos filtros en la parte de trazas.
|
||||
|
||||
---
|
||||
## [0.15.0] - 2025-06-26
|
||||
### Added
|
||||
- Se ha añadido integracion con OgGit. Ahora se pueden crear y desplegar imagenes.
|
||||
|
|
|
@ -63,8 +63,8 @@
|
|||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "7kb",
|
||||
"maximumError": "10kb"
|
||||
"maximumWarning": "35kb",
|
||||
"maximumError": "40kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
|
|
@ -31,6 +31,16 @@
|
|||
<div *ngIf="!loading" class="main-content">
|
||||
<!-- Resumen rápido del sistema -->
|
||||
<div class="system-overview">
|
||||
<div class="overview-card">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>cloud</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>Repositorios</h3>
|
||||
<p>Total: <span class="status-badge info">{{ repositories.length }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>storage</mat-icon>
|
||||
|
@ -52,73 +62,10 @@
|
|||
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="overview-icon">
|
||||
<mat-icon>cloud</mat-icon>
|
||||
</div>
|
||||
<div class="overview-content">
|
||||
<h3>Repositorios</h3>
|
||||
<p>Total: <span class="status-badge info">{{ repositories.length }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs principales -->
|
||||
<mat-tab-group (selectedTabChange)="onTabChange($event)" class="main-tabs">
|
||||
<mat-tab label="OgBoot Server">
|
||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
|
||||
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
|
||||
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<div *ngIf="loadingOgBoot" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de OgBoot...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorOgBoot" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor OgBoot</p>
|
||||
<button mat-raised-button color="primary" (click)="loadOgBootStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="DHCP Server">
|
||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
|
||||
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp" [isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<div *ngIf="loadingDhcp" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de DHCP...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorDhcp" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor DHCP</p>
|
||||
<button mat-raised-button color="primary" (click)="loadDhcpStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="{{ 'repositoryLabel' | translate }}">
|
||||
<div class="repositories-container">
|
||||
<div *ngIf="repositories.length === 0" class="no-repositories">
|
||||
|
@ -170,15 +117,15 @@
|
|||
<div *ngIf="!selectedRepositoryUuid" class="no-repository-selected">
|
||||
<mat-icon class="no-data-icon">storage</mat-icon>
|
||||
<h3>Selecciona un repositorio</h3>
|
||||
<p>Elige un repositorio del selector para ver su información detallada.</p>
|
||||
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
|
||||
</div>
|
||||
|
||||
<!-- Error del repositorio seleccionado -->
|
||||
<!-- Error al cargar repositorio -->
|
||||
<div *ngIf="selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el repositorio {{ getSelectedRepositoryName() }}</p>
|
||||
<p>No se pudo conectar con el repositorio seleccionado</p>
|
||||
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
|
@ -188,6 +135,59 @@
|
|||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="OgBoot Server">
|
||||
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
|
||||
<app-status-tab [loading]="loadingOgBoot" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
|
||||
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
|
||||
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
|
||||
[isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<div *ngIf="loadingOgBoot" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de OgBoot...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorOgBoot" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor OgBoot</p>
|
||||
<button mat-raised-button color="primary" (click)="loadOgBootStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab label="DHCP Server">
|
||||
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
|
||||
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
|
||||
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
|
||||
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true" [isRepository]="false">
|
||||
</app-status-tab>
|
||||
</div>
|
||||
<div *ngIf="loadingDhcp" class="loading-container">
|
||||
<div class="loading-content">
|
||||
<mat-spinner class="loading-spinner"></mat-spinner>
|
||||
<p class="loading-text">Cargando estado de DHCP...</p>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="errorDhcp" class="error-container">
|
||||
<div class="error-card">
|
||||
<mat-icon class="error-icon">error_outline</mat-icon>
|
||||
<h3>Error de conexión</h3>
|
||||
<p>No se pudo conectar con el servidor DHCP</p>
|
||||
<button mat-raised-button color="primary" (click)="loadDhcpStatus()">
|
||||
<mat-icon>refresh</mat-icon>
|
||||
Reintentar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
|
|
@ -44,6 +44,14 @@ export class GlobalStatusComponent implements OnInit {
|
|||
isDhcp: boolean = false;
|
||||
isRepository: boolean = false;
|
||||
|
||||
// Loading específicos para cada sección
|
||||
loadingOgBootOgLives: boolean = false;
|
||||
loadingOgBootServices: boolean = false;
|
||||
loadingOgBootDisk: boolean = false;
|
||||
loadingDhcpSubnets: boolean = false;
|
||||
loadingDhcpServices: boolean = false;
|
||||
loadingDhcpDisk: boolean = false;
|
||||
|
||||
constructor(
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
|
@ -160,6 +168,7 @@ export class GlobalStatusComponent implements OnInit {
|
|||
if (data.message.installed_oglives) {
|
||||
installedOgLives.push(...data.message.installed_oglives);
|
||||
}
|
||||
this.loadingOgBootOgLives = false;
|
||||
}
|
||||
|
||||
diskUsageChartData.length = 0;
|
||||
|
@ -284,16 +293,16 @@ export class GlobalStatusComponent implements OnInit {
|
|||
onTabChange(event: MatTabChangeEvent): void {
|
||||
switch (event.index) {
|
||||
case 0:
|
||||
this.loadOgBootStatus();
|
||||
break;
|
||||
case 1:
|
||||
this.loadDhcpStatus();
|
||||
break;
|
||||
case 2:
|
||||
if (this.repositories.length === 0) {
|
||||
this.loadRepositories(false);
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this.loadOgBootStatus();
|
||||
break;
|
||||
case 2:
|
||||
this.loadDhcpStatus();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
<div *ngIf="!loading" class="dashboard">
|
||||
<!-- Sección de uso de recursos -->
|
||||
<div class="resources-section" [ngClass]="{'repository-layout': isRepository}">
|
||||
<div class="resources-section">
|
||||
<!-- Disk Usage Section -->
|
||||
<div class="resource-card disk-usage-container">
|
||||
<div class="resource-header">
|
||||
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
||||
<span class="info-value usage-percentage">{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}</span>
|
||||
<span class="info-value usage-percentage">{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -67,7 +67,7 @@
|
|||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
|
||||
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}</span>
|
||||
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -84,7 +84,7 @@
|
|||
<div class="resource-content">
|
||||
<div class="cpu-usage-display">
|
||||
<div class="cpu-circle">
|
||||
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}</div>
|
||||
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}%</div>
|
||||
<div class="cpu-label">Uso actual</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -344,7 +344,7 @@ export class DeployImageComponent implements OnInit{
|
|||
diskNumber: this.selectedPartition.diskNumber,
|
||||
partitionNumber: this.selectedPartition.partitionNumber,
|
||||
p2pMode: this.p2pMode,
|
||||
p2pTime: this.p2pMode === 'seeder' ? this.p2pTime : null,
|
||||
p2pTime: this.p2pMode === 'seeder' ? this.p2pTime : 0,
|
||||
mcastIp: this.mcastIp,
|
||||
mcastPort: this.mcastPort,
|
||||
mcastMode: this.mcastMode,
|
||||
|
|
|
@ -418,6 +418,10 @@
|
|||
<mat-icon>pending_actions</mat-icon>
|
||||
<span>{{ 'colaAcciones' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="client.status === 'og-live'" (click)="openClientLogsInNewTab($event, client)">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Logs en tiempo real</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
|
@ -605,6 +609,10 @@
|
|||
<mat-icon>pending_actions</mat-icon>
|
||||
<span>{{ 'colaAcciones' | translate }}</span>
|
||||
</button>
|
||||
<button mat-menu-item *ngIf="client.status === 'og-live'" (click)="openClientLogsInNewTab($event, client)">
|
||||
<mat-icon>article</mat-icon>
|
||||
<span>Logs en tiempo real</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="onDeleteClick($event, client)" *ngIf="auth.userCategory !== 'ou-minimal'">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>{{ 'delete' | translate }}</span>
|
||||
|
|
|
@ -961,20 +961,38 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
}
|
||||
|
||||
openClientPendingTasks(event: MouseEvent, client: Client): void {
|
||||
this.loading = true;
|
||||
event.stopPropagation();
|
||||
|
||||
const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
|
||||
width: '1200px',
|
||||
data: { client },
|
||||
disableClose: true,
|
||||
hasBackdrop: true,
|
||||
backdropClass: 'non-clickable-backdrop',
|
||||
})
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this.loading = false;
|
||||
width: '90vw',
|
||||
height: '80vh',
|
||||
data: {
|
||||
client: client,
|
||||
parentNode: this.selectedNode
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.refreshClientData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openClientLogsInNewTab(event: MouseEvent, client: Client): void {
|
||||
event.stopPropagation();
|
||||
if (client.ip) {
|
||||
const logsUrl = `${this.baseUrl}/pcclients/${client.ip}/cgi-bin/httpd-log.sh`;
|
||||
const windowName = `logs_${client.ip.replace(/\./g, '_')}`;
|
||||
const newWindow = window.open(logsUrl, windowName);
|
||||
if (newWindow) {
|
||||
newWindow.document.write(`
|
||||
<title>Logs - ${client.ip}</title>
|
||||
<iframe src="${logsUrl}" width="100%" height="100%" style="border:none;"></iframe>
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
this.toastr.error('No se puede acceder a los logs: IP del cliente no disponible', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
openOUPendingTasks(event: MouseEvent, node: any): void {
|
||||
|
|
|
@ -61,6 +61,60 @@
|
|||
gap: 15px;
|
||||
margin: 20px 0;
|
||||
padding: 0 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stats-hint {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.stats-hint mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: #667eea;
|
||||
opacity: 0.7;
|
||||
cursor: help;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.stats-hint mat-icon:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.stats-info-message {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
|
||||
animation: fadeInUp 0.5s ease-out;
|
||||
}
|
||||
|
||||
.stats-info-message mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
|
@ -73,9 +127,60 @@
|
|||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
.stat-card.clickable {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-card.clickable::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s ease;
|
||||
}
|
||||
|
||||
.stat-card.clickable:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.stat-card.clickable:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-card.clickable:active {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.stat-card.active-filter {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.25);
|
||||
border: 2px solid #fff;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stat-card.active-filter::after {
|
||||
content: '✓';
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #28a745;
|
||||
border-radius: 50%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.stat-total {
|
||||
|
@ -161,6 +266,35 @@
|
|||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.filters-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.active-filter-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
padding: 8px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid #bbdefb;
|
||||
}
|
||||
|
||||
.active-filter-indicator mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.active-filter-indicator button {
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.filters-header h3 {
|
||||
margin: 0;
|
||||
color: #495057;
|
||||
|
@ -297,8 +431,28 @@ table {
|
|||
}
|
||||
|
||||
.command-id, .client-ip {
|
||||
font-size: 0.75rem;
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
color: #666;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.clickable-job-id {
|
||||
cursor: pointer;
|
||||
color: #1976d2 !important;
|
||||
text-decoration: underline;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.clickable-job-id:hover {
|
||||
color: #1565c0 !important;
|
||||
text-decoration: none;
|
||||
background-color: rgba(25, 118, 210, 0.1);
|
||||
padding: 2px 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.clickable-job-id:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.date-time {
|
||||
|
|
|
@ -22,23 +22,38 @@
|
|||
</div>
|
||||
|
||||
<div class="stats-container" *ngIf="!loading">
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-card stat-total clickable"
|
||||
[class.active-filter]="isFilterActive('total')"
|
||||
(click)="filterByTotal()"
|
||||
matTooltip="Ver todas las trazas">
|
||||
<div class="stat-number">{{ totalStats.total }}</div>
|
||||
<div class="stat-label">{{ 'totalTraces' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-today">
|
||||
<div class="stat-card stat-today clickable"
|
||||
[class.active-filter]="isFilterActive('today')"
|
||||
(click)="filterByToday()"
|
||||
matTooltip="Ver trazas de hoy">
|
||||
<div class="stat-number">{{ getStatusCount('today') }}</div>
|
||||
<div class="stat-label">{{ 'todayTraces' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-success">
|
||||
<div class="stat-card stat-success clickable"
|
||||
[class.active-filter]="isFilterActive('success')"
|
||||
(click)="filterBySuccess()"
|
||||
matTooltip="Ver trazas exitosas">
|
||||
<div class="stat-number">{{ getStatusCount('success') }}</div>
|
||||
<div class="stat-label">{{ 'successful' | translate }}</div>
|
||||
</div>
|
||||
<div class="stat-card stat-failed">
|
||||
<div class="stat-card stat-failed clickable"
|
||||
[class.active-filter]="isFilterActive('failed')"
|
||||
(click)="filterByFailed()"
|
||||
matTooltip="Ver trazas fallidas">
|
||||
<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-card stat-in-progress clickable"
|
||||
[class.active-filter]="isFilterActive('in-progress')"
|
||||
(click)="filterByInProgress()"
|
||||
matTooltip="Ver trazas en progreso">
|
||||
<div class="stat-number">{{ getStatusCount('in-progress') }}</div>
|
||||
<div class="stat-label">{{ 'inProgress' | translate }}</div>
|
||||
</div>
|
||||
|
@ -47,10 +62,20 @@
|
|||
<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>
|
||||
<div class="filters-header-actions">
|
||||
<div *ngIf="activeFilter" class="active-filter-indicator">
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
<span>Filtro activo: {{ getActiveFilterLabel() }}</span>
|
||||
<button mat-icon-button (click)="resetFilters(commandSearchInput, commandStatusInput, commandClientInput)"
|
||||
matTooltip="Limpiar filtros">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
<button mat-button color="primary" (click)="toggleFilters()">
|
||||
<mat-icon>{{ showAdvancedFilters ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||||
{{ showAdvancedFilters ? 'hideAdvanced' : 'showAdvanced' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container" [class.expanded]="showAdvancedFilters">
|
||||
|
@ -229,7 +254,12 @@
|
|||
<ng-container *ngSwitchCase="'command'">
|
||||
<div class="command-cell">
|
||||
<span class="command-name">{{ translateCommand(trace.command) }}</span>
|
||||
<span class="command-id">{{ trace.jobId }}</span>
|
||||
<span class="command-id"
|
||||
[class.clickable-job-id]="trace.status === 'in-progress'"
|
||||
(click)="trace.status === 'in-progress' ? openLogsInNewTab(trace) : null"
|
||||
[matTooltip]="trace.status === 'in-progress' ? 'Ver logs' : ''">
|
||||
{{ trace.jobId }}
|
||||
</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
|
@ -278,10 +308,7 @@
|
|||
<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>
|
||||
|
|
|
@ -113,6 +113,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
clientControl = new FormControl();
|
||||
filteredCommands!: Observable<any[]>;
|
||||
commandControl = new FormControl();
|
||||
activeFilter: string = '';
|
||||
|
||||
constructor(private http: HttpClient,
|
||||
private joyrideService: JoyrideService,
|
||||
|
@ -214,7 +215,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
const today = new Date();
|
||||
const todayString = this.datePipe.transform(today, 'yyyy-MM-dd');
|
||||
return this.traces.filter(trace =>
|
||||
trace.executedAt && trace.executedAt.startsWith(todayString)
|
||||
trace.executedAt && todayString && trace.executedAt.startsWith(todayString)
|
||||
).length;
|
||||
}
|
||||
|
||||
|
@ -381,6 +382,22 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
openLogsInNewTab(trace: any): void {
|
||||
if (trace.client?.ip) {
|
||||
const logsUrl = `${this.baseUrl}/pcclients/${trace.client.ip}/cgi-bin/httpd-log.sh`;
|
||||
const windowName = `logs_${trace.client.ip.replace(/\./g, '_')}`;
|
||||
const newWindow = window.open(logsUrl, windowName);
|
||||
if (newWindow) {
|
||||
newWindow.document.write(`
|
||||
<title>Logs - ${trace.client.ip}</title>
|
||||
<iframe src="${logsUrl}" width="100%" height="100%" style="border:none;"></iframe>
|
||||
`);
|
||||
}
|
||||
} else {
|
||||
this.toastService.error('No se puede acceder a los logs: IP del cliente no disponible', 'Error');
|
||||
}
|
||||
}
|
||||
|
||||
loadTraces(): void {
|
||||
this.loading = true;
|
||||
const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`;
|
||||
|
@ -461,7 +478,9 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
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`);
|
||||
const todayRequest = todayString ?
|
||||
this.http.get<any>(`${this.baseUrl}/traces?executedAt[after]=${todayString}&page=1&itemsPerPage=1`) :
|
||||
this.http.get<any>(`${this.baseUrl}/traces?page=1&itemsPerPage=1`);
|
||||
|
||||
forkJoin([totalRequest, ...requests, todayRequest]).subscribe(
|
||||
(responses) => {
|
||||
|
@ -489,7 +508,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
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
|
||||
today: this.traces.filter(trace => trace.executedAt && todayString && trace.executedAt.startsWith(todayString)).length
|
||||
};
|
||||
}
|
||||
);
|
||||
|
@ -501,6 +520,7 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
clientSearchStatusInput.value = null;
|
||||
clientSearchClientInput.value = null;
|
||||
this.filters = {};
|
||||
this.activeFilter = '';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
|
@ -583,7 +603,6 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
});
|
||||
}
|
||||
|
||||
// Métodos para paginación
|
||||
getPaginationFrom(): number {
|
||||
return (this.page * this.itemsPerPage) + 1;
|
||||
}
|
||||
|
@ -595,4 +614,67 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
|
|||
getPaginationTotal(): number {
|
||||
return this.length;
|
||||
}
|
||||
|
||||
filterByTotal(): void {
|
||||
this.resetAllFilters();
|
||||
this.activeFilter = 'total';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterByToday(): void {
|
||||
this.resetAllFilters();
|
||||
const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd');
|
||||
if (todayString) {
|
||||
this.filters['executedAt[after]'] = todayString;
|
||||
}
|
||||
this.activeFilter = 'today';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterBySuccess(): void {
|
||||
this.resetAllFilters();
|
||||
this.filters['status'] = 'success';
|
||||
this.activeFilter = 'success';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterByFailed(): void {
|
||||
this.resetAllFilters();
|
||||
this.filters['status'] = 'failed';
|
||||
this.activeFilter = 'failed';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
filterByInProgress(): void {
|
||||
this.resetAllFilters();
|
||||
this.filters['status'] = 'in-progress';
|
||||
this.activeFilter = 'in-progress';
|
||||
this.loadTraces();
|
||||
}
|
||||
|
||||
isFilterActive(filterType: string): boolean {
|
||||
return this.activeFilter === filterType;
|
||||
}
|
||||
|
||||
getActiveFilterLabel(): string {
|
||||
switch(this.activeFilter) {
|
||||
case 'total':
|
||||
return 'Todas las trazas';
|
||||
case 'today':
|
||||
return 'Trazas de hoy';
|
||||
case 'success':
|
||||
return 'Trazas exitosas';
|
||||
case 'failed':
|
||||
return 'Trazas fallidas';
|
||||
case 'in-progress':
|
||||
return 'Trazas en progreso';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private resetAllFilters(): void {
|
||||
this.filters = {};
|
||||
this.activeFilter = '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export class HeaderComponent implements OnInit {
|
|||
|
||||
showGlobalStatus() {
|
||||
this.dialog.open(GlobalStatusComponent, {
|
||||
width: '5vw',
|
||||
width: '65vw',
|
||||
height: '80vh',
|
||||
})
|
||||
}
|
||||
|
|
|
@ -580,5 +580,6 @@
|
|||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
"busy": "Busy",
|
||||
"cancelTask": "Cancel task"
|
||||
"cancelTask": "Cancel task",
|
||||
"clickStatsToFilter": "Click on the statistics cards to filter the traces"
|
||||
}
|
||||
|
|
|
@ -585,6 +585,7 @@
|
|||
"offline": "Offline",
|
||||
"online": "Online",
|
||||
"busy": "Ocupado",
|
||||
"cancelTask": "Cancelar tarea"
|
||||
"cancelTask": "Cancelar tarea",
|
||||
"clickStatsToFilter": "Haz clic en las tarjetas de estadísticas para filtrar las trazas"
|
||||
}
|
||||
|
Loading…
Reference in New Issue