diff --git a/CHANGELOG.md b/CHANGELOG.md index 34c511e..e2a0778 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [1.5.0] - 2026-04-21 +### Added +- Añadida gestión de múltiples ogBoot Servers +- Si es un ogBoot existente, permite sincronizar su información sobre oglives y plantillas pxe. +- En los Ajustes de red de equipos, aulas y grupos, ahora se debe seleccionar servidor ogBoot y luego oglive y plantilla dentro de dicho servidor. + ## [1.4.0] - 2026-04-16 ### Added - Añadida gestión de múltiples Dhcps diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts index 75ce24e..0ef750c 100644 --- a/ogWebconsole/src/app/app-routing.module.ts +++ b/ogWebconsole/src/app/app-routing.module.ts @@ -36,6 +36,7 @@ import { EnvVarsComponent } from "./components/admin/env-vars/env-vars.component import { MenusComponent } from "./components/menus/menus.component"; import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component"; import { DhcpServersComponent } from './components/ogdhcp/dhcp-servers/dhcp-servers.component'; +import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component'; import { RunScriptAssistantComponent @@ -45,6 +46,7 @@ import { LogoutGuard } from './guards/logout.guard'; import { HardwareComponent } from './components/hardware/hardware.component'; import { HardwareProfileComponent } from './components/hardware-profile/hardware-profile.component'; import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component'; +import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component'; const routes: Routes = [ { path: '', redirectTo: 'auth/login', pathMatch: 'full' }, { @@ -60,6 +62,8 @@ const routes: Routes = [ { path: 'ogboot-status', component: OgbootStatusComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'subnets', component: OgDhcpSubnetsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'dhcp-servers', component: DhcpServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'boot-servers', component: BootServersComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, + { path: 'boot-servers/:uuid', component: BootServerDashboardComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, { path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } }, diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 922ca60..1a542d8 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -58,10 +58,10 @@ import { MatProgressSpinner } from "@angular/material/progress-spinner"; import { MatProgressBarModule } from '@angular/material/progress-bar'; import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu"; import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete"; -import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips"; +import { MatChipsModule } from "@angular/material/chips"; import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component'; -import { MatTab, MatTabGroup } from "@angular/material/tabs"; -import { MatTooltip } from "@angular/material/tooltip"; +import { MatTabsModule } from "@angular/material/tabs"; +import { MatTooltipModule } from "@angular/material/tooltip"; import { MatExpansionModule } from '@angular/material/expansion'; import { DragDropModule } from '@angular/cdk/drag-drop'; import { ToastrModule } from 'ngx-toastr'; @@ -193,6 +193,9 @@ import { HardwareCollectionModalComponent } from './components/hardware-profile/ import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component'; import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component'; import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-partition-selector/disk-partition-selector.component'; +import { BootServersComponent } from './components/ogboot/boot-servers/boot-servers.component'; +import { ManageBootServerComponent } from './components/ogboot/manage-boot-server/manage-boot-server.component'; +import { BootServerDashboardComponent } from './components/ogboot/boot-server-dashboard/boot-server-dashboard.component'; export function HttpLoaderFactory(http: HttpClient) { return new TranslateHttpLoader(http, './locale/', '.json'); } @@ -324,7 +327,10 @@ registerLocaleData(localeEs, 'es-ES'); HardwareCollectionModalComponent, HardwareTypeComponent, ShowDetailsComponent, - AssetManagerModalComponent + AssetManagerModalComponent, + BootServersComponent, + ManageBootServerComponent, + BootServerDashboardComponent ], bootstrap: [AppComponent], imports: [BrowserModule, @@ -349,7 +355,7 @@ registerLocaleData(localeEs, 'es-ES'); MatProgressBarModule, MatStepperModule, DragDropModule, - MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip, + MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipsModule, MatProgressSpinner, MatTabsModule, MatTooltipModule, MatExpansionModule, NgxChartsModule, MatDatepickerModule, diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css index 3ae50b9..30efd99 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css @@ -53,4 +53,28 @@ mat-option .unit-path { justify-content: flex-end; gap: 1em; padding: 1.5em; +} + +/* Boot grouping */ +.boot-group { + grid-column: 1 / -1; + border: 1px solid #c5c5c5; + border-radius: 6px; + padding: 12px 16px 4px 16px; + margin: 0; +} + +.boot-group-legend { + font-size: 0.78rem; + color: #3f51b5; + font-weight: 600; + letter-spacing: 0.04em; + padding: 0 6px; + text-transform: uppercase; +} + +.boot-group-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + column-gap: 20px; } \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html index 75febb5..2c1cc0e 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html @@ -3,16 +3,19 @@
+ {{ 'organizationalUnitLabel' | translate }} {{ getSelectedParentName() }} - -
{{ unit.name }}
-
{{ unit.path }}
-
+ @for (unit of parentUnitsWithPaths; track unit.id) { + +
{{ unit.name }}
+
{{ unit.path }}
+
+ }
@@ -21,15 +24,6 @@ - - {{ 'ogLiveLabel' | translate }} - - - {{ ogLive.name }} - - - - {{ 'serialNumberLabel' | translate }} @@ -38,9 +32,9 @@ {{ 'netifaceLabel' | translate }} - - {{ type.name }} - + @for (type of netifaceTypes; track type.value) { + {{ type.name }} + } @@ -56,30 +50,21 @@ {{ 'ipError' | translate }} - - {{ 'templateLabel' | translate }} - - - {{ template.name }} - - - - {{ 'repositoryLabel' | translate }} - - {{ repository.name }} - + @for (repository of repositories; track repository['@id']) { + {{ repository.name }} + } {{ 'menuLabel' | translate }} - - {{ menu.name }} - + @for (menu of menus; track menu['@id']) { + {{ menu.name }} + } {{ 'menuError' | translate }} @@ -90,7 +75,7 @@ 640×480 (8-bit - 256 colores) 800×600 (8-bit - 256 colores) 1024×768 (8-bit - 256 colores) - 1280×1024 (8-bit - 256 colores) + 1280×1024 (8-bit - 256 colores) 640×480 (16-bit - 65k colores) 800×600 (16-bit - 65k colores) 1024×768 (16-bit - 65k colores) @@ -107,6 +92,47 @@ {{ 'maintenance' | translate }} + +
+ Configuración de arranque +
+ + + Servidor Boot + + @for (server of bootServers; track server['@id']) { + {{ server.name }} + } + + + + + {{ 'ogLiveLabel' | translate }} + + @for (ogLive of ogLives; track ogLive['@id']) { + {{ ogLive.name }} + } + @if (ogLives.length === 0 && clientForm.get('bootServer')?.value) { + No hay OgLives en este servidor + } + + + + + {{ 'templateLabel' | translate }} + + @for (template of templates; track template['@id']) { + {{ template.name }} + } + @if (templates.length === 0 && clientForm.get('bootServer')?.value) { + No hay plantillas en este servidor + } + + + +
+
+ {{ 'notesLabel' | translate }} + +
+ + + + + + +
diff --git a/ogWebconsole/src/app/components/ogboot/manage-boot-server/manage-boot-server.component.spec.ts b/ogWebconsole/src/app/components/ogboot/manage-boot-server/manage-boot-server.component.spec.ts new file mode 100644 index 0000000..0de8ddf --- /dev/null +++ b/ogWebconsole/src/app/components/ogboot/manage-boot-server/manage-boot-server.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ManageBootServerComponent } from './manage-boot-server.component'; + +describe('ManageBootServerComponent', () => { + let component: ManageBootServerComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ManageBootServerComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(ManageBootServerComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ogWebconsole/src/app/components/ogboot/manage-boot-server/manage-boot-server.component.ts b/ogWebconsole/src/app/components/ogboot/manage-boot-server/manage-boot-server.component.ts new file mode 100644 index 0000000..14664fd --- /dev/null +++ b/ogWebconsole/src/app/components/ogboot/manage-boot-server/manage-boot-server.component.ts @@ -0,0 +1,96 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from "@angular/forms"; +import { HttpClient } from "@angular/common/http"; +import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog"; +import { ToastrService } from "ngx-toastr"; +import { ConfigService } from "@services/config.service"; + +@Component({ + selector: 'app-manage-boot-server', + templateUrl: './manage-boot-server.component.html', + styleUrl: './manage-boot-server.component.css' +}) +export class ManageBootServerComponent implements OnInit { + baseUrl: string; + bootServerForm: FormGroup; + 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.bootServerForm = this.fb.group({ + name: [null, Validators.required], + ip: [null, Validators.required], + apiPort: [8082, Validators.required], + pxePort: [8082, Validators.required], + comments: [null], + }); + } + + ngOnInit() { + if (this.data) { + this.load() + } + } + + load(): void { + this.http.get(`${this.baseUrl}${this.data}`).subscribe({ + next: (response) => { + this.bootServerForm.patchValue({ + name: response.name, + ip: response.ip, + apiPort: response.apiPort ?? 8082, + pxePort: response.pxePort ?? 8082, + comments: response.comments, + }); + this.serverId = response['@id']; + }, + error: (err) => { + console.error('Error fetching boot server:', err); + this.toastService.error('Error cargando los datos del servidor'); + } + }); + } + + save(): void { + const payload = { + name: this.bootServerForm.value.name, + ip: this.bootServerForm.value.ip, + apiPort: parseInt(this.bootServerForm.value.apiPort), + pxePort: parseInt(this.bootServerForm.value.pxePort), + comments: this.bootServerForm.value.comments, + }; + + if (this.serverId) { + this.http.put(`${this.baseUrl}${this.serverId}`, payload).subscribe({ + next: () => { + this.toastService.success('Servidor Boot actualizado correctamente'); + this.dialogRef.close(true); + }, + error: (error) => { + this.toastService.error(error.error?.['hydra:description'] || 'Ha ocurrido un error al editar'); + } + }); + } else { + this.http.post(`${this.baseUrl}/boot-servers`, payload).subscribe({ + next: () => { + this.toastService.success('Servidor Boot añadido correctamente'); + this.dialogRef.close(true); + }, + error: (error) => { + this.toastService.error(error.error?.['hydra:description'] || 'Ha ocurrido un error al crear'); + } + }); + } + } + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html index fb12d46..953da95 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.html @@ -1,20 +1,37 @@ -
- -
-

- {{ 'advancedNetbootTitle' | translate }} -

+@if (!isFromDashboard) { +
+ +
+

+ {{ 'advancedNetbootTitle' | translate }} +

+
-
+}
+ @if (!isFromDashboard) { + + Servidor OgBoot + + @for (server of bootServers; track server['@id']) { + + {{ server.name }} + + } + + + } + {{ 'selectClassLabel' | translate }} - {{ unit.name }} + @for (unit of units; track unit.id) { + {{ unit.name }} + }
@@ -25,9 +42,11 @@ text="{{ 'applyToAllDescription' | translate }}"> {{ 'applyToAllLabel' | translate }} - - {{ option.name }} - + @for (option of ogLiveOptions; track option['@id']) { + + {{ option.name }} + + } @@ -67,14 +86,30 @@ {{ 'selectTemplateLabel' | translate }} {{ 'noTemplateOption' | translate }} - - {{ template.name }} - + @for (template of ogLiveOptions; track template['@id']) { + + {{ template.name }} + + } + + Servidor + + + {{ element.bootServer?.name || 'Ninguno' }} + @if (element.bootServer?.uuid && element.bootServer.uuid !== selectedServerId) { + warning + } + + + + diff --git a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts index b2413ce..08c2edd 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe-boot-files/pxe-boot-files.component.ts @@ -1,10 +1,14 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnInit, Input } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { HttpClient } from '@angular/common/http'; import { ToastrService } from 'ngx-toastr'; import { ConfigService } from '@services/config.service'; import { JoyrideService } from 'ngx-joyride'; import {MatTableDataSource} from "@angular/material/table"; +import { ActivatedRoute } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { DeleteModalComponent } from 'src/app/shared/delete_modal/delete-modal/delete-modal.component'; +import { forkJoin } from 'rxjs'; @Component({ selector: 'app-pxe-boot-files', @@ -19,6 +23,10 @@ export class PxeBootFilesComponent implements OnInit { dataSource = new MatTableDataSource(); taskForm: FormGroup; units: any[] = []; + selectedUnit: any = null; + bootServers: any[] = []; + @Input() selectedServerId: string | null = null; + isFromDashboard: boolean = false; ogLiveOptions: any[] = []; globalOgLive: string | null = null; length: number = 0; @@ -26,7 +34,7 @@ export class PxeBootFilesComponent implements OnInit { page: number = 0; pageSizeOptions: number[] = [5, 10, 20, 40, 100]; filters: any = {}; - displayedColumns: string[] = ['id', 'ip', 'mac', 'name', 'ogLive']; + displayedColumns: string[] = ['id', 'ip', 'mac', 'name', 'bootServer', 'ogLive']; constructor( @@ -34,7 +42,9 @@ export class PxeBootFilesComponent implements OnInit { private http: HttpClient, private toastService: ToastrService, private configService: ConfigService, - private joyrideService: JoyrideService + private joyrideService: JoyrideService, + private route: ActivatedRoute, + private dialog: MatDialog ) { this.baseUrl = this.configService.apiUrl; this.taskForm = this.fb.group({ @@ -44,10 +54,43 @@ export class PxeBootFilesComponent implements OnInit { } ngOnInit(): void { - this.fetchPxeTemplates(); + if (this.selectedServerId) { + this.isFromDashboard = true; + this.onServerChange(); + } else { + const serverIdParam = this.route.snapshot.queryParamMap.get('serverId'); + if (serverIdParam) { + this.selectedServerId = serverIdParam; + this.isFromDashboard = true; + this.onServerChange(); + } + } + this.loadBootServers(); this.loadUnits() } + loadBootServers(): void { + const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`; + this.http.get(url).subscribe( + response => { + this.bootServers = response['hydra:member']; + if (this.bootServers.length > 0 && !this.selectedServerId) { + this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop(); + this.onServerChange(); + } + }, + error => { + console.error('Error fetching BootServers:', error); + } + ); + } + + onServerChange(): void { + this.filters['bootServer.uuid'] = this.selectedServerId!; + this.page = 0; + this.fetchPxeTemplates(); + } + loadUnits() { this.http.get(`${this.baseUrl}/organizational-units?type=classroom&page=1&itemsPerPage=50`).subscribe( response => { @@ -71,7 +114,13 @@ export class PxeBootFilesComponent implements OnInit { } loadChildUnits(event: any) { - this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${event.value.id}`).subscribe( + this.selectedUnit = event.value; + this.refreshClientList(); + } + + refreshClientList(): void { + if (!this.selectedUnit) return; + this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${this.selectedUnit.id}`).subscribe( response => { this.dataSource.data = response['hydra:member']; }, @@ -87,10 +136,47 @@ export class PxeBootFilesComponent implements OnInit { } saveOgLiveTemplates(): void { + let hasDifferentServer = false; + + // Check for clients with different boot server + this.dataSource.data.forEach(client => { + if (client.ogLive && client.bootServer?.uuid !== this.selectedServerId) { + hasDifferentServer = true; + } + }); + + if (hasDifferentServer) { + const dialogRef = this.dialog.open(DeleteModalComponent, { + width: '500px', + data: { + title: 'Confirmar migración de servidores', + message: 'Se han detectado ordenadores asociados a otro servidor de boot. ¿Desea moverlos al servidor actual? (Si rechaza, solo se actualizarán los ordenadores que ya pertenecen al servidor actual)', + confirmText: 'Mover y Guardar', + cancelText: 'Solo de este servidor' + } + }); + + dialogRef.afterClosed().subscribe(result => { + // result is true if YES, false if NO, undefined if closed + if (result === undefined) return; + this.processSave(result); + }); + } else { + this.processSave(true); + } + } + + private processSave(moveClients: boolean): void { const groupedByTemplate: { [key: string]: string[] } = {}; this.dataSource.data.forEach(client => { if (client.ogLive) { + // Decide whether to include this client + const isSameServer = client.bootServer?.uuid === this.selectedServerId; + if (!isSameServer && !moveClients) { + return; // Skip if user said NO to moving + } + if (!groupedByTemplate[client.ogLive]) { groupedByTemplate[client.ogLive] = []; } @@ -98,22 +184,27 @@ export class PxeBootFilesComponent implements OnInit { } }); - Object.keys(groupedByTemplate).forEach(templateId => { + const templateIds = Object.keys(groupedByTemplate); + if (templateIds.length === 0) { + this.toastService.info('No hay cambios pendientes de realizar'); + return; + } + + const requests = templateIds.map(templateId => { const clients = groupedByTemplate[templateId]; const url = `${this.baseUrl}${templateId}/add-clients`; + const payload = { clients: clients }; + return this.http.post(url, payload); + }); - const payload = { - clients: clients - }; - - this.http.post(url, payload).subscribe({ - next: () => { - this.toastService.success(`Clientes guardados correctamente para la plantilla`); - }, - error: () => { - this.toastService.error(`Error al guardar clientes para la plantilla`); - } - }); + forkJoin(requests).subscribe({ + next: () => { + this.toastService.success(`Clientes guardados correctamente`); + this.refreshClientList(); + }, + error: () => { + this.toastService.error(`Error al guardar clientes`); + } }); } diff --git a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html index eae69c8..2272f9d 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe-images/pxe-images.component.html @@ -1,22 +1,37 @@ -
- -
-

- {{ 'adminImagesTitle' | translate }} -

-
-
- +
+ @if (!isFromDashboard) { + +
+

+ {{ 'adminImagesTitle' | translate }} +

+
+ } +
+
+ @if (!isFromDashboard) { + + Servidor OgBoot + + @for (server of bootServers; track server['@id']) { + + {{ server.name }} + + } + + + } + {{ 'searchLabel' | translate }} @@ -51,76 +66,72 @@
- - - - + +
{{ column.header }} - - - - {{ 'yesOption' | translate }} - - - {{ 'noOption' | translate }} - - - - - - {{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }} - +@if (!loading) { + + + @for (column of columns; track column.columnDef) { + + + + } - - - {{ image.name }} - - - - - - {{ getStatusLabel(image.status).label }} - - - - - {{ column.cell(image) }} - - - - - - - + - + + + + + + + + - - -
{{ column.header }} + @if (column.columnDef === 'isDefault') { + + @if (image.isDefault) { + {{ 'yesOption' | translate }} + } @else { + {{ 'noOption' | translate }} + } + + } @else if (column.columnDef === 'downloadUrl') { + + {{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }} + + } @else if (column.columnDef === 'name') { + + {{ image.name }} + + } @else if (column.columnDef === 'status') { + + {{ getStatusLabel(image.status).label }} + + } @else { + {{ column.cell(image) }} + } + {{ 'actionsColumnHeader' | translate }} - - - - - - {{ 'actionsColumnHeader' | translate }} + - - -
+
+}
(); length: number = 0; itemsPerPage: number = 20; @@ -69,15 +73,48 @@ export class PXEimagesComponent implements OnInit { private dataService: DataService, private configService: ConfigService, private toastService: ToastrService, - private joyrideService: JoyrideService + private joyrideService: JoyrideService, + private route: ActivatedRoute ) { this.baseUrl = this.configService.apiUrl; this.apiUrl = `${this.baseUrl}/og-lives`; } ngOnInit(): void { + if (this.selectedServerId) { + this.isFromDashboard = true; + this.onServerChange(); + } else { + const serverIdParam = this.route.snapshot.queryParamMap.get('serverId'); + if (serverIdParam) { + this.selectedServerId = serverIdParam; + this.isFromDashboard = true; + this.onServerChange(); + } + } + this.loadBootServers(); + } + + loadBootServers(): void { + const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`; + this.http.get(url).subscribe( + response => { + this.bootServers = response['hydra:member']; + if (this.bootServers.length > 0 && !this.selectedServerId) { + this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop(); + this.onServerChange(); + } + }, + error => { + console.error('Error fetching BootServers:', error); + } + ); + } + + onServerChange(): void { + this.filters['bootServer.uuid'] = this.selectedServerId!; + this.page = 0; this.search(); - this.loadAlert(); } addImage(): void { @@ -199,7 +236,10 @@ export class PXEimagesComponent implements OnInit { } loadAlert(): Observable { - return this.http.get(`${this.apiUrl}/server/get-collection`); + if (!this.selectedServerId) { + throw new Error('No boot server selected'); + } + return this.http.get(`${this.baseUrl}/boot-servers/${this.selectedServerId}/oglives/get-collection`); } openInfoDialog() { diff --git a/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts index f53307d..7af63e6 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/manage-pxeTemplate/create-pxe-template.component.ts @@ -64,14 +64,14 @@ export class CreatePxeTemplateComponent implements OnInit { } ngOnInit() { - this.isEditMode = !!this.data; + this.isEditMode = !!this.data?.uuid; if (this.isEditMode) { this.getPxeClients(); } this.templateForm = this.fb.group({ - name: [this.data?.name || '', Validators.required], + name: [this.data?.name || '', [Validators.required, Validators.pattern('^[a-zA-Z0-9._-]+$')]], templateContent: [this.data?.templateContent || '', Validators.required], isDefault: [this.data?.isDefault || false] }); @@ -98,12 +98,16 @@ export class CreatePxeTemplateComponent implements OnInit { createPxeTemplate(): void { const formValues = this.templateForm.value; - const payload = { + const payload: any = { name: formValues.name, templateContent: formValues.templateContent, isDefault: formValues.isDefault, }; + if (this.data?.bootServerIri) { + payload.bootServer = this.data.bootServerIri; + } + this.http.post(`${this.baseUrl}/pxe-templates`, payload).subscribe({ next: data => { this.toastService.success('Plantilla PXE creada exitosamente'); diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html index 02fd939..d065618 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.html @@ -1,19 +1,34 @@ -
- -
-

{{ 'adminPxeTitle' | - translate }}

-
-
- +
+ @if (!isFromDashboard) { + +
+

{{ 'adminPxeTitle' | + translate }}

+
+ } +
+ + text="{{ 'addTemplateButtonDescription' | translate }}" [disabled]="!selectedServerId">{{ 'addTemplateButton' | translate }}
+ @if (!isFromDashboard) { + + Servidor OgBoot + + @for (server of bootServers; track server['@id']) { + + {{ server.name }} + + } + + + } + {{ 'searchLabel' | translate }} @@ -37,34 +52,33 @@ - - - - + + @for (column of columns; track column.columnDef) { + + + + + } diff --git a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts index 2669145..47307be 100644 --- a/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts +++ b/ogWebconsole/src/app/components/ogboot/pxe/pxe.component.ts @@ -1,5 +1,5 @@ import { HttpClient } from '@angular/common/http'; -import {Component, OnInit} from '@angular/core'; +import {Component, OnInit, Input} from '@angular/core'; import { CreatePxeTemplateComponent } from './manage-pxeTemplate/create-pxe-template.component'; import { MatDialog } from '@angular/material/dialog'; import { MatTableDataSource } from '@angular/material/table'; @@ -12,6 +12,7 @@ import { JoyrideService } from 'ngx-joyride'; import { DeleteModalComponent } from "../../../shared/delete_modal/delete-modal/delete-modal.component"; import { ServerInfoDialogComponent } from "../../ogdhcp/server-info-dialog/server-info-dialog.component"; import { ConfigService } from '@services/config.service'; +import { ActivatedRoute } from '@angular/router'; @Component({ selector: 'app-pxe', @@ -22,6 +23,9 @@ export class PxeComponent implements OnInit{ baseUrl: string; private apiUrl: string; pxeTemplates: any[] = []; + bootServers: any[] = []; + @Input() selectedServerId: string | null = null; + isFromDashboard: boolean = false; currentPage: number = 1; dataSource = new MatTableDataSource(); length: number = 0; @@ -70,15 +74,48 @@ export class PxeComponent implements OnInit{ private toastService: ToastrService, private dataService: DataService, private joyrideService: JoyrideService, - private configService: ConfigService + private configService: ConfigService, + private route: ActivatedRoute ) { this.baseUrl = this.configService.apiUrl; this.apiUrl = `${this.baseUrl}/pxe-templates`; } ngOnInit(): void { + if (this.selectedServerId) { + this.isFromDashboard = true; + this.onServerChange(); + } else { + const serverIdParam = this.route.snapshot.queryParamMap.get('serverId'); + if (serverIdParam) { + this.selectedServerId = serverIdParam; + this.isFromDashboard = true; + this.onServerChange(); + } + } + this.loadBootServers(); + } + + loadBootServers(): void { + const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`; + this.http.get(url).subscribe( + response => { + this.bootServers = response['hydra:member']; + if (this.bootServers.length > 0 && !this.selectedServerId) { + this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop(); + this.onServerChange(); + } + }, + error => { + console.error('Error fetching BootServers:', error); + } + ); + } + + onServerChange(): void { + this.filters['bootServer.uuid'] = this.selectedServerId!; + this.page = 0; this.search(); - this.loadAlert() } search(): void { @@ -97,7 +134,8 @@ export class PxeComponent implements OnInit{ addPxeTemplate() { const dialogRef = this.dialog.open(CreatePxeTemplateComponent, { - width: '800px' + width: '800px', + data: { bootServerIri: this.selectedServerId ? `/boot-servers/${this.selectedServerId}` : null } }); dialogRef.afterClosed().subscribe(() => { @@ -107,7 +145,7 @@ export class PxeComponent implements OnInit{ editPxeTemplate(template: any) { const dialogRef = this.dialog.open(CreatePxeTemplateComponent, { - data: template, + data: { ...template, bootServerIri: this.selectedServerId ? `/boot-servers/${this.selectedServerId}` : null }, width: '800px' }); @@ -126,9 +164,12 @@ export class PxeComponent implements OnInit{ dialogRef.afterClosed().subscribe(result => { if (result) { - this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({ - next: () => { + this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({ + next: (response) => { this.toastService.success('Plantilla eliminada exitosamente'); + if (response && response.warning) { + this.toastService.info(response.warning, 'Información', { timeOut: 8000 }); + } this.search(); }, error: (error) => { @@ -150,7 +191,10 @@ export class PxeComponent implements OnInit{ } loadAlert(): Observable { - return this.http.get(`${this.apiUrl}/server/get-collection`); + if (!this.selectedServerId) { + throw new Error('No boot server selected'); + } + return this.http.get(`${this.baseUrl}/boot-servers/${this.selectedServerId}/pxe-templates/get-collection`); } openInfoDialog() { diff --git a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html index 4c2a9cb..63f02a0 100644 --- a/ogWebconsole/src/app/layout/sidebar/sidebar.component.html +++ b/ogWebconsole/src/app/layout/sidebar/sidebar.component.html @@ -67,38 +67,14 @@ - + - desktop_windows - {{ 'boot' | translate }} + dns + Servidores Boot - - - - album - {{ 'ogLive' | translate }} - - - - - assignment - {{ 'pxeTemplates' | translate }} - - - - - save - {{ 'pxeBootFiles' | translate }} - - - -
{{ column.header }} - - - - {{ 'yesOption' | translate }} - - - {{ 'noOption' | translate }} - - - - - - - {{ 'yesOption' | translate }} - - - {{ 'noOption' | translate }} - - - - - {{ column.cell(image) }} - - {{ column.header }} + @if (column.columnDef === 'synchronized') { + + @if (image.synchronized) { + {{ 'yesOption' | translate }} + } @else { + {{ 'noOption' | translate }} + } + + } @else if (column.columnDef === 'isDefault') { + + @if (image.isDefault) { + {{ 'yesOption' | translate }} + } @else { + {{ 'noOption' | translate }} + } + + } @else { + {{ column.cell(image) }} + } + {{ 'actionsColumn' | translate }}