Rediseño del componente que muestra las particiones para crear imagen y restaurar imagen #61

Merged
jmbardallo merged 2 commits from partitions-view into main 2026-03-31 12:18:27 +02:00
10 changed files with 282 additions and 172 deletions

View File

@ -1,4 +1,8 @@
# Changelog
## [1.2.7] - 2026-03-31
### Added
- Rediseño de la plantilla para seleccion de particiones, se ha creado un componente compartido por la vista de crear imagen y deploy de imagenes.
## [1.2.6] - 2026-03-30
### Added
- Rediseño de la pantalla detalles de un equipo, en la partición CACHE, se muestra el listado de ficheros que contiene con el tamaño de cada uno.

View File

@ -172,6 +172,7 @@ import { HardwareProfileComponent } from './components/hardware-profile/hardware
import { HardwareCollectionModalComponent } from './components/hardware-profile/hardware-collection-modal/hardware-collection-modal.component';
import { HardwareTypeComponent } from './components/hardware-type/hardware-type.component';
import { ShowDetailsComponent } from './components/hardware-type/show-details/show-details.component';
import { DiskPartitionSelectorComponent } from './components/groups/shared/disk-partition-selector/disk-partition-selector.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -348,7 +349,7 @@ registerLocaleData(localeEs, 'es-ES');
progressAnimation: 'increasing',
closeButton: true
}
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger, NgOptimizedImage, DiskPartitionSelectorComponent
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA,

View File

@ -224,39 +224,13 @@
</div>
<div class="partition-table-container">
<table mat-table [dataSource]="dataSource">
<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>
<app-disk-partition-selector
[(selectedPartition)]="selectedPartition"
[partitions]="dataSource.data"
[columns]="columns"
[displayedColumns]="displayedColumns"
[disableCondition]="isPartitionDisabled">
</app-disk-partition-selector>
</div>
</div>
</div>

View File

@ -62,12 +62,11 @@ export class CreateClientImageComponent implements OnInit{
return this.gitData && this.gitData.repo && this.gitData.branch;
}
isPartitionDisabled = (row: any): boolean => {
return !row.operativeSystem;
};
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => partition.diskNumber
},
{
columnDef: 'partitionNumber',
header: 'Particion',

View File

@ -590,6 +590,11 @@ table {
}
}
.partition-table-container {
overflow-x: auto;
margin-top: 10px;
}
.commits-table-container {
margin-top: 20px;
}

View File

@ -16,9 +16,7 @@
</div>
</div>
<div class="button-row">
<button class="action-button" id="execute-button"
[disabled]="!isFormValid()"
(click)="save()">Ejecutar</button>
<button class="action-button" id="execute-button" [disabled]="!isFormValid()" (click)="save()">Ejecutar</button>
</div>
<div class="button-row">
@ -28,9 +26,7 @@
</div>
<div class="button-row">
<button class="action-button" color="accent"
[disabled]="!isFormValid()"
(click)="openScheduleModal()">
<button class="action-button" color="accent" [disabled]="!isFormValid()" (click)="openScheduleModal()">
Opciones de programación
</button>
</div>
@ -49,7 +45,8 @@
<div class="section-hint" (click)="$event.stopPropagation()">
<mat-icon>info</mat-icon>
<span>
Puedes marcar o desmarcar un cliente haciendo clic en el área del ordenador. Además, selecciona con "Modelo" el equipo que actuará como modelo.
Puedes marcar o desmarcar un cliente haciendo clic en el área del ordenador. Además, selecciona con "Modelo" el
equipo que actuará como modelo.
</span>
</div>
@ -61,17 +58,11 @@
<div class="clients-grid">
<div *ngFor="let client of clientData" class="client-item">
<div class="client-card"
(click)="toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected}"
[matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above"
matTooltipClass="custom-tooltip">
<div class="client-card" (click)="toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected}" [matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above" matTooltipClass="custom-tooltip">
<img
[src]="'assets/images/computer_' + client.status + '.svg'"
alt="Client Icon"
class="client-image" />
<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>
@ -82,9 +73,7 @@
<div class="model-selector" (click)="$event.stopPropagation()">
<mat-radio-group [(ngModel)]="selectedModelClient" (change)="onModelSelected(selectedModelClient)">
<mat-radio-button [value]="client"
color="primary"
(click)="$event.stopPropagation()">
<mat-radio-button [value]="client" color="primary" (click)="$event.stopPropagation()">
Modelo
</mat-radio-button>
</mat-radio-group>
@ -104,7 +93,7 @@
<mat-icon>image</mat-icon>
Configuración de imagen
</div>
<div class="selector">
<mat-form-field appearance="fill" class="half-width">
<mat-label>Tipo de imagen</mat-label>
@ -119,7 +108,8 @@
<div *ngIf="imageType === 'git'" class="filters-row git-gap">
<mat-form-field appearance="fill" style="width: 300px;">
<mat-label>Seleccionar Repositorio</mat-label>
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositoryChange($event.value)" [disabled]="loadingRepositories">
<mat-select [(ngModel)]="selectedGitRepository" (selectionChange)="onGitRepositoryChange($event.value)"
[disabled]="loadingRepositories">
<mat-option *ngFor="let repo of repositories" [value]="repo">
{{ repo }}
</mat-option>
@ -129,7 +119,8 @@
<mat-form-field appearance="fill" style="width: 300px;">
<mat-label>Seleccionar Rama</mat-label>
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()" [disabled]="loadingBranches || !selectedRepository">
<mat-select [(ngModel)]="selectedBranch" (selectionChange)="onGitBranchChange()"
[disabled]="loadingBranches || !selectedRepository">
<mat-option *ngFor="let branch of branches" [value]="branch">
{{ branch }}
</mat-option>
@ -176,8 +167,8 @@
<th mat-header-cell *matHeaderCellDef>Tags</th>
<td mat-cell *matCellDef="let commit">
<mat-chip-list>
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected class="custom-tag-chip"
style="background: rgba(102, 126, 234, 0.1) !important; color: #667eea !important; border: 1px solid rgba(102, 126, 234, 0.3) !important; border-radius: 8px !important; font-weight: 500 !important; font-size: 10px !important; padding: 2px 6px !important; margin: 1px !important; box-shadow: none !important;">
<mat-chip *ngFor="let tag of commit.tags" color="primary" selected class="custom-tag-chip"
style="background: rgba(102, 126, 234, 0.1) !important; color: #667eea !important; border: 1px solid rgba(102, 126, 234, 0.3) !important; border-radius: 8px !important; font-weight: 500 !important; font-size: 10px !important; padding: 2px 6px !important; margin: 1px !important; box-shadow: none !important;">
{{ tag }}
</mat-chip>
<span *ngIf="!commit.tags || commit.tags.length === 0" class="no-tags-text">Sin tags</span>
@ -198,7 +189,7 @@
</mat-form-field>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Seleccione imagen</mat-label>
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled] = "!imageType" >
<mat-select [(ngModel)]="selectedImage" name="selectedImage" [disabled]="!imageType">
<mat-option *ngFor="let image of images" [value]="image">
<div class="unit-name"> {{ image.name }}</div>
<div style="font-size: smaller; color: gray;">{{ image.description }}</div>
@ -212,13 +203,82 @@
</div>
</div>
<!-- Sección: Opciones avanzadas -->
<div class="form-section" id="advanced-options"
*ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct') || isMethod('p2p')">
<div class="form-section-title">
<mat-icon>settings</mat-icon>
<span *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">Opciones multicast</span>
<span *ngIf="isMethod('p2p')">Opciones torrent</span>
</div>
<div class="input-group" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
<mat-form-field appearance="fill" class="input-field">
<mat-label>Puerto</mat-label>
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Dirección</mat-label>
<input matInput [(ngModel)]="mcastIp" name="mcastIp"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
<mat-select [(ngModel)]="mcastMode" name="mcastMode"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Velocidad</mat-label>
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Máximo Clientes</mat-label>
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Tiempo Máximo de Espera</mat-label>
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
</div>
<div class="input-group" *ngIf="isMethod('p2p')">
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
<mat-select [(ngModel)]="p2pMode" name="p2pMode" [required]="isMethod('p2p')">
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="input-field" *ngIf="p2pMode === 'seeder'">
<mat-label>Semilla (minutos)</mat-label>
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number"
[required]="isMethod('p2p') && p2pMode === 'seeder'">
</mat-form-field>
</div>
</div>
<!-- Sección: Selección de partición -->
<div class="form-section" id="partition-selection">
<div class="form-section-title">
<mat-icon>storage</mat-icon>
Selección de partición
</div>
<div class="partition-table-container">
<div *ngIf="showInstructions" class="instructions-box">
<mat-card class="instructions-card">
@ -234,107 +294,16 @@
</mat-card>
</div>
<table mat-table [dataSource]="filteredPartitions">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar partición</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group [(ngModel)]="selectedPartition" name="selectedPartition">
<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">
{{ column.cell(image) }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</div>
<!-- Sección: Opciones avanzadas -->
<div class="form-section" id="advanced-options" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct') || isMethod('p2p')">
<div class="form-section-title">
<mat-icon>settings</mat-icon>
<span *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">Opciones multicast</span>
<span *ngIf="isMethod('p2p')">Opciones torrent</span>
</div>
<div class="input-group" *ngIf="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
<mat-form-field appearance="fill" class="input-field">
<mat-label>Puerto</mat-label>
<input matInput [(ngModel)]="mcastPort" name="mcastPort" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Dirección</mat-label>
<input matInput [(ngModel)]="mcastIp" name="mcastIp"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@mcastModeLabel">Modo Multicast</mat-label>
<mat-select [(ngModel)]="mcastMode" name="mcastMode"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
<mat-option *ngFor="let option of multicastModeOptions" [value]="option.value">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Velocidad</mat-label>
<input matInput [(ngModel)]="mcastSpeed" name="mcastSpeed" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Máximo Clientes</mat-label>
<input matInput [(ngModel)]="mcastMaxClients" name="mcastMaxClients" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
<mat-form-field appearance="fill" class="input-field">
<mat-label>Tiempo Máximo de Espera</mat-label>
<input matInput [(ngModel)]="mcastMaxTime" name="mcastMaxTime" type="number"
[required]="isMethod('udpcast') || isMethod('uftp') || isMethod('udpcast-direct')">
</mat-form-field>
</div>
<div class="input-group" *ngIf="isMethod('p2p')">
<mat-form-field appearance="fill" class="input-field">
<mat-label i18n="@@p2pModeLabel">Modo P2P</mat-label>
<mat-select [(ngModel)]="p2pMode" name="p2pMode"
[required]="isMethod('p2p')">
<mat-option *ngFor="let option of p2pModeOptions" [value]="option.value">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="input-field" *ngIf="p2pMode === 'seeder'">
<mat-label>Semilla (minutos)</mat-label>
<input matInput [(ngModel)]="p2pTime" name="p2pTime" type="number"
[required]="isMethod('p2p') && p2pMode === 'seeder'">
</mat-form-field>
<app-disk-partition-selector
[(selectedPartition)]="selectedPartition"
[partitions]="filteredPartitions"
[columns]="columns"
[displayedColumns]="displayedColumns">
</app-disk-partition-selector>
</div>
</div>
</div>
<app-scroll-to-top
[threshold]="100"
targetElement=".header-container"
position="bottom-right"
[showTooltip]="true"
tooltipText="Volver arriba"
tooltipPosition="left">
</app-scroll-to-top>
<app-scroll-to-top [threshold]="100" targetElement=".header-container" position="bottom-right" [showTooltip]="true"
tooltipText="Volver arriba" tooltipPosition="left">
</app-scroll-to-top>

View File

@ -57,6 +57,7 @@ export class DeployImageComponent implements OnInit{
selectedClients: any[] = [];
selectedModelClient: any = null;
filteredPartitions: any[] = [];
selectedRepository: any = null;
imageType: string = 'monolithic';
@ -70,14 +71,9 @@ export class DeployImageComponent implements OnInit{
dataSource = new MatTableDataSource<any>();
columns = [
{
columnDef: 'diskNumber',
header: 'Disco',
cell: (partition: any) => `${partition.diskNumber}`
},
{
columnDef: 'partitionNumber',
header: 'Particion',
header: 'Partición',
cell: (partition: any) => `${partition.partitionNumber}`
},
{

View File

@ -0,0 +1,51 @@
/* Formatting specific to grouped partition selector */
.disk-container {
display: flex;
flex-direction: column;
gap: 32px;
padding-top: 16px;
}
.disk-card-wrapper {
margin-bottom: 20px;
}
.disk-mat-card {
border-radius: 8px;
overflow: hidden;
padding: 0;
}
.disk-card-header {
background-color: #f5f5f5;
padding: 16px 20px;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #e0e0e0;
}
.disk-card-title {
margin: 0;
font-size: 1.2rem;
color: #3f51b5;
font-weight: bold;
}
.disk-card-layout {
display: flex;
align-items: stretch;
background: white;
gap: 0;
}
.disk-table-container {
flex: 1;
overflow-x: auto;
}
.disk-table {
width: 100%;
box-shadow: none !important;
border: none !important;
}

View File

@ -0,0 +1,53 @@
<div class="disk-container">
@for (disk of diskUsageData; track disk.diskNumber) {
<div class="disk-card-wrapper">
<mat-card class="disk-mat-card mat-elevation-z4">
<div class="disk-card-header">
<h3 class="disk-card-title">Disco {{ disk.diskNumber }}</h3>
</div>
<div class="disk-card-layout">
<div class="disk-table-container">
<table mat-table [dataSource]="disk.partitions" class="disk-table">
<ng-container matColumnDef="select">
<th mat-header-cell *matHeaderCellDef style="text-align: start">Seleccionar</th>
<td mat-cell *matCellDef="let row">
<mat-radio-group
[ngModel]="selectedPartition"
(ngModelChange)="onPartitionSelected($event)"
name="selectedPartitionGroup"
[disabled]="disableCondition(row)">
<mat-radio-button [value]="row"></mat-radio-button>
</mat-radio-group>
</td>
</ng-container>
@for (column of columns; track column.columnDef) {
<ng-container [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let partition">
@if (column.columnDef === 'size') {
<div style="display: flex; flex-direction: column;">
<span>{{ partition.size }} MB</span>
<span style="font-size: 0.75rem; color: gray;">{{ partition.size / 1024 | number:'1.0-2' }} GB</span>
</div>
} @else {
{{ column.cell ? column.cell(partition) : partition[column.columnDef] }}
}
</td>
</ng-container>
}
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"
(click)="onPartitionSelected(row)"
[ngStyle]="{'cursor': disableCondition(row) ? 'default' : 'pointer', 'opacity': disableCondition(row) ? '0.5' : '1'}">
</tr>
</table>
</div>
</div>
</mat-card>
</div>
}
</div>

View File

@ -0,0 +1,58 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
import { MatTableModule } from '@angular/material/table';
import { MatRadioModule } from '@angular/material/radio';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-disk-partition-selector',
standalone: true,
imports: [CommonModule, MatCardModule, MatTableModule, MatRadioModule, FormsModule],
templateUrl: './disk-partition-selector.component.html',
styleUrl: './disk-partition-selector.component.css'
})
export class DiskPartitionSelectorComponent implements OnChanges {
@Input() partitions: any[] = [];
@Input() columns: any[] = [];
@Input() displayedColumns: string[] = [];
@Input() selectedPartition: any;
@Input() disableCondition: (row: any) => boolean = () => false;
@Output() selectedPartitionChange = new EventEmitter<any>();
diskUsageData: any[] = [];
ngOnChanges(changes: SimpleChanges): void {
if (changes['partitions']) {
this.calculateDiskUsage(this.partitions || []);
}
}
calculateDiskUsage(partitions: any[]) {
this.diskUsageData = [];
const grouped = partitions.reduce((acc: any, curr: any) => {
if (!acc[curr.diskNumber]) {
acc[curr.diskNumber] = [];
}
acc[curr.diskNumber].push(curr);
return acc;
}, {});
for (const diskNum in grouped) {
if (grouped.hasOwnProperty(diskNum)) {
this.diskUsageData.push({
diskNumber: diskNum,
partitions: grouped[diskNum]
});
}
}
}
onPartitionSelected(row: any) {
if (!this.disableCondition(row)) {
this.selectedPartition = row;
this.selectedPartitionChange.emit(this.selectedPartition);
}
}
}