From 4546a5477298fe54c553d640750427a67738885e Mon Sep 17 00:00:00 2001 From: jmbardallo Date: Thu, 16 Apr 2026 15:02:31 +0200 Subject: [PATCH] =?UTF-8?q?Gestion=20de=20m=C3=BAltiples=20DHCPs,=20ademas?= =?UTF-8?q?=20se=20permite=20importar=20la=20configuraci=C3=B3n=20de=20un?= =?UTF-8?q?=20dhcp=20junto=20con=20sus=20ordenadores=20a=20la=20estructura?= =?UTF-8?q?=20de=20grupos=20actual?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 5 + ogWebconsole/src/app/app-routing.module.ts | 5 +- ogWebconsole/src/app/app.module.ts | 10 +- .../global-status.component.html | 254 ++++++++++-------- .../global-status/global-status.component.ts | 239 +++++++++++++--- .../create-subnet/create-subnet.component.ts | 8 +- .../dhcp-servers/dhcp-servers.component.css | 62 +++++ .../dhcp-servers/dhcp-servers.component.html | 74 +++++ .../dhcp-servers/dhcp-servers.component.ts | 163 +++++++++++ .../manage-dhcp-server.component.css | 3 + .../manage-dhcp-server.component.html | 30 +++ .../manage-dhcp-server.component.ts | 94 +++++++ .../ogdhcp/og-dhcp-subnets.component.html | 84 +++--- .../ogdhcp/og-dhcp-subnets.component.ts | 94 ++++++- .../ogdhcp/status/status.component.css | 107 -------- .../ogdhcp/status/status.component.html | 65 ----- .../ogdhcp/status/status.component.spec.ts | 75 ------ .../ogdhcp/status/status.component.ts | 80 ------ .../sync-result-dialog.component.css | 57 ++++ .../sync-result-dialog.component.html | 34 +++ .../sync-result-dialog.component.ts | 22 ++ .../sync-subnet-dialog.component.css | 14 + .../sync-subnet-dialog.component.html | 29 ++ .../sync-subnet-dialog.component.ts | 58 ++++ .../app/layout/sidebar/sidebar.component.html | 4 +- ogWebconsole/src/locale/en.json | 2 + ogWebconsole/src/locale/es.json | 2 + 27 files changed, 1137 insertions(+), 537 deletions(-) create mode 100644 ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.css create mode 100644 ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html create mode 100644 ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts create mode 100644 ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.css create mode 100644 ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.html create mode 100644 ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.ts delete mode 100644 ogWebconsole/src/app/components/ogdhcp/status/status.component.css delete mode 100644 ogWebconsole/src/app/components/ogdhcp/status/status.component.html delete mode 100644 ogWebconsole/src/app/components/ogdhcp/status/status.component.spec.ts delete mode 100644 ogWebconsole/src/app/components/ogdhcp/status/status.component.ts create mode 100644 ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.css create mode 100644 ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.html create mode 100644 ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.ts create mode 100644 ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.css create mode 100644 ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.html create mode 100644 ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 23680f8..34c511e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index 0f1abee..75ce24e 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -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'] } }, diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index db56824..843e9b9 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -124,14 +124,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'; @@ -217,6 +220,8 @@ registerLocaleData(localeEs, 'es-ES'); OgbootStatusComponent, OgDhcpSubnetsComponent, CreateSubnetComponent, + DhcpServersComponent, + ManageDhcpServerComponent, AddClientsToSubnetComponent, CommandsComponent, CommandDetailComponent, @@ -235,7 +240,8 @@ registerLocaleData(localeEs, 'es-ES'); DetailTaskComponent, TaskLogsComponent, ServerInfoDialogComponent, - StatusComponent, + SyncSubnetDialogComponent, + SyncResultDialogComponent, ImagesComponent, CreateImageComponent, SoftwareComponent, diff --git a/ogWebconsole/src/app/components/global-status/global-status.component.html b/ogWebconsole/src/app/components/global-status/global-status.component.html index d75b28c..890bebe 100644 --- a/ogWebconsole/src/app/components/global-status/global-status.component.html +++ b/ogWebconsole/src/app/components/global-status/global-status.component.html @@ -17,18 +17,20 @@ -
+ @if(loading) { +

Cargando estado del sistema...

+ } -
+ @if(!loading){ +
-
+
cloud
@@ -37,36 +39,36 @@

Total: {{ repositories.length }}

- -
+ +
storage

OgBoot Server

-

Estado: Operativo

-

Estado: Error

+ @if(!errorOgBoot){ +

Estado: Operativo

+ } + @else{ +

Estado: Error

+ }
- -
+ +
router
-

DHCP Server

-

Estado: Operativo

-

Estado: Error

+

Servidores DHCP

+

Total: {{ dhcps.length }}

- -
+ +
dns
@@ -85,7 +87,7 @@

No hay repositorios disponibles

No se encontraron repositorios configurados en el sistema.

- +
@@ -98,8 +100,10 @@ storage
- -
+ +

{{ getSelectedRepositoryName() }}

@@ -108,13 +112,13 @@ Ver logs
- +

Datos del repositorio: {{ getSelectedRepositoryName() }}

Información específica del repositorio seleccionado

-
- +
storage

Selecciona un repositorio

Elige un repositorio de la lista para ver su estado detallado.

- +
error_outline @@ -164,34 +164,29 @@
-
-
-
-

OgBoot Server

- -
- - - - +
+
+
+

OgBoot Server

+ +
+ + + +

Logs de OgBoot

Logs en tiempo real del servidor OgBoot

-
@@ -215,74 +210,106 @@
-
-
-
-

DHCP Server

- + @if(selectedSection === 'dhcp'){ +
+
+ @if(dhcps.length === 0){ +
+ cloud_off +

No hay dhcps disponibles

+

No se encontraron dhcps configurados en el sistema.

+
+ } + @else { +
+
+ + Seleccionar dhcp + + + {{ dhcp.name }} + + + storage +
- - - - -
-
-

Logs de DHCP

-

Logs en tiempo real del servidor DHCP

+ + @if(selectedDhcpUuid && !errorDhcps[selectedDhcpUuid] && dhcpStatuses[selectedDhcpUuid]){ +
+
+
+

{{ getSelectedDhcpName() }}

+ +
+ +
+ + +
+
+ +
+
+

Logs de DHCP

+

Logs en tiempo real del servidor DHCP

+
+
+ +
+
-
- + } +
+ storage +

Selecciona un dhcp

+

Elige un dhcp de la lista para ver su estado detallado.

+
+ +
+
+ error_outline +

Error de conexión

+

No se pudo conectar con el dhcp seleccionado

+ +
-
-
-
- -

Cargando estado de DHCP...

-
-
-
-
- error_outline -

Error de conexión

-

No se pudo conectar con el servidor DHCP

- -
+ } + +
+ } + + + + + + +
+
+
+

OgCore Server

+
-
-
-
-

OgCore Server

-
-

Logs de OgCore

Logs en tiempo real del servidor OgCore

-
@@ -290,6 +317,7 @@
+ } diff --git a/ogWebconsole/src/app/components/global-status/global-status.component.ts b/ogWebconsole/src/app/components/global-status/global-status.component.ts index 5b87056..20d3896 100644 --- a/ogWebconsole/src/app/components/global-status/global-status.component.ts +++ b/ogWebconsole/src/app/components/global-status/global-status.component.ts @@ -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(`${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(`${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(`${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(`${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(`${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) { diff --git a/ogWebconsole/src/app/components/ogdhcp/create-subnet/create-subnet.component.ts b/ogWebconsole/src/app/components/ogdhcp/create-subnet/create-subnet.component.ts index cd4d368..59f7828 100644 --- a/ogWebconsole/src/app/components/ogdhcp/create-subnet/create-subnet.component.ts +++ b/ogWebconsole/src/app/components/ogdhcp/create-subnet/create-subnet.component.ts @@ -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({ diff --git a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.css b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.css new file mode 100644 index 0000000..30489ff --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.css @@ -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; +} diff --git a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html new file mode 100644 index 0000000..85cad1b --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.html @@ -0,0 +1,74 @@ + + +
+ +
+

+ Servidores DHCP +

+
+
+ +
+
+ + +
+ + Buscar por nombre + + search + Pulsar 'enter' para buscar + + + Buscar por IP + + search + Pulsar 'enter' para buscar + +
+ + + @for (column of columns; track column.columnDef) { + + + + + } + + + + + + + +
{{ column.header }} + @if (column.columnDef !== 'subnets') { + {{ column.cell(server) }} + } + + @if (column.columnDef === 'subnets') { + + } + {{ 'actions' | translate }} + + +
+ +
+ + +
diff --git a/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts new file mode 100644 index 0000000..8d76e64 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/dhcp-servers/dhcp-servers.component.ts @@ -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(); + 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(`${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' + }); + } +} diff --git a/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.css b/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.css new file mode 100644 index 0000000..07b4686 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.css @@ -0,0 +1,3 @@ +.full-width { + width: 100%; +} diff --git a/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.html b/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.html new file mode 100644 index 0000000..f5447f1 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.html @@ -0,0 +1,30 @@ +

{{ data ? 'Editar servidor DHCP' : 'Añadir servidor DHCP' }}

+ +
+ + Nombre + + + + + Dirección IP + + + + + Puerto API + + + + + Comentarios + + +
+
+ + + + diff --git a/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.ts b/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.ts new file mode 100644 index 0000000..dbf7185 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/manage-dhcp-server/manage-dhcp-server.component.ts @@ -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; + serverId: string | null = null; + + constructor( + private fb: FormBuilder, + private http: HttpClient, + public dialogRef: MatDialogRef, + 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(`${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(); + } +} diff --git a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.html b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.html index d786a79..6cfe1b4 100644 --- a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.html +++ b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.html @@ -4,13 +4,17 @@

Administrar Subredes

+ text="Desde aquí puedes gestionar las subredes configuradas en el servidor OgDHCP.">Administrar Subredes {{ serverName ? 'de ' + serverName : '' }}
- + [disabled]="!serverId" + text="Haz clic para ver información detallada de las subredes en el servidor.">Ver Info Externa + +
@@ -39,43 +43,47 @@
- - - - + +
{{ column.header }} - - - {{ subnet[column.columnDef] ? 'check_circle' : 'cancel' }} - +@if (!loading) { + + @for (column of columns; track column.columnDef) { + + + + } - - - + + + + - - {{ column.cell(subnet) }} - - - - - - - - - - - -
{{ column.header }} + @if (column.columnDef === 'synchronized') { + + {{ subnet[column.columnDef] ? 'check_circle' : 'cancel' }} + + } + + @if (column.columnDef === 'clients') { + + } + + @if (column.columnDef !== 'synchronized' && column.columnDef !== 'clients') { + {{ column.cell(subnet) }} + } + Acciones + + + + Acciones - - - -
+
+}
diff --git a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.ts b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.ts index 3f21df0..eb2c18c 100644 --- a/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.ts +++ b/ogWebconsole/src/app/components/ogdhcp/og-dhcp-subnets.component.ts @@ -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(`${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(`${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(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 { - return this.http.get(`${this.baseUrl}/og-dhcp/server/get-collection`); + if (this.serverId) { + return this.http.post(`${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() { diff --git a/ogWebconsole/src/app/components/ogdhcp/status/status.component.css b/ogWebconsole/src/app/components/ogdhcp/status/status.component.css deleted file mode 100644 index de87c64..0000000 --- a/ogWebconsole/src/app/components/ogdhcp/status/status.component.css +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/ogWebconsole/src/app/components/ogdhcp/status/status.component.html b/ogWebconsole/src/app/components/ogdhcp/status/status.component.html deleted file mode 100644 index 73bdbbe..0000000 --- a/ogWebconsole/src/app/components/ogdhcp/status/status.component.html +++ /dev/null @@ -1,65 +0,0 @@ - - -
- -
-

- OgDhcp server Status -

-
-
-
-
-
-

Uso de disco

- - -
-

Total: {{ diskUsage.total }}

-

Ocupado: {{ diskUsage.used }}

-

Disponible: {{ diskUsage.available }}

-

Libre: {{ diskUsage.percentage }}

-
-
- -
-

Servicios

-
    -
  • - - {{ service.name }}: {{ service.status }} -
  • -
-
-
- -
-

Subredes

- - - - - - - - - - - - - - - - - - - -
IDBoot file nameNext serverIpOrdenadores
{{ subnet.id }}{{ subnet['boot-file-name'] }}{{ subnet['next-server'] }}{{ subnet.subnet }}{{ subnet.reservations.length }}
-
-
\ No newline at end of file diff --git a/ogWebconsole/src/app/components/ogdhcp/status/status.component.spec.ts b/ogWebconsole/src/app/components/ogdhcp/status/status.component.spec.ts deleted file mode 100644 index 7621b97..0000000 --- a/ogWebconsole/src/app/components/ogdhcp/status/status.component.spec.ts +++ /dev/null @@ -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; - - 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(); - }); -}); diff --git a/ogWebconsole/src/app/components/ogdhcp/status/status.component.ts b/ogWebconsole/src/app/components/ogdhcp/status/status.component.ts deleted file mode 100644 index 79316bc..0000000 --- a/ogWebconsole/src/app/components/ogdhcp/status/status.component.ts +++ /dev/null @@ -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(`${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' - }); - } - -} diff --git a/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.css b/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.css new file mode 100644 index 0000000..409ebed --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.css @@ -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; +} diff --git a/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.html b/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.html new file mode 100644 index 0000000..50f5025 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.html @@ -0,0 +1,34 @@ +

Reporte de Sincronización

+ + +
+ check_circle + La red y las subredes han sido sincronizadas o importadas + correctamente. +
+ + @if (data.skippedClients && data.skippedClients.length > 0) { +
+
+ info + Colisiones Detectadas ({{ data.skippedClients.length }}) +
+

+ 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. +

+ +
+
    + @for (client of data.skippedClients; track client) { +
  • {{ client }}
  • + } +
+
+
+ } +
+ + + + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.ts b/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.ts new file mode 100644 index 0000000..04c3e22 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/sync-result-dialog/sync-result-dialog.component.ts @@ -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, + @Inject(MAT_DIALOG_DATA) public data: SyncResultData + ) {} + + onClose(): void { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.css b/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.css new file mode 100644 index 0000000..14e82d1 --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.css @@ -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; +} diff --git a/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.html b/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.html new file mode 100644 index 0000000..b7e958f --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.html @@ -0,0 +1,29 @@ +

Sincronización Autodescubrimiento

+ + +

+ Seleccione una Unidad Organizativa Padre (OU) donde agrupar las subredes y clientes que se detecten automáticamente en el servicio DHCP. +

+ +
+ + Unidad Organizativa (Opcional) + + @for (ou of organizationalUnits; track ou.uuid) { + + {{ ou.name }} + + } + + @if (loading) { + Cargando... + } + +
+
+ + + + + + diff --git a/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.ts b/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.ts new file mode 100644 index 0000000..1f976ab --- /dev/null +++ b/ogWebconsole/src/app/components/ogdhcp/sync-subnet-dialog/sync-subnet-dialog.component.ts @@ -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, + @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(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 }); + } +} diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html index 0d6fc28..b51359a 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html @@ -52,11 +52,11 @@ - lan - {{ 'subnets' | translate }} + {{ 'dhcpServers' | translate }} diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index c8efa4e..b4bb49b 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -431,6 +431,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", diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index b991651..b39d125 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -432,6 +432,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", -- 2.40.1