Compare commits

...

3 Commits

Author SHA1 Message Date
Manuel Aranda Rosales 4e6dbde59c Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
2025-05-23 13:28:33 +02:00
Manuel Aranda Rosales fcfcd58d93 refs #2085. RemoveCacheImage 2025-05-23 13:28:22 +02:00
Manuel Aranda Rosales 98b4d3c4f0 refs #2074. BootOs 2025-05-23 13:27:43 +02:00
10 changed files with 964 additions and 14 deletions

View File

@ -151,6 +151,8 @@ import { CreateTaskScriptComponent } from './components/commands/commands-task/c
import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
@ -259,7 +261,9 @@ registerLocaleData(localeEs, 'es-ES');
CreateTaskScriptComponent,
ViewParametersModalComponent,
OutputDialogComponent,
ClientTaskLogsComponent
ClientTaskLogsComponent,
BootSoPartitionComponent,
RemoveCacheImageComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -0,0 +1,117 @@
.dialog-content {
display: flex;
flex-direction: column;
padding: 40px;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.select-container {
margin-top: 20px;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
.button-row {
display: flex;
padding-right: 1em;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.client-item {
position: relative;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.client-details {
margin-top: 4px;
}
.client-name {
font-size: 0.9em;
font-weight: 600;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
}
.client-ip {
display: block;
font-size: 0.9em;
color: #666;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
@media (max-width: 600px) {
.form-field {
width: 100%;
}
.dialog-actions {
flex-direction: column;
align-items: stretch;
}
button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
}

View File

@ -0,0 +1,100 @@
<h2 mat-dialog-title> Seleccionar particion para arrancar SO</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<div *ngIf="!loading" class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description>
Listado de clientes para arrancar un SO
<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="button-row">
<button class="action-button" (click)="toggleSelectAll()">
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
</button>
</div>
<div class="clients-grid">
<div *ngFor="let client of data.clients" class="client-item">
<div class="client-card"
(click)="client.status === 'og-live' && toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
<img
[src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon"
class="client-image" />
<div class="client-details">
<span class="client-name">{{ client.name | slice:0:20 }}</span>
<span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span>
</div>
<mat-divider></mat-divider>
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
<mat-radio-button [value]="client"
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar partición</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
[disabled]="!row.operativeSystem"
>
<mat-radio-button [value]="row">
</mat-radio-button>
</mat-radio-group>
</td>
</ng-container>
<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 !== 'size'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<div style="display: flex; flex-direction: column;">
<span> {{ image.size }} MB</span>
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
</div>

View File

@ -0,0 +1,78 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BootSoPartitionComponent } from './boot-so-partition.component';
import {ExecuteCommandComponent} from "../execute-command.component";
import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatButtonModule} from "@angular/material/button";
import {MatMenuModule} from "@angular/material/menu";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatTableModule} from "@angular/material/table";
import {MatSelectModule} from "@angular/material/select";
import {MatIconModule} from "@angular/material/icon";
import {ToastrModule, ToastrService} from "ngx-toastr";
import {TranslateModule} from "@ngx-translate/core";
import {DataService} from "../../data.service";
import {provideHttpClient} from "@angular/common/http";
import {provideHttpClientTesting} from "@angular/common/http/testing";
import {ConfigService} from "@services/config.service";
describe('BootSoPartitionComponent', () => {
let component: BootSoPartitionComponent;
let fixture: ComponentFixture<BootSoPartitionComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [BootSoPartitionComponent],
imports: [
ReactiveFormsModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatMenuModule,
BrowserAnimationsModule,
MatTableModule,
MatSelectModule,
MatIconModule,
ToastrModule.forRoot(),
TranslateModule.forRoot()
],
providers: [
FormBuilder,
ToastrService,
DataService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
},
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(BootSoPartitionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,154 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {MatTableDataSource} from "@angular/material/table";
import {ConfigService} from "@services/config.service";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-boot-so-partition',
templateUrl: './boot-so-partition.component.html',
styleUrl: './boot-so-partition.component.css'
})
export class BootSoPartitionComponent implements OnInit{
baseUrl: string;
selectedPartition: any = null;
dataSource = new MatTableDataSource<any>();
clientId: string | null = null;
selectedClients: any[] = [];
selectedModelClient: any = null;
filteredPartitions: any[] = [];
allSelected: boolean = false;
clientData: any[] = [];
loading: boolean = false;
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
cell: (partition: any) => partition.operativeSystem?.name
}
];
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
constructor(
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
private dialogRef: MatDialogRef<BootSoPartitionComponent>,
private configService: ConfigService,
private http: HttpClient,
private toastService: ToastrService,
) {
this.baseUrl = this.configService.apiUrl;
this.clientId = this.data.clients?.length ? this.data.clients[0]['@id'] : null;
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.selectedClients = this.data.clients.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedModelClient = this.data.clients.find(
(client: { status: string }) => client.status === 'og-live'
) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);
}
}
ngOnInit() {
}
loadPartitions(client: any) {
const url = `${this.baseUrl}${client.uuid}`;
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.dataSource.data = response.partitions.filter((partition: any) => {
return partition.partitionNumber !== 0;
});
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
}
);
}
toggleClientSelection(client: any) {
client.selected = !client.selected;
this.updateSelectedClients();
}
updateSelectedClients() {
this.selectedClients = this.data.clients.filter(
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
);
if (!this.selectedClients.includes(this.selectedModelClient)) {
this.selectedModelClient = null;
this.filteredPartitions = [];
}
}
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
}
close() {
this.dialogRef.close();
}
execute(): void {
this.loading = true;
this.http.post(`${this.baseUrl}/clients/server/boot-client`, {
clients: this.selectedClients.map((client: any) => client.uuid),
partition: this.selectedPartition['@id']
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
this.dialogRef.close();
this.loading = false;
},
error => {
this.toastService.error(error.error['hydra:description']);
this.loading = false;
}
);
}
}

View File

@ -3,6 +3,9 @@ import { HttpClient } from '@angular/common/http';
import { Router } from "@angular/router";
import { ToastrService } from "ngx-toastr";
import { ConfigService } from '@services/config.service';
import {BootSoPartitionComponent} from "./boot-so-partition/boot-so-partition.component";
import {MatDialog} from "@angular/material/dialog";
import {RemoveCacheImageComponent} from "./remove-cache-image/remove-cache-image.component";
@Component({
selector: 'app-execute-command',
@ -27,7 +30,7 @@ export class ExecuteCommandComponent implements OnInit {
{ translationKey: 'executeCommands.login', slug: 'login', disabled: true },
{ translationKey: 'executeCommands.createImage', slug: 'create-image', disabled: false },
{ translationKey: 'executeCommands.deployImage', slug: 'deploy-image', disabled: false },
{ translationKey: 'executeCommands.deleteImageCache', slug: 'delete-image-cache', disabled: true },
{ translationKey: 'executeCommands.deleteImageCache', slug: 'remove-cache-image', disabled: false },
{ translationKey: 'executeCommands.partition', slug: 'partition', disabled: false },
{ translationKey: 'executeCommands.softwareInventory', slug: 'software-inventory', disabled: true },
{ translationKey: 'executeCommands.hardwareInventory', slug: 'hardware-inventory', disabled: true },
@ -40,7 +43,8 @@ export class ExecuteCommandComponent implements OnInit {
private http: HttpClient,
private router: Router,
private configService: ConfigService,
private toastService: ToastrService
private toastService: ToastrService,
private dialog: MatDialog,
) {
this.baseUrl = this.configService.apiUrl;
}
@ -74,13 +78,13 @@ export class ExecuteCommandComponent implements OnInit {
if (states[0] === 'off' || states[0] === 'disconnected') {
command.disabled = command.slug !== 'power-on';
} else {
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'partition', 'run-script'].includes(command.slug);
command.disabled = !['power-off', 'reboot', 'login', 'create-image', 'deploy-image', 'remove-cache-image', 'partition', 'run-script'].includes(command.slug);
}
} else {
if (command.slug === 'create-image') {
command.disabled = multipleClients;
} else if (
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'run-script'].includes(command.slug)
['power-on', 'power-off', 'reboot', 'login', 'deploy-image', 'partition', 'remove-cache-image', 'run-script'].includes(command.slug)
) {
command.disabled = false;
} else {
@ -123,6 +127,10 @@ export class ExecuteCommandComponent implements OnInit {
if (action === 'power-on') {
this.powerOnClient();
}
if (action === 'remove-cache-image') {
this.removeImageCache();
}
}
rebootClient(): void {
@ -139,16 +147,51 @@ export class ExecuteCommandComponent implements OnInit {
}
loginClient(): void {
this.http.post(`${this.baseUrl}/clients/server/login-client`, {
clients: this.clientData.map((client: any) => client['@id'])
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
},
error => {
this.toastService.error('Error de conexión con el cliente');
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/' + client.uuid,
status: client.status,
partitions: client.partitions,
firmwareType: client.firmwareType,
ip: client.ip
}));
const dialogRef = this.dialog.open(BootSoPartitionComponent, {
width: '70vw',
height: 'auto',
data: { clients: clientDataToSend }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Petición de arranque de SO enviada correctamente');
}
);
});
}
removeImageCache(): void {
const clientDataToSend = this.clientData.map(client => ({
name: client.name,
mac: client.mac,
uuid: '/clients/' + client.uuid,
status: client.status,
partitions: client.partitions,
firmwareType: client.firmwareType,
ip: client.ip
}));
const dialogRef = this.dialog.open(RemoveCacheImageComponent, {
width: '70vw',
height: 'auto',
data: { clients: clientDataToSend }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.toastService.success('Petición de borrado de caché de imagen enviada correctamente');
}
});
}
powerOnClient(): void {

View File

@ -0,0 +1,117 @@
.dialog-content {
display: flex;
flex-direction: column;
padding: 40px;
}
.action-container {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
}
.select-container {
margin-top: 20px;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.client-card {
background: #ffffff;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
overflow: hidden;
position: relative;
padding: 8px;
text-align: center;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
&:hover {
background-color: #f0f0f0;
transform: scale(1.02);
}
}
.button-row {
display: flex;
padding-right: 1em;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.client-item {
position: relative;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.loading-spinner {
display: block;
margin: 0 auto;
align-items: center;
justify-content: center;
}
.client-details {
margin-top: 4px;
}
.client-name {
font-size: 0.9em;
font-weight: 600;
color: #333;
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
display: inline-block;
}
.client-ip {
display: block;
font-size: 0.9em;
color: #666;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
@media (max-width: 600px) {
.form-field {
width: 100%;
}
.dialog-actions {
flex-direction: column;
align-items: stretch;
}
button {
width: 100%;
margin-left: 0;
margin-bottom: 8px;
}
}

View File

@ -0,0 +1,105 @@
<h2 mat-dialog-title> Seleccionar imagen para eliminar de la cache</h2>
<mat-dialog-content class="dialog-content">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<div *ngIf="!loading" class="select-container">
<mat-expansion-panel>
<mat-expansion-panel-header>
<mat-panel-title> Clientes </mat-panel-title>
<mat-panel-description>
Listado de clientes para arrancar un SO
<mat-icon>desktop_windows</mat-icon>
</mat-panel-description>
</mat-expansion-panel-header>
<div class="button-row">
<button class="action-button" (click)="toggleSelectAll()">
{{ allSelected ? 'Desmarcar todos' : 'Marcar todos' }}
</button>
</div>
<div class="clients-grid">
<div *ngFor="let client of data.clients" class="client-item">
<div class="client-card"
(click)="client.status === 'og-live' && toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected, 'disabled-client': client.status !== 'og-live'}" >
<img
[src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon"
class="client-image" />
<div class="client-details">
<span class="client-name">{{ client.name | slice:0:20 }}</span>
<span class="client-ip">{{ client.ip }}</span>
<span class="client-ip">{{ client.mac }}</span>
</div>
<mat-divider></mat-divider>
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="loadPartitions(selectedModelClient)">
<mat-radio-button [value]="client"
color="primary"
[disabled]="!client.selected"
(click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
</mat-radio-group>
</div>
</div>
</div>
</mat-expansion-panel>
</div>
<mat-divider *ngIf="!loading" style="margin-top: 20px;"></mat-divider>
<div *ngIf="!loading" class="partition-table-container">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: start">Seleccionar imagen</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[(ngModel)]="selectedPartition"
[disabled]="!row.operativeSystem"
>
<mat-radio-button [value]="row">
</mat-radio-button>
</mat-radio-group>
</td>
</ng-container>
<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 !== 'size' && column.columnDef !== 'operativeSystem'">
{{ column.cell(image) }}
</ng-container>
<ng-container *ngIf="column.columnDef === 'size'">
<div style="display: flex; flex-direction: column;">
<span> {{ image.size }} MB</span>
<span style="font-size: 0.75rem; color: gray;">{{ image.size / 1024 }} GB</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'operativeSystem'">
<div style="display: flex; flex-direction: column;">
<span> {{ image.operativeSystem?.name }} </span>
<span style="font-size: 0.75rem; color: gray;">{{ image.image?.name}} </span>
</div>
</ng-container>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</mat-dialog-content>
<div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="close()">Cancelar</button>
<button class="submit-button" (click)="execute()" [disabled]="!selectedPartition">Ejecutar</button>
</div>

View File

@ -0,0 +1,78 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { RemoveCacheImageComponent } from './remove-cache-image.component';
import {BootSoPartitionComponent} from "../boot-so-partition/boot-so-partition.component";
import {FormBuilder, FormsModule, ReactiveFormsModule} from "@angular/forms";
import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from "@angular/material/dialog";
import {MatFormFieldModule} from "@angular/material/form-field";
import {MatInputModule} from "@angular/material/input";
import {MatCheckboxModule} from "@angular/material/checkbox";
import {MatButtonModule} from "@angular/material/button";
import {MatMenuModule} from "@angular/material/menu";
import {BrowserAnimationsModule} from "@angular/platform-browser/animations";
import {MatTableModule} from "@angular/material/table";
import {MatSelectModule} from "@angular/material/select";
import {MatIconModule} from "@angular/material/icon";
import {ToastrModule, ToastrService} from "ngx-toastr";
import {TranslateModule} from "@ngx-translate/core";
import {DataService} from "../../data.service";
import {provideHttpClient} from "@angular/common/http";
import {provideHttpClientTesting} from "@angular/common/http/testing";
import {ConfigService} from "@services/config.service";
describe('RemoveCacheImageComponent', () => {
let component: RemoveCacheImageComponent;
let fixture: ComponentFixture<RemoveCacheImageComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [RemoveCacheImageComponent],
imports: [
ReactiveFormsModule,
FormsModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatCheckboxModule,
MatButtonModule,
MatMenuModule,
BrowserAnimationsModule,
MatTableModule,
MatSelectModule,
MatIconModule,
ToastrModule.forRoot(),
TranslateModule.forRoot()
],
providers: [
FormBuilder,
ToastrService,
DataService,
provideHttpClient(),
provideHttpClientTesting(),
{
provide: MatDialogRef,
useValue: {}
},
{
provide: MAT_DIALOG_DATA,
useValue: {}
},
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();
fixture = TestBed.createComponent(RemoveCacheImageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,154 @@
import {Component, Inject} from '@angular/core';
import {MatTableDataSource} from "@angular/material/table";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service";
import {HttpClient} from "@angular/common/http";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-remove-cache-image',
templateUrl: './remove-cache-image.component.html',
styleUrl: './remove-cache-image.component.css'
})
export class RemoveCacheImageComponent {
baseUrl: string;
selectedPartition: any = null;
dataSource = new MatTableDataSource<any>();
clientId: string | null = null;
selectedClients: any[] = [];
selectedModelClient: any = null;
filteredPartitions: any[] = [];
allSelected: boolean = false;
clientData: any[] = [];
loading: boolean = false;
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',
cell: (partition: any) => partition.partitionNumber
},
{
columnDef: 'size',
header: 'Tamaño',
cell: (partition: any) => `${partition.size} MB`
},
{
columnDef: 'partitionCode',
header: 'Tipo de partición',
cell: (partition: any) => partition.partitionCode
},
{
columnDef: 'filesystem',
header: 'Sistema de ficheros',
cell: (partition: any) => partition.filesystem
},
{
columnDef: 'operativeSystem',
header: 'SO',
cell: (partition: any) => partition.operativeSystem?.name
}
];
displayedColumns = ['select', ...this.columns.map(column => column.columnDef)];
constructor(
@Inject(MAT_DIALOG_DATA) public data: { clients: any },
private dialogRef: MatDialogRef<RemoveCacheImageComponent>,
private configService: ConfigService,
private http: HttpClient,
private toastService: ToastrService,
) {
this.baseUrl = this.configService.apiUrl;
this.clientId = this.data.clients?.length ? this.data.clients[0]['@id'] : null;
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.selectedClients = this.data.clients.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedModelClient = this.data.clients.find(
(client: { status: string }) => client.status === 'og-live'
) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);
}
}
ngOnInit() {
}
loadPartitions(client: any) {
const url = `${this.baseUrl}${client.uuid}`;
this.http.get(url).subscribe(
(response: any) => {
if (response.partitions) {
this.dataSource.data = response.partitions.filter((partition: any) => {
return partition.partitionNumber !== 0 && partition.image;
});
}
},
(error) => {
console.error('Error al cargar los datos del cliente:', error);
}
);
}
toggleClientSelection(client: any) {
client.selected = !client.selected;
this.updateSelectedClients();
}
updateSelectedClients() {
this.selectedClients = this.data.clients.filter(
(client: { selected: boolean; state: string }) => client.selected && client.state === "og-live"
);
if (!this.selectedClients.includes(this.selectedModelClient)) {
this.selectedModelClient = null;
this.filteredPartitions = [];
}
}
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.data.clients.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
}
close() {
this.dialogRef.close();
}
execute(): void {
this.loading = true;
this.http.post(`${this.baseUrl}/clients/server/remove-cache-image`, {
clients: this.selectedClients.map((client: any) => client.uuid),
partition: this.selectedPartition['@id']
}).subscribe(
response => {
this.toastService.success('Cliente actualizado correctamente');
this.dialogRef.close();
this.loading = false;
},
error => {
this.toastService.error(error.error['hydra:description']);
this.loading = false;
}
);
}
}