Merge pull request 'develop' (#20) from develop into main
testing/ogGui-multibranch/pipeline/head This commit looks good Details

Reviewed-on: #20
pull/21/head
Manuel Aranda Rosales 2025-04-16 11:29:37 +02:00
commit 3bae27d88e
18 changed files with 120 additions and 75 deletions

View File

@ -1,4 +1,14 @@
# Changelog # Changelog
## [0.11.1] - 2025-4-16
### Improved
- Nuevos campos en la tabla de clientes. Tipo de firmware y mac.
## Fixed
- Se ha corregido error al crear OUs, que no refrescaba la web.
- Se ha corregido error en el formulario de creacion de imagenes. Si se seleccionaba una imagen para un versionado, no dejaba deseleccionar.
- Se ha corregido un bug en el particionador que impedia ejecutar, cuando eliminabamos una particion.
---
## [0.11.0] - 2025-4-11 ## [0.11.0] - 2025-4-11
### Added ### Added
- Se ha diseñado el nuevo formulario para poder ejecutar script. Sistema mejorado con variables etiquetadas. - Se ha diseñado el nuevo formulario para poder ejecutar script. Sistema mejorado con variables etiquetadas.

View File

@ -8,12 +8,17 @@
[matMenuTriggerFor]="commandMenu"> [matMenuTriggerFor]="commandMenu">
{{ buttonText }} {{ buttonText }}
</button> </button>
<button mat-menu-item *ngSwitchCase="'menu-item'" [matMenuTriggerFor]="commandMenu" [disabled]="disabled">
<mat-icon>{{ icon }}</mat-icon>
<span>{{ buttonText }}</span>
</button>
</ng-container> </ng-container>
<mat-menu #commandMenu="matMenu"> <mat-menu #commandMenu="matMenu">
<button mat-menu-item [disabled]="command.disabled <button mat-menu-item [disabled]="command.disabled
|| (command.slug === 'create-image' && clientData.length > 1)" || (command.slug === 'create-image' && clientData.length > 1)" *ngFor="let command of arrayCommands"
*ngFor="let command of arrayCommands" (click)="onCommandSelect(command.slug)"> (click)="onCommandSelect(command.slug)">
{{ command.name }} {{ command.name }}
</button> </button>
</mat-menu> </mat-menu>

View File

@ -11,7 +11,7 @@ import { ConfigService } from '@services/config.service';
}) })
export class ExecuteCommandComponent implements OnInit { export class ExecuteCommandComponent implements OnInit {
@Input() clientData: any[] = []; @Input() clientData: any[] = [];
@Input() buttonType: 'icon' | 'text' = 'icon'; @Input() buttonType: 'icon' | 'text' | 'menu-item' = 'icon';
@Input() buttonText: string = 'Ejecutar Comandos'; @Input() buttonText: string = 'Ejecutar Comandos';
@Input() icon: string = 'terminal'; @Input() icon: string = 'terminal';
@Input() disabled: boolean = false; @Input() disabled: boolean = false;

View File

@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import { MatTabChangeEvent } from '@angular/material/tabs'; import { MatTabChangeEvent } from '@angular/material/tabs';
import {ToastrService} from "ngx-toastr";
@Component({ @Component({
selector: 'app-global-status', selector: 'app-global-status',
@ -40,6 +41,7 @@ export class GlobalStatusComponent implements OnInit {
constructor( constructor(
private configService: ConfigService, private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient private http: HttpClient
) { ) {
this.baseUrl = this.configService.apiUrl; this.baseUrl = this.configService.apiUrl;
@ -50,6 +52,56 @@ export class GlobalStatusComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.loadOgBootStatus(); this.loadOgBootStatus();
this.syncSubnets()
this.syncTemplates()
this.syncOgLives()
}
syncSubnets() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las subredes: tiempo de espera agotado');
}, 3500);
this.http.post(`${this.baseUrl}/subnets/sync`, {}).subscribe({
next: (response) => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con componente DHCP exitosa');
},
error: (error) => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar las subredes DHCP');
}
});
}
syncTemplates() {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las plantillas Pxe: tiempo de espera agotado');
}, 3500);
this.http.post(`${this.baseUrl}/pxe-templates/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización de las plantillas Pxe completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar las plantillas Pxe');
});
}
syncOgLives(): void {
const timeoutId = setTimeout(() => {
this.toastService.error('Error al sincronizar las imagenes ogLive : tiempo de espera agotado');
}, 3500);
this.http.post(`${this.baseUrl}/og-lives/sync`, {})
.subscribe(response => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con los ogLives completada');
}, error => {
clearTimeout(timeoutId);
this.toastService.error('Error al sincronizar imágenes ogLive');
});
} }
[key: string]: any; [key: string]: any;

View File

@ -24,6 +24,10 @@
<mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required> <mat-select [(ngModel)]="selectedImage" name="selectedImage" (selectionChange)="resetCanonicalName()" required>
<mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option> <mat-option *ngFor="let image of images" [value]="image">{{ image?.name }}</mat-option>
</mat-select> </mat-select>
<button *ngIf="selectedImage" mat-icon-button matSuffix aria-label="Clear client search"
(click)="selectedImage = null; resetCanonicalName()">
<mat-icon>close</mat-icon>
</button>
<mat-hint>Seleccione la imagen para sobreescribir si se requiere. </mat-hint> <mat-hint>Seleccione la imagen para sobreescribir si se requiere. </mat-hint>
</mat-form-field> </mat-form-field>
</div> </div>

View File

@ -288,7 +288,9 @@ export class PartitionAssistantComponent {
this.loading = true; this.loading = true;
const totalPartitionSize = this.selectedDisk.partitions.reduce((sum: any, partition: { size: any; }) => sum + partition.size, 0); const totalPartitionSize = this.selectedDisk.partitions
.filter((partition: any) => !partition.removed)
.reduce((sum: any, partition: any) => sum + partition.size, 0);
if (totalPartitionSize > this.selectedDisk.totalDiskSize) { if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
this.toastService.error('El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.'); this.toastService.error('El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.');

View File

@ -198,6 +198,9 @@
<mat-icon>delete</mat-icon> <mat-icon>delete</mat-icon>
<span>{{ 'delete' | translate }}</span> <span>{{ 'delete' | translate }}</span>
</button> </button>
<app-execute-command [clientData]="selection.selected" [buttonType]="'menu-item'"
[buttonText]="'Ejecutar comandos'" [icon]="'terminal'" [disabled]="selection.selected.length === 0">
</app-execute-command>
</mat-menu> </mat-menu>
</div> </div>
@ -245,7 +248,8 @@
<div *ngFor="let client of arrayClients" class="client-item"> <div *ngFor="let client of arrayClients" class="client-item">
<div class="client-card"> <div class="client-card">
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)" <mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(client)"
[checked]="selection.isSelected(client)" [disabled]="client.status === 'busy' || client.status === 'off' || client.status === 'disconnected'"> [checked]="selection.isSelected(client)"
[disabled]="client.status === 'busy' || client.status === 'off' || client.status === 'disconnected'">
</mat-checkbox> </mat-checkbox>
<img style="margin-top: 0.5em;" [src]="'assets/images/computer_' + client.status + '.svg'" <img style="margin-top: 0.5em;" [src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon" class="client-image" /> alt="Client Icon" class="client-image" />
@ -314,7 +318,8 @@
</th> </th>
<td mat-cell *matCellDef="let row"> <td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)" <mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
[checked]="selection.isSelected(row)" [disabled]="row.status === 'busy' || row.status === 'off' || row.status === 'disconnected'"> [checked]="selection.isSelected(row)"
[disabled]="row.status === 'busy' || row.status === 'off' || row.status === 'disconnected'">
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
@ -343,9 +348,22 @@
<th mat-header-cell *matHeaderCellDef mat-sort-header>IP </th> <th mat-header-cell *matHeaderCellDef mat-sort-header>IP </th>
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" <td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}"
matTooltipPosition="left" matTooltipShowDelay="500"> matTooltipPosition="left" matTooltipShowDelay="500">
{{ client.ip }} <div style="display: flex; flex-direction: column;">
<span>{{ client.ip }}</span>
<span style="font-size: 0.75rem; color: gray;">{{ client.mac }}</span>
</div>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="firmwareType">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'firmwareType' | translate }} </th>
<td mat-cell *matCellDef="let client">
<mat-chip s>
{{ client.firmwareType ? client.firmwareType : 'N/A' }}
</mat-chip>
</td>
</ng-container>
<ng-container matColumnDef="oglive"> <ng-container matColumnDef="oglive">
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
<td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td> <td mat-cell *matCellDef="let client"> {{ client.ogLive?.date | date }} </td>

View File

@ -87,7 +87,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
{ value: 'mac', name: 'Mac' }, { value: 'mac', name: 'Mac' },
]; ];
displayedColumns: string[] = ['select', 'status', 'ip', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
private _sort!: MatSort; private _sort!: MatSort;
@ -369,13 +369,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
onNodeClick(event: MouseEvent, node: TreeNode): void { onNodeClick(event: MouseEvent, node: TreeNode): void {
event.stopPropagation(); event.stopPropagation();
this.selectedNode = node; this.selectedNode = node;
this.fetchClientsForNode(node); const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
} }
onMenuClick(event: Event, node: any): void { onMenuClick(event: Event, node: any): void {
event.stopPropagation(); event.stopPropagation();
this.selectedNode = node; this.selectedNode = node;
this.fetchClientsForNode(node); const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
} }
@ -419,7 +421,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
width: '900px', width: '900px',
}); });
dialogRef.afterClosed().subscribe((newUnit) => { dialogRef.afterClosed().subscribe((newUnit) => {
if (newUnit?.uuid) { if (newUnit) {
this.refreshData(newUnit.uuid); this.refreshData(newUnit.uuid);
} }
}); });

View File

@ -105,6 +105,7 @@ export class ManageClientComponent implements OnInit {
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({ this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
id: unit['@id'], id: unit['@id'],
name: unit.name, name: unit.name,
netiface: unit.networkSettings?.netiface,
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits), path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits),
repository: unit.networkSettings?.repository?.['@id'], repository: unit.networkSettings?.repository?.['@id'],
hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'], hardwareProfile: unit.networkSettings?.hardwareProfile?.['@id'],
@ -226,7 +227,8 @@ export class ManageClientComponent implements OnInit {
repository: selectedUnit.repository || null, repository: selectedUnit.repository || null,
hardwareProfile: selectedUnit.hardwareProfile || null, hardwareProfile: selectedUnit.hardwareProfile || null,
ogLive: selectedUnit.ogLive || null, ogLive: selectedUnit.ogLive || null,
menu: selectedUnit.menu || null menu: selectedUnit.menu || null,
netiface: selectedUnit.netiface || null,
}); });
} }
} }

View File

@ -1,5 +1,3 @@
<app-loading [isLoading]="loading"></app-loading>
<div class="header-container"> <div class="header-container">
<button mat-icon-button color="primary" (click)="iniciarTour()"> <button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon> <mat-icon>help</mat-icon>

View File

@ -79,7 +79,6 @@ export class PXEimagesComponent implements OnInit {
this.loading = true; this.loading = true;
this.search(); this.search();
this.loadAlert(); this.loadAlert();
this.syncOgBoot()
this.loading = false; this.loading = false;
} }
@ -245,17 +244,6 @@ export class PXEimagesComponent implements OnInit {
); );
} }
syncOgBoot(): void {
this.http.post(`${this.apiUrl}/sync`, {})
.subscribe(response => {
this.toastService.success('Sincronización con oGBoot exitosa');
this.search()
}, error => {
console.error('Error al sincronizar', error);
this.toastService.error('Error al sincronizar');
});
}
iniciarTour(): void { iniciarTour(): void {
this.joyrideService.startTour({ this.joyrideService.startTour({
steps: [ steps: [

View File

@ -35,7 +35,7 @@
</div> </div>
<app-loading [isLoading]="loading"></app-loading> <app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" <table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="{{ 'tableDescription' | translate }}"> text="{{ 'tableDescription' | translate }}">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef"> <ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef>{{ column.header }}</th> <th mat-header-cell *matHeaderCellDef>{{ column.header }}</th>

View File

@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core'; import {Component, OnInit} from '@angular/core';
import { CreatePxeTemplateComponent } from './create-pxeTemplate/create-pxe-template.component'; import { CreatePxeTemplateComponent } from './create-pxeTemplate/create-pxe-template.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatTableDataSource } from '@angular/material/table'; import { MatTableDataSource } from '@angular/material/table';
@ -19,7 +19,7 @@ import { ConfigService } from '@services/config.service';
templateUrl: './pxe.component.html', templateUrl: './pxe.component.html',
styleUrls: ['./pxe.component.css'] styleUrls: ['./pxe.component.css']
}) })
export class PxeComponent { export class PxeComponent implements OnInit{
baseUrl: string; baseUrl: string;
private apiUrl: string; private apiUrl: string;
pxeTemplates: any[] = []; pxeTemplates: any[] = [];
@ -76,7 +76,6 @@ export class PxeComponent {
this.loading = true; this.loading = true;
this.search(); this.search();
this.loadAlert() this.loadAlert()
this.syncTemplates()
this.loading = false; this.loading = false;
} }
@ -147,17 +146,6 @@ export class PxeComponent {
const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px' }); const dialogRef = this.dialog.open(ShowTemplateContentComponent, { data: { data }, width: '700px' });
} }
syncTemplates() {
this.http.post(`${this.apiUrl}/sync`, {})
.subscribe(response => {
this.toastService.success('Sincronización completada');
this.search()
}, error => {
console.error('Error al sincronizar', error);
this.toastService.error('Error al sincronizar');
});
}
applyFilter() { applyFilter() {
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({ this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
next: (response) => { next: (response) => {

View File

@ -72,10 +72,4 @@ describe('OgDhcpSubnetsComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should call syncSubnets and handle success', () => {
component.syncSubnets();
expect(mockHttpClient.post).toHaveBeenCalledWith(`${component.baseUrl}/subnets/sync`, {});
expect(mockToastrService.success).toHaveBeenCalledWith('Sincronización con componente DHCP exitosa');
});
}); });

View File

@ -69,19 +69,21 @@ export class OgDhcpSubnetsComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.loading = true;
this.loadAlert() this.loadAlert()
this.syncSubnets() this.loadSubnets()
} }
loadSubnets() { loadSubnets() {
this.loading = true;
this.http.get<any>(`${this.baseUrl}/subnets?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`).subscribe({ this.http.get<any>(`${this.baseUrl}/subnets?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`).subscribe({
next: (response) => { next: (response) => {
this.dataSource.data = response['hydra:member']; this.dataSource.data = response['hydra:member'];
this.length = response['hydra:totalItems']; this.length = response['hydra:totalItems'];
this.loading = false;
}, },
error: error => { error: error => {
this.toastService.error(error.error['hydra:description']); this.toastService.error(error.error['hydra:description']);
this.loading = false;
} }
}); });
@ -90,28 +92,6 @@ export class OgDhcpSubnetsComponent implements OnInit {
} }
} }
syncSubnets() {
this.loading = true;
const timeoutId = setTimeout(() => {
this.loading = false;
this.toastService.error('Error al sincronizar: tiempo de espera agotado');
}, 3500);
this.http.post(`${this.apiUrl}/sync`, {}).subscribe({
next: (response) => {
clearTimeout(timeoutId);
this.toastService.success('Sincronización con componente DHCP exitosa');
this.loadSubnets();
this.loading = false;
},
error: (error) => {
clearTimeout(timeoutId);
this.loading = false;
this.toastService.error('Error al sincronizar');
}
});
}
toggleAction(subnet: any, action: string): void { toggleAction(subnet: any, action: string): void {
switch (action) { switch (action) {
case 'get': case 'get':

View File

@ -12,7 +12,7 @@ export class ConfigService {
constructor(private http: HttpClient) {} constructor(private http: HttpClient) {}
loadConfig() { loadConfig() {
return this.http.get('/assets/config.json').pipe( return this.http.get('assets/config.json').pipe(
catchError((error) => { catchError((error) => {
console.error('Error loading config.json', error); console.error('Error loading config.json', error);
return of({}); return of({});

View File

@ -21,6 +21,7 @@
"labelOrganizationalUnit": "Organizational Unit", "labelOrganizationalUnit": "Organizational Unit",
"buttonCancel": "Cancel", "buttonCancel": "Cancel",
"buttonAdd": "Add", "buttonAdd": "Add",
"firmwareType": "Firmware",
"addRule": "Add rule", "addRule": "Add rule",
"rulesHeader": "Rules", "rulesHeader": "Rules",
"statusUnavailable": "Unavailable", "statusUnavailable": "Unavailable",

View File

@ -21,6 +21,7 @@
"labelOrganizationalUnit": "Unidad organizativa", "labelOrganizationalUnit": "Unidad organizativa",
"buttonCancel": "Cancelar", "buttonCancel": "Cancelar",
"buttonAdd": "Añadir", "buttonAdd": "Añadir",
"firmwareType": "Firmware",
"back": "Atrás", "back": "Atrás",
"addClientDialogTitle": "Añadir Cliente", "addClientDialogTitle": "Añadir Cliente",
"dialogTitleEditUser": "Editar usuario", "dialogTitleEditUser": "Editar usuario",