refs 2337. Global status new UX
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/28/head
Manuel Aranda Rosales 2025-06-26 15:49:44 +02:00
parent 5b8dba4835
commit 3e8f8cc3db
5 changed files with 1557 additions and 251 deletions

View File

@ -1,44 +1,557 @@
/* ===== HEADER DE BIENVENIDA ===== */
.welcome-header {
background: #3f51b5;
color: white;
padding: 2rem 2rem 1.5rem 2rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
position: relative;
overflow: hidden;
}
.welcome-header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="grain" width="100" height="100" patternUnits="userSpaceOnUse"><circle cx="25" cy="25" r="1" fill="white" opacity="0.1"/><circle cx="75" cy="75" r="1" fill="white" opacity="0.1"/><circle cx="50" cy="10" r="0.5" fill="white" opacity="0.1"/><circle cx="10" cy="60" r="0.5" fill="white" opacity="0.1"/><circle cx="90" cy="40" r="0.5" fill="white" opacity="0.1"/></pattern></defs><rect width="100" height="100" fill="url(%23grain)"/></svg>');
opacity: 0.2;
}
.welcome-content {
display: flex;
align-items: center;
gap: 1.5rem;
position: relative;
z-index: 1;
}
.welcome-icon {
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
width: 60px;
height: 60px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
}
.welcome-icon mat-icon {
font-size: 32px;
width: 32px;
height: 32px;
color: white;
}
.welcome-text {
flex: 1;
}
.welcome-title {
margin: 0 0 0.5rem 0;
font-size: 2rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.welcome-subtitle {
margin: 0 0 0.25rem 0;
font-size: 1.1rem;
font-weight: 500;
opacity: 0.95;
}
.welcome-description {
margin: 0;
font-size: 0.9rem;
opacity: 0.8;
}
.welcome-actions {
display: flex;
gap: 0.5rem;
position: relative;
z-index: 1;
}
.help-button,
.refresh-button {
background: rgba(255, 255, 255, 0.2) !important;
color: white !important;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.help-button:hover,
.refresh-button:hover {
background: rgba(255, 255, 255, 0.3) !important;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
/* ===== CONTENIDO PRINCIPAL ===== */
mat-dialog-content {
height: calc(100% - 64px);
overflow: auto;
padding-top: 0.5em !important;
height: calc(100% - 200px);
overflow: auto;
padding: 0 !important;
background: #f8f9fa;
}
.content-container {
min-height: 100%;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
.main-content {
padding: 2rem;
}
/* ===== SPINNER DE CARGA ===== */
.spinner-container {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1000;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.loading-content {
text-align: center;
}
.loading-spinner {
width: 100px;
height: 100px;
width: 60px !important;
height: 60px !important;
margin-bottom: 1rem;
}
.loading-text {
margin: 0;
color: #666;
font-weight: 500;
}
/* ===== RESUMEN DEL SISTEMA ===== */
.system-overview {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.overview-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
}
.overview-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
opacity: 0;
transition: opacity 0.3s ease;
}
.overview-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
border-color: #667eea;
}
.overview-card:hover::before {
opacity: 1;
}
.overview-icon {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.overview-icon mat-icon {
color: white;
font-size: 24px;
width: 24px;
height: 24px;
}
.overview-content h3 {
margin: 0 0 0.5rem 0;
font-size: 1.1rem;
font-weight: 600;
color: #2c3e50;
}
.overview-content p {
margin: 0;
font-size: 0.9rem;
color: #6c757d;
}
/* ===== BADGES DE ESTADO ===== */
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.status-badge.online {
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
}
.status-badge.offline {
background: linear-gradient(135deg, #dc3545, #c82333);
color: white;
}
.status-badge.info {
background: linear-gradient(135deg, #17a2b8, #138496);
color: white;
}
/* ===== TABS PRINCIPALES ===== */
.main-tabs {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
}
::ng-deep .main-tabs .mat-tab-header {
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
::ng-deep .main-tabs .mat-tab-label {
font-weight: 600;
color: #6c757d;
transition: all 0.3s ease;
}
::ng-deep .main-tabs .mat-tab-label-active {
color: #667eea;
}
::ng-deep .main-tabs .mat-ink-bar {
background: linear-gradient(90deg, #667eea, #764ba2);
height: 3px;
}
.tab-content {
padding: 2rem;
}
/* ===== CONTENEDOR DE ERRORES ===== */
.error-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
padding: 2rem;
}
.error-card {
margin: 20px auto;
max-width: 600px;
text-align: center;
background-color: rgb(243, 243, 243);
color: rgb(48, 48, 48);
background: white;
border-radius: 16px;
padding: 3rem 2rem;
text-align: center;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
max-width: 400px;
}
.error-icon {
font-size: 64px;
width: 64px;
height: 64px;
color: #dc3545;
margin-bottom: 1rem;
}
.error-card h3 {
margin: 0 0 1rem 0;
color: #2c3e50;
font-weight: 600;
}
.error-card p {
margin-top: 0;
margin: 0 0 2rem 0;
color: #6c757d;
line-height: 1.5;
}
/* ===== REPOSITORIOS ===== */
.repositories-container {
padding: 1rem;
}
.repositories-selector-container {
display: flex;
flex-direction: column;
gap: 2rem;
}
.repository-selector {
display: flex;
justify-content: center;
padding: 1rem 0;
}
.repository-content {
padding: 1rem;
}
.repository-select-field {
min-width: 300px;
max-width: 500px;
width: 100%;
}
.selected-repository-content {
animation: fadeInUp 0.6s ease-out;
}
.no-repository-selected {
text-align: center;
padding: 3rem 2rem;
color: #6c757d;
}
.no-repository-selected .no-data-icon {
font-size: 4rem;
width: 4rem;
height: 4rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.no-repository-selected h3 {
margin: 0 0 1rem 0;
color: #2c3e50;
font-weight: 600;
}
.no-repository-selected p {
margin: 0;
font-size: 1rem;
line-height: 1.5;
}
/* ===== FOOTER CON ACCIONES ===== */
.action-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
background: white;
border-top: 1px solid #e9ecef;
border-radius: 0 0 12px 12px;
}
.action-info {
flex: 1;
}
.last-update {
margin: 0;
font-size: 0.85rem;
color: #6c757d;
}
.action-buttons {
display: flex;
gap: 1rem;
}
.secondary-button {
color: #6c757d !important;
border: 1px solid #dee2e6 !important;
transition: all 0.3s ease;
}
.secondary-button:hover {
background: #f8f9fa !important;
border-color: #667eea !important;
color: #667eea !important;
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.welcome-header {
padding: 1.5rem 1rem 1rem 1rem;
flex-direction: column;
gap: 1rem;
text-align: center;
}
.welcome-content {
flex-direction: column;
gap: 1rem;
}
.welcome-title {
font-size: 1.5rem;
}
.welcome-subtitle {
font-size: 1rem;
}
.main-content {
padding: 1rem;
}
.system-overview {
grid-template-columns: 1fr;
gap: 1rem;
}
.overview-card {
padding: 1rem;
}
.action-container {
flex-direction: column;
gap: 1rem;
text-align: center;
}
.action-buttons {
width: 100%;
justify-content: center;
}
}
@media (max-width: 480px) {
.welcome-header {
padding: 1rem;
}
.welcome-icon {
width: 50px;
height: 50px;
}
.welcome-icon mat-icon {
font-size: 28px;
width: 28px;
height: 28px;
}
.welcome-title {
font-size: 1.25rem;
}
.overview-card {
flex-direction: column;
text-align: center;
}
.tab-content {
padding: 1rem;
}
.repository-select-field {
min-width: 250px;
max-width: 100%;
}
}
/* ===== ANIMACIONES ===== */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.system-overview,
.main-tabs {
animation: fadeInUp 0.6s ease-out;
}
.overview-card {
animation: fadeInUp 0.6s ease-out;
}
.overview-card:nth-child(1) { animation-delay: 0.1s; }
.overview-card:nth-child(2) { animation-delay: 0.2s; }
.overview-card:nth-child(3) { animation-delay: 0.3s; }
/* ===== ESTILOS GLOBALES DEL DIALOG ===== */
::ng-deep .mat-dialog-container {
border-radius: 16px !important;
overflow: hidden !important;
padding: 0 !important;
}
::ng-deep .mat-dialog-content {
margin: 0 !important;
padding: 0 !important;
}
/* ===== LOADING INDIVIDUAL POR TAB ===== */
.loading-container {
display: flex;
justify-content: center;
align-items: center;
min-height: 300px;
padding: 2rem;
}
.loading-container .loading-content {
text-align: center;
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 2rem;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(10px);
}
.loading-container .loading-spinner {
width: 60px !important;
height: 60px !important;
margin-bottom: 1rem;
}
.loading-container .loading-text {
margin: 0;
color: #666;
font-weight: 500;
}

View File

@ -1,68 +1,209 @@
<header>
<h1 mat-dialog-title>{{'GlobalStatus' | translate}}</h1>
</header>
<mat-dialog-content [ngClass]="{'loading': loading}">
<div class="spinner-container" *ngIf="loading">
<mat-spinner class="loading-spinner"></mat-spinner>
<!-- Header con bienvenida -->
<div class="welcome-header">
<div class="welcome-content">
<div class="welcome-icon">
<mat-icon>dashboard</mat-icon>
</div>
<div class="welcome-text">
<h1 class="welcome-title">{{ 'GlobalStatus' | translate }}</h1>
<p class="welcome-subtitle">Bienvenido a la consola de administración de OpenGnsys</p>
<p class="welcome-description">Estado general de todos los componentes de la plataforma</p>
</div>
</div>
<mat-tab-group (selectedTabChange)="onTabChange($event)">
<mat-tab label="OgBoot">
<div *ngIf="!loading && !errorOgBoot" class="content-container">
<app-status-tab [loading]="loading" [diskUsage]="ogBootDiskUsage" [servicesStatus]="ogBootServicesStatus"
[installedOgLives]="installedOgLives" [diskUsageChartData]="ogBootDiskUsageChartData" [view]="view"
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp"
[isRepository]="false">
</app-status-tab>
</div>
<mat-card *ngIf="!loading && errorOgBoot" class="error-card">
<mat-card-content>
<p>{{ 'errorLoadingData' | translate }}</p>
</mat-card-content>
</mat-card>
</mat-tab>
<div class="welcome-actions">
<button mat-icon-button class="refresh-button" (click)="refreshAll()" matTooltip="Actualizar datos">
<mat-icon>refresh</mat-icon>
</button>
</div>
</div>
<mat-tab label="Dhcp">
<div *ngIf="!loading && !errorDhcp" class="content-container">
<app-status-tab [loading]="loading" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="isDhcp" [isRepository]="false">
</app-status-tab>
</div>
<mat-card *ngIf="!loading && errorDhcp" class="error-card">
<mat-card-content>
<p>{{ 'errorLoadingData' | translate }}</p>
</mat-card-content>
</mat-card>
</mat-tab>
<!-- Contenido principal -->
<mat-dialog-content [ngClass]="{'loading': loading}">
<!-- Spinner de carga -->
<div class="spinner-container" *ngIf="loading">
<div class="loading-content">
<mat-spinner class="loading-spinner"></mat-spinner>
<p class="loading-text">Cargando estado del sistema...</p>
</div>
</div>
<mat-tab label="{{ 'repositoryLabel' | translate }}">
<mat-tab-group>
<mat-tab *ngFor="let repository of repositories" [label]="repository.name">
<div *ngIf="!loading && !errorRepositories[repository.uuid] && repositoryStatuses[repository.uuid]">
<app-status-tab [loading]="loading" [diskUsage]="repositoryStatuses[repository.uuid].disk"
[servicesStatus]="repositoryStatuses[repository.uuid].services"
[processesStatus]="repositoryStatuses[repository.uuid].processes"
[ramUsage]="repositoryStatuses[repository.uuid].ram" [cpuUsage]="repositoryStatuses[repository.uuid].cpu"
[diskUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[repository.uuid].disk.used },
{ name: 'Disponible', value: repositoryStatuses[repository.uuid].disk.available }
]" [ramUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[repository.uuid].ram.used },
{ name: 'Disponible', value: repositoryStatuses[repository.uuid].ram.available }
]" [view]="view" [colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels"
[isDhcp]="false" [isRepository]="true">
</app-status-tab>
<!-- Contenido principal cuando no está cargando -->
<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>storage</mat-icon>
</div>
<div class="overview-content">
<h3>OgBoot Server</h3>
<p *ngIf="!errorOgBoot">Estado: <span class="status-badge online">Operativo</span></p>
<p *ngIf="errorOgBoot">Estado: <span class="status-badge offline">Error</span></p>
</div>
</div>
<div class="overview-card">
<div class="overview-icon">
<mat-icon>router</mat-icon>
</div>
<div class="overview-content">
<h3>DHCP Server</h3>
<p *ngIf="!errorDhcp">Estado: <span class="status-badge online">Operativo</span></p>
<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>
<mat-card *ngIf="!loading && errorRepositories[repository.uuid]" class="error-card">
<mat-card-content>
<p>{{ 'errorLoadingData' | translate }}</p>
</mat-card-content>
</mat-card>
</mat-tab>
</mat-tab-group>
</mat-tab>
</mat-tab-group>
</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">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>No hay repositorios disponibles</h3>
<p>No se encontraron repositorios configurados en el sistema.</p>
</div>
<div *ngIf="repositories.length > 0" class="repositories-selector-container">
<!-- Selector de repositorio -->
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>Seleccionar repositorio</mat-label>
<mat-select [(ngModel)]="selectedRepositoryUuid" (selectionChange)="onRepositoryChange($event.value)">
<mat-option *ngFor="let repository of repositories" [value]="repository.uuid">
{{ repository.name }}
</mat-option>
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
<!-- Información del repositorio seleccionado -->
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
<div class="repository-item">
<div class="repository-header">
<h3>{{ getSelectedRepositoryName() }}</h3>
</div>
<div class="repository-content">
<app-status-tab [loading]="loading" [diskUsage]="repositoryStatuses[selectedRepositoryUuid].disk"
[servicesStatus]="repositoryStatuses[selectedRepositoryUuid].services"
[processesStatus]="repositoryStatuses[selectedRepositoryUuid].processes"
[ramUsage]="repositoryStatuses[selectedRepositoryUuid].ram" [cpuUsage]="repositoryStatuses[selectedRepositoryUuid].cpu"
[diskUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].disk.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].disk.available }
]" [ramUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].ram.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].ram.available }
]" [view]="view" [colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels"
[isDhcp]="false" [isRepository]="true">
</app-status-tab>
</div>
</div>
</div>
<!-- Mensaje cuando no hay repositorio seleccionado -->
<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>
</div>
<!-- Error del repositorio seleccionado -->
<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>
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
</div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</mat-dialog-content>
<!-- Footer con acciones -->
<mat-dialog-actions class="action-container">
<button class="ordinary-button" [mat-dialog-close]="true">{{ 'closeButton' | translate }}</button>
<div class="action-info">
<p class="last-update">Última actualización: {{ lastUpdateTime }}</p>
</div>
<div class="action-buttons">
<button mat-button class="secondary-button" (click)="refreshAll()">
<mat-icon>refresh</mat-icon>
Actualizar
</button>
<button mat-raised-button color="primary" [mat-dialog-close]="true">
{{ 'closeButton' | translate }}
</button>
</div>
</mat-dialog-actions>

View File

@ -12,6 +12,9 @@ import {ToastrService} from "ngx-toastr";
export class GlobalStatusComponent implements OnInit {
baseUrl: string;
loading: boolean = false;
loadingOgBoot: boolean = false;
loadingDhcp: boolean = false;
loadingRepositories: boolean = false;
errorOgBoot: boolean = false;
errorDhcp: boolean = false;
errorRepositories: { [key: string]: boolean } = {};
@ -26,6 +29,8 @@ export class GlobalStatusComponent implements OnInit {
repositoriesUrl: string;
repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
lastUpdateTime: string = '';
selectedRepositoryUuid: string = '';
ogBootApiUrl: string;
ogBootDiskUsage: any = {};
@ -48,13 +53,21 @@ export class GlobalStatusComponent implements OnInit {
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
this.ogBootDiskUsageChartData = [];
this.dhcpDiskUsageChartData = [];
}
ngOnInit(): void {
this.updateLastUpdateTime();
this.loadOgBootStatus();
this.syncSubnets()
this.syncTemplates()
this.syncOgLives()
this.loadDhcpStatus();
this.loadRepositories(false);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
}
syncSubnets() {
@ -106,11 +119,26 @@ export class GlobalStatusComponent implements OnInit {
[key: string]: any;
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string): void {
this.loading = true;
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
if (isDhcp) {
this.loadingDhcp = true;
} else {
this.loadingOgBoot = true;
}
if (showLoading) {
this.loading = true;
}
this[errorState] = false;
const timeoutId = setTimeout(() => {
this.loading = false;
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcp = false;
} else {
this.loadingOgBoot = false;
}
this[errorState] = true;
}, 3500);
this.http.get<any>(apiUrl).subscribe({
@ -140,23 +168,41 @@ export class GlobalStatusComponent implements OnInit {
{ name: 'Disponible', value: parseFloat(diskUsage.available) }
);
this.loading = false;
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcp = false;
} else {
this.loadingOgBoot = false;
}
clearTimeout(timeoutId);
},
error: error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
this.loading = false;
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcp = false;
} else {
this.loadingOgBoot = false;
}
this[errorState] = true;
clearTimeout(timeoutId);
}
});
}
loadRepositories(): void {
this.loading = true;
loadRepositories(showLoading: boolean = true): void {
if (showLoading) {
this.loading = true;
}
this.errorRepositories = {};
const timeoutId = setTimeout(() => {
this.loading = false;
if (showLoading) {
this.loading = false;
}
this.repositories.forEach(repository => {
if (!(repository.uuid in this.errorRepositories)) {
this.errorRepositories[repository.uuid] = true;
@ -176,14 +222,15 @@ export class GlobalStatusComponent implements OnInit {
this.errorRepositories[repository.uuid] = errorOccurred;
if (remainingRepositories === 0) {
this.loading = false;
if (showLoading) {
this.loading = false;
}
clearTimeout(timeoutId);
}
});
});
},
error => {
console.error('Error fetching repositories', error);
this.loading = false;
this.repositories.forEach(repository => {
this.errorRepositories[repository.uuid] = true;
@ -226,23 +273,86 @@ export class GlobalStatusComponent implements OnInit {
loadOgBootStatus(): void {
this.isDhcp = false;
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot');
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
}
loadDhcpStatus(): void {
this.isDhcp = true;
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp');
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', false);
}
onTabChange(event: MatTabChangeEvent): void {
if (event.tab.textLabel === 'OgBoot') {
this.loadOgBootStatus();
} else if (event.tab.textLabel === 'Dhcp') {
this.loadDhcpStatus();
} else if (event.tab.textLabel === 'Repositorios') {
if (this.repositories.length === 0) {
this.loadRepositories();
}
switch (event.index) {
case 0:
this.loadOgBootStatus();
break;
case 1:
this.loadDhcpStatus();
break;
case 2:
if (this.repositories.length === 0) {
this.loadRepositories(false);
}
break;
default:
break;
}
}
onRepositoryChange(repositoryUuid: string): void {
this.selectedRepositoryUuid = repositoryUuid;
if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorRepositories[repositoryUuid] = true;
}
});
}
}
getSelectedRepositoryName(): string {
const selectedRepo = this.repositories.find(repo => repo.uuid === this.selectedRepositoryUuid);
return selectedRepo ? selectedRepo.name : '';
}
refreshAll(): void {
this.loading = true;
this.updateLastUpdateTime();
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, true, 'errorDhcp', true);
this.loadRepositories(true);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
this.toastService.success('Datos actualizados correctamente');
}
updateLastUpdateTime(): void {
const now = new Date();
this.lastUpdateTime = now.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
getCurrentTime(): string {
const now = new Date();
return now.toLocaleTimeString('es-ES', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
}
retryRepositoryStatus(repositoryUuid: string): void {
this.loadRepositoryStatus(repositoryUuid, (errorOccurred: boolean) => {
this.errorRepositories[repositoryUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success('Estado del repositorio actualizado correctamente');
} else {
this.toastService.error('Error al cargar el estado del repositorio');
}
});
}
}

View File

@ -1,84 +1,533 @@
/* ===== LAYOUT PRINCIPAL ===== */
.dashboard {
display: flex;
flex-direction: column;
gap: 2rem;
padding: 0;
}
.disk-usage-container,
.ram-usage-container {
/* ===== SECCIÓN DE RECURSOS ===== */
.resources-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 1.5rem;
}
/* Layout específico para repositorios - gráficas en una sola fila */
.resources-section.repository-layout {
grid-template-columns: 1fr !important;
gap: 1.5rem;
}
.resources-section.repository-layout .resource-card {
min-width: 0;
}
.resource-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.resource-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #667eea, #764ba2);
opacity: 0;
transition: opacity 0.3s ease;
}
.resource-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
border-color: #667eea;
}
.resource-card:hover::before {
opacity: 1;
}
.resource-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.disk-usage,
.ram-usage {
.resource-icon {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.resource-icon mat-icon {
color: white;
font-size: 20px;
width: 20px;
height: 20px;
}
.resource-title {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.resource-content {
display: flex;
gap: 1.5rem;
align-items: center;
}
.chart-container {
flex: 1;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.service-list,
.process-list {
margin-top: 0em;
margin-bottom: 0.5em;
}
.services-status,
.processes-status {
.resource-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.services-status li {
margin: 5px 0;
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #f1f3f4;
}
.info-item:last-child {
border-bottom: none;
}
.info-label {
font-size: 0.9rem;
color: #6c757d;
font-weight: 500;
}
.info-value {
font-size: 0.9rem;
color: #2c3e50;
font-weight: 600;
}
.usage-percentage {
color: #667eea;
font-weight: 700;
}
/* ===== CPU USAGE DISPLAY ===== */
.cpu-usage-display {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
.cpu-circle {
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
width: 120px;
height: 120px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
text-align: center;
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.cpu-percentage {
font-size: 1.8rem;
font-weight: 700;
line-height: 1;
margin-bottom: 0.25rem;
}
.cpu-label {
font-size: 0.8rem;
opacity: 0.9;
}
/* ===== SECCIÓN DE SERVICIOS ===== */
.services-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.service-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.service-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.service-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.processes-status li {
margin: 5px 0;
.service-icon {
background: linear-gradient(135deg, #28a745, #20c997);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.status-led {
width: 10px;
height: 10px;
.service-icon mat-icon {
color: white;
font-size: 20px;
width: 20px;
height: 20px;
}
.service-title {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.service-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.service-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
background: #f8f9fa;
border-radius: 8px;
transition: all 0.3s ease;
}
.service-item:hover {
background: #e9ecef;
transform: translateX(4px);
}
.service-status {
display: flex;
align-items: center;
gap: 0.75rem;
}
.status-indicator {
width: 12px;
height: 12px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
transition: all 0.3s ease;
}
.status-led.active {
background-color: green;
.status-indicator.active {
background: linear-gradient(135deg, #28a745, #20c997);
box-shadow: 0 0 8px rgba(40, 167, 69, 0.4);
}
.status-led.inactive {
background-color: red;
.status-indicator.inactive {
background: linear-gradient(135deg, #dc3545, #c82333);
box-shadow: 0 0 8px rgba(220, 53, 69, 0.4);
}
.disk-title,
.ram-title {
margin-bottom: 0px;
.service-name {
font-size: 0.9rem;
font-weight: 500;
color: #2c3e50;
}
.service-title,
.process-title {
margin-top: 0px;
.service-state {
font-size: 0.8rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 0.25rem 0.75rem;
border-radius: 12px;
transition: all 0.3s ease;
}
table {
.service-state.active {
background: linear-gradient(135deg, #28a745, #20c997);
color: white;
}
.service-state.inactive {
background: linear-gradient(135deg, #dc3545, #c82333);
color: white;
}
/* ===== SECCIÓN DE DATOS ===== */
.data-section {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.data-card {
background: white;
border-radius: 16px;
padding: 1.5rem;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
}
.data-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.data-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.data-icon {
background: linear-gradient(135deg, #17a2b8, #138496);
border-radius: 50%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.data-icon mat-icon {
color: white;
font-size: 20px;
width: 20px;
height: 20px;
}
.data-title {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
color: #2c3e50;
}
.table-container {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.data-table {
width: 100%;
border-collapse: collapse;
background: white;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
.data-table th {
background: linear-gradient(135deg, #f8f9fa, #e9ecef);
color: #495057;
padding: 1rem;
font-weight: 600;
text-align: left;
border-bottom: 2px solid #dee2e6;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
th {
background-color: #f4f4f4;
.data-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #f1f3f4;
color: #2c3e50;
font-size: 0.9rem;
}
.data-table tr:hover {
background: #f8f9fa;
}
.data-table tr:last-child td {
border-bottom: none;
}
/* ===== RESPONSIVE ===== */
@media (max-width: 768px) {
.dashboard {
gap: 1rem;
}
.resources-section {
grid-template-columns: 1fr;
gap: 1rem;
}
.services-section {
grid-template-columns: 1fr;
gap: 1rem;
}
.resource-content {
flex-direction: column;
gap: 1rem;
}
.resource-card,
.service-card,
.data-card {
padding: 1rem;
}
.cpu-circle {
width: 100px;
height: 100px;
}
.cpu-percentage {
font-size: 1.5rem;
}
.service-item {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.service-item:hover {
transform: none;
}
}
@media (max-width: 480px) {
.resource-header,
.service-header,
.data-header {
flex-direction: column;
text-align: center;
gap: 0.5rem;
}
.resource-icon,
.service-icon,
.data-icon {
width: 35px;
height: 35px;
}
.resource-icon mat-icon,
.service-icon mat-icon,
.data-icon mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.resource-title,
.service-title,
.data-title {
font-size: 1.1rem;
}
.info-item {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
}
.data-table th,
.data-table td {
padding: 0.5rem;
font-size: 0.8rem;
}
}
/* ===== ANIMACIONES ===== */
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.resource-card,
.service-card,
.data-card {
animation: slideInUp 0.6s ease-out;
}
.resource-card:nth-child(1) { animation-delay: 0.1s; }
.resource-card:nth-child(2) { animation-delay: 0.2s; }
.resource-card:nth-child(3) { animation-delay: 0.3s; }
.service-card:nth-child(1) { animation-delay: 0.4s; }
.service-card:nth-child(2) { animation-delay: 0.5s; }
.data-card { animation-delay: 0.6s; }
/* ===== ESTILOS PARA LOS GRÁFICOS ===== */
::ng-deep .chart-container ngx-charts-pie-chart {
width: 100% !important;
height: auto !important;
}
::ng-deep .chart-container ngx-charts-pie-chart .chart-legend {
position: relative !important;
top: auto !important;
left: auto !important;
right: auto !important;
bottom: auto !important;
display: block !important;
margin-top: 1rem !important;
}
::ng-deep .chart-container ngx-charts-pie-chart .chart-legend .legend-labels {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: center !important;
}
::ng-deep .chart-container ngx-charts-pie-chart .chart-legend .legend-label {
display: flex !important;
align-items: center !important;
margin: 0.25rem 0 !important;
}

View File

@ -1,113 +1,206 @@
<app-loading [isLoading]="loading"></app-loading>
<div *ngIf="!loading" class="dashboard">
<!-- Disk Usage Section -->
<div class="disk-usage-container">
<h3 class="disk-title">{{ 'diskUsageTitle' | translate }}</h3>
<div class="disk-usage" joyrideStep="diskUsageStep" text="{{ 'diskUsageDescription' | translate }}">
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="diskUsageChartData" [doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<p>{{ 'totalLabel' | translate }}: <strong>{{ isRepository ? diskUsage.total : formatBytes(diskUsage.total) }}</strong></p>
<p>{{ 'usedLabel' | translate }}: <strong>{{ isRepository ? diskUsage.used : formatBytes(diskUsage.used) }}</strong></p>
<p>{{ 'availableLabel' | translate }}: <strong>{{ isRepository ? diskUsage.available : formatBytes(diskUsage.available) }}</strong></p>
<p>{{ 'usedPercentageLabel' | translate }}: <strong>{{ isRepository ? diskUsage.used_percentage : diskUsage.percentage }}</strong></p>
<!-- Sección de uso de recursos -->
<div class="resources-section" [ngClass]="{'repository-layout': isRepository}">
<!-- Disk Usage Section -->
<div class="resource-card disk-usage-container">
<div class="resource-header">
<div class="resource-icon">
<mat-icon>storage</mat-icon>
</div>
<h3 class="resource-title">{{ 'diskUsageTitle' | translate }}</h3>
</div>
<div class="resource-content">
<div class="chart-container" joyrideStep="diskUsageStep" text="{{ 'diskUsageDescription' | translate }}">
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="diskUsageChartData" [doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
</div>
<div class="resource-info">
<div class="info-item">
<span class="info-label">{{ 'totalLabel' | translate }}:</span>
<span class="info-value">{{ isRepository ? diskUsage.total : formatBytes(diskUsage.total) }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedLabel' | translate }}:</span>
<span class="info-value">{{ isRepository ? diskUsage.used : formatBytes(diskUsage.used) }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'availableLabel' | translate }}:</span>
<span class="info-value">{{ isRepository ? diskUsage.available : formatBytes(diskUsage.available) }}</span>
</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>
</div>
</div>
</div>
</div>
<!-- RAM Usage Section -->
<div class="resource-card ram-usage-container" *ngIf="isRepository">
<div class="resource-header">
<div class="resource-icon">
<mat-icon>memory</mat-icon>
</div>
<h3 class="resource-title">{{ 'RamUsage' | translate }}</h3>
</div>
<div class="resource-content">
<div class="chart-container">
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="ramUsageChartData" [doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
</div>
<div class="resource-info">
<div class="info-item">
<span class="info-label">{{ 'totalLabel' | translate }}:</span>
<span class="info-value">{{ ramUsage.total }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedLabel' | translate }}:</span>
<span class="info-value">{{ ramUsage.used }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'availableLabel' | translate }}:</span>
<span class="info-value">{{ ramUsage.available }}</span>
</div>
<div class="info-item">
<span class="info-label">{{ 'usedPercentageLabel' | translate }}:</span>
<span class="info-value usage-percentage">{{ ramUsage.used_percentage }}</span>
</div>
</div>
</div>
</div>
<!-- CPU Usage Section -->
<div class="resource-card cpu-usage-container" *ngIf="isRepository">
<div class="resource-header">
<div class="resource-icon">
<mat-icon>speed</mat-icon>
</div>
<h3 class="resource-title">{{ 'CpuUsage' | translate }}</h3>
</div>
<div class="resource-content">
<div class="cpu-usage-display">
<div class="cpu-circle">
<div class="cpu-percentage">{{ cpuUsage.used_percentage }}</div>
<div class="cpu-label">Uso actual</div>
</div>
</div>
</div>
</div>
</div>
<!-- RAM Usage Section -->
<div class="ram-usage-container" *ngIf="isRepository">
<h3 class="ram-title">{{ 'RamUsage' | translate }}</h3>
<div class="ram-usage">
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="ramUsageChartData" [doughnut]="isDoughnut"
[labels]="showLabels">
</ngx-charts-pie-chart>
<div class="ram-usage-info">
<p>{{ 'totalLabel' | translate }}: <strong>{{ ramUsage.total }}</strong></p>
<p>{{ 'usedLabel' | translate }}: <strong>{{ ramUsage.used }}</strong></p>
<p>{{ 'availableLabel' | translate }}: <strong>{{ ramUsage.available }}</strong></p>
<p>{{ 'usedPercentageLabel' | translate }}: <strong>{{ ramUsage.used_percentage }}</strong></p>
<!-- Sección de servicios y procesos -->
<div class="services-section">
<!-- Services Status Section -->
<div class="service-card services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
<div class="service-header">
<div class="service-icon">
<mat-icon>settings</mat-icon>
</div>
<h3 class="service-title">{{ 'servicesTitle' | translate }}</h3>
</div>
<div class="service-list">
<div class="service-item" *ngFor="let service of getServices()">
<div class="service-status">
<span class="status-indicator"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"></span>
<span class="service-name">{{ service.name }}</span>
</div>
<span class="service-state" [ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }">
{{ service.status | translate }}
</span>
</div>
</div>
</div>
<!-- Processes Status Section -->
<div class="service-card processes-status" *ngIf="isRepository">
<div class="service-header">
<div class="service-icon">
<mat-icon>list_alt</mat-icon>
</div>
<h3 class="service-title">{{ 'processes' | translate }}</h3>
</div>
<div class="service-list">
<div class="service-item" *ngFor="let process of getProcesses()">
<div class="service-status">
<span class="status-indicator"
[ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }"></span>
<span class="service-name">{{ process.name }}</span>
</div>
<span class="service-state" [ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }">
{{ process.status }}
</span>
</div>
</div>
</div>
</div>
<!-- CPU Usage Section -->
<div class="cpu-usage-container" *ngIf="isRepository">
<h3 class="cpu-title">{{ 'CpuUsage' | translate }}</h3>
<div class="cpu-usage">
<p>{{ 'usedLabel' | translate }}: <strong>{{ cpuUsage.used_percentage }}</strong></p>
<!-- Sección de datos específicos -->
<div class="data-section">
<!-- Installed OgLives Section -->
<div class="data-card" *ngIf="!isRepository && !isDhcp">
<div class="data-header">
<div class="data-icon">
<mat-icon>computer</mat-icon>
</div>
<h3 class="data-title">{{ 'InstalledOglivesTitle' | translate }}</h3>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'kernelLabel' | translate }}</th>
<th>{{ 'architectureLabel' | translate }}</th>
<th>{{ 'revisionLabel' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of installedOgLives">
<td>{{ item.id }}</td>
<td>{{ item.kernel }}</td>
<td>{{ item.architecture }}</td>
<td>{{ item.revision }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Services Status Section -->
<div class="services-status" joyrideStep="servicesStatusStep" text="{{ 'servicesStatusDescription' | translate }}">
<h3 class="service-title">{{ 'servicesTitle' | translate }}</h3>
<ul class="service-list">
<li *ngFor="let service of getServices()">
<span class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"></span>
{{ service.name }}: {{ service.status | translate }}
</li>
</ul>
</div>
<!-- Processes Status Section -->
<div class="processes-status" *ngIf="isRepository">
<h3 class="process-title">{{ 'processes' | translate }}</h3>
<ul class="process-list">
<li *ngFor="let process of getProcesses()">
<span class="status-led"
[ngClass]="{ 'active': process.status === 'running', 'inactive': process.status !== 'running' }"></span>
{{ process.name }}: {{ process.status }}
</li>
</ul>
</div>
<!-- Installed OgLives / Subnets Section -->
<div *ngIf="!isRepository && !isDhcp">
<h3>{{ 'InstalledOglivesTitle' | translate }}</h3>
<table>
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'kernelLabel' | translate }}</th>
<th>{{ 'architectureLabel' | translate }}</th>
<th>{{ 'revisionLabel' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of installedOgLives">
<td>{{ item.id }}</td>
<td>{{ item.kernel }}</td>
<td>{{ item.architecture }}</td>
<td>{{ item.revision }}</td>
</tr>
</tbody>
</table>
</div>
<div *ngIf="isDhcp">
<h3>{{ 'subnets' | translate }}</h3>
<table>
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'bootFileNameLabel' | translate }}</th>
<th>{{ 'nextServerLabel' | translate }}</th>
<th>{{ 'ipLabel' | translate }}</th>
<th>{{ 'clientsLabel' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of subnets">
<td>{{ item.id }}</td>
<td>{{ item['boot-file-name'] }}</td>
<td>{{ item['next-server'] }}</td>
<td>{{ item.subnet }}</td>
<td>{{ item.reservations.length }}</td>
</tr>
</tbody>
</table>
<!-- Subnets Section -->
<div class="data-card" *ngIf="isDhcp">
<div class="data-header">
<div class="data-icon">
<mat-icon>router</mat-icon>
</div>
<h3 class="data-title">{{ 'subnets' | translate }}</h3>
</div>
<div class="table-container">
<table class="data-table">
<thead>
<tr>
<th>{{ 'idLabel' | translate }}</th>
<th>{{ 'bootFileNameLabel' | translate }}</th>
<th>{{ 'nextServerLabel' | translate }}</th>
<th>{{ 'ipLabel' | translate }}</th>
<th>{{ 'clientsLabel' | translate }}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let item of subnets">
<td>{{ item.id }}</td>
<td>{{ item['boot-file-name'] }}</td>
<td>{{ item['next-server'] }}</td>
<td>{{ item.subnet }}</td>
<td>{{ item.reservations.length }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>