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