Estructurado componente de estado global para que cada elemento sea un componente independiente, de esta forma hay un componente angular por cada componente OG
parent
a0f36d85e9
commit
0f10f18474
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue