Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop

develop-jenkins
Manuel Aranda Rosales 2024-10-17 16:15:50 +02:00
commit c4845e4899
8 changed files with 335 additions and 52 deletions

View File

@ -63,8 +63,8 @@
<div class="assistants-container" *ngIf="isPartitionAssistantVisible"> <div class="assistants-container" *ngIf="isPartitionAssistantVisible">
<app-partition-assistant [data]="clientData"></app-partition-assistant> <app-partition-assistant [data]="clientData" [clientUuid]="clientUuid"></app-partition-assistant>
</div> </div>
<div class="assistants-container" *ngIf="isBootImageVisible"> <div class="assistants-container" *ngIf="isBootImageVisible">
<app-restore-image></app-restore-image> <app-restore-image [data]="clientData"></app-restore-image>
</div> </div>

View File

@ -28,7 +28,7 @@
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let partition of disk.partitions; let j = index"> <tr *ngFor="let partition of disk.partitions; let j = index">
<td>{{ j + 1 }}</td> <td>{{ partition.partitionNumber }}</td>
<td> <td>
<select [(ngModel)]="partition.type"> <select [(ngModel)]="partition.type">
<option value="NTFS">NTFS</option> <option value="NTFS">NTFS</option>
@ -47,7 +47,7 @@
<input type="checkbox" [(ngModel)]="partition.format" /> <input type="checkbox" [(ngModel)]="partition.format" />
</td> </td>
<td> <td>
<button (click)="removePartition(disk.diskNumber, j)" class="remove-btn">X</button> <button (click)="removePartition(disk.diskNumber, partition)" class="remove-btn">X</button>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -2,6 +2,8 @@ import { Component, Input, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
interface Partition { interface Partition {
uuid?: string; // Agregamos uuid opcional
partitionNumber: number;
size: number; size: number;
type: string; type: string;
sizeBytes: number; sizeBytes: number;
@ -17,11 +19,11 @@ interface Partition {
}) })
export class PartitionAssistantComponent implements OnInit { export class PartitionAssistantComponent implements OnInit {
@Input() data: any; @Input() data: any;
@Input() clientUuid: string | undefined; // El clientUuid que necesitas @Input() clientUuid: string | undefined;
errorMessage = ''; errorMessage = '';
originalPartitions: any[] = []; originalPartitions: any[] = [];
disks: { diskNumber: number, totalDiskSize: number, partitions: Partition[] }[] = []; disks: { diskNumber: number; totalDiskSize: number; partitions: Partition[] }[] = [];
private apiUrl = 'http://127.0.0.1:8001/partitions'; private apiUrl = 'http://127.0.0.1:8001/partitions';
@ -33,9 +35,9 @@ export class PartitionAssistantComponent implements OnInit {
initializeDisks() { initializeDisks() {
const partitionsFromData = this.data.partitions; const partitionsFromData = this.data.partitions;
this.originalPartitions = JSON.parse(JSON.stringify(partitionsFromData)); // Guardar una copia de las particiones originales this.originalPartitions = JSON.parse(JSON.stringify(partitionsFromData));
const disksMap = new Map<number, { totalDiskSize: number, partitions: Partition[] }>(); const disksMap = new Map<number, { totalDiskSize: number; partitions: Partition[] }>();
partitionsFromData.forEach((partition: any) => { partitionsFromData.forEach((partition: any) => {
if (!disksMap.has(partition.diskNumber)) { if (!disksMap.has(partition.diskNumber)) {
@ -47,8 +49,10 @@ export class PartitionAssistantComponent implements OnInit {
disk!.totalDiskSize = this.convertBytesToGB(partition.size); disk!.totalDiskSize = this.convertBytesToGB(partition.size);
} else { } else {
disk!.partitions.push({ disk!.partitions.push({
uuid: partition.uuid, // Incluimos el uuid
partitionNumber: partition.partitionNumber,
size: this.convertBytesToGB(partition.size), size: this.convertBytesToGB(partition.size),
type: 'NTFS', // Puedes cambiar el tipo según sea necesario type: partition.type,
sizeBytes: partition.size, sizeBytes: partition.size,
format: false, format: false,
color: '#' + Math.floor(Math.random() * 16777215).toString(16), color: '#' + Math.floor(Math.random() * 16777215).toString(16),
@ -72,24 +76,28 @@ export class PartitionAssistantComponent implements OnInit {
} }
updatePartitionPercentages(partitions: Partition[], totalDiskSize: number) { updatePartitionPercentages(partitions: Partition[], totalDiskSize: number) {
partitions.forEach(partition => { partitions.forEach((partition) => {
partition.percentage = (partition.size / totalDiskSize) * 100; partition.percentage = (partition.size / totalDiskSize) * 100;
}); });
} }
// Añadir una nueva partición
addPartition(diskNumber: number) { addPartition(diskNumber: number) {
const disk = this.disks.find(d => d.diskNumber === diskNumber); const disk = this.disks.find((d) => d.diskNumber === diskNumber);
if (disk) { if (disk) {
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize); const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize);
if (remainingGB > 0) { if (remainingGB > 0) {
const maxPartitionNumber =
disk.partitions.length > 0 ? Math.max(...disk.partitions.map((p) => p.partitionNumber)) : 0;
const newPartitionNumber = maxPartitionNumber + 1;
disk.partitions.push({ disk.partitions.push({
partitionNumber: newPartitionNumber,
size: 0, size: 0,
type: 'NTFS', // Tipo por defecto, puede ser cambiado por el usuario type: 'NTFS',
sizeBytes: 0, sizeBytes: 0,
format: false, format: false,
color: '#' + Math.floor(Math.random() * 16777215).toString(16), // Color aleatorio color: '#' + Math.floor(Math.random() * 16777215).toString(16),
percentage: 0 percentage: 0
}); });
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
@ -99,15 +107,16 @@ export class PartitionAssistantComponent implements OnInit {
} }
} }
// Actualizar el tamaño de una partición
updatePartitionSize(diskNumber: number, index: number, size: number) { updatePartitionSize(diskNumber: number, index: number, size: number) {
const disk = this.disks.find(d => d.diskNumber === diskNumber); const disk = this.disks.find((d) => d.diskNumber === diskNumber);
if (disk) { if (disk) {
const partition = disk.partitions[index]; const partition = disk.partitions[index];
const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize) + partition.size; const remainingGB = this.getRemainingGB(disk.partitions, disk.totalDiskSize) + partition.size;
if (size > remainingGB) { if (size > remainingGB) {
this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(2)} GB).`; this.errorMessage = `El tamaño de la partición no puede superar el espacio libre (${remainingGB.toFixed(
2
)} GB).`;
} else { } else {
this.errorMessage = ''; this.errorMessage = '';
partition.size = size; partition.size = size;
@ -132,17 +141,28 @@ export class PartitionAssistantComponent implements OnInit {
getModifiedOrNewPartitions() { getModifiedOrNewPartitions() {
const modifiedPartitions: any[] = []; const modifiedPartitions: any[] = [];
this.disks.forEach(disk => { this.disks.forEach((disk) => {
disk.partitions.forEach((partition, index) => { disk.partitions.forEach((partition) => {
const originalPartition = this.originalPartitions.find( const originalPartition = this.originalPartitions.find(
p => p.diskNumber === disk.diskNumber && p.partitionNumber === index + 1 (p) => p.diskNumber === disk.diskNumber && p.partitionNumber === partition.partitionNumber
); );
// Si no existe en las originales, es nueva
if (!originalPartition) { if (!originalPartition) {
modifiedPartitions.push({ partition, diskNumber: disk.diskNumber, partitionNumber: index + 1 }); // Es una nueva partición
modifiedPartitions.push({
partition,
diskNumber: disk.diskNumber,
partitionNumber: partition.partitionNumber,
isNew: true
});
} else if (this.isPartitionModified(originalPartition, partition)) { } else if (this.isPartitionModified(originalPartition, partition)) {
modifiedPartitions.push({ partition, diskNumber: disk.diskNumber, partitionNumber: index + 1 }); // La partición ha sido modificada
modifiedPartitions.push({
partition,
diskNumber: disk.diskNumber,
partitionNumber: partition.partitionNumber,
isNew: false
});
} }
}); });
}); });
@ -158,33 +178,62 @@ export class PartitionAssistantComponent implements OnInit {
return; return;
} }
modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber }) => { modifiedPartitions.forEach(({ partition, diskNumber, partitionNumber, isNew }) => {
const payload = { const payload = {
diskNumber: diskNumber, diskNumber: diskNumber,
partitionNumber: partitionNumber, partitionNumber: partitionNumber,
size: partition.size, size: partition.size,
filesystem: partition.type, filesystem: partition.type,
client: `https://example.com/${this.clientUuid}` client: `/clients/${this.clientUuid}`
}; };
this.http.post(this.apiUrl, payload).subscribe( if (isNew) {
response => { // Es una nueva partición, usamos POST
console.log('Partición guardada exitosamente:', response); this.http.post(this.apiUrl, payload).subscribe(
}, (response) => {
error => { console.log('Partición creada exitosamente:', response);
console.error('Error al guardar la partición:', error); },
} (error) => {
); console.error('Error al crear la partición:', error);
}
);
} else if (partition.uuid) {
// Es una partición existente modificada, usamos PATCH
const patchUrl = `${this.apiUrl}/${partition.uuid}`;
this.http.patch(patchUrl, payload).subscribe(
(response) => {
console.log('Partición actualizada exitosamente:', response);
},
(error) => {
console.error('Error al actualizar la partición:', error);
}
);
}
}); });
} }
// Eliminar partición de un disco removePartition(diskNumber: number, partition: Partition) {
removePartition(diskNumber: number, index: number) { const disk = this.disks.find((d) => d.diskNumber === diskNumber);
const disk = this.disks.find(d => d.diskNumber === diskNumber);
if (disk) { if (disk) {
disk.partitions.splice(index, 1); const index = disk.partitions.indexOf(partition);
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize); if (index !== -1) {
disk.partitions.splice(index, 1);
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
if (partition.uuid) {
// La partición existía originalmente, enviamos DELETE
const deleteUrl = `${this.apiUrl}/${partition.uuid}`;
this.http.delete(deleteUrl).subscribe(
(response) => {
console.log('Partición eliminada exitosamente:', response);
},
(error) => {
console.error('Error al eliminar la partición:', error);
}
);
}
}
} }
} }
} }

View File

@ -0,0 +1,71 @@
.partition-assistant {
font-family: 'Roboto', sans-serif;
background-color: #f9f9f9;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
margin: 20px auto;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 15px;
padding: 10px;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.partition-table {
width: 100%;
border-collapse: collapse;
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
overflow: hidden;
margin-bottom: 20px;
}
.partition-table th {
background-color: #f5f5f5;
color: #333;
padding: 12px;
font-weight: 600;
}
.partition-table td {
padding: 10px;
text-align: center;
border-bottom: 1px solid #eee;
}
.partition-table select {
padding: 5px;
border-radius: 4px;
border: 1px solid #ccc;
width: 100%;
}
.actions {
display: flex;
justify-content: flex-end;
padding-top: 10px;
}
button.mat-flat-button {
background-color: #28a745;
color: white;
padding: 10px 20px;
border-radius: 4px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: background-color 0.3s ease;
}
button.mat-flat-button:hover {
background-color: #218838;
}

View File

@ -1 +1,38 @@
<p>restore-image works!</p> <div *ngFor="let disk of disks" class="partition-assistant">
<div class="header">
<label>Disco {{ disk.diskNumber }}</label>
</div>
<table class="partition-table">
<thead>
<tr>
<th>Partición</th>
<th>Imagen ISO</th>
<th>OgLive</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let partition of disk.partitions">
<td>{{ partition.partitionNumber }}</td>
<td>
<select [(ngModel)]="partition.associatedImageId" (change)="onImageSelected(partition, $event)" name="associatedImage-{{partition.partitionNumber}}">
<option value="">Seleccionar imagen</option>
<option *ngFor="let image of availableImages" [value]="image['@id']">
{{ image.name }}
</option>
</select>
</td>
<td>
<select (change)="onOgLiveSelected(partition, $event)">
<option value="">Seleccionar OgLive</option>
<option *ngFor="let ogLive of availableOgLives" [value]="ogLive">{{ ogLive }}</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<div class="actions">
<button mat-flat-button color="primary" (click)="saveAssociations()">Guardar Asociaciones</button>
</div>

View File

@ -1,10 +1,95 @@
import { Component } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
interface Image {
'@id': string;
'@type': string;
name: string;
description: string;
comments: string;
uuid: string;
id: number;
}
interface Partition {
diskNumber: number;
partitionNumber: number;
associatedImageId?: string;
associatedOgLive?: string;
}
@Component({ @Component({
selector: 'app-restore-image', selector: 'app-restore-image',
templateUrl: './restore-image.component.html', templateUrl: './restore-image.component.html',
styleUrl: './restore-image.component.css' styleUrls: ['./restore-image.component.css']
}) })
export class RestoreImageComponent { export class RestoreImageComponent implements OnInit {
@Input() data: any;
disks: { diskNumber: number; partitions: Partition[] }[] = [];
availableImages: Image[] = [];
availableOgLives: string[] = [];
constructor(private http: HttpClient) {}
ngOnInit(): void {
this.initializeDisks();
this.fetchAvailableImages();
this.availableOgLives = ['LiveCD1', 'LiveCD2', 'LiveCD3'];
}
initializeDisks() {
const partitionsFromData = this.data.partitions;
const disksMap = new Map<number, Partition[]>();
partitionsFromData.forEach((partition: any) => {
if (!disksMap.has(partition.diskNumber)) {
disksMap.set(partition.diskNumber, []);
}
disksMap.get(partition.diskNumber)!.push({
diskNumber: partition.diskNumber,
partitionNumber: partition.partitionNumber
});
});
disksMap.forEach((partitions, diskNumber) => {
this.disks.push({ diskNumber, partitions });
});
}
fetchAvailableImages() {
const url = 'http://127.0.0.1:8001/images?page=1&itemsPerPage=30';
this.http.get(url).subscribe(
(response: any) => {
this.availableImages = response['hydra:member'];
},
(error) => {
console.error('Error al obtener las imágenes:', error);
}
);
}
onImageSelected(partition: Partition, event: Event) {
const selectElement = event.target as HTMLSelectElement;
partition.associatedImageId = selectElement.value;
}
onOgLiveSelected(partition: Partition, event: Event) {
const selectElement = event.target as HTMLSelectElement;
partition.associatedOgLive = selectElement.value;
}
saveAssociations() {
this.disks.forEach(disk => {
disk.partitions.forEach(partition => {
if (partition.associatedImageId || partition.associatedOgLive) {
console.log(
`Guardando para disco ${partition.diskNumber}, partición ${partition.partitionNumber}, ` +
`Imagen ID: ${partition.associatedImageId}, OgLive: ${partition.associatedOgLive}`
);
}
});
});
}
} }

View File

@ -19,7 +19,11 @@
<mat-form-field appearance="fill" class="form-field"> <mat-form-field appearance="fill" class="form-field">
<mat-label>Perfil de software</mat-label> <mat-label>Perfil de software</mat-label>
<input matInput [(ngModel)]="imagePayload.softwareProfile" name="softwareProfile"> <mat-select [(ngModel)]="imagePayload.softwareProfile" name="softwareProfile">
<mat-option *ngFor="let profile of softwareProfiles" [value]="profile['@id']">
{{ profile.description }}
</mat-option>
</mat-select>
</mat-form-field> </mat-form-field>
</form> </form>
</mat-dialog-content> </mat-dialog-content>

View File

@ -1,16 +1,30 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog'; import { MatDialogRef } from '@angular/material/dialog';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
interface ImagePayload {
[key: string]: any;
name: string | null;
description: string | null;
comments: string | null;
type: string | null;
path: string | null;
revision: string | null;
info: string | null;
size: number | null;
client: string | null;
softwareProfile: string | null;
}
@Component({ @Component({
selector: 'app-create-image', selector: 'app-create-image',
templateUrl: './create-image.component.html', templateUrl: './create-image.component.html',
styleUrls: ['./create-image.component.css'] styleUrls: ['./create-image.component.css']
}) })
export class CreateImageComponent { export class CreateImageComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
imagePayload = { imagePayload: ImagePayload = {
name: null, name: null,
description: null, description: null,
comments: null, comments: null,
@ -23,21 +37,44 @@ export class CreateImageComponent {
softwareProfile: null softwareProfile: null
}; };
softwareProfiles: any[] = [];
constructor( constructor(
public dialogRef: MatDialogRef<CreateImageComponent>, public dialogRef: MatDialogRef<CreateImageComponent>,
private http: HttpClient, private http: HttpClient,
private toastService: ToastrService private toastService: ToastrService
) {} ) {}
saveImage(): void { ngOnInit() {
// Remover propiedades que son null antes de enviar la solicitud this.fetchSoftwareProfiles();
const payload = { ...this.imagePayload }; }
// Enviar la solicitud POST al servidor fetchSoftwareProfiles() {
const url = 'http://127.0.0.1:8001/software-profiles?page=1&itemsPerPage=30';
this.http.get(url).subscribe({
next: (response: any) => {
this.softwareProfiles = response['hydra:member'];
},
error: (error) => {
console.error('Error al obtener los perfiles de software:', error);
this.toastService.error('Error al obtener los perfiles de software');
}
});
}
saveImage(): void {
const payload = { ...this.imagePayload };
Object.keys(payload).forEach(key => {
if (payload[key] == null) {
delete payload[key];
}
});
console.log('Payload:', payload);
this.http.post(`${this.baseUrl}/images`, payload).subscribe({ this.http.post(`${this.baseUrl}/images`, payload).subscribe({
next: () => { next: () => {
this.toastService.success('Imagen creada con éxito'); this.toastService.success('Imagen creada con éxito');
this.dialogRef.close(true); // Cierra el diálogo y retorna true this.dialogRef.close(true);
}, },
error: (error) => { error: (error) => {
console.error('Error al crear la imagen:', error); console.error('Error al crear la imagen:', error);
@ -47,6 +84,6 @@ export class CreateImageComponent {
} }
close(): void { close(): void {
this.dialogRef.close(); // Cierra el diálogo sin retorno this.dialogRef.close();
} }
} }