multiple-ogboot #71
|
|
@ -1,5 +1,11 @@
|
|||
# Changelog
|
||||
|
||||
## [1.5.0] - 2026-04-21
|
||||
### Added
|
||||
- Añadida gestión de múltiples ogBoot Servers
|
||||
- Si es un ogBoot existente, permite sincronizar su información sobre oglives y plantillas pxe.
|
||||
- En los Ajustes de red de equipos, aulas y grupos, ahora se debe seleccionar servidor ogBoot y luego oglive y plantilla dentro de dicho servidor.
|
||||
|
||||
## [1.4.0] - 2026-04-16
|
||||
### Added
|
||||
- Añadida gestión de múltiples Dhcps
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component
|
|||
import { MenusComponent } from "./components/menus/menus.component";
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
|
||||
import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component';
|
||||
import {
|
||||
|
||||
RunScriptAssistantComponent
|
||||
|
|
@ -45,6 +46,7 @@ import { LogoutGuard } from './guards/logout.guard';
|
|||
import { HardwareComponent } from './components/hardware/hardware.component';
|
||||
import { HardwareProfileComponent } from './components/hardware-profile/hardware-profile.component';
|
||||
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
|
||||
import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component';
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{
|
||||
|
|
@ -60,6 +62,8 @@ const routes: Routes = [
|
|||
{ path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'dhcp-servers', component: DhcpServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'boot-servers', component: BootServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'boot-servers/:uuid', component: BootServerDashboardComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
|
||||
|
|
|
|||
|
|
@ -58,10 +58,10 @@ import { MatProgressSpinner } from "@angular/material/progress-spinner";
|
|||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
|
||||
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
|
||||
import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips";
|
||||
import { MatChipsModule } from "@angular/material/chips";
|
||||
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
|
||||
import { MatTab, MatTabGroup } from "@angular/material/tabs";
|
||||
import { MatTooltip } from "@angular/material/tooltip";
|
||||
import { MatTabsModule } from "@angular/material/tabs";
|
||||
import { MatTooltipModule } from "@angular/material/tooltip";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
|
@ -193,6 +193,13 @@ import { HardwareCollectionModalComponent } from './components/hardware-profile/
|
|||
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
|
||||
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
|
||||
import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-partition-selector/disk-partition-selector.component';
|
||||
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');
|
||||
}
|
||||
|
|
@ -324,7 +331,14 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
HardwareCollectionModalComponent,
|
||||
HardwareTypeComponent,
|
||||
ShowDetailsComponent,
|
||||
AssetManagerModalComponent
|
||||
AssetManagerModalComponent,
|
||||
BootServersComponent,
|
||||
ManageBootServerComponent,
|
||||
BootServerDashboardComponent,
|
||||
OgcoreStatusComponent,
|
||||
GlobalOgbootStatusComponent,
|
||||
OgrepositoryStatusComponent,
|
||||
OgdhcpStatusComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
|
@ -349,7 +363,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
MatProgressBarModule,
|
||||
MatStepperModule,
|
||||
DragDropModule,
|
||||
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
|
||||
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipsModule, MatProgressSpinner, MatTabsModule, MatTooltipModule,
|
||||
MatExpansionModule,
|
||||
NgxChartsModule,
|
||||
MatDatepickerModule,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -53,4 +53,28 @@ mat-option .unit-path {
|
|||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
/* Boot grouping */
|
||||
.boot-group {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px 4px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.boot-group-legend {
|
||||
font-size: 0.78rem;
|
||||
color: #3f51b5;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0 6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.boot-group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
|
@ -3,16 +3,19 @@
|
|||
<div [ngClass]="{'loading': loading}">
|
||||
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
|
||||
<form *ngIf="clientForm && !loading" [formGroup]="clientForm" class="client-form grid-form">
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onParentChange($event)">
|
||||
<mat-select-trigger>
|
||||
{{ getSelectedParentName() }}
|
||||
</mat-select-trigger>
|
||||
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
@for (unit of parentUnitsWithPaths; track unit.id) {
|
||||
<mat-option [value]="unit.id">
|
||||
<div class="unit-name">{{ unit.name }}</div>
|
||||
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
|
|
@ -21,15 +24,6 @@
|
|||
<input matInput formControlName="name" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
|
||||
{{ ogLive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
|
||||
<input matInput formControlName="serialNumber">
|
||||
|
|
@ -38,9 +32,9 @@
|
|||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="netiface">
|
||||
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
|
||||
{{ type.name }}
|
||||
</mat-option>
|
||||
@for (type of netifaceTypes; track type.value) {
|
||||
<mat-option [value]="type.value">{{ type.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
|
|
@ -56,30 +50,21 @@
|
|||
<mat-error>{{ 'ipError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="pxeTemplate">
|
||||
<mat-option *ngFor="let template of templates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository">
|
||||
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
|
||||
{{ repository.name }}
|
||||
</mat-option>
|
||||
@for (repository of repositories; track repository['@id']) {
|
||||
<mat-option [value]="repository['@id']">{{ repository.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="menu">
|
||||
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
|
||||
{{ menu.name }}
|
||||
</mat-option>
|
||||
@for (menu of menus; track menu['@id']) {
|
||||
<mat-option [value]="menu['@id']">{{ menu.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
<mat-error>{{ 'menuError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
|
|
@ -90,7 +75,7 @@
|
|||
<mat-option value="769">640×480 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="771">800×600 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="773">1024×768 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
|
||||
<mat-option value="784">640×480 (16-bit - 65k colores)</mat-option>
|
||||
<mat-option value="787">800×600 (16-bit - 65k colores)</mat-option>
|
||||
<mat-option value="791">1024×768 (16-bit - 65k colores)</mat-option>
|
||||
|
|
@ -107,6 +92,47 @@
|
|||
{{ 'maintenance' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<!-- ===== Agrupación: Configuración de arranque ===== -->
|
||||
<fieldset class="boot-group">
|
||||
<legend class="boot-group-legend">Configuración de arranque</legend>
|
||||
<div class="boot-group-grid">
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Servidor Boot</mat-label>
|
||||
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
|
||||
@for (server of bootServers; track server['@id']) {
|
||||
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="ogLive">
|
||||
@for (ogLive of ogLives; track ogLive['@id']) {
|
||||
<mat-option [value]="ogLive['@id']">{{ ogLive.name }}</mat-option>
|
||||
}
|
||||
@if (ogLives.length === 0 && clientForm.get('bootServer')?.value) {
|
||||
<mat-option disabled>No hay OgLives en este servidor</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="pxeTemplate">
|
||||
@for (template of templates; track template['@id']) {
|
||||
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
|
||||
}
|
||||
@if (templates.length === 0 && clientForm.get('bootServer')?.value) {
|
||||
<mat-option disabled>No hay plantillas en este servidor</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<mat-form-field class="form-field full-width">
|
||||
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" rows="4"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ export class ManageClientComponent implements OnInit {
|
|||
menus: any[] = [];
|
||||
templates: any[] = [];
|
||||
repositories: any[] = [];
|
||||
bootServers: any[] = [];
|
||||
loading: boolean = false;
|
||||
displayedColumns: string[] = ['name', 'ip'];
|
||||
dialogTitle: string;
|
||||
|
|
@ -49,8 +50,7 @@ export class ManageClientComponent implements OnInit {
|
|||
this.initForm();
|
||||
const observables = [
|
||||
this.loadParentUnits(),
|
||||
this.loadOgLives(),
|
||||
this.loadPxeTemplates(),
|
||||
this.loadBootServers(),
|
||||
this.loadRepositories(),
|
||||
this.loadMenus()
|
||||
];
|
||||
|
|
@ -84,6 +84,7 @@ export class ManageClientComponent implements OnInit {
|
|||
mac: ['', Validators.pattern(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/)],
|
||||
ip: ['', Validators.required],
|
||||
pxeTemplate: [null],
|
||||
bootServer: [null],
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
menu: [null],
|
||||
|
|
@ -106,6 +107,7 @@ export class ManageClientComponent implements OnInit {
|
|||
netiface: unit.networkSettings?.netiface,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
|
||||
repository: unit.networkSettings?.repository?.['@id'],
|
||||
bootServer: unit.networkSettings?.bootServer?.['@id'],
|
||||
ogLive: unit.networkSettings?.ogLive?.['@id'],
|
||||
menu: unit.networkSettings?.menu?.['@id'],
|
||||
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
|
||||
|
|
@ -132,40 +134,74 @@ export class ManageClientComponent implements OnInit {
|
|||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadOgLives(): Promise<void> {
|
||||
loadOgLivesByServer(bootServerIri: string): Promise<void> {
|
||||
// Extract UUID from IRI (e.g. "/boot-servers/xxxx-xxx" -> "xxxx-xxx")
|
||||
const uuid = bootServerIri.split('/').pop();
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
|
||||
|
||||
const url = `${this.baseUrl}/og-lives?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
reject(error);
|
||||
console.error('Error fetching ogLives by server:', error);
|
||||
this.ogLives = [];
|
||||
resolve(); // resolve anyway so form doesn't block
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadPxeTemplates(): Promise<void> {
|
||||
loadPxeTemplatesByServer(bootServerIri: string): Promise<void> {
|
||||
const uuid = bootServerIri.split('/').pop();
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
const url = `${this.baseUrl}/pxe-templates?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.templates = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching PXE templates:', error);
|
||||
console.error('Error fetching PXE templates by server:', error);
|
||||
this.templates = [];
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadBootServers(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.bootServers = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching BootServers:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onBootServerChange(event: any): void {
|
||||
const serverIri: string | null = event.value;
|
||||
// Reset dependent fields
|
||||
this.clientForm.patchValue({ ogLive: null, pxeTemplate: null });
|
||||
this.ogLives = [];
|
||||
this.templates = [];
|
||||
if (serverIri) {
|
||||
Promise.all([
|
||||
this.loadOgLivesByServer(serverIri),
|
||||
this.loadPxeTemplatesByServer(serverIri)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
loadMenus(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
|
||||
|
|
@ -223,6 +259,7 @@ export class ManageClientComponent implements OnInit {
|
|||
if (selectedUnit) {
|
||||
this.clientForm.patchValue({
|
||||
repository: selectedUnit.repository || null,
|
||||
bootServer: selectedUnit.bootServer || null,
|
||||
ogLive: selectedUnit.ogLive || null,
|
||||
menu: selectedUnit.menu || null,
|
||||
netiface: selectedUnit.netiface || null,
|
||||
|
|
@ -238,6 +275,11 @@ export class ManageClientComponent implements OnInit {
|
|||
|
||||
this.http.get<any>(url).subscribe(
|
||||
data => {
|
||||
const bootServerIri: string | null = data.bootServer ? data.bootServer['@id'] : null;
|
||||
const savedOgLive: string | null = data.ogLive ? data.ogLive['@id'] : null;
|
||||
const savedPxeTemplate: string | null = data.pxeTemplate ? data.pxeTemplate['@id'] : null;
|
||||
|
||||
// Patch fields that don't depend on server first
|
||||
this.clientForm.patchValue({
|
||||
name: data.name,
|
||||
ip: data.ip,
|
||||
|
|
@ -246,15 +288,29 @@ export class ManageClientComponent implements OnInit {
|
|||
netDriver: 'generic',
|
||||
serialNumber: data.serialNumber,
|
||||
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
|
||||
bootServer: bootServerIri,
|
||||
repository: data.repository ? data.repository['@id'] : null,
|
||||
ogLive: data.ogLive ? data.ogLive['@id'] : null,
|
||||
pxeTemplate: data.pxeTemplate ? data.pxeTemplate['@id'] : null,
|
||||
menu: data.menu ? data.menu['@id'] : null,
|
||||
maintenance: data.maintenance,
|
||||
resolution: data.resolution,
|
||||
notes: data.notes || ''
|
||||
});
|
||||
resolve();
|
||||
|
||||
if (bootServerIri) {
|
||||
// Load server-specific resources first, then restore saved values
|
||||
Promise.all([
|
||||
this.loadOgLivesByServer(bootServerIri),
|
||||
this.loadPxeTemplatesByServer(bootServerIri)
|
||||
]).then(() => {
|
||||
this.clientForm.patchValue({
|
||||
ogLive: savedOgLive,
|
||||
pxeTemplate: savedPxeTemplate
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Error al cargar datos del cliente:', error);
|
||||
|
|
|
|||
|
|
@ -64,4 +64,28 @@ h1 {
|
|||
align-items: center;
|
||||
grid-column: span 2;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Boot grouping */
|
||||
.boot-group {
|
||||
grid-column: 1 / -1;
|
||||
border: 1px solid #c5c5c5;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px 4px 16px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.boot-group-legend {
|
||||
font-size: 0.78rem;
|
||||
color: #3f51b5;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0 6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.boot-group-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
|
@ -74,22 +74,46 @@
|
|||
<!-- Paso 3: Configuración de Red -->
|
||||
<span *ngIf="!loading" class="step-title">{{ 'networkSettingsStepLabel' | translate }}</span>
|
||||
<form *ngIf="networkSettingsFormGroup && !loading" [formGroup]="networkSettingsFormGroup" class="grid-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>OgLive</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
|
||||
{{ oglive.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
|
||||
<mat-option *ngFor="let template of pxeTemplates" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- ===== Agrupación: Configuración de arranque ===== -->
|
||||
<fieldset class="boot-group">
|
||||
<legend class="boot-group-legend">Configuración de arranque</legend>
|
||||
<div class="boot-group-grid">
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>Servidor Boot</mat-label>
|
||||
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
|
||||
@for (server of bootServers; track server['@id']) {
|
||||
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>OgLive</mat-label>
|
||||
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
|
||||
@for (oglive of ogLives; track oglive['@id']) {
|
||||
<mat-option [value]="oglive['@id']">{{ oglive.name }}</mat-option>
|
||||
}
|
||||
@if (ogLives.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
|
||||
<mat-option disabled>No hay OgLives en este servidor</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
|
||||
@for (template of pxeTemplates; track template['@id']) {
|
||||
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
|
||||
}
|
||||
@if (pxeTemplates.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
|
||||
<mat-option disabled>No hay plantillas en este servidor</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
</div>
|
||||
</fieldset>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
currentCalendar: any = [];
|
||||
ogLives: any[] = [];
|
||||
pxeTemplates: any[] = [];
|
||||
bootServers: any[] = [];
|
||||
menus: any[] = [];
|
||||
repositories: any[] = [];
|
||||
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
|
||||
|
|
@ -78,6 +79,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
});
|
||||
|
||||
this.networkSettingsFormGroup = this._formBuilder.group({
|
||||
bootServer: [null],
|
||||
ogLive: [null],
|
||||
repository: [null],
|
||||
pxeTemplate: [null],
|
||||
|
|
@ -112,10 +114,9 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
this.loadParentUnits(),
|
||||
this.loadHardwareProfiles(),
|
||||
this.loadCalendars(),
|
||||
this.loadOgLives(),
|
||||
this.loadRepositories(),
|
||||
this.loadMenus(),
|
||||
this.loadPxeTemplates()
|
||||
this.loadBootServers()
|
||||
];
|
||||
|
||||
Promise.all(observables).then(() => {
|
||||
|
|
@ -147,6 +148,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
name: unit.name,
|
||||
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
|
||||
repository: unit.networkSettings?.repository?.['@id'],
|
||||
bootServer: unit.networkSettings?.bootServer?.['@id'],
|
||||
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
|
||||
ogLive: unit.networkSettings?.ogLive?.['@id'],
|
||||
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
|
||||
|
|
@ -190,6 +192,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
if (selectedUnit && !exclude) {
|
||||
this.networkSettingsFormGroup.patchValue({
|
||||
repository: selectedUnit.repository || null,
|
||||
bootServer: selectedUnit.bootServer || null,
|
||||
hardwareProfile: selectedUnit.hardwareProfile || null,
|
||||
ogLive: selectedUnit.ogLive || null,
|
||||
pxeTemplate: selectedUnit.pxeTemplate || null,
|
||||
|
|
@ -215,20 +218,29 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
|
||||
this.http.get<any>(url).subscribe(
|
||||
data => {
|
||||
const exclude = data.excludeParentChanges;
|
||||
const bootServerIri: string | null = data.networkSettings?.bootServer
|
||||
? data.networkSettings.bootServer['@id']
|
||||
: null;
|
||||
const savedOgLive: string | null = data.networkSettings?.ogLive
|
||||
? data.networkSettings.ogLive['@id']
|
||||
: null;
|
||||
const savedPxeTemplate: string | null = data.networkSettings?.pxeTemplate
|
||||
? data.networkSettings.pxeTemplate['@id']
|
||||
: null;
|
||||
|
||||
this.generalFormGroup.patchValue({
|
||||
name: data.name,
|
||||
parent: data.parent ? data.parent['@id'] : '',
|
||||
description: data.description,
|
||||
type: data.type,
|
||||
excludeParentChanges: exclude
|
||||
excludeParentChanges: data.excludeParentChanges
|
||||
});
|
||||
|
||||
this.additionalInfoFormGroup.patchValue({
|
||||
comments: data.comments
|
||||
});
|
||||
|
||||
// Patch all network fields except ogLive and pxeTemplate
|
||||
this.networkSettingsFormGroup.patchValue({
|
||||
proxy: data.networkSettings?.proxy,
|
||||
dns: data.networkSettings?.dns,
|
||||
|
|
@ -243,10 +255,9 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
mcastPort: data.networkSettings?.mcastPort,
|
||||
mcastMode: data.networkSettings?.mcastMode,
|
||||
menu: data.networkSettings?.menu ? data.networkSettings.menu['@id'] : null,
|
||||
bootServer: bootServerIri,
|
||||
hardwareProfile: data.networkSettings?.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null,
|
||||
ogLive: data.networkSettings?.ogLive ? data.networkSettings.ogLive['@id'] : null,
|
||||
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null,
|
||||
pxeTemplate: data.networkSettings?.pxeTemplate ? data.networkSettings.pxeTemplate['@id'] : null
|
||||
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null
|
||||
});
|
||||
|
||||
this.classroomInfoFormGroup.patchValue({
|
||||
|
|
@ -257,7 +268,21 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
remoteCalendar: data.remoteCalendar ? data.remoteCalendar['@id'] : null
|
||||
});
|
||||
|
||||
resolve();
|
||||
if (bootServerIri) {
|
||||
// Load server-specific lists first, then restore saved values
|
||||
Promise.all([
|
||||
this.loadOgLivesByServer(bootServerIri),
|
||||
this.loadPxeTemplatesByServer(bootServerIri)
|
||||
]).then(() => {
|
||||
this.networkSettingsFormGroup.patchValue({
|
||||
ogLive: savedOgLive,
|
||||
pxeTemplate: savedPxeTemplate
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
},
|
||||
|
||||
error => {
|
||||
|
|
@ -384,6 +409,70 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
|
||||
}
|
||||
|
||||
loadBootServers(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.bootServers = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching BootServers:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadOgLivesByServer(bootServerIri: string): Promise<void> {
|
||||
const uuid = bootServerIri.split('/').pop();
|
||||
return new Promise((resolve) => {
|
||||
this.http.get<any>(`${this.baseUrl}/og-lives?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives by server:', error);
|
||||
this.ogLives = [];
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadPxeTemplatesByServer(bootServerIri: string): Promise<void> {
|
||||
const uuid = bootServerIri.split('/').pop();
|
||||
return new Promise((resolve) => {
|
||||
this.http.get<any>(`${this.baseUrl}/pxe-templates?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`).subscribe(
|
||||
response => {
|
||||
this.pxeTemplates = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching PXE templates by server:', error);
|
||||
this.pxeTemplates = [];
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
onBootServerChange(event: any): void {
|
||||
const serverIri: string | null = event.value;
|
||||
this.networkSettingsFormGroup.patchValue({ ogLive: null, pxeTemplate: null });
|
||||
this.ogLives = [];
|
||||
this.pxeTemplates = [];
|
||||
if (serverIri) {
|
||||
Promise.all([
|
||||
this.loadOgLivesByServer(serverIri),
|
||||
this.loadPxeTemplatesByServer(serverIri)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
loadHardwareProfiles(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.dataService.getHardwareProfiles().subscribe(
|
||||
|
|
@ -399,39 +488,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
|
|||
});
|
||||
}
|
||||
|
||||
loadPxeTemplates(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.pxeTemplates = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching pxe templates:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
loadOgLives(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.ogLives = response['hydra:member'];
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching ogLives:', error);
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
// loadPxeTemplates and loadOgLives removed: resources are now loaded on-demand per BootServer
|
||||
|
||||
loadMenus(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,115 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-boot-server-dashboard',
|
||||
template: `
|
||||
<div class="dashboard-container">
|
||||
<div class="header">
|
||||
<button mat-icon-button (click)="goBack()" matTooltip="Volver a servidores">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<h1>Gestión de Servidor: {{ serverName }}</h1>
|
||||
</div>
|
||||
|
||||
<mat-tab-group animationDuration="0ms" class="dashboard-tabs">
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">album</mat-icon>
|
||||
<div class="tab-label-content">
|
||||
<span class="tab-title">OgLives</span>
|
||||
<span class="tab-subtitle">Versiones de OgLive disponibles</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="tab-content">
|
||||
@if (serverUuid) {
|
||||
<app-pxe-images [selectedServerId]="serverUuid"></app-pxe-images>
|
||||
}
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">assignment</mat-icon>
|
||||
<div class="tab-label-content">
|
||||
<span class="tab-title">Plantillas PXE</span>
|
||||
<span class="tab-subtitle">Configuraciones de arranque PXE</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="tab-content">
|
||||
@if (serverUuid) {
|
||||
<app-pxe [selectedServerId]="serverUuid"></app-pxe>
|
||||
}
|
||||
</div>
|
||||
</mat-tab>
|
||||
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-icon">save</mat-icon>
|
||||
<div class="tab-label-content">
|
||||
<span class="tab-title">Arranque PXE</span>
|
||||
<span class="tab-subtitle">Ficheros de arranque de clientes</span>
|
||||
</div>
|
||||
</ng-template>
|
||||
<div class="tab-content">
|
||||
@if (serverUuid) {
|
||||
<app-pxe-boot-files [selectedServerId]="serverUuid"></app-pxe-boot-files>
|
||||
}
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.dashboard-container { padding: 0; }
|
||||
.header { display: flex; align-items: center; padding: 20px 20px 10px 20px; gap: 10px; border-bottom: 1px solid #eee; background: #fff; }
|
||||
.header h1 { margin: 0; font-size: 20px; font-weight: 500; }
|
||||
.dashboard-tabs { background: #fafafa; }
|
||||
.tab-icon { margin-right: 12px; font-size: 24px; color: #3f51b5; }
|
||||
.tab-label-content { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; padding: 10px 0; }
|
||||
.tab-title { font-weight: 600; font-size: 14px; }
|
||||
.tab-subtitle { font-size: 11px; color: #666; font-weight: 400; }
|
||||
.tab-content { padding: 0; background: #fff; min-height: 400px; }
|
||||
::ng-deep .mat-mdc-tab-body-wrapper { padding-top: 0 !important; }
|
||||
`]
|
||||
})
|
||||
export class BootServerDashboardComponent implements OnInit {
|
||||
serverUuid: string | null = null;
|
||||
serverName: string = 'Cargando...';
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService
|
||||
) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.serverUuid = this.route.snapshot.paramMap.get('uuid');
|
||||
if (this.serverUuid) {
|
||||
this.loadServerInfo();
|
||||
}
|
||||
}
|
||||
|
||||
loadServerInfo() {
|
||||
const url = `${this.configService.apiUrl}/boot-servers/${this.serverUuid}`;
|
||||
this.http.get<any>(url).subscribe(res => {
|
||||
this.serverName = res.name;
|
||||
});
|
||||
}
|
||||
|
||||
navigateTo(view: string) {
|
||||
const queryParams = { serverId: this.serverUuid };
|
||||
switch(view) {
|
||||
case 'oglives': this.router.navigate(['/pxe-images'], { queryParams }); break;
|
||||
case 'pxe-templates': this.router.navigate(['/pxe'], { queryParams }); break;
|
||||
case 'pxe-boot-files': this.router.navigate(['/pxe-boot-file'], { queryParams }); break;
|
||||
}
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.router.navigate(['/boot-servers']);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.images-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
background-color: #3f51b5;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background-color: #303f9f;
|
||||
}
|
||||
|
||||
.action-button:disabled {
|
||||
background-color: #9fa8da;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="initTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="bootServerTitleStep" text="Vista de Servidores de Arranque">
|
||||
Servidores OgBoot
|
||||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button
|
||||
class="action-button" (click)="addServer()"
|
||||
joyrideStep="addStep"
|
||||
text="Añadir Servidor Boot">Añadir Servidor Boot</button>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
|
||||
<div class="search-container" joyrideStep="searchStep" text="Búsqueda rápida de servidores">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Buscar por nombre</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Buscar por IP</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableDateStep" text="Tabla de Servidores OgBoot">
|
||||
@for (column of columns; track column.columnDef) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let server">
|
||||
{{ column.cell(server) }}
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let server" style="text-align: center;" joyrideStep="actionsStep">
|
||||
<button mat-icon-button color="accent" (click)="openServerDashboard(server)" matTooltip="Gestionar servidor">
|
||||
<mat-icon>open_in_new</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="syncServer(server)" matTooltip="Sincronizar servidor">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editServer($event, server)" matTooltip="Editar servidor">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteServer($event, server)" matTooltip="Eliminar servidor">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="Paginación de resultados">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="[5, 10, 20, 40, 100]"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { BootServersComponent } from './boot-servers.component';
|
||||
|
||||
describe('BootServersComponent', () => {
|
||||
let component: BootServersComponent;
|
||||
let fixture: ComponentFixture<BootServersComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [BootServersComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(BootServersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
import {Component, OnInit} from '@angular/core';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import {DatePipe} from "@angular/common";
|
||||
import {MatDialog} from "@angular/material/dialog";
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { Router } from '@angular/router';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import {ManageBootServerComponent} from "../manage-boot-server/manage-boot-server.component";
|
||||
import { forkJoin } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-boot-servers',
|
||||
templateUrl: './boot-servers.component.html',
|
||||
styleUrl: './boot-servers.component.css'
|
||||
})
|
||||
export class BootServersComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'Id',
|
||||
cell: (server: any) => `${server.id}`
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre',
|
||||
cell: (server: any) => server.name
|
||||
},
|
||||
{
|
||||
columnDef: 'ip',
|
||||
header: 'Ip',
|
||||
cell: (server: any) => server.ip
|
||||
},
|
||||
{
|
||||
columnDef: 'apiPort',
|
||||
header: 'Puerto API',
|
||||
cell: (server: any) => server.apiPort
|
||||
},
|
||||
{
|
||||
columnDef: 'pxePort',
|
||||
header: 'Puerto PXE',
|
||||
cell: (server: any) => server.pxePort
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'Fecha de creación',
|
||||
cell: (server: any) => `${this.datePipe.transform(server.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
|
||||
}
|
||||
];
|
||||
displayedColumns: string[] = ['id', 'name', 'ip', 'apiPort', 'pxePort', 'createdAt', 'actions'];
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService,
|
||||
private router: Router,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/boot-servers`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
}
|
||||
|
||||
addServer(): void {
|
||||
const dialogRef = this.dialog.open(ManageBootServerComponent, {
|
||||
width: '600px'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
editServer(event: MouseEvent, server: any): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(ManageBootServerComponent, {
|
||||
width: '600px',
|
||||
data: server['@id']
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteServer(event: MouseEvent, server: any): void {
|
||||
event.stopPropagation();
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: server.name },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/${server.uuid}`).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Servidor eliminado con éxito');
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error al eliminar servidor:', error);
|
||||
this.toastService.error('Error al eliminar el servidor');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
openServerDashboard(server: any): void {
|
||||
this.router.navigate(['/boot-servers', server.uuid]);
|
||||
}
|
||||
|
||||
syncServer(server: any): void {
|
||||
this.loading = true;
|
||||
const ogLiveSyncUrl = `${this.baseUrl}/boot-servers/${server.uuid}/oglives/sync`;
|
||||
const pxeSyncUrl = `${this.baseUrl}/boot-servers/${server.uuid}/pxe-templates/sync`;
|
||||
|
||||
// Concurrent sync is fine as they target different entities
|
||||
const syncRequests = [
|
||||
this.http.post(ogLiveSyncUrl, {}),
|
||||
this.http.post(pxeSyncUrl, {})
|
||||
];
|
||||
|
||||
forkJoin(syncRequests).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Sincronización completada con éxito');
|
||||
this.loading = false;
|
||||
this.search();
|
||||
},
|
||||
error: (error: any) => {
|
||||
console.error('Error durante la sincronización:', error);
|
||||
this.toastService.error('Error durante la sincronización');
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'bootServerTitleStep',
|
||||
'addStep',
|
||||
'searchStep',
|
||||
'tableDateStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
.dialog-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dialog-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.boot-server-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.port-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
mat-dialog-actions {
|
||||
margin-top: 24px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
mat-dialog-actions button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<div class="dialog-container">
|
||||
<div class="dialog-header">
|
||||
<h2 mat-dialog-title>{{ data ? 'Editar Servidor Boot' : 'Añadir Servidor Boot' }}</h2>
|
||||
<button mat-icon-button (click)="close()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-dialog-content>
|
||||
<form [formGroup]="bootServerForm" class="boot-server-form">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Nombre</mat-label>
|
||||
<input matInput formControlName="name" placeholder="Ej: Servidor Principal TFTP">
|
||||
@if (bootServerForm.controls['name'].invalid && bootServerForm.controls['name'].touched) {
|
||||
<mat-error>El nombre es obligatorio</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>IP</mat-label>
|
||||
<input matInput formControlName="ip" placeholder="192.168.1.10">
|
||||
@if (bootServerForm.controls['ip'].invalid && bootServerForm.controls['ip'].touched) {
|
||||
<mat-error>La IP es obligatoria</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<div class="port-row">
|
||||
<mat-form-field appearance="outline" class="half-width">
|
||||
<mat-label>Puerto API</mat-label>
|
||||
<input matInput type="number" formControlName="apiPort">
|
||||
@if (bootServerForm.controls['apiPort'].invalid && bootServerForm.controls['apiPort'].touched) {
|
||||
<mat-error>El puerto es obligatorio</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="half-width">
|
||||
<mat-label>Puerto PXE (TFTP)</mat-label>
|
||||
<input matInput type="number" formControlName="pxePort">
|
||||
@if (bootServerForm.controls['pxePort'].invalid && bootServerForm.controls['pxePort'].touched) {
|
||||
<mat-error>El puerto es obligatorio</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<mat-label>Comentarios</mat-label>
|
||||
<textarea matInput formControlName="comments" rows="3" placeholder="Información adicional..."></textarea>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-button (click)="close()">Cancelar</button>
|
||||
<button mat-raised-button color="primary" [disabled]="bootServerForm.invalid" (click)="save()">
|
||||
{{ data ? 'Guardar' : 'Añadir' }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ManageBootServerComponent } from './manage-boot-server.component';
|
||||
|
||||
describe('ManageBootServerComponent', () => {
|
||||
let component: ManageBootServerComponent;
|
||||
let fixture: ComponentFixture<ManageBootServerComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ManageBootServerComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ManageBootServerComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from "@services/config.service";
|
||||
|
||||
@Component({
|
||||
selector: 'app-manage-boot-server',
|
||||
templateUrl: './manage-boot-server.component.html',
|
||||
styleUrl: './manage-boot-server.component.css'
|
||||
})
|
||||
export class ManageBootServerComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
bootServerForm: FormGroup<any>;
|
||||
serverId: string | null = null;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
public dialogRef: MatDialogRef<ManageBootServerComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.bootServerForm = this.fb.group({
|
||||
name: [null, Validators.required],
|
||||
ip: [null, Validators.required],
|
||||
apiPort: [8082, Validators.required],
|
||||
pxePort: [8082, Validators.required],
|
||||
comments: [null],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
if (this.data) {
|
||||
this.load()
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.http.get<any>(`${this.baseUrl}${this.data}`).subscribe({
|
||||
next: (response) => {
|
||||
this.bootServerForm.patchValue({
|
||||
name: response.name,
|
||||
ip: response.ip,
|
||||
apiPort: response.apiPort ?? 8082,
|
||||
pxePort: response.pxePort ?? 8082,
|
||||
comments: response.comments,
|
||||
});
|
||||
this.serverId = response['@id'];
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error fetching boot server:', err);
|
||||
this.toastService.error('Error cargando los datos del servidor');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
save(): void {
|
||||
const payload = {
|
||||
name: this.bootServerForm.value.name,
|
||||
ip: this.bootServerForm.value.ip,
|
||||
apiPort: parseInt(this.bootServerForm.value.apiPort),
|
||||
pxePort: parseInt(this.bootServerForm.value.pxePort),
|
||||
comments: this.bootServerForm.value.comments,
|
||||
};
|
||||
|
||||
if (this.serverId) {
|
||||
this.http.put(`${this.baseUrl}${this.serverId}`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Servidor Boot actualizado correctamente');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error?.['hydra:description'] || 'Ha ocurrido un error al editar');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/boot-servers`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Servidor Boot añadido correctamente');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error?.['hydra:description'] || 'Ha ocurrido un error al crear');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +1,37 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'advancedNetbootDescription' | translate }}">
|
||||
{{ 'advancedNetbootTitle' | translate }}
|
||||
</h2>
|
||||
@if (!isFromDashboard) {
|
||||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'advancedNetbootDescription' | translate }}">
|
||||
{{ 'advancedNetbootTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div [formGroup]="taskForm" class="search-container">
|
||||
@if (!isFromDashboard) {
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Servidor OgBoot</mat-label>
|
||||
<mat-select [(ngModel)]="selectedServerId" [ngModelOptions]="{standalone: true}" (selectionChange)="onServerChange()">
|
||||
@for (server of bootServers; track server['@id']) {
|
||||
<mat-option [value]="server.uuid || server['@id'].split('/').pop()">
|
||||
{{ server.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectClassStep"
|
||||
text="{{ 'selectClassDescription' | translate }}">
|
||||
<mat-label>{{ 'selectClassLabel' | translate }}</mat-label>
|
||||
<mat-select (selectionChange)="loadChildUnits($event)">
|
||||
<mat-option *ngFor="let unit of units" [value]="unit">{{ unit.name }}</mat-option>
|
||||
@for (unit of units; track unit.id) {
|
||||
<mat-option [value]="unit">{{ unit.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
|
@ -25,9 +42,11 @@
|
|||
text="{{ 'applyToAllDescription' | translate }}">
|
||||
<mat-label>{{ 'applyToAllLabel' | translate }}</mat-label>
|
||||
<mat-select [(value)]="globalOgLive" (selectionChange)="applyToAll()">
|
||||
<mat-option *ngFor="let option of ogLiveOptions" [value]="option['@id']">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
@for (option of ogLiveOptions; track option['@id']) {
|
||||
<mat-option [value]="option['@id']">
|
||||
{{ option.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
|
|
@ -67,14 +86,30 @@
|
|||
<mat-label>{{ 'selectTemplateLabel' | translate }}</mat-label>
|
||||
<mat-select [(ngModel)]="client.ogLive" [name]="'ogLive' + client.id">
|
||||
<mat-option [value]="null">{{ 'noTemplateOption' | translate }}</mat-option>
|
||||
<mat-option *ngFor="let template of ogLiveOptions" [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
@for (template of ogLiveOptions; track template['@id']) {
|
||||
<mat-option [value]="template['@id']">
|
||||
{{ template.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="bootServer">
|
||||
<mat-header-cell *matHeaderCellDef> Servidor </mat-header-cell>
|
||||
<mat-cell *matCellDef="let element">
|
||||
<span [class.text-warning]="element.bootServer?.uuid !== selectedServerId"
|
||||
style="display: flex; align-items: center;"
|
||||
[title]="element.bootServer?.uuid !== selectedServerId ? 'Este equipo cambiará al servidor actual al guardar' : ''">
|
||||
{{ element.bootServer?.name || 'Ninguno' }}
|
||||
@if (element.bootServer?.uuid && element.bootServer.uuid !== selectedServerId) {
|
||||
<mat-icon style="font-size: 16px; width: 16px; height: 16px; margin-left: 4px; color: orange;">warning</mat-icon>
|
||||
}
|
||||
</span>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, Input } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import {MatTableDataSource} from "@angular/material/table";
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { forkJoin } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pxe-boot-files',
|
||||
|
|
@ -19,6 +23,10 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
dataSource = new MatTableDataSource<any>();
|
||||
taskForm: FormGroup;
|
||||
units: any[] = [];
|
||||
selectedUnit: any = null;
|
||||
bootServers: any[] = [];
|
||||
@Input() selectedServerId: string | null = null;
|
||||
isFromDashboard: boolean = false;
|
||||
ogLiveOptions: any[] = [];
|
||||
globalOgLive: string | null = null;
|
||||
length: number = 0;
|
||||
|
|
@ -26,7 +34,7 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
filters: any = {};
|
||||
displayedColumns: string[] = ['id', 'ip', 'mac', 'name', 'ogLive'];
|
||||
displayedColumns: string[] = ['id', 'ip', 'mac', 'name', 'bootServer', 'ogLive'];
|
||||
|
||||
|
||||
constructor(
|
||||
|
|
@ -34,7 +42,9 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private joyrideService: JoyrideService
|
||||
private joyrideService: JoyrideService,
|
||||
private route: ActivatedRoute,
|
||||
private dialog: MatDialog
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.taskForm = this.fb.group({
|
||||
|
|
@ -44,10 +54,43 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fetchPxeTemplates();
|
||||
if (this.selectedServerId) {
|
||||
this.isFromDashboard = true;
|
||||
this.onServerChange();
|
||||
} else {
|
||||
const serverIdParam = this.route.snapshot.queryParamMap.get('serverId');
|
||||
if (serverIdParam) {
|
||||
this.selectedServerId = serverIdParam;
|
||||
this.isFromDashboard = true;
|
||||
this.onServerChange();
|
||||
}
|
||||
}
|
||||
this.loadBootServers();
|
||||
this.loadUnits()
|
||||
}
|
||||
|
||||
loadBootServers(): void {
|
||||
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.bootServers = response['hydra:member'];
|
||||
if (this.bootServers.length > 0 && !this.selectedServerId) {
|
||||
this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop();
|
||||
this.onServerChange();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching BootServers:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onServerChange(): void {
|
||||
this.filters['bootServer.uuid'] = this.selectedServerId!;
|
||||
this.page = 0;
|
||||
this.fetchPxeTemplates();
|
||||
}
|
||||
|
||||
loadUnits() {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?type=classroom&page=1&itemsPerPage=50`).subscribe(
|
||||
response => {
|
||||
|
|
@ -71,7 +114,13 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadChildUnits(event: any) {
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${event.value.id}`).subscribe(
|
||||
this.selectedUnit = event.value;
|
||||
this.refreshClientList();
|
||||
}
|
||||
|
||||
refreshClientList(): void {
|
||||
if (!this.selectedUnit) return;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${this.selectedUnit.id}`).subscribe(
|
||||
response => {
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
},
|
||||
|
|
@ -87,10 +136,47 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
}
|
||||
|
||||
saveOgLiveTemplates(): void {
|
||||
let hasDifferentServer = false;
|
||||
|
||||
// Check for clients with different boot server
|
||||
this.dataSource.data.forEach(client => {
|
||||
if (client.ogLive && client.bootServer?.uuid !== this.selectedServerId) {
|
||||
hasDifferentServer = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasDifferentServer) {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '500px',
|
||||
data: {
|
||||
title: 'Confirmar migración de servidores',
|
||||
message: 'Se han detectado ordenadores asociados a otro servidor de boot. ¿Desea moverlos al servidor actual? (Si rechaza, solo se actualizarán los ordenadores que ya pertenecen al servidor actual)',
|
||||
confirmText: 'Mover y Guardar',
|
||||
cancelText: 'Solo de este servidor'
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
// result is true if YES, false if NO, undefined if closed
|
||||
if (result === undefined) return;
|
||||
this.processSave(result);
|
||||
});
|
||||
} else {
|
||||
this.processSave(true);
|
||||
}
|
||||
}
|
||||
|
||||
private processSave(moveClients: boolean): void {
|
||||
const groupedByTemplate: { [key: string]: string[] } = {};
|
||||
|
||||
this.dataSource.data.forEach(client => {
|
||||
if (client.ogLive) {
|
||||
// Decide whether to include this client
|
||||
const isSameServer = client.bootServer?.uuid === this.selectedServerId;
|
||||
if (!isSameServer && !moveClients) {
|
||||
return; // Skip if user said NO to moving
|
||||
}
|
||||
|
||||
if (!groupedByTemplate[client.ogLive]) {
|
||||
groupedByTemplate[client.ogLive] = [];
|
||||
}
|
||||
|
|
@ -98,22 +184,27 @@ export class PxeBootFilesComponent implements OnInit {
|
|||
}
|
||||
});
|
||||
|
||||
Object.keys(groupedByTemplate).forEach(templateId => {
|
||||
const templateIds = Object.keys(groupedByTemplate);
|
||||
if (templateIds.length === 0) {
|
||||
this.toastService.info('No hay cambios pendientes de realizar');
|
||||
return;
|
||||
}
|
||||
|
||||
const requests = templateIds.map(templateId => {
|
||||
const clients = groupedByTemplate[templateId];
|
||||
const url = `${this.baseUrl}${templateId}/add-clients`;
|
||||
const payload = { clients: clients };
|
||||
return this.http.post(url, payload);
|
||||
});
|
||||
|
||||
const payload = {
|
||||
clients: clients
|
||||
};
|
||||
|
||||
this.http.post(url, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(`Clientes guardados correctamente para la plantilla`);
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error(`Error al guardar clientes para la plantilla`);
|
||||
}
|
||||
});
|
||||
forkJoin(requests).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success(`Clientes guardados correctamente`);
|
||||
this.refreshClientList();
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error(`Error al guardar clientes`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,22 +1,37 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" [text]="'adminImagesDescription' | translate">
|
||||
{{ 'adminImagesTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<div class="header-container" [style.border-bottom]="isFromDashboard ? 'none' : '1px solid #ddd'" [style.padding]="isFromDashboard ? '10px 0' : '10px 10px'">
|
||||
@if (!isFromDashboard) {
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" [text]="'adminImagesDescription' | translate">
|
||||
{{ 'adminImagesTitle' | translate }}
|
||||
</h2>
|
||||
</div>
|
||||
}
|
||||
<div class="images-button-row" [style.margin-left]="isFromDashboard ? 'auto' : '0'">
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()" [disabled]="!selectedServerId">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" (click)="addImage()" joyrideStep="addImageStep"
|
||||
[text]="'addOgLiveButtonDescription' | translate">
|
||||
[text]="'addOgLiveButtonDescription' | translate" [disabled]="!selectedServerId">
|
||||
{{ 'addOgLiveButton' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
@if (!isFromDashboard) {
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Servidor OgBoot</mat-label>
|
||||
<mat-select [(ngModel)]="selectedServerId" (selectionChange)="onServerChange()">
|
||||
@for (server of bootServers; track server['@id']) {
|
||||
<mat-option [value]="server.uuid || server['@id'].split('/').pop()">
|
||||
{{ server.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
[text]="'searchNameDescription' | translate">
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
|
|
@ -51,76 +66,72 @@
|
|||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
[text]="'tableDescription' | translate">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault'">
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="image.isDefault">
|
||||
{{ 'yesOption' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!image.isDefault">
|
||||
{{ 'noOption' | translate }}
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'downloadUrl'">
|
||||
<span matTooltip="{{ image.downloadUrl }}">
|
||||
{{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }}
|
||||
</span>
|
||||
@if (!loading) {
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
[text]="'tableDescription' | translate">
|
||||
|
||||
@for (column of columns; track column.columnDef) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
@if (column.columnDef === 'isDefault') {
|
||||
<mat-chip>
|
||||
@if (image.isDefault) {
|
||||
{{ 'yesOption' | translate }}
|
||||
} @else {
|
||||
{{ 'noOption' | translate }}
|
||||
}
|
||||
</mat-chip>
|
||||
} @else if (column.columnDef === 'downloadUrl') {
|
||||
<span matTooltip="{{ image.downloadUrl }}">
|
||||
{{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }}
|
||||
</span>
|
||||
} @else if (column.columnDef === 'name') {
|
||||
<span matTooltip="{{ image.name }}">
|
||||
{{ image.name }}
|
||||
</span>
|
||||
} @else if (column.columnDef === 'status') {
|
||||
<mat-chip [ngClass]="getStatusLabel(image.status).class">
|
||||
{{ getStatusLabel(image.status).label }}
|
||||
</mat-chip>
|
||||
} @else {
|
||||
{{ column.cell(image) }}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'name'">
|
||||
<span matTooltip="{{ image.name }}">
|
||||
{{ image.name }}
|
||||
</span>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'status'">
|
||||
<mat-chip [ngClass]="getStatusLabel(image.status).class">
|
||||
{{ getStatusLabel(image.status).label }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container
|
||||
*ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'downloadUrl' && column.columnDef !== 'status' && column.columnDef !== 'name'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumnHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let image" style="text-align: center;" joyrideStep="actionsStep"
|
||||
[text]="'actionsDescription' | translate">
|
||||
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteImage(image)">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item [disabled]="image.status !== 'inactive'" (click)="toggleAction(image, 'install')">{{ 'installOption' |
|
||||
translate }}</button>
|
||||
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'uninstall')">
|
||||
{{ 'uninstallOption' | translate }}
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumnHeader' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let image" style="text-align: center;" joyrideStep="actionsStep"
|
||||
[text]="'actionsDescription' | translate">
|
||||
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
|
||||
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'set-default')">
|
||||
{{ 'setDefaultOption' | translate }}
|
||||
<button mat-icon-button color="warn" (click)="deleteImage(image)">
|
||||
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>menu</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item [disabled]="image.status !== 'inactive'" (click)="toggleAction(image, 'install')">{{ 'installOption' |
|
||||
translate }}</button>
|
||||
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'uninstall')">
|
||||
{{ 'uninstallOption' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'set-default')">
|
||||
{{ 'setDefaultOption' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
}
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep" [text]="'paginationDescription' | translate">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { Component, OnInit, signal, Input } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { CreatePXEImageComponent } from './create-image/create-image/create-image.component';
|
||||
|
|
@ -13,6 +13,7 @@ import { Observable } from "rxjs";
|
|||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ServerInfoDialogComponent } from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pxe-images',
|
||||
|
|
@ -23,6 +24,9 @@ export class PXEimagesComponent implements OnInit {
|
|||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
||||
bootServers: any[] = [];
|
||||
@Input() selectedServerId: string | null = null;
|
||||
isFromDashboard: boolean = false;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 20;
|
||||
|
|
@ -69,15 +73,48 @@ export class PXEimagesComponent implements OnInit {
|
|||
private dataService: DataService,
|
||||
private configService: ConfigService,
|
||||
private toastService: ToastrService,
|
||||
private joyrideService: JoyrideService
|
||||
private joyrideService: JoyrideService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/og-lives`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.selectedServerId) {
|
||||
this.isFromDashboard = true;
|
||||
this.onServerChange();
|
||||
} else {
|
||||
const serverIdParam = this.route.snapshot.queryParamMap.get('serverId');
|
||||
if (serverIdParam) {
|
||||
this.selectedServerId = serverIdParam;
|
||||
this.isFromDashboard = true;
|
||||
this.onServerChange();
|
||||
}
|
||||
}
|
||||
this.loadBootServers();
|
||||
}
|
||||
|
||||
loadBootServers(): void {
|
||||
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.bootServers = response['hydra:member'];
|
||||
if (this.bootServers.length > 0 && !this.selectedServerId) {
|
||||
this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop();
|
||||
this.onServerChange();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching BootServers:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onServerChange(): void {
|
||||
this.filters['bootServer.uuid'] = this.selectedServerId!;
|
||||
this.page = 0;
|
||||
this.search();
|
||||
this.loadAlert();
|
||||
}
|
||||
|
||||
addImage(): void {
|
||||
|
|
@ -199,7 +236,10 @@ export class PXEimagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadAlert(): Observable<any> {
|
||||
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
|
||||
if (!this.selectedServerId) {
|
||||
throw new Error('No boot server selected');
|
||||
}
|
||||
return this.http.get<any>(`${this.baseUrl}/boot-servers/${this.selectedServerId}/oglives/get-collection`);
|
||||
}
|
||||
|
||||
openInfoDialog() {
|
||||
|
|
|
|||
|
|
@ -64,14 +64,14 @@ export class CreatePxeTemplateComponent implements OnInit {
|
|||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.isEditMode = !!this.data;
|
||||
this.isEditMode = !!this.data?.uuid;
|
||||
|
||||
if (this.isEditMode) {
|
||||
this.getPxeClients();
|
||||
}
|
||||
|
||||
this.templateForm = this.fb.group({
|
||||
name: [this.data?.name || '', Validators.required],
|
||||
name: [this.data?.name || '', [Validators.required, Validators.pattern('^[a-zA-Z0-9._-]+$')]],
|
||||
templateContent: [this.data?.templateContent || '', Validators.required],
|
||||
isDefault: [this.data?.isDefault || false]
|
||||
});
|
||||
|
|
@ -98,12 +98,16 @@ export class CreatePxeTemplateComponent implements OnInit {
|
|||
|
||||
createPxeTemplate(): void {
|
||||
const formValues = this.templateForm.value;
|
||||
const payload = {
|
||||
const payload: any = {
|
||||
name: formValues.name,
|
||||
templateContent: formValues.templateContent,
|
||||
isDefault: formValues.isDefault,
|
||||
};
|
||||
|
||||
if (this.data?.bootServerIri) {
|
||||
payload.bootServer = this.data.bootServerIri;
|
||||
}
|
||||
|
||||
this.http.post<any>(`${this.baseUrl}/pxe-templates`, payload).subscribe({
|
||||
next: data => {
|
||||
this.toastService.success('Plantilla PXE creada exitosamente');
|
||||
|
|
|
|||
|
|
@ -1,19 +1,34 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="initTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'adminPxeDescription' | translate }}">{{ 'adminPxeTitle' |
|
||||
translate }}</h2>
|
||||
</div>
|
||||
<div class="template-button-row">
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
|
||||
<div class="header-container" [style.border-bottom]="isFromDashboard ? 'none' : '1px solid #ddd'" [style.padding]="isFromDashboard ? '10px 0' : '10px 10px'">
|
||||
@if (!isFromDashboard) {
|
||||
<button mat-icon-button color="primary" (click)="initTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'adminPxeDescription' | translate }}">{{ 'adminPxeTitle' |
|
||||
translate }}</h2>
|
||||
</div>
|
||||
}
|
||||
<div class="template-button-row" [style.margin-left]="isFromDashboard ? 'auto' : '0'">
|
||||
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()" [disabled]="!selectedServerId">{{ 'viewInfoButton' | translate }}</button>
|
||||
<button class="action-button" (click)="addPxeTemplate()" joyrideStep="addTemplateStep"
|
||||
text="{{ 'addTemplateButtonDescription' | translate }}">{{ 'addTemplateButton' | translate }}</button>
|
||||
text="{{ 'addTemplateButtonDescription' | translate }}" [disabled]="!selectedServerId">{{ 'addTemplateButton' | translate }}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
@if (!isFromDashboard) {
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>Servidor OgBoot</mat-label>
|
||||
<mat-select [(ngModel)]="selectedServerId" (selectionChange)="onServerChange()">
|
||||
@for (server of bootServers; track server['@id']) {
|
||||
<mat-option [value]="server.uuid || server['@id'].split('/').pop()">
|
||||
{{ server.name }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="{{ 'searchNameDescription' | translate }}">
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
|
|
@ -37,34 +52,33 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableDatePxeTemplateText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef === 'synchronized'">
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="image.synchronized">
|
||||
{{ 'yesOption' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!image.synchronized">
|
||||
{{ 'noOption' | translate }}
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault'">
|
||||
<mat-chip>
|
||||
<ng-container *ngIf="image.isDefault">
|
||||
{{ 'yesOption' | translate }}
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!image.isDefault">
|
||||
{{ 'noOption' | translate }}
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'synchronized' && column.columnDef !== 'isDefault'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@for (column of columns; track column.columnDef) {
|
||||
<ng-container [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
@if (column.columnDef === 'synchronized') {
|
||||
<mat-chip>
|
||||
@if (image.synchronized) {
|
||||
{{ 'yesOption' | translate }}
|
||||
} @else {
|
||||
{{ 'noOption' | translate }}
|
||||
}
|
||||
</mat-chip>
|
||||
} @else if (column.columnDef === 'isDefault') {
|
||||
<mat-chip>
|
||||
@if (image.isDefault) {
|
||||
{{ 'yesOption' | translate }}
|
||||
} @else {
|
||||
{{ 'noOption' | translate }}
|
||||
}
|
||||
</mat-chip>
|
||||
} @else {
|
||||
{{ column.cell(image) }}
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumn' | translate }}</th>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import {Component, OnInit} from '@angular/core';
|
||||
import {Component, OnInit, Input} from '@angular/core';
|
||||
import { CreatePxeTemplateComponent } from './manage-pxeTemplate/create-pxe-template.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
|
|
@ -12,6 +12,7 @@ import { JoyrideService } from 'ngx-joyride';
|
|||
import { DeleteModalComponent } from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { ServerInfoDialogComponent } from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-pxe',
|
||||
|
|
@ -22,6 +23,9 @@ export class PxeComponent implements OnInit{
|
|||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
pxeTemplates: any[] = [];
|
||||
bootServers: any[] = [];
|
||||
@Input() selectedServerId: string | null = null;
|
||||
isFromDashboard: boolean = false;
|
||||
currentPage: number = 1;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
|
|
@ -70,15 +74,48 @@ export class PxeComponent implements OnInit{
|
|||
private toastService: ToastrService,
|
||||
private dataService: DataService,
|
||||
private joyrideService: JoyrideService,
|
||||
private configService: ConfigService
|
||||
private configService: ConfigService,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/pxe-templates`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.selectedServerId) {
|
||||
this.isFromDashboard = true;
|
||||
this.onServerChange();
|
||||
} else {
|
||||
const serverIdParam = this.route.snapshot.queryParamMap.get('serverId');
|
||||
if (serverIdParam) {
|
||||
this.selectedServerId = serverIdParam;
|
||||
this.isFromDashboard = true;
|
||||
this.onServerChange();
|
||||
}
|
||||
}
|
||||
this.loadBootServers();
|
||||
}
|
||||
|
||||
loadBootServers(): void {
|
||||
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
|
||||
this.http.get<any>(url).subscribe(
|
||||
response => {
|
||||
this.bootServers = response['hydra:member'];
|
||||
if (this.bootServers.length > 0 && !this.selectedServerId) {
|
||||
this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop();
|
||||
this.onServerChange();
|
||||
}
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching BootServers:', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
onServerChange(): void {
|
||||
this.filters['bootServer.uuid'] = this.selectedServerId!;
|
||||
this.page = 0;
|
||||
this.search();
|
||||
this.loadAlert()
|
||||
}
|
||||
|
||||
search(): void {
|
||||
|
|
@ -97,7 +134,8 @@ export class PxeComponent implements OnInit{
|
|||
|
||||
addPxeTemplate() {
|
||||
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
|
||||
width: '800px'
|
||||
width: '800px',
|
||||
data: { bootServerIri: this.selectedServerId ? `/boot-servers/${this.selectedServerId}` : null }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
|
|
@ -107,7 +145,7 @@ export class PxeComponent implements OnInit{
|
|||
|
||||
editPxeTemplate(template: any) {
|
||||
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
|
||||
data: template,
|
||||
data: { ...template, bootServerIri: this.selectedServerId ? `/boot-servers/${this.selectedServerId}` : null },
|
||||
width: '800px'
|
||||
});
|
||||
|
||||
|
|
@ -126,9 +164,12 @@ export class PxeComponent implements OnInit{
|
|||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
|
||||
next: () => {
|
||||
this.http.post<any>(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Plantilla eliminada exitosamente');
|
||||
if (response && response.warning) {
|
||||
this.toastService.info(response.warning, 'Información', { timeOut: 8000 });
|
||||
}
|
||||
this.search();
|
||||
},
|
||||
error: (error) => {
|
||||
|
|
@ -150,7 +191,10 @@ export class PxeComponent implements OnInit{
|
|||
}
|
||||
|
||||
loadAlert(): Observable<any> {
|
||||
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
|
||||
if (!this.selectedServerId) {
|
||||
throw new Error('No boot server selected');
|
||||
}
|
||||
return this.http.get<any>(`${this.baseUrl}/boot-servers/${this.selectedServerId}/pxe-templates/get-collection`);
|
||||
}
|
||||
|
||||
openInfoDialog() {
|
||||
|
|
|
|||
|
|
@ -67,38 +67,14 @@
|
|||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item (click)="toggleOgBootSub()" matTooltip="{{ 'TOOLTIP_BOOT' | translate }}" matTooltipShowDelay="1000"
|
||||
*ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<mat-list-item routerLink="/boot-servers" (click)="onItemClick()" matTooltip="Servidores Boot"
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">desktop_windows</mat-icon>
|
||||
<span>{{ 'boot' | translate }}</span>
|
||||
<mat-icon class="icon">dns</mat-icon>
|
||||
<span> Servidores Boot</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-nav-list *ngIf="showOgBootSub" style="padding-left: 20px;">
|
||||
<mat-list-item routerLink="/pxe-images" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_PXE_IMAGES' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">album</mat-icon>
|
||||
<span>{{ 'ogLive' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item routerLink="/pxe" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_PXE_TEMPLATES' | translate }}"
|
||||
matTooltipShowDelay="1000">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">assignment</mat-icon>
|
||||
<span>{{ 'pxeTemplates' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
<mat-list-item routerLink="/pxe-boot-file" (click)="onItemClick()"
|
||||
matTooltip="{{ 'TOOLTIP_PXE_BOOT_FILES' | translate }}" matTooltipShowDelay="1000">
|
||||
<span class="entry">
|
||||
<mat-icon class="icon">save</mat-icon>
|
||||
<span>{{ 'pxeBootFiles' | translate }}</span>
|
||||
</span>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
|
||||
<mat-list-item routerLink="/calendars" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_CALENDARS' | translate }}"
|
||||
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
|
||||
<span class="entry">
|
||||
|
|
|
|||
Loading…
Reference in New Issue