Gestion de múltiples ogBoots
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/71/head
Juan Manuel Bardallo 2026-04-21 14:41:00 +02:00
parent ea309bae65
commit d5d883e7a6
26 changed files with 1427 additions and 300 deletions

View File

@ -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

View File

@ -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'] } },

View File

@ -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,

View File

@ -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;
}

View File

@ -3,16 +3,19 @@
<div [ngClass]="{'loading': loading}">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form *ngIf="clientForm && !loading" [formGroup]="clientForm" class="client-form grid-form">
<mat-form-field class="form-field">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit" (selectionChange)="onParentChange($event)">
<mat-select-trigger>
{{ getSelectedParentName() }}
</mat-select-trigger>
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
<div class="unit-name">{{ unit.name }}</div>
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
</mat-option>
@for (unit of parentUnitsWithPaths; track unit.id) {
<mat-option [value]="unit.id">
<div class="unit-name">{{ unit.name }}</div>
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
</mat-option>
}
</mat-select>
</mat-form-field>
@ -21,15 +24,6 @@
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive">
<mat-option *ngFor="let ogLive of ogLives" [value]="ogLive['@id']">
{{ ogLive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
<input matInput formControlName="serialNumber">
@ -38,9 +32,9 @@
<mat-form-field class="form-field">
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
<mat-select formControlName="netiface">
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
{{ type.name }}
</mat-option>
@for (type of netifaceTypes; track type.value) {
<mat-option [value]="type.value">{{ type.name }}</mat-option>
}
</mat-select>
</mat-form-field>
@ -56,30 +50,21 @@
<mat-error>{{ 'ipError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate">
<mat-option *ngFor="let template of templates" [value]="template['@id']">
{{ template.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
<mat-select formControlName="repository">
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
{{ repository.name }}
</mat-option>
@for (repository of repositories; track repository['@id']) {
<mat-option [value]="repository['@id']">{{ repository.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'menuLabel' | translate }}</mat-label>
<mat-select formControlName="menu">
<mat-option *ngFor="let menu of menus" [value]="menu['@id']">
{{ menu.name }}
</mat-option>
@for (menu of menus; track menu['@id']) {
<mat-option [value]="menu['@id']">{{ menu.name }}</mat-option>
}
</mat-select>
<mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field>
@ -90,7 +75,7 @@
<mat-option value="769">640×480 (8-bit - 256 colores)</mat-option>
<mat-option value="771">800×600 (8-bit - 256 colores)</mat-option>
<mat-option value="773">1024×768 (8-bit - 256 colores)</mat-option>
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
<mat-option value="784">640×480 (16-bit - 65k colores)</mat-option>
<mat-option value="787">800×600 (16-bit - 65k colores)</mat-option>
<mat-option value="791">1024×768 (16-bit - 65k colores)</mat-option>
@ -107,6 +92,47 @@
{{ 'maintenance' | translate }}
</mat-checkbox>
<!-- ===== Agrupación: Configuración de arranque ===== -->
<fieldset class="boot-group">
<legend class="boot-group-legend">Configuración de arranque</legend>
<div class="boot-group-grid">
<mat-form-field class="form-field">
<mat-label>Servidor Boot</mat-label>
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive">
@for (ogLive of ogLives; track ogLive['@id']) {
<mat-option [value]="ogLive['@id']">{{ ogLive.name }}</mat-option>
}
@if (ogLives.length === 0 && clientForm.get('bootServer')?.value) {
<mat-option disabled>No hay OgLives en este servidor</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate">
@for (template of templates; track template['@id']) {
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
}
@if (templates.length === 0 && clientForm.get('bootServer')?.value) {
<mat-option disabled>No hay plantillas en este servidor</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
<mat-form-field class="form-field full-width">
<mat-label>{{ 'notesLabel' | translate }}</mat-label>
<textarea matInput formControlName="notes" rows="4"

View File

@ -20,6 +20,7 @@ export class ManageClientComponent implements OnInit {
menus: any[] = [];
templates: any[] = [];
repositories: any[] = [];
bootServers: any[] = [];
loading: boolean = false;
displayedColumns: string[] = ['name', 'ip'];
dialogTitle: string;
@ -49,8 +50,7 @@ export class ManageClientComponent implements OnInit {
this.initForm();
const observables = [
this.loadParentUnits(),
this.loadOgLives(),
this.loadPxeTemplates(),
this.loadBootServers(),
this.loadRepositories(),
this.loadMenus()
];
@ -84,6 +84,7 @@ export class ManageClientComponent implements OnInit {
mac: ['', Validators.pattern(/^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/)],
ip: ['', Validators.required],
pxeTemplate: [null],
bootServer: [null],
ogLive: [null],
repository: [null],
menu: [null],
@ -106,6 +107,7 @@ export class ManageClientComponent implements OnInit {
netiface: unit.networkSettings?.netiface,
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
repository: unit.networkSettings?.repository?.['@id'],
bootServer: unit.networkSettings?.bootServer?.['@id'],
ogLive: unit.networkSettings?.ogLive?.['@id'],
menu: unit.networkSettings?.menu?.['@id'],
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
@ -132,40 +134,74 @@ export class ManageClientComponent implements OnInit {
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
}
loadOgLives(): Promise<void> {
loadOgLivesByServer(bootServerIri: string): Promise<void> {
// Extract UUID from IRI (e.g. "/boot-servers/xxxx-xxx" -> "xxxx-xxx")
const uuid = bootServerIri.split('/').pop();
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
const url = `${this.baseUrl}/og-lives?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.ogLives = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching ogLives:', error);
reject(error);
console.error('Error fetching ogLives by server:', error);
this.ogLives = [];
resolve(); // resolve anyway so form doesn't block
}
);
});
}
loadPxeTemplates(): Promise<void> {
loadPxeTemplatesByServer(bootServerIri: string): Promise<void> {
const uuid = bootServerIri.split('/').pop();
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
const url = `${this.baseUrl}/pxe-templates?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.templates = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching PXE templates:', error);
console.error('Error fetching PXE templates by server:', error);
this.templates = [];
resolve();
}
);
});
}
loadBootServers(): Promise<void> {
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.bootServers = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching BootServers:', error);
reject(error);
}
);
});
}
onBootServerChange(event: any): void {
const serverIri: string | null = event.value;
// Reset dependent fields
this.clientForm.patchValue({ ogLive: null, pxeTemplate: null });
this.ogLives = [];
this.templates = [];
if (serverIri) {
Promise.all([
this.loadOgLivesByServer(serverIri),
this.loadPxeTemplatesByServer(serverIri)
]);
}
}
loadMenus(): Promise<void> {
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/menus?page=1&itemsPerPage=10000`;
@ -223,6 +259,7 @@ export class ManageClientComponent implements OnInit {
if (selectedUnit) {
this.clientForm.patchValue({
repository: selectedUnit.repository || null,
bootServer: selectedUnit.bootServer || null,
ogLive: selectedUnit.ogLive || null,
menu: selectedUnit.menu || null,
netiface: selectedUnit.netiface || null,
@ -238,6 +275,11 @@ export class ManageClientComponent implements OnInit {
this.http.get<any>(url).subscribe(
data => {
const bootServerIri: string | null = data.bootServer ? data.bootServer['@id'] : null;
const savedOgLive: string | null = data.ogLive ? data.ogLive['@id'] : null;
const savedPxeTemplate: string | null = data.pxeTemplate ? data.pxeTemplate['@id'] : null;
// Patch fields that don't depend on server first
this.clientForm.patchValue({
name: data.name,
ip: data.ip,
@ -246,15 +288,29 @@ export class ManageClientComponent implements OnInit {
netDriver: 'generic',
serialNumber: data.serialNumber,
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null,
bootServer: bootServerIri,
repository: data.repository ? data.repository['@id'] : null,
ogLive: data.ogLive ? data.ogLive['@id'] : null,
pxeTemplate: data.pxeTemplate ? data.pxeTemplate['@id'] : null,
menu: data.menu ? data.menu['@id'] : null,
maintenance: data.maintenance,
resolution: data.resolution,
notes: data.notes || ''
});
resolve();
if (bootServerIri) {
// Load server-specific resources first, then restore saved values
Promise.all([
this.loadOgLivesByServer(bootServerIri),
this.loadPxeTemplatesByServer(bootServerIri)
]).then(() => {
this.clientForm.patchValue({
ogLive: savedOgLive,
pxeTemplate: savedPxeTemplate
});
resolve();
});
} else {
resolve();
}
},
error => {
console.error('Error al cargar datos del cliente:', error);

View File

@ -64,4 +64,28 @@ h1 {
align-items: center;
grid-column: span 2;
margin-bottom: 10px;
}
/* Boot grouping */
.boot-group {
grid-column: 1 / -1;
border: 1px solid #c5c5c5;
border-radius: 6px;
padding: 12px 16px 4px 16px;
margin: 0;
}
.boot-group-legend {
font-size: 0.78rem;
color: #3f51b5;
font-weight: 600;
letter-spacing: 0.04em;
padding: 0 6px;
text-transform: uppercase;
}
.boot-group-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
column-gap: 20px;
}

View File

@ -74,22 +74,46 @@
<!-- Paso 3: Configuración de Red -->
<span *ngIf="!loading" class="step-title">{{ 'networkSettingsStepLabel' | translate }}</span>
<form *ngIf="networkSettingsFormGroup && !loading" [formGroup]="networkSettingsFormGroup" class="grid-form">
<mat-form-field class="form-field">
<mat-label>OgLive</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
<mat-option *ngFor="let template of pxeTemplates" [value]="template['@id']">
{{ template.name }}
</mat-option>
</mat-select>
</mat-form-field>
<!-- ===== Agrupación: Configuración de arranque ===== -->
<fieldset class="boot-group">
<legend class="boot-group-legend">Configuración de arranque</legend>
<div class="boot-group-grid">
<mat-form-field class="form-field">
<mat-label>Servidor Boot</mat-label>
<mat-select formControlName="bootServer" (selectionChange)="onBootServerChange($event)">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server['@id']">{{ server.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>OgLive</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
@for (oglive of ogLives; track oglive['@id']) {
<mat-option [value]="oglive['@id']">{{ oglive.name }}</mat-option>
}
@if (ogLives.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
<mat-option disabled>No hay OgLives en este servidor</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="pxeTemplate" (selectionChange)="onPxeTemplateChange($event)">
@for (template of pxeTemplates; track template['@id']) {
<mat-option [value]="template['@id']">{{ template.name }}</mat-option>
}
@if (pxeTemplates.length === 0 && networkSettingsFormGroup.get('bootServer')?.value) {
<mat-option disabled>No hay plantillas en este servidor</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</fieldset>
<mat-form-field class="form-field">
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">

View File

@ -32,6 +32,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
currentCalendar: any = [];
ogLives: any[] = [];
pxeTemplates: any[] = [];
bootServers: any[] = [];
menus: any[] = [];
repositories: any[] = [];
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
@ -78,6 +79,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
});
this.networkSettingsFormGroup = this._formBuilder.group({
bootServer: [null],
ogLive: [null],
repository: [null],
pxeTemplate: [null],
@ -112,10 +114,9 @@ export class ManageOrganizationalUnitComponent implements OnInit {
this.loadParentUnits(),
this.loadHardwareProfiles(),
this.loadCalendars(),
this.loadOgLives(),
this.loadRepositories(),
this.loadMenus(),
this.loadPxeTemplates()
this.loadBootServers()
];
Promise.all(observables).then(() => {
@ -147,6 +148,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
name: unit.name,
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
repository: unit.networkSettings?.repository?.['@id'],
bootServer: unit.networkSettings?.bootServer?.['@id'],
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
ogLive: unit.networkSettings?.ogLive?.['@id'],
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
@ -190,6 +192,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
if (selectedUnit && !exclude) {
this.networkSettingsFormGroup.patchValue({
repository: selectedUnit.repository || null,
bootServer: selectedUnit.bootServer || null,
hardwareProfile: selectedUnit.hardwareProfile || null,
ogLive: selectedUnit.ogLive || null,
pxeTemplate: selectedUnit.pxeTemplate || null,
@ -215,20 +218,29 @@ export class ManageOrganizationalUnitComponent implements OnInit {
this.http.get<any>(url).subscribe(
data => {
const exclude = data.excludeParentChanges;
const bootServerIri: string | null = data.networkSettings?.bootServer
? data.networkSettings.bootServer['@id']
: null;
const savedOgLive: string | null = data.networkSettings?.ogLive
? data.networkSettings.ogLive['@id']
: null;
const savedPxeTemplate: string | null = data.networkSettings?.pxeTemplate
? data.networkSettings.pxeTemplate['@id']
: null;
this.generalFormGroup.patchValue({
name: data.name,
parent: data.parent ? data.parent['@id'] : '',
description: data.description,
type: data.type,
excludeParentChanges: exclude
excludeParentChanges: data.excludeParentChanges
});
this.additionalInfoFormGroup.patchValue({
comments: data.comments
});
// Patch all network fields except ogLive and pxeTemplate
this.networkSettingsFormGroup.patchValue({
proxy: data.networkSettings?.proxy,
dns: data.networkSettings?.dns,
@ -243,10 +255,9 @@ export class ManageOrganizationalUnitComponent implements OnInit {
mcastPort: data.networkSettings?.mcastPort,
mcastMode: data.networkSettings?.mcastMode,
menu: data.networkSettings?.menu ? data.networkSettings.menu['@id'] : null,
bootServer: bootServerIri,
hardwareProfile: data.networkSettings?.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null,
ogLive: data.networkSettings?.ogLive ? data.networkSettings.ogLive['@id'] : null,
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null,
pxeTemplate: data.networkSettings?.pxeTemplate ? data.networkSettings.pxeTemplate['@id'] : null
repository: data.networkSettings?.repository ? data.networkSettings.repository['@id'] : null
});
this.classroomInfoFormGroup.patchValue({
@ -257,7 +268,21 @@ export class ManageOrganizationalUnitComponent implements OnInit {
remoteCalendar: data.remoteCalendar ? data.remoteCalendar['@id'] : null
});
resolve();
if (bootServerIri) {
// Load server-specific lists first, then restore saved values
Promise.all([
this.loadOgLivesByServer(bootServerIri),
this.loadPxeTemplatesByServer(bootServerIri)
]).then(() => {
this.networkSettingsFormGroup.patchValue({
ogLive: savedOgLive,
pxeTemplate: savedPxeTemplate
});
resolve();
});
} else {
resolve();
}
},
error => {
@ -384,6 +409,70 @@ export class ManageOrganizationalUnitComponent implements OnInit {
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
}
loadBootServers(): Promise<void> {
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.bootServers = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching BootServers:', error);
reject(error);
}
);
});
}
loadOgLivesByServer(bootServerIri: string): Promise<void> {
const uuid = bootServerIri.split('/').pop();
return new Promise((resolve) => {
this.http.get<any>(`${this.baseUrl}/og-lives?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`).subscribe(
response => {
this.ogLives = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching ogLives by server:', error);
this.ogLives = [];
resolve();
}
);
});
}
loadPxeTemplatesByServer(bootServerIri: string): Promise<void> {
const uuid = bootServerIri.split('/').pop();
return new Promise((resolve) => {
this.http.get<any>(`${this.baseUrl}/pxe-templates?bootServer.uuid=${uuid}&page=1&itemsPerPage=10000`).subscribe(
response => {
this.pxeTemplates = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching PXE templates by server:', error);
this.pxeTemplates = [];
resolve();
}
);
});
}
onBootServerChange(event: any): void {
const serverIri: string | null = event.value;
this.networkSettingsFormGroup.patchValue({ ogLive: null, pxeTemplate: null });
this.ogLives = [];
this.pxeTemplates = [];
if (serverIri) {
Promise.all([
this.loadOgLivesByServer(serverIri),
this.loadPxeTemplatesByServer(serverIri)
]);
}
}
loadHardwareProfiles(): Promise<void> {
return new Promise((resolve, reject) => {
this.dataService.getHardwareProfiles().subscribe(
@ -399,39 +488,7 @@ export class ManageOrganizationalUnitComponent implements OnInit {
});
}
loadPxeTemplates(): Promise<void> {
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/pxe-templates?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.pxeTemplates = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching pxe templates:', error);
reject(error);
}
);
});
}
loadOgLives(): Promise<void> {
return new Promise((resolve, reject) => {
const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`;
this.http.get<any>(url).subscribe(
response => {
this.ogLives = response['hydra:member'];
resolve();
},
error => {
console.error('Error fetching ogLives:', error);
reject(error);
}
);
});
}
// loadPxeTemplates and loadOgLives removed: resources are now loaded on-demand per BootServer
loadMenus(): Promise<void> {
return new Promise((resolve, reject) => {

View File

@ -0,0 +1,115 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { HttpClient } from '@angular/common/http';
import { ConfigService } from '@services/config.service';
@Component({
selector: 'app-boot-server-dashboard',
template: `
<div class="dashboard-container">
<div class="header">
<button mat-icon-button (click)="goBack()" matTooltip="Volver a servidores">
<mat-icon>arrow_back</mat-icon>
</button>
<h1>Gestión de Servidor: {{ serverName }}</h1>
</div>
<mat-tab-group animationDuration="0ms" class="dashboard-tabs">
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">album</mat-icon>
<div class="tab-label-content">
<span class="tab-title">OgLives</span>
<span class="tab-subtitle">Versiones de OgLive disponibles</span>
</div>
</ng-template>
<div class="tab-content">
@if (serverUuid) {
<app-pxe-images [selectedServerId]="serverUuid"></app-pxe-images>
}
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">assignment</mat-icon>
<div class="tab-label-content">
<span class="tab-title">Plantillas PXE</span>
<span class="tab-subtitle">Configuraciones de arranque PXE</span>
</div>
</ng-template>
<div class="tab-content">
@if (serverUuid) {
<app-pxe [selectedServerId]="serverUuid"></app-pxe>
}
</div>
</mat-tab>
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">save</mat-icon>
<div class="tab-label-content">
<span class="tab-title">Arranque PXE</span>
<span class="tab-subtitle">Ficheros de arranque de clientes</span>
</div>
</ng-template>
<div class="tab-content">
@if (serverUuid) {
<app-pxe-boot-files [selectedServerId]="serverUuid"></app-pxe-boot-files>
}
</div>
</mat-tab>
</mat-tab-group>
</div>
`,
styles: [`
.dashboard-container { padding: 0; }
.header { display: flex; align-items: center; padding: 20px 20px 10px 20px; gap: 10px; border-bottom: 1px solid #eee; background: #fff; }
.header h1 { margin: 0; font-size: 20px; font-weight: 500; }
.dashboard-tabs { background: #fafafa; }
.tab-icon { margin-right: 12px; font-size: 24px; color: #3f51b5; }
.tab-label-content { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.2; padding: 10px 0; }
.tab-title { font-weight: 600; font-size: 14px; }
.tab-subtitle { font-size: 11px; color: #666; font-weight: 400; }
.tab-content { padding: 0; background: #fff; min-height: 400px; }
::ng-deep .mat-mdc-tab-body-wrapper { padding-top: 0 !important; }
`]
})
export class BootServerDashboardComponent implements OnInit {
serverUuid: string | null = null;
serverName: string = 'Cargando...';
constructor(
private route: ActivatedRoute,
private router: Router,
private http: HttpClient,
private configService: ConfigService
) {}
ngOnInit() {
this.serverUuid = this.route.snapshot.paramMap.get('uuid');
if (this.serverUuid) {
this.loadServerInfo();
}
}
loadServerInfo() {
const url = `${this.configService.apiUrl}/boot-servers/${this.serverUuid}`;
this.http.get<any>(url).subscribe(res => {
this.serverName = res.name;
});
}
navigateTo(view: string) {
const queryParams = { serverId: this.serverUuid };
switch(view) {
case 'oglives': this.router.navigate(['/pxe-images'], { queryParams }); break;
case 'pxe-templates': this.router.navigate(['/pxe'], { queryParams }); break;
case 'pxe-boot-files': this.router.navigate(['/pxe-boot-file'], { queryParams }); break;
}
}
goBack() {
this.router.navigate(['/boot-servers']);
}
}

View File

@ -0,0 +1,63 @@
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
}
.header-container-title {
flex-grow: 1;
text-align: left;
margin-left: 1em;
}
.images-button-row {
display: flex;
gap: 15px;
}
.action-button {
background-color: #3f51b5;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s;
}
.action-button:hover {
background-color: #303f9f;
}
.action-button:disabled {
background-color: #9fa8da;
cursor: not-allowed;
}
.search-container {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
box-sizing: border-box;
margin: 1.5rem 0rem 1.5rem 0rem;
}
.search-string {
flex: 2;
padding: 5px;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
}
.paginator-container {
display: flex;
justify-content: end;
margin-bottom: 30px;
}

View File

@ -0,0 +1,71 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="header-container">
<button mat-icon-button color="primary" (click)="initTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="bootServerTitleStep" text="Vista de Servidores de Arranque">
Servidores OgBoot
</h2>
</div>
<div class="images-button-row">
<button
class="action-button" (click)="addServer()"
joyrideStep="addStep"
text="Añadir Servidor Boot">Añadir Servidor Boot</button>
</div>
</div>
<mat-divider class="divider"></mat-divider>
<div class="search-container" joyrideStep="searchStep" text="Búsqueda rápida de servidores">
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar por nombre</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
<mat-form-field appearance="fill" class="search-string">
<mat-label>Buscar por IP</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['ip']" (keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar</mat-hint>
</mat-form-field>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableDateStep" text="Tabla de Servidores OgBoot">
@for (column of columns; track column.columnDef) {
<ng-container [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let server">
{{ column.cell(server) }}
</td>
</ng-container>
}
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actions' | translate }}</th>
<td mat-cell *matCellDef="let server" style="text-align: center;" joyrideStep="actionsStep">
<button mat-icon-button color="accent" (click)="openServerDashboard(server)" matTooltip="Gestionar servidor">
<mat-icon>open_in_new</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="syncServer(server)" matTooltip="Sincronizar servidor">
<mat-icon>sync</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="editServer($event, server)" matTooltip="Editar servidor">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteServer($event, server)" matTooltip="Eliminar servidor">
<mat-icon>delete</mat-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<div class="paginator-container" joyrideStep="paginationStep" text="Paginación de resultados">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="[5, 10, 20, 40, 100]"
(page)="onPageChange($event)">
</mat-paginator>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BootServersComponent } from './boot-servers.component';
describe('BootServersComponent', () => {
let component: BootServersComponent;
let fixture: ComponentFixture<BootServersComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [BootServersComponent]
})
.compileComponents();
fixture = TestBed.createComponent(BootServersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,188 @@
import {Component, OnInit} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import {MatDialog} from "@angular/material/dialog";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
import {DeleteModalComponent} from "../../../shared/delete_modal/delete-modal/delete-modal.component";
import { JoyrideService } from 'ngx-joyride';
import { Router } from '@angular/router';
import { ConfigService } from '@services/config.service';
import {ManageBootServerComponent} from "../manage-boot-server/manage-boot-server.component";
import { forkJoin } from 'rxjs';
@Component({
selector: 'app-boot-servers',
templateUrl: './boot-servers.component.html',
styleUrl: './boot-servers.component.css'
})
export class BootServersComponent implements OnInit {
baseUrl: string;
private apiUrl: string;
dataSource = new MatTableDataSource<any>();
length: number = 0;
itemsPerPage: number = 20;
page: number = 0;
loading: boolean = false;
filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
columnDef: 'id',
header: 'Id',
cell: (server: any) => `${server.id}`
},
{
columnDef: 'name',
header: 'Nombre',
cell: (server: any) => server.name
},
{
columnDef: 'ip',
header: 'Ip',
cell: (server: any) => server.ip
},
{
columnDef: 'apiPort',
header: 'Puerto API',
cell: (server: any) => server.apiPort
},
{
columnDef: 'pxePort',
header: 'Puerto PXE',
cell: (server: any) => server.pxePort
},
{
columnDef: 'createdAt',
header: 'Fecha de creación',
cell: (server: any) => `${this.datePipe.transform(server.createdAt, 'dd/MM/yyyy hh:mm:ss')}`
}
];
displayedColumns: string[] = ['id', 'name', 'ip', 'apiPort', 'pxePort', 'createdAt', 'actions'];
constructor(
public dialog: MatDialog,
private http: HttpClient,
private toastService: ToastrService,
private joyrideService: JoyrideService,
private router: Router,
private configService: ConfigService
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/boot-servers`;
}
ngOnInit(): void {
this.search();
}
addServer(): void {
const dialogRef = this.dialog.open(ManageBootServerComponent, {
width: '600px'
});
dialogRef.afterClosed().subscribe((result) => {
if (result) {
this.search();
}
});
}
search(): void {
this.loading = true;
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
data => {
this.dataSource.data = data['hydra:member'];
this.length = data['hydra:totalItems'];
this.loading = false;
},
error => {
this.loading = false;
}
);
}
editServer(event: MouseEvent, server: any): void {
event.stopPropagation();
this.dialog.open(ManageBootServerComponent, {
width: '600px',
data: server['@id']
}).afterClosed().subscribe((result) => {
if (result) {
this.search();
}
});
}
deleteServer(event: MouseEvent, server: any): void {
event.stopPropagation();
this.dialog.open(DeleteModalComponent, {
width: '300px',
data: { name: server.name },
}).afterClosed().subscribe((result) => {
if (result) {
this.http.delete(`${this.apiUrl}/${server.uuid}`).subscribe({
next: () => {
this.toastService.success('Servidor eliminado con éxito');
this.search();
},
error: (error) => {
console.error('Error al eliminar servidor:', error);
this.toastService.error('Error al eliminar el servidor');
}
});
}
});
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.length = event.length;
this.search();
}
openServerDashboard(server: any): void {
this.router.navigate(['/boot-servers', server.uuid]);
}
syncServer(server: any): void {
this.loading = true;
const ogLiveSyncUrl = `${this.baseUrl}/boot-servers/${server.uuid}/oglives/sync`;
const pxeSyncUrl = `${this.baseUrl}/boot-servers/${server.uuid}/pxe-templates/sync`;
// Concurrent sync is fine as they target different entities
const syncRequests = [
this.http.post(ogLiveSyncUrl, {}),
this.http.post(pxeSyncUrl, {})
];
forkJoin(syncRequests).subscribe({
next: () => {
this.toastService.success('Sincronización completada con éxito');
this.loading = false;
this.search();
},
error: (error: any) => {
console.error('Error durante la sincronización:', error);
this.toastService.error('Error durante la sincronización');
this.loading = false;
}
});
}
initTour(): void {
this.joyrideService.startTour({
steps: [
'bootServerTitleStep',
'addStep',
'searchStep',
'tableDateStep',
'actionsStep',
'paginationStep'
],
showPrevButton: true,
themeColor: '#3f51b5'
});
}
}

View File

@ -0,0 +1,48 @@
.dialog-container {
display: flex;
flex-direction: column;
padding: 24px;
}
.dialog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.dialog-header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 500;
color: #333;
}
.boot-server-form {
display: flex;
flex-direction: column;
gap: 16px;
}
.full-width {
width: 100%;
}
.port-row {
display: flex;
flex-direction: row;
gap: 16px;
}
.half-width {
width: 50%;
}
mat-dialog-actions {
margin-top: 24px;
padding: 0;
}
mat-dialog-actions button {
margin-left: 8px;
}

View File

@ -0,0 +1,58 @@
<div class="dialog-container">
<div class="dialog-header">
<h2 mat-dialog-title>{{ data ? 'Editar Servidor Boot' : 'Añadir Servidor Boot' }}</h2>
<button mat-icon-button (click)="close()">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-dialog-content>
<form [formGroup]="bootServerForm" class="boot-server-form">
<mat-form-field appearance="outline" class="full-width">
<mat-label>Nombre</mat-label>
<input matInput formControlName="name" placeholder="Ej: Servidor Principal TFTP">
@if (bootServerForm.controls['name'].invalid && bootServerForm.controls['name'].touched) {
<mat-error>El nombre es obligatorio</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="full-width">
<mat-label>IP</mat-label>
<input matInput formControlName="ip" placeholder="192.168.1.10">
@if (bootServerForm.controls['ip'].invalid && bootServerForm.controls['ip'].touched) {
<mat-error>La IP es obligatoria</mat-error>
}
</mat-form-field>
<div class="port-row">
<mat-form-field appearance="outline" class="half-width">
<mat-label>Puerto API</mat-label>
<input matInput type="number" formControlName="apiPort">
@if (bootServerForm.controls['apiPort'].invalid && bootServerForm.controls['apiPort'].touched) {
<mat-error>El puerto es obligatorio</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="half-width">
<mat-label>Puerto PXE (TFTP)</mat-label>
<input matInput type="number" formControlName="pxePort">
@if (bootServerForm.controls['pxePort'].invalid && bootServerForm.controls['pxePort'].touched) {
<mat-error>El puerto es obligatorio</mat-error>
}
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>Comentarios</mat-label>
<textarea matInput formControlName="comments" rows="3" placeholder="Información adicional..."></textarea>
</mat-form-field>
</form>
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-button (click)="close()">Cancelar</button>
<button mat-raised-button color="primary" [disabled]="bootServerForm.invalid" (click)="save()">
{{ data ? 'Guardar' : 'Añadir' }}
</button>
</mat-dialog-actions>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageBootServerComponent } from './manage-boot-server.component';
describe('ManageBootServerComponent', () => {
let component: ManageBootServerComponent;
let fixture: ComponentFixture<ManageBootServerComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ManageBootServerComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ManageBootServerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,96 @@
import { Component, Inject, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from "@angular/forms";
import { HttpClient } from "@angular/common/http";
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
import { ToastrService } from "ngx-toastr";
import { ConfigService } from "@services/config.service";
@Component({
selector: 'app-manage-boot-server',
templateUrl: './manage-boot-server.component.html',
styleUrl: './manage-boot-server.component.css'
})
export class ManageBootServerComponent implements OnInit {
baseUrl: string;
bootServerForm: FormGroup<any>;
serverId: string | null = null;
constructor(
private fb: FormBuilder,
private http: HttpClient,
public dialogRef: MatDialogRef<ManageBootServerComponent>,
private toastService: ToastrService,
private configService: ConfigService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.baseUrl = this.configService.apiUrl;
this.bootServerForm = this.fb.group({
name: [null, Validators.required],
ip: [null, Validators.required],
apiPort: [8082, Validators.required],
pxePort: [8082, Validators.required],
comments: [null],
});
}
ngOnInit() {
if (this.data) {
this.load()
}
}
load(): void {
this.http.get<any>(`${this.baseUrl}${this.data}`).subscribe({
next: (response) => {
this.bootServerForm.patchValue({
name: response.name,
ip: response.ip,
apiPort: response.apiPort ?? 8082,
pxePort: response.pxePort ?? 8082,
comments: response.comments,
});
this.serverId = response['@id'];
},
error: (err) => {
console.error('Error fetching boot server:', err);
this.toastService.error('Error cargando los datos del servidor');
}
});
}
save(): void {
const payload = {
name: this.bootServerForm.value.name,
ip: this.bootServerForm.value.ip,
apiPort: parseInt(this.bootServerForm.value.apiPort),
pxePort: parseInt(this.bootServerForm.value.pxePort),
comments: this.bootServerForm.value.comments,
};
if (this.serverId) {
this.http.put(`${this.baseUrl}${this.serverId}`, payload).subscribe({
next: () => {
this.toastService.success('Servidor Boot actualizado correctamente');
this.dialogRef.close(true);
},
error: (error) => {
this.toastService.error(error.error?.['hydra:description'] || 'Ha ocurrido un error al editar');
}
});
} else {
this.http.post(`${this.baseUrl}/boot-servers`, payload).subscribe({
next: () => {
this.toastService.success('Servidor Boot añadido correctamente');
this.dialogRef.close(true);
},
error: (error) => {
this.toastService.error(error.error?.['hydra:description'] || 'Ha ocurrido un error al crear');
}
});
}
}
close(): void {
this.dialogRef.close();
}
}

View File

@ -1,20 +1,37 @@
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" text="{{ 'advancedNetbootDescription' | translate }}">
{{ 'advancedNetbootTitle' | translate }}
</h2>
@if (!isFromDashboard) {
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" text="{{ 'advancedNetbootDescription' | translate }}">
{{ 'advancedNetbootTitle' | translate }}
</h2>
</div>
</div>
</div>
}
<div [formGroup]="taskForm" class="search-container">
@if (!isFromDashboard) {
<mat-form-field appearance="fill" class="search-string">
<mat-label>Servidor OgBoot</mat-label>
<mat-select [(ngModel)]="selectedServerId" [ngModelOptions]="{standalone: true}" (selectionChange)="onServerChange()">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server.uuid || server['@id'].split('/').pop()">
{{ server.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="fill" class="search-boolean" joyrideStep="selectClassStep"
text="{{ 'selectClassDescription' | translate }}">
<mat-label>{{ 'selectClassLabel' | translate }}</mat-label>
<mat-select (selectionChange)="loadChildUnits($event)">
<mat-option *ngFor="let unit of units" [value]="unit">{{ unit.name }}</mat-option>
@for (unit of units; track unit.id) {
<mat-option [value]="unit">{{ unit.name }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
@ -25,9 +42,11 @@
text="{{ 'applyToAllDescription' | translate }}">
<mat-label>{{ 'applyToAllLabel' | translate }}</mat-label>
<mat-select [(value)]="globalOgLive" (selectionChange)="applyToAll()">
<mat-option *ngFor="let option of ogLiveOptions" [value]="option['@id']">
{{ option.name }}
</mat-option>
@for (option of ogLiveOptions; track option['@id']) {
<mat-option [value]="option['@id']">
{{ option.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
@ -67,14 +86,30 @@
<mat-label>{{ 'selectTemplateLabel' | translate }}</mat-label>
<mat-select [(ngModel)]="client.ogLive" [name]="'ogLive' + client.id">
<mat-option [value]="null">{{ 'noTemplateOption' | translate }}</mat-option>
<mat-option *ngFor="let template of ogLiveOptions" [value]="template['@id']">
{{ template.name }}
</mat-option>
@for (template of ogLiveOptions; track template['@id']) {
<mat-option [value]="template['@id']">
{{ template.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</mat-cell>
</ng-container>
<ng-container matColumnDef="bootServer">
<mat-header-cell *matHeaderCellDef> Servidor </mat-header-cell>
<mat-cell *matCellDef="let element">
<span [class.text-warning]="element.bootServer?.uuid !== selectedServerId"
style="display: flex; align-items: center;"
[title]="element.bootServer?.uuid !== selectedServerId ? 'Este equipo cambiará al servidor actual al guardar' : ''">
{{ element.bootServer?.name || 'Ninguno' }}
@if (element.bootServer?.uuid && element.bootServer.uuid !== selectedServerId) {
<mat-icon style="font-size: 16px; width: 16px; height: 16px; margin-left: 4px; color: orange;">warning</mat-icon>
}
</span>
</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
</table>

View File

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

View File

@ -1,22 +1,37 @@
<div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" [text]="'adminImagesDescription' | translate">
{{ 'adminImagesTitle' | translate }}
</h2>
</div>
<div class="images-button-row">
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
<div class="header-container" [style.border-bottom]="isFromDashboard ? 'none' : '1px solid #ddd'" [style.padding]="isFromDashboard ? '10px 0' : '10px 10px'">
@if (!isFromDashboard) {
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" [text]="'adminImagesDescription' | translate">
{{ 'adminImagesTitle' | translate }}
</h2>
</div>
}
<div class="images-button-row" [style.margin-left]="isFromDashboard ? 'auto' : '0'">
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()" [disabled]="!selectedServerId">{{ 'viewInfoButton' | translate }}</button>
<button class="action-button" (click)="addImage()" joyrideStep="addImageStep"
[text]="'addOgLiveButtonDescription' | translate">
[text]="'addOgLiveButtonDescription' | translate" [disabled]="!selectedServerId">
{{ 'addOgLiveButton' | translate }}
</button>
</div>
</div>
<div class="search-container">
@if (!isFromDashboard) {
<mat-form-field appearance="fill" class="search-string">
<mat-label>Servidor OgBoot</mat-label>
<mat-select [(ngModel)]="selectedServerId" (selectionChange)="onServerChange()">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server.uuid || server['@id'].split('/').pop()">
{{ server.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
[text]="'searchNameDescription' | translate">
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
@ -51,76 +66,72 @@
</div>
<app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
[text]="'tableDescription' | translate">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef === 'isDefault'">
<mat-chip>
<ng-container *ngIf="image.isDefault">
{{ 'yesOption' | translate }}
</ng-container>
<ng-container *ngIf="!image.isDefault">
{{ 'noOption' | translate }}
</ng-container>
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'downloadUrl'">
<span matTooltip="{{ image.downloadUrl }}">
{{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }}
</span>
@if (!loading) {
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
[text]="'tableDescription' | translate">
@for (column of columns; track column.columnDef) {
<ng-container [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
<td mat-cell *matCellDef="let image">
@if (column.columnDef === 'isDefault') {
<mat-chip>
@if (image.isDefault) {
{{ 'yesOption' | translate }}
} @else {
{{ 'noOption' | translate }}
}
</mat-chip>
} @else if (column.columnDef === 'downloadUrl') {
<span matTooltip="{{ image.downloadUrl }}">
{{ image.downloadUrl ? image.downloadUrl.substring(0, 20) + '...' : '' }}
</span>
} @else if (column.columnDef === 'name') {
<span matTooltip="{{ image.name }}">
{{ image.name }}
</span>
} @else if (column.columnDef === 'status') {
<mat-chip [ngClass]="getStatusLabel(image.status).class">
{{ getStatusLabel(image.status).label }}
</mat-chip>
} @else {
{{ column.cell(image) }}
}
</td>
</ng-container>
}
<ng-container *ngIf="column.columnDef === 'name'">
<span matTooltip="{{ image.name }}">
{{ image.name }}
</span>
</ng-container>
<ng-container *ngIf="column.columnDef === 'status'">
<mat-chip [ngClass]="getStatusLabel(image.status).class">
{{ getStatusLabel(image.status).label }}
</mat-chip>
</ng-container>
<ng-container
*ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'downloadUrl' && column.columnDef !== 'status' && column.columnDef !== 'name'">
{{ column.cell(image) }}
</ng-container>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumnHeader' | translate }}</th>
<td mat-cell *matCellDef="let image" style="text-align: center;" joyrideStep="actionsStep"
[text]="'actionsDescription' | translate">
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="deleteImage(image)">
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
</button>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item [disabled]="image.status !== 'inactive'" (click)="toggleAction(image, 'install')">{{ 'installOption' |
translate }}</button>
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'uninstall')">
{{ 'uninstallOption' | translate }}
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumnHeader' | translate }}</th>
<td mat-cell *matCellDef="let image" style="text-align: center;" joyrideStep="actionsStep"
[text]="'actionsDescription' | translate">
<button mat-icon-button color="info" (click)="showOgLive($event, image)">
<mat-icon>{{ 'viewIcon' | translate }}</mat-icon>
</button>
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'set-default')">
{{ 'setDefaultOption' | translate }}
<button mat-icon-button color="warn" (click)="deleteImage(image)">
<mat-icon>{{ 'deleteIcon' | translate }}</mat-icon>
</button>
</mat-menu>
</td>
</ng-container>
<button mat-icon-button [matMenuTriggerFor]="menu">
<mat-icon>menu</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item [disabled]="image.status !== 'inactive'" (click)="toggleAction(image, 'install')">{{ 'installOption' |
translate }}</button>
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'uninstall')">
{{ 'uninstallOption' | translate }}
</button>
<button mat-menu-item [disabled]="image.status !== 'active'" (click)="toggleAction(image, 'set-default')">
{{ 'setDefaultOption' | translate }}
</button>
</mat-menu>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
}
<div class="paginator-container" joyrideStep="paginationStep" [text]="'paginationDescription' | translate">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"

View File

@ -1,4 +1,4 @@
import { Component, OnInit, signal } from '@angular/core';
import { Component, OnInit, signal, Input } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http';
import { CreatePXEImageComponent } from './create-image/create-image/create-image.component';
@ -13,6 +13,7 @@ import { Observable } from "rxjs";
import { JoyrideService } from 'ngx-joyride';
import { ServerInfoDialogComponent } from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
import { ConfigService } from '@services/config.service';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-pxe-images',
@ -23,6 +24,9 @@ export class PXEimagesComponent implements OnInit {
baseUrl: string;
private apiUrl: string;
images: { downloadUrl: string; name: string; uuid: string }[] = [];
bootServers: any[] = [];
@Input() selectedServerId: string | null = null;
isFromDashboard: boolean = false;
dataSource = new MatTableDataSource<any>();
length: number = 0;
itemsPerPage: number = 20;
@ -69,15 +73,48 @@ export class PXEimagesComponent implements OnInit {
private dataService: DataService,
private configService: ConfigService,
private toastService: ToastrService,
private joyrideService: JoyrideService
private joyrideService: JoyrideService,
private route: ActivatedRoute
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/og-lives`;
}
ngOnInit(): void {
if (this.selectedServerId) {
this.isFromDashboard = true;
this.onServerChange();
} else {
const serverIdParam = this.route.snapshot.queryParamMap.get('serverId');
if (serverIdParam) {
this.selectedServerId = serverIdParam;
this.isFromDashboard = true;
this.onServerChange();
}
}
this.loadBootServers();
}
loadBootServers(): void {
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.bootServers = response['hydra:member'];
if (this.bootServers.length > 0 && !this.selectedServerId) {
this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop();
this.onServerChange();
}
},
error => {
console.error('Error fetching BootServers:', error);
}
);
}
onServerChange(): void {
this.filters['bootServer.uuid'] = this.selectedServerId!;
this.page = 0;
this.search();
this.loadAlert();
}
addImage(): void {
@ -199,7 +236,10 @@ export class PXEimagesComponent implements OnInit {
}
loadAlert(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
if (!this.selectedServerId) {
throw new Error('No boot server selected');
}
return this.http.get<any>(`${this.baseUrl}/boot-servers/${this.selectedServerId}/oglives/get-collection`);
}
openInfoDialog() {

View File

@ -64,14 +64,14 @@ export class CreatePxeTemplateComponent implements OnInit {
}
ngOnInit() {
this.isEditMode = !!this.data;
this.isEditMode = !!this.data?.uuid;
if (this.isEditMode) {
this.getPxeClients();
}
this.templateForm = this.fb.group({
name: [this.data?.name || '', Validators.required],
name: [this.data?.name || '', [Validators.required, Validators.pattern('^[a-zA-Z0-9._-]+$')]],
templateContent: [this.data?.templateContent || '', Validators.required],
isDefault: [this.data?.isDefault || false]
});
@ -98,12 +98,16 @@ export class CreatePxeTemplateComponent implements OnInit {
createPxeTemplate(): void {
const formValues = this.templateForm.value;
const payload = {
const payload: any = {
name: formValues.name,
templateContent: formValues.templateContent,
isDefault: formValues.isDefault,
};
if (this.data?.bootServerIri) {
payload.bootServer = this.data.bootServerIri;
}
this.http.post<any>(`${this.baseUrl}/pxe-templates`, payload).subscribe({
next: data => {
this.toastService.success('Plantilla PXE creada exitosamente');

View File

@ -1,19 +1,34 @@
<div class="header-container">
<button mat-icon-button color="primary" (click)="initTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" text="{{ 'adminPxeDescription' | translate }}">{{ 'adminPxeTitle' |
translate }}</h2>
</div>
<div class="template-button-row">
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()">{{ 'viewInfoButton' | translate }}</button>
<div class="header-container" [style.border-bottom]="isFromDashboard ? 'none' : '1px solid #ddd'" [style.padding]="isFromDashboard ? '10px 0' : '10px 10px'">
@if (!isFromDashboard) {
<button mat-icon-button color="primary" (click)="initTour()">
<mat-icon>help</mat-icon>
</button>
<div class="header-container-title">
<h2 joyrideStep="titleStep" text="{{ 'adminPxeDescription' | translate }}">{{ 'adminPxeTitle' |
translate }}</h2>
</div>
}
<div class="template-button-row" [style.margin-left]="isFromDashboard ? 'auto' : '0'">
<button class="action-button" joyrideStep="viewInfoStep" [text]="'viewInfoStepText' | translate" (click)="openInfoDialog()" [disabled]="!selectedServerId">{{ 'viewInfoButton' | translate }}</button>
<button class="action-button" (click)="addPxeTemplate()" joyrideStep="addTemplateStep"
text="{{ 'addTemplateButtonDescription' | translate }}">{{ 'addTemplateButton' | translate }}</button>
text="{{ 'addTemplateButtonDescription' | translate }}" [disabled]="!selectedServerId">{{ 'addTemplateButton' | translate }}</button>
</div>
</div>
<div class="search-container">
@if (!isFromDashboard) {
<mat-form-field appearance="fill" class="search-string">
<mat-label>Servidor OgBoot</mat-label>
<mat-select [(ngModel)]="selectedServerId" (selectionChange)="onServerChange()">
@for (server of bootServers; track server['@id']) {
<mat-option [value]="server.uuid || server['@id'].split('/').pop()">
{{ server.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="{{ 'searchNameDescription' | translate }}">
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
@ -37,34 +52,33 @@
<app-loading [isLoading]="loading"></app-loading>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="{{ 'tableDatePxeTemplateText' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
<td mat-cell *matCellDef="let image">
<ng-container *ngIf="column.columnDef === 'synchronized'">
<mat-chip>
<ng-container *ngIf="image.synchronized">
{{ 'yesOption' | translate }}
</ng-container>
<ng-container *ngIf="!image.synchronized">
{{ 'noOption' | translate }}
</ng-container>
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef === 'isDefault'">
<mat-chip>
<ng-container *ngIf="image.isDefault">
{{ 'yesOption' | translate }}
</ng-container>
<ng-container *ngIf="!image.isDefault">
{{ 'noOption' | translate }}
</ng-container>
</mat-chip>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'synchronized' && column.columnDef !== 'isDefault'">
{{ column.cell(image) }}
</ng-container>
</td>
</ng-container>
@for (column of columns; track column.columnDef) {
<ng-container [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>
<td mat-cell *matCellDef="let image">
@if (column.columnDef === 'synchronized') {
<mat-chip>
@if (image.synchronized) {
{{ 'yesOption' | translate }}
} @else {
{{ 'noOption' | translate }}
}
</mat-chip>
} @else if (column.columnDef === 'isDefault') {
<mat-chip>
@if (image.isDefault) {
{{ 'yesOption' | translate }}
} @else {
{{ 'noOption' | translate }}
}
</mat-chip>
} @else {
{{ column.cell(image) }}
}
</td>
</ng-container>
}
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'actionsColumn' | translate }}</th>

View File

@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http';
import {Component, OnInit} from '@angular/core';
import {Component, OnInit, Input} from '@angular/core';
import { CreatePxeTemplateComponent } from './manage-pxeTemplate/create-pxe-template.component';
import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table';
@ -12,6 +12,7 @@ import { JoyrideService } from 'ngx-joyride';
import { DeleteModalComponent } from "../../../shared/delete_modal/delete-modal/delete-modal.component";
import { ServerInfoDialogComponent } from "../../ogdhcp/server-info-dialog/server-info-dialog.component";
import { ConfigService } from '@services/config.service';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-pxe',
@ -22,6 +23,9 @@ export class PxeComponent implements OnInit{
baseUrl: string;
private apiUrl: string;
pxeTemplates: any[] = [];
bootServers: any[] = [];
@Input() selectedServerId: string | null = null;
isFromDashboard: boolean = false;
currentPage: number = 1;
dataSource = new MatTableDataSource<any>();
length: number = 0;
@ -70,15 +74,48 @@ export class PxeComponent implements OnInit{
private toastService: ToastrService,
private dataService: DataService,
private joyrideService: JoyrideService,
private configService: ConfigService
private configService: ConfigService,
private route: ActivatedRoute
) {
this.baseUrl = this.configService.apiUrl;
this.apiUrl = `${this.baseUrl}/pxe-templates`;
}
ngOnInit(): void {
if (this.selectedServerId) {
this.isFromDashboard = true;
this.onServerChange();
} else {
const serverIdParam = this.route.snapshot.queryParamMap.get('serverId');
if (serverIdParam) {
this.selectedServerId = serverIdParam;
this.isFromDashboard = true;
this.onServerChange();
}
}
this.loadBootServers();
}
loadBootServers(): void {
const url = `${this.baseUrl}/boot-servers?page=1&itemsPerPage=10000`;
this.http.get<any>(url).subscribe(
response => {
this.bootServers = response['hydra:member'];
if (this.bootServers.length > 0 && !this.selectedServerId) {
this.selectedServerId = this.bootServers[0].uuid || this.bootServers[0]['@id'].split('/').pop();
this.onServerChange();
}
},
error => {
console.error('Error fetching BootServers:', error);
}
);
}
onServerChange(): void {
this.filters['bootServer.uuid'] = this.selectedServerId!;
this.page = 0;
this.search();
this.loadAlert()
}
search(): void {
@ -97,7 +134,8 @@ export class PxeComponent implements OnInit{
addPxeTemplate() {
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
width: '800px'
width: '800px',
data: { bootServerIri: this.selectedServerId ? `/boot-servers/${this.selectedServerId}` : null }
});
dialogRef.afterClosed().subscribe(() => {
@ -107,7 +145,7 @@ export class PxeComponent implements OnInit{
editPxeTemplate(template: any) {
const dialogRef = this.dialog.open(CreatePxeTemplateComponent, {
data: template,
data: { ...template, bootServerIri: this.selectedServerId ? `/boot-servers/${this.selectedServerId}` : null },
width: '800px'
});
@ -126,9 +164,12 @@ export class PxeComponent implements OnInit{
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.http.post(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
next: () => {
this.http.post<any>(`${this.apiUrl}/server/${image.uuid}/delete`, {}).subscribe({
next: (response) => {
this.toastService.success('Plantilla eliminada exitosamente');
if (response && response.warning) {
this.toastService.info(response.warning, 'Información', { timeOut: 8000 });
}
this.search();
},
error: (error) => {
@ -150,7 +191,10 @@ export class PxeComponent implements OnInit{
}
loadAlert(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/server/get-collection`);
if (!this.selectedServerId) {
throw new Error('No boot server selected');
}
return this.http.get<any>(`${this.baseUrl}/boot-servers/${this.selectedServerId}/pxe-templates/get-collection`);
}
openInfoDialog() {

View File

@ -67,38 +67,14 @@
</span>
</mat-list-item>
<mat-list-item (click)="toggleOgBootSub()" matTooltip="{{ 'TOOLTIP_BOOT' | translate }}" matTooltipShowDelay="1000"
*ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
<mat-list-item routerLink="/boot-servers" (click)="onItemClick()" matTooltip="Servidores Boot"
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
<span class="entry">
<mat-icon class="icon">desktop_windows</mat-icon>
<span>{{ 'boot' | translate }}</span>
<mat-icon class="icon">dns</mat-icon>
<span> Servidores Boot</span>
</span>
</mat-list-item>
<mat-nav-list *ngIf="showOgBootSub" style="padding-left: 20px;">
<mat-list-item routerLink="/pxe-images" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_PXE_IMAGES' | translate }}"
matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">album</mat-icon>
<span>{{ 'ogLive' | translate }}</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/pxe" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_PXE_TEMPLATES' | translate }}"
matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">assignment</mat-icon>
<span>{{ 'pxeTemplates' | translate }}</span>
</span>
</mat-list-item>
<mat-list-item routerLink="/pxe-boot-file" (click)="onItemClick()"
matTooltip="{{ 'TOOLTIP_PXE_BOOT_FILES' | translate }}" matTooltipShowDelay="1000">
<span class="entry">
<mat-icon class="icon">save</mat-icon>
<span>{{ 'pxeBootFiles' | translate }}</span>
</span>
</mat-list-item>
</mat-nav-list>
<mat-list-item routerLink="/calendars" (click)="onItemClick()" matTooltip="{{ 'TOOLTIP_CALENDARS' | translate }}"
matTooltipShowDelay="1000" *ngIf="auth.userCategory !== 'ou-operator' && auth.userCategory !== 'ou-minimal'">
<span class="entry">