Mejorada usabilidad de la funcion updateCache para que no haga falta elegir disco y particion para lanzar el comando. Mejorada usabilidad de la funcion updateCache para que no haga falta elegir disco y particion para lanzar el comando.
testing/ogGui-multibranch/pipeline/pr-main There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/66/head
Juan Manuel Bardallo 2026-04-13 15:12:01 +02:00
parent 2ec541732e
commit c0916c0587
6 changed files with 201 additions and 116 deletions

View File

@ -1,5 +1,10 @@
# Changelog
## [1.3.3] - 2026-04-13
### Improved
- Mejorada la visualizacion del selector de particiones mostrando el contenido de cache y la imagen asociada a la partición.
- Mejorada usabilidad de la funcion updateCache para que no haga falta elegir disco y particion para lanzar el comando.
## [1.3.2] - 2026-04-13
### Added
- Añadida opción para restaurar sólo cache o cache y particion.

View File

@ -302,36 +302,57 @@
</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">
<mat-card-title>
Instrucciones generadas
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
<mat-icon>close</mat-icon>
</button>
</mat-card-title>
<mat-card-content>
<pre>{{ ogInstructions }}</pre>
</mat-card-content>
</mat-card>
<!-- Sección: Selección de partición (oculta en modo solo-caché) -->
@if (deployMode !== 'update-cache') {
<div class="form-section" id="partition-selection">
<div class="form-section-title">
<mat-icon>storage</mat-icon>
Selección de partición
</div>
<app-disk-partition-selector
[(selectedPartition)]="selectedPartition"
[partitions]="filteredPartitions"
[columns]="columns"
[displayedColumns]="displayedColumns">
</app-disk-partition-selector>
<div class="partition-table-container">
@if (showInstructions) {
<mat-card class="instructions-card">
<mat-card-title>
Instrucciones generadas
<button mat-icon-button (click)="showInstructions = false" style="float: right;">
<mat-icon>close</mat-icon>
</button>
</mat-card-title>
<mat-card-content>
<pre>{{ ogInstructions }}</pre>
</mat-card-content>
</mat-card>
}
<app-disk-partition-selector
[(selectedPartition)]="selectedPartition"
[partitions]="filteredPartitions"
[columns]="columns"
[displayedColumns]="displayedColumns">
</app-disk-partition-selector>
</div>
</div>
</div>
}
<!-- En modo solo-caché: tabla informativa de particiones (sin selección) -->
@if (deployMode === 'update-cache') {
<div class="form-section" id="partition-info">
<div class="form-section-title">
<mat-icon>info</mat-icon>
Estado actual de particiones
</div>
<div class="partition-table-container">
<app-disk-partition-selector
[(selectedPartition)]="selectedPartition"
[partitions]="filteredPartitions"
[columns]="columns"
[displayedColumns]="displayedColumns"
[readonly]="true">
</app-disk-partition-selector>
</div>
</div>
}
</div>
<app-scroll-to-top [threshold]="100" targetElement=".header-container" position="bottom-right" [showTooltip]="true"

View File

@ -1,20 +1,20 @@
import {Component, EventEmitter, OnInit, Output} from '@angular/core';
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import { MatTableDataSource } from "@angular/material/table";
import { SelectionModel } from "@angular/cdk/collections";
import { HttpClient } from "@angular/common/http";
import { ToastrService } from "ngx-toastr";
import { ActivatedRoute, Router } from "@angular/router";
import { ConfigService } from '@services/config.service';
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
import {MatDialog} from "@angular/material/dialog";
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
import { CreateTaskComponent } from "../../../../commands/commands-task/create-task/create-task.component";
import { MatDialog } from "@angular/material/dialog";
import { QueueConfirmationModalComponent } from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
@Component({
selector: 'app-deploy-image',
templateUrl: './deploy-image.component.html',
styleUrl: './deploy-image.component.css'
})
export class DeployImageComponent implements OnInit{
export class DeployImageComponent implements OnInit {
baseUrl: string;
@Output() dataChange = new EventEmitter<any>();
@ -58,7 +58,7 @@ export class DeployImageComponent implements OnInit{
selectedClients: any[] = [];
selectedModelClient: any = null;
filteredPartitions: any[] = [];
selectedRepository: any = null;
imageType: string = 'monolithic';
@ -127,9 +127,9 @@ export class DeployImageComponent implements OnInit{
}
});
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null;
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = true; });
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string }) => client.selected);
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string }) => client.selected) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);
@ -202,7 +202,7 @@ export class DeployImageComponent implements OnInit{
}
return client.partitions
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024 }GB`)
.map((p: { partitionNumber: any; size: any; filesystem: any }) => `#${p.partitionNumber} ${p.filesystem} - ${p.size / 1024}GB`)
.join('\n');
}
@ -273,17 +273,18 @@ export class DeployImageComponent implements OnInit{
}
}
this.errorMessage = "";
if (this.selectedMethod) {
setTimeout(() => {
this.scrollToPartitionSection();
}, 300);
}
return true;
}
save(): void {
const needsPartition = this.deployMode !== 'update-cache';
if (!this.selectedClients.length) {
this.toastService.error('Debe seleccionar al menos un cliente');
@ -300,7 +301,7 @@ export class DeployImageComponent implements OnInit{
return;
}
if (!this.selectedPartition) {
if (needsPartition && !this.selectedPartition) {
this.toastService.error('Debe seleccionar una partición');
return;
}
@ -322,15 +323,15 @@ export class DeployImageComponent implements OnInit{
dialogRef.afterClosed().subscribe(result => {
if (result !== undefined) {
this.loading = true;
let payload: any;
let url: string;
if (this.imageType === 'git') {
payload = {
type: 'git',
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
diskNumber: this.selectedPartition?.diskNumber || 1,
partitionNumber: this.selectedPartition?.partitionNumber || 1,
repositoryName: this.selectedGitRepository,
branch: this.selectedBranch,
hexsha: this.selectedCommit.hexsha,
@ -342,8 +343,8 @@ export class DeployImageComponent implements OnInit{
} else {
payload = {
method: this.selectedMethod,
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
diskNumber: this.selectedPartition?.diskNumber || 1,
partitionNumber: this.selectedPartition?.partitionNumber || 1,
p2pMode: this.p2pMode,
p2pTime: this.p2pMode === 'seeder' ? this.p2pTime : 0,
mcastIp: this.mcastIp,
@ -386,7 +387,8 @@ export class DeployImageComponent implements OnInit{
}
isFormValid(): boolean {
if (!this.allSelected || !this.selectedModelClient || !this.selectedPartition) {
const needsPartition = this.deployMode !== 'update-cache';
if (!this.allSelected || !this.selectedModelClient || (needsPartition && !this.selectedPartition)) {
return false;
}
@ -422,21 +424,21 @@ export class DeployImageComponent implements OnInit{
isDirectMethod(): boolean {
return this.selectedMethod === 'unicast-direct' ||
this.selectedMethod === 'udpcast-direct';
this.selectedMethod === 'udpcast-direct';
}
openScheduleModal(): void {
let scope = this.runScriptContext?.type || 'clients';
let selectedClients = null;
if (this.selectedClients.length === 0 && this.clientData.length > 0) {
this.updateSelectedClients();
}
const isOrganizationalContext = this.runScriptContext?.type &&
const isOrganizationalContext = this.runScriptContext?.type &&
['organizational-unit', 'classroom', 'classrooms-group', 'clients-group'].includes(this.runScriptContext.type);
if (!isOrganizationalContext && this.selectedClients && this.selectedClients.length > 0) {
scope = 'clients';
selectedClients = this.selectedClients;
@ -449,7 +451,7 @@ export class DeployImageComponent implements OnInit{
width: '800px',
data: {
scope: scope,
selectedClients: selectedClients,
selectedClients: selectedClients,
organizationalUnit: this.runScriptContext?.['@id'],
source: 'assistant',
runScriptContext: this.runScriptContext
@ -458,10 +460,11 @@ export class DeployImageComponent implements OnInit{
dialogRef.afterClosed().subscribe((result: { [x: string]: any; }) => {
if (result) {
// Si es un updateCache no hay que indicar disco ni partición, se envían a 1 ambas
const basePayload: any = {
method: this.selectedMethod,
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
diskNumber: this.selectedPartition?.diskNumber || 1,
partitionNumber: this.selectedPartition?.partitionNumber || 1,
imageName: this.selectedImage.name,
imageUuid: this.selectedImage.uuid,
type: this.imageType,
@ -519,27 +522,27 @@ export class DeployImageComponent implements OnInit{
generateOgInstructions() {
this.showInstructions = true;
this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition.partitionNumber} --method ${this.selectedMethod}`;
this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition?.partitionNumber || 1} --method ${this.selectedMethod}`;
}
scrollToPartitionSection() {
const partitionSection = document.getElementById('partition-selection');
scrollToPartitionSection() {
const partitionSection = document.getElementById('partition-selection');
if (partitionSection) {
partitionSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
partitionSection.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
console.log('Scroll ejecutado');
}
}
scrollToAdvancedOptions() {
scrollToAdvancedOptions() {
const advancedOptions = document.getElementById('advanced-options');
if (advancedOptions) {
advancedOptions.scrollIntoView({
behavior: 'smooth',
block: 'start'
advancedOptions.scrollIntoView({
behavior: 'smooth',
block: 'start'
});
console.log('Scroll hacia opciones avanzadas ejecutado');
}
@ -621,7 +624,7 @@ export class DeployImageComponent implements OnInit{
this.loadingCommits = false;
this.filterCommits();
if (this.filteredCommits.length > 0) {
this.selectedCommit = this.filteredCommits.reduce((a, b) =>
this.selectedCommit = this.filteredCommits.reduce((a, b) =>
new Date(a.committed_date * 1000) > new Date(b.committed_date * 1000) ? a : b
);
}
@ -635,7 +638,7 @@ export class DeployImageComponent implements OnInit{
filterCommits() {
if (this.showOnlyTagged) {
this.filteredCommits = this.commits.filter(commit =>
this.filteredCommits = this.commits.filter(commit =>
commit.tags && commit.tags.length > 0
);
} else {
@ -646,7 +649,7 @@ export class DeployImageComponent implements OnInit{
onTagFilterChange() {
this.filterCommits();
if (this.filteredCommits.length > 0 && !this.filteredCommits.includes(this.selectedCommit)) {
this.selectedCommit = this.filteredCommits.reduce((a, b) =>
this.selectedCommit = this.filteredCommits.reduce((a, b) =>
new Date(a.committed_date * 1000) > new Date(b.committed_date * 1000) ? a : b
);
}

View File

@ -49,3 +49,32 @@
box-shadow: none !important;
border: none !important;
}
/* OS Column Classes */
.os-col-wrapper {
display: flex;
flex-direction: column;
}
.cache-title {
font-weight: bold;
font-size: 0.8rem;
color: #666;
}
.cache-item {
font-size: 0.75rem;
color: #666;
}
.image-name {
font-size: 0.75rem;
color: gray;
}
.image-date {
font-size: 0.75rem;
color: #555;
font-style: italic;
margin-top: 2px;
}

View File

@ -1,53 +1,71 @@
<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>
<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>
@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>
}
<div class="disk-card-layout">
<div class="disk-table-container">
<table mat-table [dataSource]="disk.partitions" class="disk-table">
<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>
<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]="readonly || 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 if (column.columnDef === 'operativeSystem') {
<div class="os-col-wrapper">
@if (partition.filesystem === 'CACHE' && partition.cacheContent) {
<span class="cache-title">Contenido:</span>
@for (item of parseCacheContent(partition.cacheContent); track item.name) {
<div class="cache-item">
• {{ item.name }} ({{ item.size }})
</div>
}
}
@if (partition.filesystem !== 'CACHE' || !partition.cacheContent) {
<span>{{partition.operativeSystem?.name }}</span>
<span class="image-name">{{ partition.image?.name }}</span>
@if (partition.deployDate) {
<span class="image-date">Desplegada: {{ partition.deployDate | date:'dd/MM/yyyy HH:mm'
}}</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)="!readonly && onPartitionSelected(row)"
[ngStyle]="{'cursor': (readonly || disableCondition(row)) ? 'default' : 'pointer', 'opacity': (readonly || disableCondition(row)) ? '0.5' : '1'}">
</tr>
</table>
</div>
</mat-card>
</div>
</div>
</mat-card>
</div>
}
</div>
</div>

View File

@ -18,7 +18,8 @@ export class DiskPartitionSelectorComponent implements OnChanges {
@Input() displayedColumns: string[] = [];
@Input() selectedPartition: any;
@Input() disableCondition: (row: any) => boolean = () => false;
@Input() readonly: boolean = false;
@Output() selectedPartitionChange = new EventEmitter<any>();
diskUsageData: any[] = [];
@ -55,4 +56,12 @@ export class DiskPartitionSelectorComponent implements OnChanges {
this.selectedPartitionChange.emit(this.selectedPartition);
}
}
parseCacheContent(content: string): { name: string, size: string }[] {
if (!content) return [];
return content.split(',').map(item => {
const [name, bytes] = item.split(':');
const sizeMB = (parseInt(bytes) / (1024 * 1024)).toFixed(2) + ' MB';
return { name, size: sizeMB };
});
}
}