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