multiple-ogdhcp #68

Merged
jmbardallo merged 2 commits from multiple-ogdhcp into main 2026-04-17 09:07:30 +02:00
27 changed files with 1137 additions and 537 deletions

View File

@ -1,5 +1,10 @@
# Changelog
## [1.4.0] - 2026-04-16
### Added
- Añadida gestión de múltiples Dhcps
- Si el Dhcp tiene ordenadores añadidos en su fichero kea, se pueden sincronizar para añadirlos a la estructura de Grupos actual.
## [1.3.3] - 2026-04-13
### Improved
- Mejorada la visualizacion del selector de particiones mostrando el contenido de cache y la imagen asociada a la partición.

View File

@ -35,8 +35,9 @@ import {
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 { StatusComponent } from "./components/ogdhcp/status/status.component";
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
import {
RunScriptAssistantComponent
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
import { roleGuard } from './guards/role.guard';
@ -58,7 +59,7 @@ const routes: Routes = [
{ path: 'pxe-boot-file', component: PxeBootFilesComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ 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: 'ogdhcp-status', component: StatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
{ path: 'dhcp-servers', component: DhcpServersComponent, 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'] } },

View File

@ -141,14 +141,17 @@ import { InputDialogComponent } from './components/task-logs/input-dialog/input-
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
import { StatusComponent } from "./components/ogdhcp/status/status.component";
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
import { SyncSubnetDialogComponent } from "./components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component";
import { SyncResultDialogComponent } from "./components/ogdhcp/sync-result-dialog/sync-result-dialog.component";
import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create-subnet.component";
import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component";
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component';
import { ManageDhcpServerComponent } from './components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component';
import {NgOptimizedImage, registerLocaleData} from '@angular/common';
import localeEs from '@angular/common/locales/es';
import { GlobalStatusComponent } from './components/global-status/global-status.component';
@ -234,6 +237,8 @@ registerLocaleData(localeEs, 'es-ES');
OgbootStatusComponent,
OgDhcpSubnetsComponent,
CreateSubnetComponent,
DhcpServersComponent,
ManageDhcpServerComponent,
AddClientsToSubnetComponent,
CommandsComponent,
CommandDetailComponent,
@ -252,7 +257,8 @@ registerLocaleData(localeEs, 'es-ES');
DetailTaskComponent,
TaskLogsComponent,
ServerInfoDialogComponent,
StatusComponent,
SyncSubnetDialogComponent,
SyncResultDialogComponent,
ImagesComponent,
CreateImageComponent,
SoftwareComponent,

View File

@ -17,18 +17,20 @@
</div>
<mat-dialog-content [ngClass]="{'loading': loading}">
<div class="spinner-container" *ngIf="loading">
@if(loading) {
<div class="spinner-container">
<div class="loading-content">
<mat-spinner class="loading-spinner"></mat-spinner>
<p class="loading-text">Cargando estado del sistema...</p>
</div>
</div>
}
<div *ngIf="!loading" class="main-content">
@if(!loading){
<div class="main-content">
<div class="system-overview">
<div class="overview-card clickable"
[ngClass]="{'active': selectedSection === 'repositories'}"
(click)="selectSection('repositories')">
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'repositories'}"
(click)="selectSection('repositories')">
<div class="overview-icon">
<mat-icon>cloud</mat-icon>
</div>
@ -37,36 +39,36 @@
<p>Total: <span class="status-badge info">{{ repositories.length }}</span></p>
</div>
</div>
<div class="overview-card clickable"
[ngClass]="{'active': selectedSection === 'ogboot'}"
(click)="selectSection('ogboot')">
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'ogboot'}"
(click)="selectSection('ogboot')">
<div class="overview-icon">
<mat-icon>storage</mat-icon>
</div>
<div class="overview-content">
<h3>OgBoot Server</h3>
<p *ngIf="!errorOgBoot">Estado: <span class="status-badge online">Operativo</span></p>
<p *ngIf="errorOgBoot">Estado: <span class="status-badge offline">Error</span></p>
@if(!errorOgBoot){
<p>Estado: <span class="status-badge online">Operativo</span></p>
}
@else{
<p>Estado: <span class="status-badge offline">Error</span></p>
}
</div>
</div>
<div class="overview-card clickable"
[ngClass]="{'active': selectedSection === 'dhcp'}"
(click)="selectSection('dhcp')">
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'dhcp'}"
(click)="selectSection('dhcp')">
<div class="overview-icon">
<mat-icon>router</mat-icon>
</div>
<div class="overview-content">
<h3>DHCP Server</h3>
<p *ngIf="!errorDhcp">Estado: <span class="status-badge online">Operativo</span></p>
<p *ngIf="errorDhcp">Estado: <span class="status-badge offline">Error</span></p>
<h3>Servidores DHCP</h3>
<p>Total: <span class="status-badge info">{{ dhcps.length }}</span></p>
</div>
</div>
<div class="overview-card clickable"
[ngClass]="{'active': selectedSection === 'ogcore'}"
(click)="selectSection('ogcore')">
<div class="overview-card clickable" [ngClass]="{'active': selectedSection === 'ogcore'}"
(click)="selectSection('ogcore')">
<div class="overview-icon">
<mat-icon>dns</mat-icon>
</div>
@ -85,7 +87,7 @@
<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">
@ -98,8 +100,10 @@
<mat-icon matSuffix>storage</mat-icon>
</mat-form-field>
</div>
<div *ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]" class="selected-repository-content">
<div
*ngIf="selectedRepositoryUuid && !errorRepositories[selectedRepositoryUuid] && repositoryStatuses[selectedRepositoryUuid]"
class="selected-repository-content">
<div class="repository-item">
<div class="section-header">
<h2>{{ getSelectedRepositoryName() }}</h2>
@ -108,13 +112,13 @@
<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]="[
[ramUsage]="repositoryStatuses[selectedRepositoryUuid].ram"
[cpuUsage]="repositoryStatuses[selectedRepositoryUuid].cpu" [diskUsageChartData]="[
{ name: 'Usado', value: repositoryStatuses[selectedRepositoryUuid].disk.used },
{ name: 'Disponible', value: repositoryStatuses[selectedRepositoryUuid].disk.available }
]" [ramUsageChartData]="[
@ -125,30 +129,26 @@
</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"
<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>
@ -164,34 +164,29 @@
</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 *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 [src]="getOgBootIframeUrl()" width="100%" height="800" frameborder="0" class="ogboot-logs-iframe">
</iframe>
</div>
</div>
@ -215,74 +210,106 @@
</div>
</div>
<div *ngIf="selectedSection === 'dhcp'" class="section-content">
<div *ngIf="!errorDhcp && !loadingDhcp" class="tab-content">
<div class="section-header">
<h2>DHCP Server</h2>
<button class="logs-button" (click)="scrollToLogs()">
<mat-icon>article</mat-icon>
<span>Ver logs</span>
</button>
@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>
<app-status-tab [loading]="loadingDhcp" [diskUsage]="dhcpDiskUsage" [servicesStatus]="dhcpServicesStatus"
[subnets]="subnets" [diskUsageChartData]="dhcpDiskUsageChartData" [view]="view" [colorScheme]="colorScheme"
[isDoughnut]="isDoughnut" [showLabels]="showLabels" [isDhcp]="true" [isRepository]="false">
</app-status-tab>
<div 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>
@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 class="logs-container">
<iframe
[src]="getDhcpIframeUrl()"
width="100%"
height="800"
frameborder="0"
class="dhcp-logs-iframe">
</iframe>
}
<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 *ngIf="loadingDhcp" class="loading-container">
<div class="loading-content">
<mat-spinner class="loading-spinner"></mat-spinner>
<p class="loading-text">Cargando estado de DHCP...</p>
</div>
</div>
<div *ngIf="errorDhcp" class="error-container">
<div class="error-card">
<mat-icon class="error-icon">error_outline</mat-icon>
<h3>Error de conexión</h3>
<p>No se pudo conectar con el servidor DHCP</p>
<button mat-raised-button color="primary" (click)="loadDhcpStatus()">
<mat-icon>refresh</mat-icon>
Reintentar
</button>
</div>
}
</div>
</div>
}
<div *ngIf="selectedSection === 'ogcore'" class="section-content">
<div class="tab-content">
<div class="section-header">
<h2>OgCore Server</h2>
</div>
<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 [src]="getOgCoreIframeUrl()" width="100%" height="800" frameborder="0" class="ogcore-logs-iframe">
</iframe>
</div>
</div>
@ -290,6 +317,7 @@
</div>
</div>
</div>
}
</mat-dialog-content>

View File

@ -13,10 +13,10 @@ export class GlobalStatusComponent implements OnInit {
baseUrl: string;
loading: boolean = false;
loadingOgBoot: boolean = false;
loadingDhcp: boolean = false;
loadingDhcps: boolean = false;
loadingRepositories: boolean = false;
errorOgBoot: boolean = false;
errorDhcp: boolean = false;
errorDhcps: { [key: string]: boolean } = {};
errorRepositories: { [key: string]: boolean } = {};
installedOgLives: any[] = [];
subnets: any[] = [];
@ -38,10 +38,12 @@ export class GlobalStatusComponent implements OnInit {
ogBootServicesStatus: any = {};
ogBootDiskUsageChartData: any[] = [];
dhcpApiUrl: string;
dhcpsApiUrl: string;
dhcps: any[] = [];
dhcpDiskUsage: any = {};
dhcpServicesStatus: any = {};
dhcpStatuses: { [key: string]: any } = {};
dhcpDiskUsageChartData: any[] = [];
selectedDhcpUuid: string = '';
isDhcp: boolean = false;
isRepository: boolean = false;
@ -62,40 +64,62 @@ export class GlobalStatusComponent implements OnInit {
) {
this.baseUrl = this.configService.apiUrl;
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
this.dhcpApiUrl = `${this.baseUrl}/og-dhcp/status`;
this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`;
this.ogCoreApiUrl = `${this.baseUrl}`;
this.repositoriesUrl = `${this.baseUrl}/image-repositories`;
this.ogBootDiskUsageChartData = [];
this.dhcpDiskUsageChartData = [];
}
ngOnInit(): void {
this.updateLastUpdateTime();
this.loadOgBootStatus();
this.loadDhcpStatus();
this.loadDhcps(false);
this.loadRepositories(false);
this.syncSubnets();
this.syncTemplates();
this.syncOgLives();
}
syncSubnets() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las subredes: tiempo de espera agotado');
}, 3500);
this.http.get<any>(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(data => {
const dhcps = data['hydra:member'] || [];
if (dhcps.length === 0) return;
this.http.post(`${this.baseUrl}/subnets/sync`, {}).subscribe({
next: (response) => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con componente DHCP exitosa');
},
error: (error) => {
clearTimeout(timeoutId);
this.toastService.error(error.error['hydra:description'] || 'Error al sincronizar las subredes');
}
let pending = dhcps.length;
let hasError = false;
const timeoutId = setTimeout(() => {
if (!hasError) {
this.toastService.error('Error al sincronizar las subredes: tiempo de espera agotado');
hasError = true;
}
}, 5000);
dhcps.forEach((dhcp: any) => {
this.http.post(`${this.baseUrl}/dhcp-servers/${dhcp.uuid}/sync`, {}).subscribe({
next: () => {
pending--;
if (pending === 0 && !hasError) {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de subredes de servidores DHCP exitosa');
}
},
error: (error) => {
pending--;
if (!hasError) {
hasError = true;
clearTimeout(timeoutId);
this.toastService.error(error.error['hydra:description'] || `Error al sincronizar subredes del DHCP ${dhcp.name || dhcp.uuid}`);
}
}
});
});
}, error => {
console.error('Error fetching DHCP servers for subnet sync', error);
});
}
@ -133,11 +157,11 @@ export class GlobalStatusComponent implements OnInit {
loadStatus(apiUrl: string, diskUsage: any, servicesStatus: any, diskUsageChartData: any[], installedOgLives: any[], isDhcp: boolean, errorState: string, showLoading: boolean = true): void {
if (isDhcp) {
this.loadingDhcp = true;
this.loadingDhcps = true;
} else {
this.loadingOgBoot = true;
}
if (showLoading) {
this.loading = true;
}
@ -147,7 +171,7 @@ export class GlobalStatusComponent implements OnInit {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcp = false;
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
@ -185,7 +209,7 @@ export class GlobalStatusComponent implements OnInit {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcp = false;
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
@ -197,7 +221,7 @@ export class GlobalStatusComponent implements OnInit {
this.loading = false;
}
if (isDhcp) {
this.loadingDhcp = false;
this.loadingDhcps = false;
} else {
this.loadingOgBoot = false;
}
@ -212,11 +236,11 @@ export class GlobalStatusComponent implements OnInit {
this.loading = true;
}
this.errorRepositories = {};
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
this.repositories = data['hydra:member'] || [];
if (this.repositories.length === 0) {
if (showLoading) {
this.loading = false;
@ -260,14 +284,14 @@ export class GlobalStatusComponent implements OnInit {
}
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
console.log(`Cargando estado del repositorio: ${repositoryUuid}`);
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);
@ -276,7 +300,7 @@ export class GlobalStatusComponent implements OnInit {
const details = data.details;
const { disk, services, ram, cpu, processes } = details;
this.repositoryStatuses[repositoryUuid] = {
disk: {
...disk,
@ -314,9 +338,124 @@ export class GlobalStatusComponent implements OnInit {
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorOgBoot', false);
}
loadDhcpStatus(): void {
this.isDhcp = true;
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, this.isDhcp, 'errorDhcp', 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 => {
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);
}
});
});
},
error => {
this.loading = false;
this.dhcps = [];
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;
}
});
}
}
getSelectedDhcpName(): string {
const selectedDhcp = this.dhcps.find(dhcp => dhcp.uuid === this.selectedDhcpUuid);
return selectedDhcp ? selectedDhcp.name : '';
}
@ -352,8 +491,14 @@ export class GlobalStatusComponent implements OnInit {
}
getDhcpIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
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 {
@ -365,7 +510,7 @@ export class GlobalStatusComponent implements OnInit {
this.loading = true;
this.updateLastUpdateTime();
this.loadStatus(this.ogBootApiUrl, this.ogBootDiskUsage, this.ogBootServicesStatus, this.ogBootDiskUsageChartData, this.installedOgLives, false, 'errorOgBoot', true);
this.loadStatus(this.dhcpApiUrl, this.dhcpDiskUsage, this.dhcpServicesStatus, this.dhcpDiskUsageChartData, this.installedOgLives, true, 'errorDhcp', true);
this.loadDhcps(true);
this.loadRepositories(true);
this.syncSubnets();
this.syncTemplates();
@ -402,11 +547,22 @@ export class GlobalStatusComponent implements OnInit {
});
}
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;
@ -416,9 +572,6 @@ export class GlobalStatusComponent implements OnInit {
}
break;
case 'dhcp':
if (!this.dhcpDiskUsage) {
this.loadDhcpStatus();
}
break;
case 'ogcore':
break;
@ -444,7 +597,7 @@ export class GlobalStatusComponent implements OnInit {
default:
return;
}
const checkAndScroll = () => {
const logsSection = document.getElementById(logsSectionId);
if (logsSection) {

View File

@ -38,7 +38,7 @@ export class CreateSubnetComponent implements OnInit {
}
ngOnInit(): void {
if (this.data) {
if (this.data && this.data.uuid) {
this.loadData();
this.isEditMode = true;
}
@ -67,7 +67,11 @@ export class CreateSubnetComponent implements OnInit {
return;
}
const payload = this.subnetForm.value;
const payload: any = { ...this.subnetForm.value };
if (!this.isEditMode && this.data?.serverUuid) {
payload.dhcpServer = `/dhcp-servers/${this.data.serverUuid}`;
}
if (!this.isEditMode) {
this.http.post(`${this.baseUrl}/subnets`, payload).subscribe({

View File

@ -0,0 +1,62 @@
.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;
}

View File

@ -0,0 +1,74 @@
<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="dhcpServerTitleStep" text="Vista de Servidores DHCP">
Servidores DHCP
</h2>
</div>
<div class="images-button-row">
<button
class="action-button" (click)="addServer()"
joyrideStep="addStep"
text="Añadir Servidor DHCP">Añadir Servidor DHCP</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 DHCP">
@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">
@if (column.columnDef !== 'subnets') {
{{ column.cell(server) }}
}
@if (column.columnDef === 'subnets') {
<button
class="action-button"
(click)="openSubnetsView(server)">
Ver Subredes
</button>
}
</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="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>

View File

@ -0,0 +1,163 @@
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 {ManageDhcpServerComponent} from "../manage-dhcp-server/manage-dhcp-server.component";
@Component({
selector: 'app-dhcp-servers',
templateUrl: './dhcp-servers.component.html',
styleUrl: './dhcp-servers.component.css'
})
export class DhcpServersComponent 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: 'subnets',
header: 'Subredes',
cell: (server: any) => server.subnets
},
{
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', 'subnets', '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}/dhcp-servers`;
}
ngOnInit(): void {
this.search();
}
addServer(): void {
const dialogRef = this.dialog.open(ManageDhcpServerComponent, {
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(ManageDhcpServerComponent, {
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');
}
});
}
});
}
openSubnetsView(server: any) {
// Navigate to subnets view filtered by this server uuid
this.router.navigate(['/subnets'], { queryParams: { serverId: server.uuid } });
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.search();
}
initTour(): void {
this.joyrideService.startTour({
steps: [
'dhcpServerTitleStep',
'addStep',
'searchStep',
'tableDateStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -0,0 +1,3 @@
.full-width {
width: 100%;
}

View File

@ -0,0 +1,30 @@
<h2 mat-dialog-title>{{ data ? 'Editar servidor DHCP' : 'Añadir servidor DHCP' }}</h2>
<mat-dialog-content>
<form [formGroup]="dhcpServerForm">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Nombre</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Dirección IP</mat-label>
<input matInput formControlName="ip" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Puerto API</mat-label>
<input matInput formControlName="apiPort" type="number" required>
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Comentarios</mat-label>
<textarea matInput formControlName="comments" rows="3"></textarea>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">Cancelar</button>
<button mat-button color="primary" [disabled]="dhcpServerForm.invalid" (click)="save()">
Guardar
</button>
</mat-dialog-actions>

View File

@ -0,0 +1,94 @@
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-dhcp-server',
templateUrl: './manage-dhcp-server.component.html',
styleUrl: './manage-dhcp-server.component.css'
})
export class ManageDhcpServerComponent implements OnInit {
baseUrl: string;
dhcpServerForm: FormGroup<any>;
serverId: string | null = null;
constructor(
private fb: FormBuilder,
private http: HttpClient,
public dialogRef: MatDialogRef<ManageDhcpServerComponent>,
private toastService: ToastrService,
private configService: ConfigService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
this.dhcpServerForm = this.fb.group({
name: [null, Validators.required],
ip: [null, Validators.required],
apiPort: [8081, Validators.required],
comments: [null],
});
}
ngOnInit() {
if (this.data) {
this.load()
}
}
load(): void {
// data comes as /dhcp-servers/{uuid}
this.http.get<any>(`${this.baseUrl}${this.data}`).subscribe({
next: (response) => {
this.dhcpServerForm.patchValue({
name: response.name,
ip: response.ip,
apiPort: response.apiPort ?? 8081,
comments: response.comments,
});
this.serverId = response['@id'];
},
error: (err) => {
console.error('Error fetching dhcp server:', err);
this.toastService.error('Error cargando los datos');
}
});
}
save(): void {
const payload = {
name: this.dhcpServerForm.value.name,
ip: this.dhcpServerForm.value.ip,
apiPort: parseInt(this.dhcpServerForm.value.apiPort),
comments: this.dhcpServerForm.value.comments,
};
if (this.serverId) {
this.http.put(`${this.baseUrl}${this.serverId}`, payload).subscribe({
next: () => {
this.toastService.success('Servidor DHCP 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}/dhcp-servers`, payload).subscribe({
next: () => {
this.toastService.success('Servidor DHCP 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();
}
}

View File

@ -4,13 +4,17 @@
</button>
<div class="header-container-title">
<h2 class="title" i18n="@@subnetsTitle" joyrideStep="titleStep"
text="Desde aquí puedes gestionar las subredes configuradas en el servidor OgDHCP.">Administrar Subredes</h2>
text="Desde aquí puedes gestionar las subredes configuradas en el servidor OgDHCP.">Administrar Subredes {{ serverName ? 'de ' + serverName : '' }}</h2>
</div>
<div class="subnets-button-row">
<button class="action-button" (click)="openSubnetInfoDialog()" joyrideStep="viewInfoStep"
text="Haz clic para ver información detallada de las subredes en el servidor.">Ver Información</button>
<button class="action-button" (click)="addSubnet()" joyrideStep="addSubnetStep"
text="Haz clic para añadir una nueva subred.">Añadir Subred</button>
[disabled]="!serverId"
text="Haz clic para ver información detallada de las subredes en el servidor.">Ver Info Externa</button>
<button class="action-button" (click)="syncSubnets()" style="margin-left: 0.5rem;"
[disabled]="!serverId"
text="Sincronizar las subredes configuradas externamente con oggui.">Sincronizar</button>
<button class="action-button" (click)="addSubnet()" joyrideStep="addSubnetStep" style="margin-left: 0.5rem;"
text="Haz clic para añadir una nueva subred.">Crear Subred Local</button>
</div>
</div>
@ -39,43 +43,47 @@
</div>
<app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let subnet">
<ng-container *ngIf="column.columnDef === 'synchronized'">
<mat-icon [color]="subnet[column.columnDef] ? 'primary' : 'warn'">
{{ subnet[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
@if (!loading) {
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="Visualiza y administra las subredes listadas según los filtros aplicados.">
@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 subnet">
@if (column.columnDef === 'synchronized') {
<mat-icon [color]="subnet[column.columnDef] ? 'primary' : 'warn'">
{{ subnet[column.columnDef] ? 'check_circle' : 'cancel' }}
</mat-icon>
}
@if (column.columnDef === 'clients') {
<button class="action-button" (click)="openShowClientsDialog(subnet)" >Ver clientes</button>
}
@if (column.columnDef !== 'synchronized' && column.columnDef !== 'clients') {
{{ column.cell(subnet) }}
}
</td>
</ng-container>
}
<ng-container *ngIf="column.columnDef === 'clients'">
<button class="action-button" (click)="openShowClientsDialog(subnet)" >Ver clientes</button>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let subnet" style="text-align: center;" joyrideStep="actionsStep"
text="Gestiona cada subred con opciones para editar, eliminar y más.">
<button mat-icon-button color="primary" (click)="editSubnet(subnet)" i18n="@@editSubnet">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="primary"
(click)="addClientsToSubnet(subnet)"><mat-icon>computer</mat-icon></button>
<button mat-icon-button color="warn" (click)="toggleAction(subnet, 'delete')"><mat-icon>delete</mat-icon></button>
</td>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'synchronized' && column.columnDef !== 'clients'">
{{ column.cell(subnet) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let subnet" style="text-align: center;" joyrideStep="actionsStep"
text="Gestiona cada subred con opciones para editar, eliminar y más.">
<button mat-icon-button color="primary" (click)="editSubnet(subnet)" i18n="@@editSubnet">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="primary"
(click)="addClientsToSubnet(subnet)"><mat-icon>computer</mat-icon></button>
<button mat-icon-button color="warn" (click)="toggleAction(subnet, 'delete')"><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>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
}
<div class="paginator-container" joyrideStep="paginationStep"
text="Navega entre las páginas de subredes usando el paginador.">

View File

@ -7,11 +7,14 @@ import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr';
import { AddClientsToSubnetComponent } from './add-clients-to-subnet/add-clients-to-subnet.component';
import { ServerInfoDialogComponent } from "./server-info-dialog/server-info-dialog.component";
import { Observable } from "rxjs";
import { Observable, of } from "rxjs";
import { JoyrideService } from 'ngx-joyride';
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
import { ShowClientsComponent } from "./show-clients/show-clients.component";
import { ConfigService } from '@services/config.service';
import { ActivatedRoute } from '@angular/router';
import { SyncSubnetDialogComponent } from "./sync-subnet-dialog/sync-subnet-dialog.component";
import { SyncResultDialogComponent } from "./sync-result-dialog/sync-result-dialog.component";
export interface Subnet {
'@id': string;
@ -47,6 +50,8 @@ export class OgDhcpSubnetsComponent implements OnInit {
pageSizeOptions: number[] = [5, 10, 20];
alertMessage: string | null = null;
loading: boolean = false;
serverId: string | null = null;
serverName: string = '';
@ViewChild(MatPaginator) paginator: MatPaginator | undefined;
@ -63,26 +68,54 @@ export class OgDhcpSubnetsComponent implements OnInit {
private apiUrl: string;
constructor(public dialog: MatDialog, private http: HttpClient, private toastService: ToastrService,
private joyrideService: JoyrideService, private configService: ConfigService) {
private joyrideService: JoyrideService, private configService: ConfigService, private route: ActivatedRoute) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/subnets`;
}
ngOnInit() {
this.loadAlert()
this.loadSubnets()
this.route.queryParams.subscribe(params => {
if (params['serverId']) {
this.serverId = params['serverId'];
this.filters['dhcpServer.uuid'] = params['serverId'];
this.loadServerName();
}
this.loadAlert();
this.loadSubnets();
});
}
loadServerName() {
this.http.get<any>(`${this.baseUrl}/dhcp-servers/${this.serverId}`).subscribe({
next: (response) => {
this.serverName = response.name;
},
error: () => {
console.error('No se pudo resolver el nombre del servidor DHCP');
}
});
}
loadSubnets() {
this.loading = true;
this.http.get<any>(`${this.baseUrl}/subnets?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`).subscribe({
let url = `${this.baseUrl}/subnets?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`;
// Añadir filtros a la URL
Object.keys(this.filters).forEach(key => {
if (this.filters[key]) {
url += `&${key}=${this.filters[key]}`;
}
});
this.http.get<any>(url).subscribe({
next: (response) => {
this.dataSource.data = response['hydra:member'];
this.length = response['hydra:totalItems'];
this.loading = false;
},
error: error => {
this.toastService.error(error.error['hydra:description']);
const errorMsg = error.error?.['hydra:description'] || 'Error al cargar subredes';
this.toastService.error(errorMsg);
this.loading = false;
}
});
@ -92,6 +125,11 @@ export class OgDhcpSubnetsComponent implements OnInit {
}
}
search() {
this.page = 0;
this.loadSubnets();
}
toggleAction(subnet: any, action: string): void {
switch (action) {
case 'get':
@ -159,7 +197,8 @@ export class OgDhcpSubnetsComponent implements OnInit {
addSubnet() {
const dialogRef = this.dialog.open(CreateSubnetComponent, {
width: '600px'
width: '600px',
data: { serverUuid: this.serverId }
});
dialogRef.afterClosed().subscribe(result => {
@ -179,7 +218,46 @@ export class OgDhcpSubnetsComponent implements OnInit {
}
loadAlert(): Observable<any> {
return this.http.get<any>(`${this.baseUrl}/og-dhcp/server/get-collection`);
if (this.serverId) {
return this.http.post<any>(`${this.baseUrl}/dhcp-servers/${this.serverId}/get-collection`, {});
}
return of({message: 'Seleccione un servidor DHCP para consultar el estado'});
}
syncSubnets(): void {
if (!this.serverId) {
this.toastService.error('Debe seleccionar un servidor DHCP para sincronizar');
return;
}
const dialogRef = this.dialog.open(SyncSubnetDialogComponent, {
width: '600px',
data: { serverUuid: this.serverId }
});
dialogRef.afterClosed().subscribe(result => {
if (result === undefined) {
return; // dialog cancelled
}
this.loading = true;
const payload = result.parentOuId ? { parentOuId: result.parentOuId } : {};
this.http.post(`${this.baseUrl}/dhcp-servers/${this.serverId}/sync`, payload).subscribe({
next: (response: any) => {
this.dialog.open(SyncResultDialogComponent, {
width: '600px',
data: { skippedClients: response.skipped_clients || [] }
});
this.loadSubnets();
},
error: (error) => {
console.error('Error al sincronizar subredes', error);
this.toastService.error(error.error?.['hydra:description'] || 'Error al sincronizar subredes');
this.loading = false;
}
});
});
}
openSubnetInfoDialog() {

View File

@ -1,107 +0,0 @@
.disk-usage-info {
display: flex;
justify-content: start;
margin-top: 10px;
}
p {
margin-left: 15px;
}
.dashboard {
padding: 0.5rem 0.5rem 0.5rem 1rem;
}
.disk-usage-container {
display: flex;
align-items: flex-start;
margin-bottom: 20px;
}
.disk-usage {
flex: 2;
margin-right: 20px;
}
.services-status {
flex: 1;
}
.services-status ul {
display: flex;
flex-direction: column;
justify-content: center;
list-style-type: none;
padding: 0;
}
.services-status li {
justify-content: left;
margin: 5px 0;
display: flex;
align-items: center;
}
.status-led {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
margin-right: 10px;
}
.status-led.active {
background-color: green;
}
.status-led.inactive {
background-color: red;
}
.installed-oglives {
margin-top: 20px;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
border: 1px solid #ddd;
padding: 8px;
}
th {
background-color: #f4f4f4;
}
.button-container {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 50px;
}
.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;
}
.btn:first-child {
margin-left: 0;
}
.btn:last-child {
margin-right: 0;
}

View File

@ -1,65 +0,0 @@
<app-loading [isLoading]="loading"></app-loading>
<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="Esta sección muestra el estado general del servidor OgDhcp.">
OgDhcp server Status
</h2>
</div>
</div>
<div class="dashboard">
<div class="disk-usage-container">
<div class="disk-usage" joyrideStep="diskUsageStep" text="Visualiza el uso del disco del servidor.">
<h3>Uso de disco</h3>
<ngx-charts-pie-chart [view]="view" [scheme]="colorScheme" [results]="diskUsageChartData" [gradient]="gradient"
[doughnut]="isDoughnut" [labels]="showLabels" [legend]="showLegend">
</ngx-charts-pie-chart>
<div class="disk-usage-info">
<p>Total: {{ diskUsage.total }}</p>
<p>Ocupado: {{ diskUsage.used }}</p>
<p>Disponible: {{ diskUsage.available }}</p>
<p>Libre: {{ diskUsage.percentage }}</p>
</div>
</div>
<div class="services-status" joyrideStep="servicesStatusStep"
text="Aquí puedes ver el estado de los servicios importantes del servidor.">
<h3>Servicios</h3>
<ul>
<li *ngFor="let service of getServices()">
<span class="status-led"
[ngClass]="{ 'active': service.status === 'active', 'inactive': service.status !== 'active' }"></span>
{{ service.name }}: {{ service.status }}
</li>
</ul>
</div>
</div>
<div class="installed-oglives" joyrideStep="subnetsStep"
text="Consulta la información de las subredes configuradas en el servidor.">
<h3>Subredes</h3>
<table>
<thead>
<tr>
<th>ID</th>
<th>Boot file name</th>
<th>Next server</th>
<th>Ip</th>
<th>Ordenadores</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let subnet of subnets">
<td>{{ subnet.id }}</td>
<td>{{ subnet['boot-file-name'] }}</td>
<td>{{ subnet['next-server'] }}</td>
<td>{{ subnet.subnet }}</td>
<td>{{ subnet.reservations.length }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -1,75 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { StatusComponent } from './status.component';
import { MatInputModule } from '@angular/material/input';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatOptionModule } from '@angular/material/core';
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { MatTableModule } from '@angular/material/table';
import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { NgxChartsModule } from '@swimlane/ngx-charts';
import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride';
import {LoadingComponent} from "../../../shared/loading/loading.component";
import { ConfigService } from '@services/config.service';
describe('StatusComponent', () => {
let component: StatusComponent;
let fixture: ComponentFixture<StatusComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [StatusComponent, LoadingComponent],
imports: [
HttpClientTestingModule,
ToastrModule.forRoot(),
BrowserAnimationsModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatButtonModule,
MatTableModule,
MatPaginatorModule,
MatTooltipModule,
FormsModule,
MatProgressSpinnerModule,
MatOptionModule,
ReactiveFormsModule,
MatSelectModule,
MatDialogModule,
NgxChartsModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: {} },
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(StatusComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,80 +0,0 @@
import { Component } from '@angular/core';
import { HttpClient } from "@angular/common/http";
import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
@Component({
selector: 'app-status',
templateUrl: './status.component.html',
styleUrl: './status.component.css'
})
export class StatusComponent {
baseUrl: string;
diskUsage: any = {};
servicesStatus: any = {};
subnets: any[] = [];
diskUsageChartData: any[] = [];
loading: boolean = false;
view: [number, number] = [1100, 500];
gradient: boolean = true;
showLegend: boolean = true;
showLabels: boolean = true;
isDoughnut: boolean = true;
colorScheme: any = {
domain: ['#FF6384', '#3f51b5']
};
constructor(private http: HttpClient, private joyrideService: JoyrideService, private configService: ConfigService) {
this.baseUrl = this.configService.apiUrl;
}
ngOnInit(): void {
this.loadStatus();
}
loadStatus(): void {
this.loading = true;
this.http.get<any>(`${this.baseUrl}/og-dhcp/status`).subscribe(data => {
this.diskUsage = data.message.disk_usage;
this.servicesStatus = data.message.services_status;
this.subnets = data.message.subnets;
this.diskUsageChartData = [
{
name: 'Usado',
value: parseFloat(this.diskUsage.used)
},
{
name: 'Disponible',
value: parseFloat(this.diskUsage.available)
}
];
this.loading = false;
}, error => {
this.loading = false;
console.error('Error fetching status', error);
});
}
getServices(): { name: string, status: string }[] {
return Object.keys(this.servicesStatus).map(key => ({
name: key,
status: this.servicesStatus[key]
}));
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: [
'titleStep',
'diskUsageStep',
'servicesStatusStep',
'subnetsStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -0,0 +1,57 @@
.sync-result-container {
margin-bottom: 20px;
}
.sync-result-header {
display: flex;
align-items: center;
gap: 8px;
color: #2e7d32;
margin-bottom: 15px;
}
.sync-result-header-text {
font-weight: 500;
}
.collision-box {
margin-top: 20px;
padding: 15px;
border-radius: 8px;
background-color: #fff3e0;
border: 1px solid #ffe0b2;
}
.collision-header {
display: flex;
align-items: center;
gap: 8px;
color: #e65100;
margin-bottom: 10px;
}
.collision-header-text {
font-weight: 500;
}
.collision-description {
margin-bottom: 10px;
font-size: 14px;
color: #666;
}
.collision-list-container {
max-height: 250px;
overflow-y: auto;
background: white;
padding: 10px;
border-radius: 4px;
border: 1px solid #ddd;
}
.collision-list {
margin: 0;
padding-left: 20px;
font-family: monospace;
font-size: 13px;
}

View File

@ -0,0 +1,34 @@
<h2 mat-dialog-title>Reporte de Sincronización</h2>
<mat-dialog-content class="dialog-content">
<div class="sync-result-header">
<mat-icon>check_circle</mat-icon>
<span class="sync-result-header-text">La red y las subredes han sido sincronizadas o importadas
correctamente.</span>
</div>
@if (data.skippedClients && data.skippedClients.length > 0) {
<div class="collision-box">
<div class="collision-header">
<mat-icon>info</mat-icon>
<span class="collision-header-text">Colisiones Detectadas ({{ data.skippedClients.length }})</span>
</div>
<p class="collision-description">
Los siguientes ordenadores ya existían previamente en la jerarquía (en otras aulas o Unidades). Sólo se ha
actualizado su dependencia de subred, pero no se han movido de su ubicación actual.
</p>
<div class="collision-list-container">
<ul class="collision-list">
@for (client of data.skippedClients; track client) {
<li>{{ client }}</li>
}
</ul>
</div>
</div>
}
</mat-dialog-content>
<mat-dialog-actions class="action-container" style="justify-content: flex-end;">
<button class="submit-button" (click)="onClose()" cdkFocusInitial>Aceptar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,22 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
export interface SyncResultData {
skippedClients: string[];
}
@Component({
selector: 'app-sync-result-dialog',
templateUrl: './sync-result-dialog.component.html',
styleUrls: ['./sync-result-dialog.component.css']
})
export class SyncResultDialogComponent {
constructor(
public dialogRef: MatDialogRef<SyncResultDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: SyncResultData
) {}
onClose(): void {
this.dialogRef.close();
}
}

View File

@ -0,0 +1,14 @@
.dialog-title {
margin-bottom: 20px;
color: #666;
}
.action-buttons {
justify-content: flex-end;
gap: 10px;
}
.cancel-button {
background-color: #f44336;
color: white;
}

View File

@ -0,0 +1,29 @@
<h2 mat-dialog-title>Sincronización Autodescubrimiento</h2>
<mat-dialog-content class="dialog-content">
<p class="dialog-title">
Seleccione una Unidad Organizativa Padre (OU) donde agrupar las subredes y clientes que se detecten automáticamente en el servicio DHCP.
</p>
<div class="spacing-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Unidad Organizativa (Opcional)</mat-label>
<mat-select [(ngModel)]="selectedOuUuid" [disabled]="loading">
@for (ou of organizationalUnits; track ou.uuid) {
<mat-option [value]="ou.uuid">
{{ ou.name }}
</mat-option>
}
</mat-select>
@if (loading) {
<mat-hint>Cargando...</mat-hint>
}
</mat-form-field>
</div>
</mat-dialog-content>
<mat-dialog-actions class="action-container action-buttons">
<button class="ordinary-button cancel-button" (click)="onCancel()">Cancelar</button>
<button class="ordinary-button" (click)="onSyncWithoutOu()">Solo Subredes</button>
<button class="submit-button" (click)="onSync()" [disabled]="!selectedOuUuid" cdkFocusInitial>Sincronizar todo</button>
</mat-dialog-actions>

View File

@ -0,0 +1,58 @@
import { Component, Inject, OnInit } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
import { ToastrService } from 'ngx-toastr';
@Component({
selector: 'app-sync-subnet-dialog',
templateUrl: './sync-subnet-dialog.component.html',
styleUrls: ['./sync-subnet-dialog.component.css']
})
export class SyncSubnetDialogComponent implements OnInit {
organizationalUnits: any[] = [];
selectedOuUuid: string | null = null;
loading: boolean = false;
constructor(
public dialogRef: MatDialogRef<SyncSubnetDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private http: HttpClient,
private configService: ConfigService,
private toastService: ToastrService
) {}
ngOnInit(): void {
this.loadParentOUs();
}
loadParentOUs(): void {
this.loading = true;
const url = `${this.configService.apiUrl}/organizational-units?page=1&itemsPerPage=100`;
this.http.get<any>(url).subscribe({
next: (response) => {
// Filter out types that act as 'classrooms' assuming 'group' or 'classroom', we only want tree parents.
// Actually, we can just list all OUs and let the admin decide where to put them, but conventionally groups are ignored.
this.organizationalUnits = (response['hydra:member'] || []).filter((ou: any) => ou.type !== 'group' && ou.type !== 'classroom');
this.loading = false;
},
error: (error) => {
this.toastService.error('Error cargando las Unidades Organizativas');
this.loading = false;
}
});
}
onCancel(): void {
this.dialogRef.close();
}
onSync(): void {
this.dialogRef.close({ parentOuId: this.selectedOuUuid });
}
onSyncWithoutOu(): void {
this.dialogRef.close({ parentOuId: null });
}
}

View File

@ -59,11 +59,11 @@
</mat-list-item>
</mat-nav-list>
<mat-list-item routerLink="/subnets" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_SUBNETS' | translate }}"
<mat-list-item routerLink="/dhcp-servers" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_DHCP_SERVERS' | translate }}"
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
<span class="entry">
<mat-icon class="icon">lan</mat-icon>
<span> {{ 'subnets' | translate }}</span>
<span> {{ 'dhcpServers' | translate }}</span>
</span>
</mat-list-item>

View File

@ -501,6 +501,8 @@
"TOOLTIP_DHCP_STATUS": "Current status of the DHCP service",
"subnets": "Subnets",
"TOOLTIP_SUBNETS": "Manage and create subnets",
"dhcpServers": "DHCP Servers",
"TOOLTIP_DHCP_SERVERS": "Manage and register DHCP servers",
"boot": "Boot",
"TOOLTIP_BOOT": "Configure and manage boot options",
"ogLive": "ogLive",

View File

@ -502,6 +502,8 @@
"TOOLTIP_DHCP_STATUS": "Estado actual del servicio DHCP",
"subnets": "Subredes",
"TOOLTIP_SUBNETS": "Gestionar y crear subredes",
"dhcpServers": "Servidores DHCP",
"TOOLTIP_DHCP_SERVERS": "Gestionar y registrar servidores DHCP",
"boot": "Boot",
"TOOLTIP_BOOT": "Configurar y administrar opciones de arranque",
"ogLive": "ogLive",