refs #2074. BootOs
							parent
							
								
									b55f15f16b
								
							
						
					
					
						commit
						98b4d3c4f0
					
				|  | @ -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; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | @ -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> | ||||||
|  | @ -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(); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
|  | @ -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; | ||||||
|  |       } | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
		Loading…
	
		Reference in New Issue