Estructurado componente de estado global para que cada elemento sea un componente independiente, de esta forma hay un componente angular por cada componente OG
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/pr-main Build queued... Details

pull/71/head
Juan Manuel Bardallo 2026-04-22 10:17:17 +02:00
parent a0f36d85e9
commit 0f10f18474
19 changed files with 4528 additions and 701 deletions

View File

@ -196,6 +196,10 @@ import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
import { ManageBootServerComponent } from './components/ogboot/manage-boot-server/manage-boot-server.component';
import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
import { OgcoreStatusComponent } from './components/global-status/ogcore-status/ogcore-status.component';
import { GlobalOgbootStatusComponent } from './components/global-status/ogboot-status/ogboot-status.component';
import { OgrepositoryStatusComponent } from './components/global-status/ogrepository-status/ogrepository-status.component';
import { OgdhcpStatusComponent } from './components/global-status/ogdhcp-status/ogdhcp-status.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -330,7 +334,11 @@ registerLocaleData(localeEs, 'es-ES');
AssetManagerModalComponent,
BootServersComponent,
ManageBootServerComponent,
BootServerDashboardComponent
BootServerDashboardComponent,
OgcoreStatusComponent,
GlobalOgbootStatusComponent,
OgrepositoryStatusComponent,
OgdhcpStatusComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -26,7 +26,7 @@
</div>
}
@if(!loading){
@if(!loading) {
<div class="main-content">
<div class="system-overview">
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'repositories'}"
@ -46,13 +46,8 @@
<mat-icon>storage</mat-icon>
</div>
<div class="overview-content">
<h3>OgBoot Server</h3>
@if(!errorOgBoot){
<p>Estado: <span class="status-badge online">Operativo</span></p>
}
@else{
<p>Estado: <span class="status-badge offline">Error</span></p>
}
<h3>Servidores OgBoot</h3>
<p>Total: <span class="status-badge info">{{ bootServers.length }}</span></p>
</div>
</div>
@ -80,245 +75,24 @@
</div>
<div class="dynamic-content">
<div *ngIf="selectedSection === 'repositories'" class="section-content">
<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">
<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>
<div
*ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]"
class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedRepositoryName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</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 class="repository-logs-section" id="repository-logs-section">
<div class="logs-header">
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
<p>Información específica del repositorio seleccionado</p>
</div>
<div class="logs-container">
<iframe [src]="getRepositoryIframeUrl()" width="100%" height="600" frameborder="0"
class="repository-logs-iframe">
</iframe>
</div>
</div>
</div>
<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 de la lista para ver su estado detallado.</p>
</div>
<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 seleccionado</p>
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
</div>
</div>
</div>
<div *ngIf="selectedSection === 'ogboot'" class="section-content">
<div *ngIf="!errorOgBoot && !loadingOgBoot" class="tab-content">
<div class="section-header">
<h2>OgBoot Server</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<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 class="logs-section" id="ogboot-logs-section">
<div class="logs-header">
<h3>Logs de OgBoot</h3>
<p>Logs en tiempo real del servidor OgBoot</p>
</div>
<div class="logs-container">
<iframe [src]="getOgBootIframeUrl()" width="100%" height="800" frameborder="0" class="ogboot-logs-iframe">
</iframe>
</div>
</div>
</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>
</div>
@if(selectedSection === 'dhcp'){
<div class="section-content">
<div class="repositories-container">
@if(dhcps.length === 0){
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>No hay dhcps disponibles</h3>
<p>No se encontraron dhcps configurados en el sistema.</p>
</div>
}
@else {
<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>Seleccionar dhcp</mat-label>
<mat-select [(ngModel)]="selectedDhcpUuid" (selectionChange)="onDhcpChange($event.value)">
<mat-option *ngFor="let dhcp of dhcps" [value]="dhcp.uuid">
{{ dhcp.name }}
</mat-option>
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]){
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedDhcpName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loadingDhcps" [diskUsage]="dhcpStatuses[selectedDhcpUuid].disk"
[servicesStatus]="dhcpStatuses[selectedDhcpUuid].services" [subnets]="subnets"
[diskUsageChartData]="dhcpStatuses[selectedDhcpUuid].diskUsageChartData" [view]="view"
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true"
[isRepository]="false">
</app-status-tab>
</div>
</div>
<div class="logs-section" id="dhcp-logs-section">
<div class="logs-header">
<h3>Logs de DHCP</h3>
<p>Logs en tiempo real del servidor DHCP</p>
</div>
<div class="logs-container">
<iframe [src]="getDhcpIframeUrl()" width="100%" height="800" frameborder="0" class="dhcp-logs-iframe">
</iframe>
</div>
</div>
</div>
}
<div *ngIf="!selectedDhcpUuid" class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>Selecciona un dhcp</h3>
<p>Elige un dhcp de la lista para ver su estado detallado.</p>
</div>
<div *ngIf="selectedDhcpUuid && errorDhcps[selectedDhcpUuid]" 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 dhcp seleccionado</p>
<button mat-raised-button color="primary" (click)="retryDhcpStatus(selectedDhcpUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
</div>
}
</div>
</div>
@if(selectedSection === 'repositories') {
<app-ogrepository-status [repositories]="repositories"></app-ogrepository-status>
}
@if(selectedSection === 'ogboot') {
<app-global-ogboot-status [bootServers]="bootServers"></app-global-ogboot-status>
}
@if(selectedSection === 'dhcp') {
<app-ogdhcp-status [dhcps]="dhcps"></app-ogdhcp-status>
}
<div *ngIf="selectedSection === 'ogcore'" class="section-content">
<div class="tab-content">
<div class="section-header">
<h2>OgCore Server</h2>
</div>
<div class="logs-section" id="ogcore-logs-section">
<div class="logs-header">
<h3>Logs de OgCore</h3>
<p>Logs en tiempo real del servidor OgCore</p>
</div>
<div class="logs-container">
<iframe [src]="getOgCoreIframeUrl()" width="100%" height="800" frameborder="0" class="ogcore-logs-iframe">
</iframe>
</div>
</div>
</div>
</div>
@if(selectedSection === 'ogcore') {
<app-ogcore-status></app-ogcore-status>
}
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions class="action-container">

View File

@ -2,7 +2,6 @@ import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-global-status',
@ -12,13 +11,14 @@ import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
export class GlobalStatusComponent implements OnInit {
baseUrl: string;
loading: boolean = false;
loadingOgBoot: boolean = false;
loadingDhcps: boolean = false;
loadingRepositories: boolean = false;
errorOgBoot: boolean = false;
loadingBootServers: boolean = false;
errorDhcps: { [key: string]: boolean } = {};
errorRepositories: { [key: string]: boolean } = {};
installedOgLives: any[] = [];
errorBootServers: { [key: string]: boolean } = {};
subnets: any[] = [];
showLabels: boolean = true;
isDoughnut: boolean = true;
@ -26,62 +26,49 @@ export class GlobalStatusComponent implements OnInit {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
repositoriesUrl: string;
repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
lastUpdateTime: string = '';
selectedRepositoryUuid: string = '';
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
ogBootApiUrl: string;
ogBootDiskUsage: any = {};
ogBootServicesStatus: any = {};
ogBootDiskUsageChartData: any[] = [];
dhcpsApiUrl: string;
dhcps: any[] = [];
dhcpDiskUsage: any = {};
dhcpStatuses: { [key: string]: any } = {};
dhcpDiskUsageChartData: any[] = [];
selectedDhcpUuid: string = '';
isDhcp: boolean = false;
isRepository: boolean = false;
bootServersApiUrl: string;
bootServers: any[] = [];
bootServerStatuses: { [key: string]: any } = {};
selectedBootServerUuid: string = '';
lastUpdateTime: string = '';
selectedSection: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore' = 'repositories';
ogCoreApiUrl: string;
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,
private http: HttpClient,
private sanitizer: DomSanitizer
private http: HttpClient
) {
this.baseUrl = this.configService.apiUrl;
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
this.ogCoreApiUrl = `${this.baseUrl}`;
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
this.ogBootDiskUsageChartData = [];
this.dhcpDiskUsageChartData = [];
this.bootServersApiUrl = `${this.baseUrl}/boot-servers`;
this.ogCoreApiUrl = `${this.baseUrl}`;
}
ngOnInit(): void {
this.loading = true;
this.updateLastUpdateTime();
this.loadOgBootStatus();
this.loadDhcps(false);
this.loadRepositories(false);
this.loadDhcps(true);
this.loadRepositories(true);
this.loadBootServers(true);
this.loadBootServers(false);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
}
syncSubnets() {
@ -124,397 +111,145 @@ export class GlobalStatusComponent implements OnInit {
}
syncTemplates() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
}, 3500);
if (this.bootServers.length === 0) return;
this.http.post(`${this.baseUrl}/pxe-templates/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de las plantillas Pxe completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las plantillas Pxe');
let pending = this.bootServers.length;
let hasError = false;
const timeoutId = setTimeout(() => {
if (!hasError) {
this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
hasError = true;
}
}, 5000);
this.bootServers.forEach(server => {
this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/pxe-templates/sync`, {}).subscribe({
next: () => {
pending--;
if (pending === 0 && !hasError) {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de plantillas Pxe exitosa');
}
},
error: (error) => {
pending--;
if (!hasError) {
hasError = true;
clearTimeout(timeoutId);
this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar plantillas Pxe del servidor ${server.name || server.uuid}`);
}
}
});
});
}
syncOgLives(): void {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las imagenes ogLive : tiempo de espera agotado');
}, 3500);
if (this.bootServers.length === 0) return;
this.http.post(`${this.baseUrl}/og-lives/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con los ogLives completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las imagenes ogLive');
let pending = this.bootServers.length;
let hasError = false;
const timeoutId = setTimeout(() => {
if (!hasError) {
this.toastService.error('Error al sincronizar imagenes ogLive: tiempo de espera agotado');
hasError = true;
}
}, 5000);
this.bootServers.forEach(server => {
this.http.post(`${this.baseUrl}/boot-servers/${server.uuid}/oglives/sync`, {}).subscribe({
next: () => {
pending--;
if (pending === 0 && !hasError) {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con ogLives completada');
}
},
error: (error) => {
pending--;
if (!hasError) {
hasError = true;
clearTimeout(timeoutId);
this.toastService.error(error.error?.['hydra:description'] || `Error al sincronizar ogLives del servidor ${server.name || server.uuid}`);
}
}
});
}
[key: string]: any;
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
if (isDhcp) {
this.loadingDhcps = true;
} else {
this.loadingOgBoot = true;
}
if (showLoading) {
this.loading = true;
}
this[errorState] = false;
const timeoutId = setTimeout(() => {
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
this[errorState] = true;
}, 3500);
this.http.get<any>(apiUrl).subscribe({
next: data => {
diskUsage.used = data.message.disk_usage.used;
diskUsage.available = data.message.disk_usage.available;
diskUsage.total = data.message.disk_usage.total;
diskUsage.percentage = data.message.disk_usage.percentage;
Object.assign(servicesStatus, data.message.services_status);
if (isDhcp) {
this.subnets.length = 0;
if (data.message.subnets) {
this.subnets.push(...data.message.subnets);
}
} else {
installedOgLives.length = 0;
if (data.message.installed_oglives) {
installedOgLives.push(...data.message.installed_oglives);
}
this.loadingOgBootOgLives = false;
}
diskUsageChartData.length = 0;
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(diskUsage.used) },
{ name: 'Disponible', value: parseFloat(diskUsage.available) }
);
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
clearTimeout(timeoutId);
},
error: error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado de ogBoot');
if (showLoading) {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
this[errorState] = true;
clearTimeout(timeoutId);
}
});
}
loadRepositories(showLoading: boolean = true): void {
if (showLoading) {
this.loading = true;
}
this.errorRepositories = {};
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
if (showLoading) this.loadingRepositories = true;
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe({
next: data => {
this.repositories = data['hydra:member'] || [];
if (this.repositories.length === 0) {
if (showLoading) {
this.loading = false;
}
return;
}
let remainingRepositories = this.repositories.length;
const timeoutId = setTimeout(() => {
if (showLoading) {
this.loading = false;
}
this.repositories.forEach(repository => {
if (!(repository.uuid in this.errorRepositories)) {
this.errorRepositories[repository.uuid] = true;
}
});
}, 5000);
this.repositories.forEach(repository => {
this.loadRepositoryStatus(repository.uuid, (errorOccurred: boolean) => {
remainingRepositories--;
this.errorRepositories[repository.uuid] = errorOccurred;
if (remainingRepositories === 0) {
if (showLoading) {
this.loading = false;
}
clearTimeout(timeoutId);
}
});
});
if (showLoading) this.loadingRepositories = false;
this.checkOverallLoading();
},
error => {
this.loading = false;
error: () => {
this.repositories = [];
if (showLoading) this.loadingRepositories = false;
this.checkOverallLoading();
this.toastService.error('Error al cargar la lista de repositorios');
}
);
});
}
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
console.log(`Cargando estado del repositorio: ${repositoryUuid}`);
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
data => {
if (!data.success) {
clearTimeout(timeoutId);
callback(true);
return;
loadBootServers(showLoading: boolean = true): void {
if (showLoading) this.loadingBootServers = true;
this.http.get<any>(`${this.bootServersApiUrl}?page=1&itemsPerPage=10`).subscribe({
next: data => {
this.bootServers = data['hydra:member'] || [];
if (showLoading) this.loadingBootServers = false;
if (this.bootServers.length > 0) {
this.syncTemplates();
this.syncOgLives();
}
const details = data.details;
const { disk, services, ram, cpu, processes } = details;
this.repositoryStatuses[repositoryUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage
},
ram: {
...ram,
used: parseFloat(ram.used.replace('GB', '')),
available: parseFloat(ram.available.replace('GB', '')),
total: parseFloat(ram.total.replace('GB', '')),
percentage: ram.used_percentage
},
cpu: {
used_percentage: cpu.used_percentage
},
services: services,
processes: processes
};
clearTimeout(timeoutId);
callback(false);
this.checkOverallLoading();
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
clearTimeout(timeoutId);
callback(true);
error: () => {
this.bootServers = [];
if (showLoading) this.loadingBootServers = false;
this.checkOverallLoading();
this.toastService.error('Error al cargar la lista de Servidores de Boot');
}
);
});
}
loadOgBootStatus(): void {
this.isDhcp = false;
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
}
loadDhcps(showLoading: boolean = true): void {
if (showLoading) {
this.loading = true;
}
this.errorDhcps = {};
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
if (showLoading) this.loadingDhcps = true;
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe({
next: data => {
this.dhcps = data['hydra:member'] || [];
if (this.dhcps.length === 0) {
if (showLoading) {
this.loading = false;
}
return;
}
let remainingDhcps = this.dhcps.length;
const timeoutId = setTimeout(() => {
if (showLoading) {
this.loading = false;
}
this.dhcps.forEach(dhcp => {
if (!(dhcp.uuid in this.errorDhcps)) {
this.errorDhcps[dhcp.uuid] = true;
}
});
}, 5000);
this.dhcps.forEach(dhcp => {
this.loadDhcpStatus(dhcp.uuid, (errorOccurred: boolean) => {
remainingDhcps--;
this.errorDhcps[dhcp.uuid] = errorOccurred;
if (remainingDhcps === 0) {
if (showLoading) {
this.loading = false;
}
clearTimeout(timeoutId);
}
});
});
if (showLoading) this.loadingDhcps = false;
this.checkOverallLoading();
},
error => {
this.loading = false;
error: () => {
this.dhcps = [];
if (showLoading) this.loadingDhcps = false;
this.checkOverallLoading();
this.toastService.error('Error al cargar la lista de Dhcps');
}
);
});
}
loadDhcpStatus(dhcpUuid: string, callback: (errorOccurred: boolean) => void): void {
console.log(`Cargando estado del Dhcp: ${dhcpUuid}`);
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/dhcp-servers/server/${dhcpUuid}/status`).subscribe(
data => {
if (!data.success) {
clearTimeout(timeoutId);
callback(true);
return;
}
const disk = data.message.disk_usage;
const services = data.message.services_status;
let diskUsageChartData: any[] = [];
diskUsageChartData.length = 0;
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(disk.used) },
{ name: 'Disponible', value: parseFloat(disk.available) }
);
this.subnets.length = 0;
if (data.message.subnets) {
this.subnets.push(...data.message.subnets);
}
this.dhcpStatuses[dhcpUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage
},
services: services,
diskUsageChartData: diskUsageChartData
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del dhcp');
clearTimeout(timeoutId);
callback(true);
}
);
}
onDhcpChange(dhcpUuid: string): void {
this.selectedDhcpUuid = dhcpUuid;
if (dhcpUuid && !this.dhcpStatuses[dhcpUuid]) {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorDhcps[dhcpUuid] = true;
}
});
checkOverallLoading(): void {
if (!this.loadingRepositories && !this.loadingBootServers && !this.loadingDhcps) {
this.loading = false;
}
}
getSelectedDhcpName(): string {
const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
return selectedDhcp ? selectedDhcp.name : '';
}
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 : '';
}
getRepositoryIframeUrl(): SafeResourceUrl {
const repositoryName = this.getSelectedRepositoryName();
if (repositoryName) {
const encodedName = encodeURIComponent(repositoryName);
const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
getOgBootIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
getDhcpIframeUrl(): SafeResourceUrl {
const dhcpName = this.getSelectedDhcpName();
if (dhcpName) {
const encodedName = encodeURIComponent(dhcpName);
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
getOgCoreIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
refreshAll(): void {
this.loading = true;
this.updateLastUpdateTime();
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
this.loadBootServers(true);
this.loadDhcps(true);
this.loadRepositories(true);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
this.toastService.success('Datos actualizados correctamente');
}
@ -536,88 +271,11 @@ export class GlobalStatusComponent implements OnInit {
});
}
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');
}
});
}
retryDhcpStatus(dhcpUuid: string): void {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
this.errorDhcps[dhcpUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success('Estado del dhcp actualizado correctamente');
} else {
this.toastService.error('Error al cargar el estado del dhcp');
}
});
}
selectSection(section: 'repositories' | 'ogboot' | 'dhcp' | 'ogcore'): void {
this.selectedSection = section;
switch (section) {
case 'repositories':
break;
case 'ogboot':
if (!this.ogBootDiskUsage) {
this.loadOgBootStatus();
}
break;
case 'dhcp':
break;
case 'ogcore':
break;
}
}
scrollToLogs(): void {
let logsSectionId: string;
switch (this.selectedSection) {
case 'repositories':
logsSectionId = 'repository-logs-section';
break;
case 'ogboot':
logsSectionId = 'ogboot-logs-section';
break;
case 'dhcp':
logsSectionId = 'dhcp-logs-section';
break;
case 'ogcore':
logsSectionId = 'ogcore-logs-section';
break;
default:
return;
}
const checkAndScroll = () => {
const logsSection = document.getElementById(logsSectionId);
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -0,0 +1,886 @@
/* ===== 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% - 200px);
overflow: auto;
padding: 0 !important;
background: #f8f9fa;
}
.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;
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: 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-card.clickable {
cursor: pointer;
transition: all 0.3s ease;
}
.overview-card.clickable:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.overview-card.clickable:active {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.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;
}
.click-hint {
margin-top: 0.5rem !important;
font-size: 0.75rem !important;
color: #667eea !important;
font-weight: 500;
opacity: 0.8;
transition: opacity 0.3s ease;
display: flex;
align-items: center;
gap: 0.25rem;
}
.hint-icon {
font-size: 0.875rem !important;
width: 0.875rem !important;
height: 0.875rem !important;
}
.overview-card.clickable:hover .click-hint {
opacity: 1;
}
/* ===== 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 {
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: 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;
}
/* ===== SECCIÓN DE LOGS ===== */
.logs-section {
margin-top: 2rem;
padding: 1.5rem;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 1rem;
}
.logs-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logs-header h3::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-header p {
margin: 0;
font-size: 0.9rem;
color: #6c757d;
}
.logs-container {
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e9ecef;
}
.ogboot-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
.dhcp-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
/* Responsive para logs */
@media (max-width: 768px) {
.logs-section {
margin-top: 1.5rem;
padding: 1rem;
}
.logs-container {
padding: 0.5rem;
}
.ogboot-logs-iframe {
width: 100% !important;
height: 150px !important;
}
}
/* ===== TARJETAS ACTIVAS ===== */
.overview-card.clickable.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.overview-card.clickable.active .overview-icon {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .status-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .click-hint {
color: rgba(255, 255, 255, 0.8);
}
.overview-card.clickable.active h3 {
color: white;
}
.overview-card.clickable.active p {
color: rgba(255, 255, 255, 0.9);
}
/* ===== CONTENIDO DINÁMICO ===== */
.dynamic-content {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
animation: fadeInUp 0.6s ease-out;
}
.section-content {
padding: 2rem;
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}

View File

@ -0,0 +1,83 @@
<div class="section-content">
<div class="repositories-container">
@if(bootServers.length === 0) {
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>No hay servidores de boot disponibles</h3>
<p>No se encontraron servidores OgBoot configurados en el sistema.</p>
</div>
} @else {
<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>Seleccionar servidor OgBoot</mat-label>
<mat-select [(ngModel)]="selectedBootServerUuid" (selectionChange)="onBootServerChange($event.value)">
@for(server of bootServers; track server.uuid) {
<mat-option [value]="server.uuid">
{{ server.name }}
</mat-option>
}
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedBootServerUuid && !errorBootServers[selectedBootServerUuid] && bootServerStatuses[selectedBootServerUuid]) {
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedBootServerName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loadingBootServers" [diskUsage]="bootServerStatuses[selectedBootServerUuid].disk"
[servicesStatus]="bootServerStatuses[selectedBootServerUuid].services"
[installedOgLives]="bootServerStatuses[selectedBootServerUuid].installedOgLives"
[diskUsageChartData]="bootServerStatuses[selectedBootServerUuid].diskUsageChartData" [view]="view" [colorScheme]="colorScheme"
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="false" [isRepository]="false">
</app-status-tab>
</div>
</div>
<div class="logs-section" id="ogboot-logs-section">
<div class="logs-header">
<h3>Logs de OgBoot</h3>
<p>Logs en tiempo real del servidor OgBoot seleccionado</p>
</div>
<div class="logs-container">
<iframe [src]="getBootServerIframeUrl()" width="100%" height="800" frameborder="0" class="ogboot-logs-iframe">
</iframe>
</div>
</div>
</div>
}
@if(!selectedBootServerUuid) {
<div class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>Selecciona un servidor de boot</h3>
<p>Elige un servidor de boot de la lista para ver su estado detallado.</p>
</div>
}
@if(selectedBootServerUuid && errorBootServers[selectedBootServerUuid]) {
<div 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 seleccionado</p>
<button mat-raised-button color="primary" (click)="retryBootServerStatus(selectedBootServerUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
}
</div>
}
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OgbootStatusComponent } from './ogboot-status.component';
describe('OgbootStatusComponent', () => {
let component: OgbootStatusComponent;
let fixture: ComponentFixture<OgbootStatusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OgbootStatusComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OgbootStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,150 @@
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-global-ogboot-status',
templateUrl: './ogboot-status.component.html',
styleUrl: './ogboot-status.component.css'
})
export class GlobalOgbootStatusComponent implements OnInit {
baseUrl: string;
loadingBootServers: boolean = false;
@Input() bootServers: any[] = [];
bootServerStatuses: { [key: string]: any } = {};
selectedBootServerUuid: string = '';
errorBootServers: { [key: string]: boolean } = {};
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
}
loadBootServerStatus(bootServerUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/boot-servers/server/${bootServerUuid}/status`).subscribe(
data => {
if (data.code && data.code !== 200) {
clearTimeout(timeoutId);
callback(true);
return;
}
const message = data.message || data;
const disk = message.disk_usage;
const services = message.services_status;
const installedOgLives = message.installed_oglives || [];
let diskUsageChartData: any[] = [];
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(disk.used) },
{ name: 'Disponible', value: parseFloat(disk.available) }
);
this.bootServerStatuses[bootServerUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage || disk.percentage
},
services: services,
installedOgLives: installedOgLives,
diskUsageChartData: diskUsageChartData
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del servidor de boot');
clearTimeout(timeoutId);
callback(true);
}
);
}
onBootServerChange(bootServerUuid: string): void {
this.selectedBootServerUuid = bootServerUuid;
if (bootServerUuid && !this.bootServerStatuses[bootServerUuid]) {
this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorBootServers[bootServerUuid] = true;
}
});
}
}
getSelectedBootServerName(): string {
const selectedBootServer = this.bootServers.find(server => server.uuid === this.selectedBootServerUuid);
return selectedBootServer ? selectedBootServer.name : '';
}
getBootServerIframeUrl(): SafeResourceUrl {
const bootServerName = this.getSelectedBootServerName();
if (bootServerName) {
const encodedName = encodeURIComponent(bootServerName);
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
retryBootServerStatus(bootServerUuid: string): void {
this.loadBootServerStatus(bootServerUuid, (errorOccurred: boolean) => {
this.errorBootServers[bootServerUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success('Estado del servidor de boot actualizado correctamente');
} else {
this.toastService.error('Error al cargar el estado del servidor de boot');
}
});
}
scrollToLogs(): void {
const checkAndScroll = () => {
const logsSection = document.getElementById('ogboot-logs-section');
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -0,0 +1,886 @@
/* ===== 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% - 200px);
overflow: auto;
padding: 0 !important;
background: #f8f9fa;
}
.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;
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: 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-card.clickable {
cursor: pointer;
transition: all 0.3s ease;
}
.overview-card.clickable:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.overview-card.clickable:active {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.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;
}
.click-hint {
margin-top: 0.5rem !important;
font-size: 0.75rem !important;
color: #667eea !important;
font-weight: 500;
opacity: 0.8;
transition: opacity 0.3s ease;
display: flex;
align-items: center;
gap: 0.25rem;
}
.hint-icon {
font-size: 0.875rem !important;
width: 0.875rem !important;
height: 0.875rem !important;
}
.overview-card.clickable:hover .click-hint {
opacity: 1;
}
/* ===== 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 {
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: 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;
}
/* ===== SECCIÓN DE LOGS ===== */
.logs-section {
margin-top: 2rem;
padding: 1.5rem;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 1rem;
}
.logs-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logs-header h3::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-header p {
margin: 0;
font-size: 0.9rem;
color: #6c757d;
}
.logs-container {
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e9ecef;
}
.ogboot-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
.dhcp-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
/* Responsive para logs */
@media (max-width: 768px) {
.logs-section {
margin-top: 1.5rem;
padding: 1rem;
}
.logs-container {
padding: 0.5rem;
}
.ogboot-logs-iframe {
width: 100% !important;
height: 150px !important;
}
}
/* ===== TARJETAS ACTIVAS ===== */
.overview-card.clickable.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.overview-card.clickable.active .overview-icon {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .status-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .click-hint {
color: rgba(255, 255, 255, 0.8);
}
.overview-card.clickable.active h3 {
color: white;
}
.overview-card.clickable.active p {
color: rgba(255, 255, 255, 0.9);
}
/* ===== CONTENIDO DINÁMICO ===== */
.dynamic-content {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
animation: fadeInUp 0.6s ease-out;
}
.section-content {
padding: 2rem;
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}

View File

@ -0,0 +1,18 @@
<div class="section-content">
<div class="tab-content">
<div class="section-header">
<h2>OgCore Server</h2>
</div>
<div class="logs-section" id="ogcore-logs-section">
<div class="logs-header">
<h3>Logs de OgCore</h3>
<p>Logs en tiempo real del servidor OgCore</p>
</div>
<div class="logs-container">
<iframe [src]="getOgCoreIframeUrl()" width="100%" height="800" frameborder="0" class="ogcore-logs-iframe">
</iframe>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OgcoreStatusComponent } from './ogcore-status.component';
describe('OgcoreStatusComponent', () => {
let component: OgcoreStatusComponent;
let fixture: ComponentFixture<OgcoreStatusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OgcoreStatusComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OgcoreStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
import { Component } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-ogcore-status',
templateUrl: './ogcore-status.component.html',
styleUrl: './ogcore-status.component.css'
})
export class OgcoreStatusComponent {
constructor(
private configService: ConfigService,
private sanitizer: DomSanitizer
) {}
getOgCoreIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
}

View File

@ -0,0 +1,886 @@
/* ===== 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% - 200px);
overflow: auto;
padding: 0 !important;
background: #f8f9fa;
}
.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;
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: 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-card.clickable {
cursor: pointer;
transition: all 0.3s ease;
}
.overview-card.clickable:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.overview-card.clickable:active {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.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;
}
.click-hint {
margin-top: 0.5rem !important;
font-size: 0.75rem !important;
color: #667eea !important;
font-weight: 500;
opacity: 0.8;
transition: opacity 0.3s ease;
display: flex;
align-items: center;
gap: 0.25rem;
}
.hint-icon {
font-size: 0.875rem !important;
width: 0.875rem !important;
height: 0.875rem !important;
}
.overview-card.clickable:hover .click-hint {
opacity: 1;
}
/* ===== 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 {
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: 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;
}
/* ===== SECCIÓN DE LOGS ===== */
.logs-section {
margin-top: 2rem;
padding: 1.5rem;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 1rem;
}
.logs-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logs-header h3::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-header p {
margin: 0;
font-size: 0.9rem;
color: #6c757d;
}
.logs-container {
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e9ecef;
}
.ogboot-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
.dhcp-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
/* Responsive para logs */
@media (max-width: 768px) {
.logs-section {
margin-top: 1.5rem;
padding: 1rem;
}
.logs-container {
padding: 0.5rem;
}
.ogboot-logs-iframe {
width: 100% !important;
height: 150px !important;
}
}
/* ===== TARJETAS ACTIVAS ===== */
.overview-card.clickable.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.overview-card.clickable.active .overview-icon {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .status-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .click-hint {
color: rgba(255, 255, 255, 0.8);
}
.overview-card.clickable.active h3 {
color: white;
}
.overview-card.clickable.active p {
color: rgba(255, 255, 255, 0.9);
}
/* ===== CONTENIDO DINÁMICO ===== */
.dynamic-content {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
animation: fadeInUp 0.6s ease-out;
}
.section-content {
padding: 2rem;
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}

View File

@ -0,0 +1,83 @@
<div class="section-content">
<div class="repositories-container">
@if(dhcps.length === 0) {
<div class="no-repositories">
<mat-icon class="no-data-icon">cloud_off</mat-icon>
<h3>No hay dhcps disponibles</h3>
<p>No se encontraron dhcps configurados en el sistema.</p>
</div>
} @else {
<div class="repositories-selector-container">
<div class="repository-selector">
<mat-form-field appearance="outline" class="repository-select-field">
<mat-label>Seleccionar dhcp</mat-label>
<mat-select [(ngModel)]="selectedDhcpUuid" (selectionChange)="onDhcpChange($event.value)">
@for(dhcp of dhcps; track dhcp.uuid) {
<mat-option [value]="dhcp.uuid">
{{ dhcp.name }}
</mat-option>
}
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]) {
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedDhcpName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</div>
<div class="repository-content">
<app-status-tab [loading]="loadingDhcps" [diskUsage]="dhcpStatuses[selectedDhcpUuid].disk"
[servicesStatus]="dhcpStatuses[selectedDhcpUuid].services" [subnets]="subnets"
[diskUsageChartData]="dhcpStatuses[selectedDhcpUuid].diskUsageChartData" [view]="view"
[colorScheme]="colorScheme" [isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true"
[isRepository]="false">
</app-status-tab>
</div>
</div>
<div class="logs-section" id="dhcp-logs-section">
<div class="logs-header">
<h3>Logs de DHCP</h3>
<p>Logs en tiempo real del servidor DHCP</p>
</div>
<div class="logs-container">
<iframe [src]="getDhcpIframeUrl()" width="100%" height="800" frameborder="0" class="dhcp-logs-iframe">
</iframe>
</div>
</div>
</div>
}
@if(!selectedDhcpUuid) {
<div class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>Selecciona un dhcp</h3>
<p>Elige un dhcp de la lista para ver su estado detallado.</p>
</div>
}
@if(selectedDhcpUuid && errorDhcps[selectedDhcpUuid]) {
<div 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 dhcp seleccionado</p>
<button mat-raised-button color="primary" (click)="retryDhcpStatus(selectedDhcpUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
}
</div>
}
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OgdhcpStatusComponent } from './ogdhcp-status.component';
describe('OgdhcpStatusComponent', () => {
let component: OgdhcpStatusComponent;
let fixture: ComponentFixture<OgdhcpStatusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OgdhcpStatusComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OgdhcpStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,153 @@
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-ogdhcp-status',
templateUrl: './ogdhcp-status.component.html',
styleUrl: './ogdhcp-status.component.css'
})
export class OgdhcpStatusComponent implements OnInit {
baseUrl: string;
loadingDhcps: boolean = false;
@Input() dhcps: any[] = [];
dhcpStatuses: { [key: string]: any } = {};
selectedDhcpUuid: string = '';
errorDhcps: { [key: string]: boolean } = {};
subnets: any[] = [];
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
}
loadDhcpStatus(dhcpUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/dhcp-servers/server/${dhcpUuid}/status`).subscribe(
data => {
if (!data.success && !data.message) {
clearTimeout(timeoutId);
callback(true);
return;
}
const message = data.message || data;
const disk = message.disk_usage;
const services = message.services_status;
let diskUsageChartData: any[] = [];
diskUsageChartData.push(
{ name: 'Usado', value: parseFloat(disk.used) },
{ name: 'Disponible', value: parseFloat(disk.available) }
);
this.subnets.length = 0;
if (message.subnets) {
this.subnets.push(...message.subnets);
}
this.dhcpStatuses[dhcpUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage || disk.percentage
},
services: services,
diskUsageChartData: diskUsageChartData
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error?.['hydra:description'] || 'Error al cargar el estado del dhcp');
clearTimeout(timeoutId);
callback(true);
}
);
}
onDhcpChange(dhcpUuid: string): void {
this.selectedDhcpUuid = dhcpUuid;
if (dhcpUuid && !this.dhcpStatuses[dhcpUuid]) {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
if (errorOccurred) {
this.errorDhcps[dhcpUuid] = true;
}
});
}
}
getSelectedDhcpName(): string {
const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
return selectedDhcp ? selectedDhcp.name : '';
}
getDhcpIframeUrl(): SafeResourceUrl {
const dhcpName = this.getSelectedDhcpName();
if (dhcpName) {
const encodedName = encodeURIComponent(dhcpName);
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
retryDhcpStatus(dhcpUuid: string): void {
this.loadDhcpStatus(dhcpUuid, (errorOccurred: boolean) => {
this.errorDhcps[dhcpUuid] = errorOccurred;
if (!errorOccurred) {
this.toastService.success('Estado del dhcp actualizado correctamente');
} else {
this.toastService.error('Error al cargar el estado del dhcp');
}
});
}
scrollToLogs(): void {
const checkAndScroll = () => {
const logsSection = document.getElementById('dhcp-logs-section');
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}

View File

@ -0,0 +1,886 @@
/* ===== 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% - 200px);
overflow: auto;
padding: 0 !important;
background: #f8f9fa;
}
.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;
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: 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-card.clickable {
cursor: pointer;
transition: all 0.3s ease;
}
.overview-card.clickable:hover {
transform: translateY(-4px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
}
.overview-card.clickable:active {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.12);
}
.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;
}
.click-hint {
margin-top: 0.5rem !important;
font-size: 0.75rem !important;
color: #667eea !important;
font-weight: 500;
opacity: 0.8;
transition: opacity 0.3s ease;
display: flex;
align-items: center;
gap: 0.25rem;
}
.hint-icon {
font-size: 0.875rem !important;
width: 0.875rem !important;
height: 0.875rem !important;
}
.overview-card.clickable:hover .click-hint {
opacity: 1;
}
/* ===== 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 {
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: 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;
}
/* ===== SECCIÓN DE LOGS ===== */
.logs-section {
margin-top: 2rem;
padding: 1.5rem;
background: white;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #e9ecef;
}
.logs-header {
margin-bottom: 1rem;
}
.logs-header h3 {
margin: 0 0 0.5rem 0;
font-size: 1.25rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.logs-header h3::before {
content: '';
width: 4px;
height: 20px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-header p {
margin: 0;
font-size: 0.9rem;
color: #6c757d;
}
.logs-container {
display: flex;
justify-content: center;
align-items: center;
background: #f8f9fa;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e9ecef;
}
.ogboot-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
.dhcp-logs-iframe {
border: none;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
background: white;
}
/* Responsive para logs */
@media (max-width: 768px) {
.logs-section {
margin-top: 1.5rem;
padding: 1rem;
}
.logs-container {
padding: 0.5rem;
}
.ogboot-logs-iframe {
width: 100% !important;
height: 150px !important;
}
}
/* ===== TARJETAS ACTIVAS ===== */
.overview-card.clickable.active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.3);
}
.overview-card.clickable.active .overview-icon {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .status-badge {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.overview-card.clickable.active .click-hint {
color: rgba(255, 255, 255, 0.8);
}
.overview-card.clickable.active h3 {
color: white;
}
.overview-card.clickable.active p {
color: rgba(255, 255, 255, 0.9);
}
/* ===== CONTENIDO DINÁMICO ===== */
.dynamic-content {
background: white;
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
overflow: hidden;
animation: fadeInUp 0.6s ease-out;
}
.section-content {
padding: 2rem;
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}
/* ===== HEADER DE SECCIÓN Y BOTÓN DE LOGS ===== */
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem;
border-bottom: 2px solid #e9ecef;
}
.section-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-header h2::before {
content: '';
width: 4px;
height: 24px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 2px;
}
.logs-button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 25px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.3);
}
.logs-button:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(102, 126, 234, 0.4);
background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
}
.logs-button:active {
transform: translateY(0);
box-shadow: 0 2px 10px rgba(102, 126, 234, 0.3);
}
.logs-button mat-icon {
font-size: 1.1rem;
width: 1.1rem;
height: 1.1rem;
}
/* Responsive para el header de sección */
@media (max-width: 768px) {
.section-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.section-header h2 {
font-size: 1.25rem;
}
.logs-button {
padding: 0.5rem 1.25rem;
font-size: 0.8rem;
}
.logs-button mat-icon {
font-size: 1rem;
width: 1rem;
height: 1rem;
}
}

View File

@ -0,0 +1,91 @@
<div class="section-content">
<div class="repositories-container">
@if(repositories.length === 0) {
<div 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>
} @else {
<div class="repositories-selector-container">
<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)">
@for(repository of repositories; track repository.uuid) {
<mat-option [value]="repository.uuid">
{{ repository.name }}
</mat-option>
}
</mat-select>
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
@if(selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]) {
<div class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedRepositoryName() }}</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
</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 class="repository-logs-section" id="repository-logs-section">
<div class="logs-header">
<h3>Datos del repositorio: {{ getSelectedRepositoryName() }}</h3>
<p>Información específica del repositorio seleccionado</p>
</div>
<div class="logs-container">
<iframe [src]="getRepositoryIframeUrl()" width="100%" height="600" frameborder="0"
class="repository-logs-iframe">
</iframe>
</div>
</div>
</div>
}
@if(!selectedRepositoryUuid) {
<div class="no-repository-selected">
<mat-icon class="no-data-icon">storage</mat-icon>
<h3>Selecciona un repositorio</h3>
<p>Elige un repositorio de la lista para ver su estado detallado.</p>
</div>
}
@if(selectedRepositoryUuid && errorRepositories[selectedRepositoryUuid]) {
<div 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 seleccionado</p>
<button mat-raised-button color="primary" (click)="retryRepositoryStatus(selectedRepositoryUuid)">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
</div>
}
</div>
}
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { OgrepositoryStatusComponent } from './ogrepository-status.component';
describe('OgrepositoryStatusComponent', () => {
let component: OgrepositoryStatusComponent;
let fixture: ComponentFixture<OgrepositoryStatusComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [OgrepositoryStatusComponent]
})
.compileComponents();
fixture = TestBed.createComponent(OgrepositoryStatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,152 @@
import { Component, OnInit, Input } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-ogrepository-status',
templateUrl: './ogrepository-status.component.html',
styleUrl: './ogrepository-status.component.css'
})
export class OgrepositoryStatusComponent implements OnInit {
baseUrl: string;
loading: boolean = false;
@Input() repositories: any[] = [];
repositoryStatuses: { [key: string]: any } = {};
selectedRepositoryUuid: string = '';
errorRepositories: { [key: string]: boolean } = {};
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#df200d', '#26a700']
};
view: [number, number] = [400, 220];
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
// Parent passes repositories list
}
// removed loadRepositories function that fetched from API
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
const timeoutId = setTimeout(() => {
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
data => {
if (!data.success) {
clearTimeout(timeoutId);
callback(true);
return;
}
const details = data.details;
const { disk, services, ram, cpu, processes } = details;
this.repositoryStatuses[repositoryUuid] = {
disk: {
...disk,
used: parseFloat(disk.used.replace('GB', '')),
available: parseFloat(disk.available.replace('GB', '')),
total: parseFloat(disk.total.replace('GB', '')),
percentage: disk.used_percentage
},
ram: {
...ram,
used: parseFloat(ram.used.replace('GB', '')),
available: parseFloat(ram.available.replace('GB', '')),
total: parseFloat(ram.total.replace('GB', '')),
percentage: ram.used_percentage
},
cpu: {
used_percentage: cpu.used_percentage
},
services: services,
processes: processes
};
clearTimeout(timeoutId);
callback(false);
},
error => {
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
clearTimeout(timeoutId);
callback(true);
}
);
}
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 : '';
}
getRepositoryIframeUrl(): SafeResourceUrl {
const repositoryName = this.getSelectedRepositoryName();
if (repositoryName) {
const encodedName = encodeURIComponent(repositoryName);
const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
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');
}
});
}
scrollToLogs(): void {
const checkAndScroll = () => {
const logsSection = document.getElementById('repository-logs-section');
if (logsSection) {
logsSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
return true;
}
return false;
};
const interval = setInterval(() => {
if (checkAndScroll()) {
clearInterval(interval);
}
}, 100);
setTimeout(() => {
clearInterval(interval);
}, 3000);
}
}