refs #2339. Assistants new UX
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details

pull/28/head
Manuel Aranda Rosales 2025-06-26 15:58:56 +02:00
parent 212c4f9eec
commit ce00b92751
15 changed files with 1521 additions and 529 deletions

View File

@ -22,8 +22,8 @@
<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'}" >
(click)="toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected}" >
<img
[src]="'assets/images/computer_' + client.status + '.svg'"

View File

@ -66,19 +66,11 @@ export class BootSoPartitionComponent implements OnInit {
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.data.clients.forEach((client: { selected: boolean; status: string }) => client.selected = true);
this.selectedClients = this.data.clients.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedClients = this.data.clients.filter((client: { selected: boolean }) => client.selected);
this.selectedModelClient = this.data.clients.find(
(client: { status: string }) => client.status === 'og-live'
) || null;
this.selectedModelClient = this.data.clients.find((client: { selected: boolean }) => client.selected) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);

View File

@ -5,9 +5,15 @@
<h2>
{{ 'partitionTitle' | translate }}
</h2>
<h4>
{{ runScriptTitle }}
</h4>
<div class="destination-info">
<div class="destination-badge">
<mat-icon class="destination-icon">cloud_download</mat-icon>
<div class="destination-content">
<span class="destination-label">Destino</span>
<span class="destination-value">{{ runScriptTitle }}</span>
</div>
</div>
</div>
</div>
<div class="button-row">
<button class="action-button" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="save()">Ejecutar</button>
@ -19,9 +25,9 @@
</button>
</div>
<div>
<button mat-stroked-button color="accent" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
<mat-icon>schedule</mat-icon> Opciones de programación
<div class="button-row">
<button class="action-button" color="accent" [disabled]="data.status === 'busy' || !selectedModelClient || !allSelected || !selectedDisk || (selectedDisk.totalDiskSize - selectedDisk.used) <= 0" (click)="openScheduleModal()">
Opciones de programación
</button>
</div>
</div>
@ -46,7 +52,7 @@
<div *ngFor="let client of clientData" 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'}"
[ngClass]="{'selected-client': client.selected}"
[matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above"
matTooltipClass="custom-tooltip">
@ -77,18 +83,52 @@
</mat-expansion-panel>
</div>
<mat-divider style="margin-top: 20px;"></mat-divider>
<mat-dialog-content>
<div class="disk-select">
<mat-form-field appearance="fill">
<mat-label>Seleccionar disco</mat-label>
<mat-select [(ngModel)]="selectedDiskNumber">
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
Disco {{ disk.diskNumber }} ({{ (disk.totalDiskSize / 1024).toFixed(2) }} GB)
</mat-option>
</mat-select>
</mat-form-field>
<div class="disk-selector-card">
<div class="card-header">
<mat-icon class="card-icon">storage</mat-icon>
<div class="card-title">
<h3>Selección de Disco</h3>
<p>Elige el disco donde se realizarán las operaciones de particionado</p>
</div>
</div>
<div class="card-content">
<mat-form-field appearance="fill" class="disk-select-field">
<mat-label>Seleccionar disco</mat-label>
<mat-select [(ngModel)]="selectedDiskNumber" (selectionChange)="onDiskSelectionChange()">
<mat-option *ngFor="let disk of disks" [value]="disk.diskNumber">
<div class="disk-option">
<div class="disk-info">
<span class="disk-name">Disco {{ disk.diskNumber }}</span>
<span class="disk-size"> {{ (disk.totalDiskSize / 1024).toFixed(2) }} GB</span>
</div>
<div class="disk-details">
<span class="usage-percent"> {{ (disk.percentage || 0).toFixed(1) }}% usado</span>
</div>
</div>
</mat-option>
</mat-select>
<mat-hint>Selecciona el disco que deseas particionar</mat-hint>
</mat-form-field>
<div class="selection-info" *ngIf="selectedDisk">
<mat-icon class="info-icon">info</mat-icon>
<div class="info-text">
<span class="info-title">Disco seleccionado: {{ selectedDisk.diskNumber }}</span>
<span class="info-subtitle">Tamaño total: {{ (selectedDisk.totalDiskSize / 1024).toFixed(2) }} GB</span>
</div>
</div>
<div class="no-disks-message" *ngIf="!disks || disks.length === 0">
<mat-icon class="warning-icon">warning</mat-icon>
<div class="message-text">
<span class="message-title">No hay discos disponibles</span>
<span class="message-subtitle">Asegúrate de que el cliente modelo tenga discos configurados</span>
</div>
</div>
</div>
</div>
<div class="partition-assistant" *ngIf="selectedDisk">
@ -108,33 +148,66 @@
<div class="row-button">
<button class="action-button" [disabled]="partitionCode === 'MSDOS'" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
<mat-chip *ngIf="selectedModelClient.firmwareType">
Firmware: {{ selectedModelClient.firmwareType }}
</mat-chip>
<mat-chip color="info" *ngIf="partitionCode">
Tabla de particiones: {{ partitionCode }}
</mat-chip>
<div class="info-badge" *ngIf="selectedModelClient.firmwareType">
<mat-icon class="info-icon">memory</mat-icon>
<div class="info-content">
<span class="info-label">Firmware</span>
<span class="info-value">{{ selectedModelClient.firmwareType }}</span>
</div>
</div>
<div class="info-badge" *ngIf="partitionCode">
<mat-icon class="info-icon">storage</mat-icon>
<div class="info-content">
<span class="info-label">Tabla de particiones</span>
<span class="info-value">{{ partitionCode }}</span>
</div>
</div>
</div>
<mat-divider style="padding: 10px;"></mat-divider>
<div class="disk-space-info-container">
<div class="disk-space-info" [ngClass]="selectedDisk.used < selectedDisk.totalDiskSize ? 'chip-free' : 'chip-full'">
Espacio usado: {{ selectedDisk.used | number:'1.2-2' }} MB
</div>
<div class="disk-space-info" [ngClass]="selectedDisk.used < selectedDisk.totalDiskSize ? 'chip-free' : 'chip-full'">
Espacio libre: {{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.2-2' }} MB
</div>
<div class="disk-space-info">
Espacio total: {{ selectedDisk.totalDiskSize | number:'1.2-2' }} MB
</div>
</div>
<div class="row">
<div class="form-container">
<table class="partition-table">
<div class="disk-space-info-container" id="disk-info">
<div class="disk-space-card">
<div class="space-info-item">
<mat-icon class="space-icon used-icon">storage</mat-icon>
<div class="space-details">
<span class="space-label">Espacio usado</span>
<span class="space-value">{{ selectedDisk.used | number:'1.2-2' }} MB</span>
</div>
</div>
<div class="space-info-item">
<mat-icon class="space-icon free-icon" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">cloud_done</mat-icon>
<div class="space-details">
<span class="space-label">Espacio libre</span>
<span class="space-value" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ (selectedDisk.totalDiskSize - selectedDisk.used) | number:'1.2-2' }} MB</span>
</div>
</div>
<div class="space-info-item">
<mat-icon class="space-icon total-icon">dns</mat-icon>
<div class="space-details">
<span class="space-label">Espacio total</span>
<span class="space-value">{{ selectedDisk.totalDiskSize | number:'1.2-2' }} MB</span>
</div>
</div>
</div>
<div class="disk-usage-bar">
<div class="usage-bar-container">
<div class="usage-bar-fill"
[style.width.%]="(selectedDisk.used / selectedDisk.totalDiskSize) * 100"
[ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}"></div>
</div>
<span class="usage-percentage" [ngClass]="{'warning': (selectedDisk.used / selectedDisk.totalDiskSize) >= 0.9 && (selectedDisk.used / selectedDisk.totalDiskSize) < 1, 'danger': (selectedDisk.used / selectedDisk.totalDiskSize) >= 1}">{{ ((selectedDisk.used / selectedDisk.totalDiskSize) * 100) | number:'1.1-1' }}% usado</span>
</div>
</div>
<table class="partition-table" id="partition-table">
<thead>
<tr>
<th>Partición</th>
@ -182,11 +255,29 @@
</tbody>
</table>
</div>
<div class="chart-container">
<ngx-charts-pie-chart [view]="view" [results]="selectedDisk.chartData" [doughnut]="true">
<div class="chart-container" *ngIf="selectedDisk">
<div class="chart-header">
<h3>Distribución de Particiones</h3>
</div>
<ngx-charts-pie-chart
[results]="selectedDisk.chartData"
[doughnut]="true"
[gradient]="true"
[labels]="true"
[tooltipDisabled]="false"
[animations]="true">
</ngx-charts-pie-chart>
</div>
</div>
</div>
</mat-dialog-content>
<app-scroll-to-top
[threshold]="200"
targetElement=".header-container"
position="bottom-right"
[showTooltip]="true"
tooltipText="Volver arriba"
tooltipPosition="left">
</app-scroll-to-top>

View File

@ -7,6 +7,7 @@ import { FILESYSTEM_TYPES } from '../../../../../shared/constants/filesystem-typ
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";
interface Partition {
uuid?: string;
@ -46,7 +47,7 @@ export class PartitionAssistantComponent implements OnInit{
runScriptContext: any = null;
showInstructions = false;
view: [number, number] = [400, 300];
view: [number, number] = [300, 200];
showLegend = true;
showLabels = true;
allSelected = true;
@ -74,19 +75,11 @@ export class PartitionAssistantComponent implements OnInit{
}
});
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
this.clientData.forEach((client: { selected: boolean; status: string}) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
this.selectedClients = this.clientData.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
this.selectedModelClient = this.clientData.find(
(client: { status: string }) => client.status === 'og-live'
) || null;
this.selectedModelClient = this.clientData.find((client: { selected: boolean; status: string}) => client.selected) || null;
if (this.selectedModelClient) {
this.loadPartitions(this.selectedModelClient);
@ -136,15 +129,18 @@ export class PartitionAssistantComponent implements OnInit{
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.clientData.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
}
initializeDisks() {
this.disks = [];
// Verificar que hay datos válidos
if (!this.data || !this.data.partitions || !Array.isArray(this.data.partitions)) {
console.warn('No hay datos de particiones válidos');
return;
}
const partitionsFromData = this.data.partitions;
this.originalPartitions = JSON.parse(JSON.stringify(partitionsFromData));
@ -320,56 +316,63 @@ export class PartitionAssistantComponent implements OnInit{
return;
}
this.loading = true;
const totalPartitionSize = this.selectedDisk.partitions
.filter((partition: any) => !partition.removed)
.reduce((sum: any, partition: any) => sum + partition.size, 0);
if (totalPartitionSize > this.selectedDisk.totalDiskSize) {
this.toastService.error('El tamaño total de las particiones en el disco seleccionado excede el tamaño total del disco.');
this.loading = false;
return;
}
const modifiedPartitions = this.selectedDisk.partitions.filter((partition: { removed: any; format: any; }) => !partition.removed || partition.format);
if (modifiedPartitions.length === 0) {
this.loading = false;
this.toastService.info('No hay cambios para guardar en el disco seleccionado.');
return;
}
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
diskNumber: this.selectedDisk.diskNumber,
partitionNumber: partition.partitionNumber,
memoryUsage: partition.memoryUsage,
size: partition.size,
partitionCode: partition.partitionCode,
filesystem: partition.filesystem,
uuid: partition.uuid,
removed: partition.removed || false,
format: partition.format || false,
}));
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
width: '400px',
disableClose: true,
hasBackdrop: true,
backdropClass: 'non-clickable-backdrop'
});
if (newPartitions.length > 0) {
const bulkPayload = {
partitions: newPartitions,
clients: this.selectedClients.map((client: any) => client.uuid),
};
dialogRef.afterClosed().subscribe(result => {
if (result !== undefined) {
this.loading = true;
const newPartitions = modifiedPartitions.map((partition: { partitionNumber: any; memoryUsage: any; size: any; partitionCode: any; filesystem: any; uuid: any; removed: any; format: any; }) => ({
diskNumber: this.selectedDisk.diskNumber,
partitionNumber: partition.partitionNumber,
memoryUsage: partition.memoryUsage,
size: partition.size,
partitionCode: partition.partitionCode,
filesystem: partition.filesystem,
uuid: partition.uuid,
removed: partition.removed || false,
format: partition.format || false,
}));
this.http.post(this.apiUrl, bulkPayload).subscribe(
(response) => {
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
this.loading = false;
this.router.navigate(['/commands-logs']);
},
(error) => {
this.loading = false;
this.toastService.error('Error al crear las particiones.');
}
);
}
const bulkPayload = {
partitions: newPartitions,
clients: this.selectedClients.map((client: any) => client.uuid),
queue: result
};
this.http.post(this.apiUrl, bulkPayload).subscribe(
(response) => {
this.toastService.success('Particiones creadas exitosamente para el disco seleccionado.');
this.loading = false;
this.router.navigate(['/commands-logs']);
},
(error) => {
this.loading = false;
this.toastService.error('Error al crear las particiones.');
}
);
}
});
}
@ -409,11 +412,20 @@ export class PartitionAssistantComponent implements OnInit{
generateChartData(partitions: Partition[]): any[] {
return partitions.map((partition) => ({
name: `Partición ${partition.partitionNumber}`,
value: partition.percentage,
color: partition.color
}));
const colors = [
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9',
'#F8C471', '#82E0AA', '#F1948A', '#85C1E9', '#D7BDE2'
];
return partitions
.filter(partition => !partition.removed)
.map((partition, index) => ({
name: `Partición ${partition.partitionNumber}`,
value: partition.size,
color: colors[index % colors.length],
partition: partition
}));
}
updateDiskChart(disk: any) {
@ -478,38 +490,50 @@ export class PartitionAssistantComponent implements OnInit{
}
generateInstructions(): void {
if (!this.selectedDisk || !this.selectedDisk.partitions) {
this.generatedInstructions = 'No hay particiones configuradas para generar instrucciones.';
return;
this.showInstructions = true;
this.generatedInstructions = `og-partition --disk ${this.selectedDiskNumber} --partitions ${this.selectedDisk.partitions.map((p: Partition) => `${p.partitionNumber}:${p.size}:${p.partitionCode}:${p.filesystem}:${p.format}`).join(',')}`;
}
onDiskSelected(diskNumber: number) {
this.selectedDiskNumber = diskNumber;
this.scrollToPartitionTable();
}
onDiskSelectionChange() {
if (this.selectedDiskNumber) {
this.scrollToPartitionTable();
}
}
const diskNumber = this.selectedDisk.diskNumber;
const partitionTable = this.partitionCode || 'MSDOS';
scrollToPartitionTable() {
// Pequeño delay para asegurar que el contenido se haya renderizado
setTimeout(() => {
const diskInfo = document.getElementById('disk-info');
if (diskInfo) {
diskInfo.scrollIntoView({
behavior: 'smooth',
block: 'start',
inline: 'nearest'
});
}
}, 100);
}
let instructions = `ogCreatePartitionTable ${diskNumber} ${partitionTable}\n`;
instructions += `ogEcho log session "[0] $MSG_HELP_ogCreatePartitions"\n`;
instructions += `ogEcho session "[10] $MSG_HELP_ogUnmountAll ${diskNumber}"\n`;
instructions += `ogUnmountAll ${diskNumber} 2>/dev/null\n`;
instructions += `ogUnmountCache\n`;
instructions += `ogEcho session "[30] $MSG_HELP_ogUpdatePartitionTable ${diskNumber}"\n`;
instructions += `ogDeletePartitionTable ${diskNumber}\n`;
instructions += `ogUpdatePartitionTable ${diskNumber}\n`;
this.selectedDisk.partitions.forEach((partition: { removed: any; partitionNumber: any; partitionCode: any; filesystem: any; size: any; format: any; }, index: any) => {
if (partition.removed) return;
const partNumber = partition.partitionNumber;
const partType = partition.partitionCode;
const fs = partition.filesystem;
const size = partition.size;
const shouldFormat = partition.format ? 'yes' : 'no';
instructions += `ogCreatePartition ${diskNumber} ${partNumber} ${partType} ${fs} ${size}MB ${shouldFormat}\n`;
});
instructions += `ogExecAndLog command session ogListPartitions ${diskNumber}\n`;
this.generatedInstructions = instructions;
this.showInstructions = true
scrollToExecuteButton() {
console.log('scrollToExecuteButton llamado');
const executeButton = document.getElementById('execute-button');
console.log('Botón ejecutar encontrado:', executeButton);
if (executeButton) {
executeButton.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
console.log('Scroll hacia botón ejecutar completado');
} else {
console.error('No se encontró el botón execute-button');
}
}
}

View File

@ -1,4 +1,3 @@
.divider {
margin: 20px 0;
}
@ -102,10 +101,287 @@ table {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 10px;
border-bottom: 1px solid #ddd;
padding: 24px 32px;
background: white;
border-radius: 12px;
margin-bottom: 20px;
}
.header-container-title {
flex-grow: 1;
text-align: left;
}
.header-container-title h2 {
margin: 0 0 8px 0;
color: #333;
font-weight: 600;
}
.header-container-title h4 {
margin: 0;
font-size: 16px;
opacity: 0.9;
font-weight: 400;
}
.button-row {
display: flex;
padding-right: 1em;
gap: 12px;
align-items: center;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
cursor: pointer;
}
.action-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.action-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.select-container {
background: white !important;
margin-top: 20px;
align-items: center;
padding: 20px;
box-sizing: border-box;
}
.form-section {
background: white !important;
border-radius: 16px;
padding: 20px !important;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
border: 1px solid #bbdefb;
}
.form-section-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
font-size: 20px;
font-weight: 600;
color: #2c3e50;
padding-bottom: 16px;
border-bottom: 2px solid #f8f9fa;
}
.form-section-title mat-icon {
color: #667eea;
font-size: 24px;
width: 24px;
height: 24px;
}
/* Badges y chips */
.destination-badge {
display: inline-flex;
align-items: center;
background: #e3f2fd;
color: #1565c0;
padding: 12px 16px;
border-radius: 12px;
border: 1px solid #bbdefb;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
}
.destination-icon {
font-size: 20px;
width: 20px;
height: 20px;
margin-right: 12px;
color: #1976d2;
}
.destination-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.destination-label {
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #1976d2;
line-height: 1;
}
.destination-value {
font-size: 14px;
font-weight: 600;
line-height: 1.2;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #0d47a1;
}
.info-badge {
display: inline-flex;
align-items: center;
background: #e8f5e8;
color: #2e7d32;
padding: 12px 16px;
border-radius: 12px;
border: 1px solid #c8e6c9;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
margin: 0 8px;
}
.info-badge:hover {
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.info-content {
display: flex;
flex-direction: column;
gap: 2px;
}
.info-label {
font-size: 11px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #388e3c;
line-height: 1;
}
.info-value {
font-size: 14px;
font-weight: 600;
line-height: 1.2;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #1b5e20;
}
/* Clientes y tarjetas */
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
margin-top: 20px;
}
.client-item {
position: relative;
}
.client-card {
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
overflow: hidden;
position: relative;
padding: 12px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.client-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
border-color: #667eea;
}
.client-image {
width: 32px;
height: 32px;
margin-bottom: 8px;
}
.client-details {
margin-bottom: 12px;
}
.client-name {
font-size: 12px;
font-weight: 600;
color: #2c3e50;
margin-bottom: 2px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.client-ip {
font-size: 10px;
color: #6c757d;
display: block;
margin-bottom: 1px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.selected-client {
background: linear-gradient(135deg, #8fa1f0 0%, #9b7bc8 100%);
color: white;
border-color: #667eea;
}
.selected-client .client-name,
.selected-client .client-ip {
color: white;
}
::ng-deep .mat-expansion-panel {
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
border-radius: 12px !important;
margin-bottom: 20px;
background: #f7fbff !important;
border: 1px solid #bbdefb !important;
}
::ng-deep .mat-expansion-panel-header {
padding: 20px 24px !important;
border-radius: 12px !important;
}
::ng-deep .mat-expansion-panel-header-title {
font-weight: 600 !important;
color: #2c3e50 !important;
}
::ng-deep .mat-expansion-panel-header-description {
color: #6c757d !important;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}
@ -116,117 +392,11 @@ table {
margin-bottom: 30px;
}
.clients-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 8px;
}
.client-item {
position: relative;
}
.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);
}
}
.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;
}
.header-container-title {
flex-grow: 1;
text-align: left;
padding-left: 1em;
}
.button-row {
display: flex;
padding-right: 1em;
}
.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);
}
}
::ng-deep .custom-tooltip {
white-space: pre-line !important;
max-width: 200px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px;
border-radius: 4px;
}
.selected-client {
background-color: #a0c2e5 !important;
color: white !important;
}
.button-row {
display: flex;
padding-right: 1em;
}
.disabled-client {
pointer-events: none;
opacity: 0.5;
}
.action-button {
margin-top: 10px;
margin-bottom: 10px;
}
.mat-expansion-panel-header-description {
justify-content: space-between;
align-items: center;
}
.new-command-container {
display: flex;
flex-direction: column;
@ -261,15 +431,85 @@ table {
width: 100%;
}
.script-selector-card {
margin: 20px 20px;
padding: 16px;
/* Secciones del formulario */
.form-section {
background: white !important;
border-radius: 8px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 20px;
border: 1px solid #bbdefb;
padding: 20px;
}
.form-section-title {
font-size: 18px;
font-weight: 600;
color: #333;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 8px;
}
.form-section-title mat-icon {
color: #2196f3;
}
.toggle-options {
display: flex;
justify-content: start;
margin: 16px 0;
gap: 10px;
margin-bottom: 20px;
}
.selected-toggle {
background: linear-gradient(135deg, #8fa1f0 0%, #9b7bc8 100%) !important;
color: white !important;
}
mat-spinner {
margin: 20px auto;
display: block;
}
/* Estilo para hacer el backdrop no clickeable */
::ng-deep .non-clickable-backdrop {
pointer-events: none !important;
}
::ng-deep .action-chip {
margin: 8px !important;
padding: 12px 20px !important;
border-radius: px !important;
font-weight: 500 !important;
font-size: 14px !important;
transition: all 0.3s ease !important;
border: 2px solid transparent !important;
background: white !important;
color: #6c757d !important;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1) !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
gap: 8px !important;
min-height: 48px !important;
}
::ng-deep .action-chip:hover {
transform: translateY(-2px) !important;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15) !important;
}
::ng-deep .action-chip.mat-mdc-chip-selected {
border-color: #667eea !important;
box-shadow: 0 4px 16px rgba(102, 126, 234, 0.2) !important;
}
::ng-deep .create-chip.mat-mdc-chip-selected {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%) !important;
color: white !important;
}
::ng-deep .update-chip.mat-mdc-chip-selected {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%) !important;
color: white !important;
}

View File

@ -10,12 +10,12 @@
</h4>
</div>
<div class="button-row">
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="save()">Ejecutar</button>
<button class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || loading" (click)="save()">Ejecutar</button>
</div>
<div>
<button mat-stroked-button color="accent" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript)" (click)="openScheduleModal()">
<mat-icon>schedule</mat-icon> Opciones de programación
<div class="button-row">
<button color="accent" class="action-button" [disabled]="selectedClients.length < 1 || (commandType === 'existing' && !selectedScript) || loading" (click)="openScheduleModal()">
Opciones de programación
</button>
</div>
</div>
@ -40,8 +40,8 @@
<div class="clients-grid">
<div *ngFor="let client of clientData" 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'}"
(click)="toggleClientSelection(client)"
[ngClass]="{'selected-client': client.selected}"
[matTooltip]="getPartitionsTooltip(client)"
matTooltipPosition="above"
matTooltipClass="custom-tooltip">
@ -62,55 +62,69 @@
</mat-expansion-panel>
</div>
<mat-divider style="margin-top: 20px;"></mat-divider>
<div class="select-container">
<mat-card class="script-selector-card">
<mat-card-title>Seleccione el tipo de comando</mat-card-title>
<div class="toggle-options">
<mat-button-toggle-group [(ngModel)]="commandType" exclusive>
<mat-button-toggle value="new">
<mat-icon>edit</mat-icon> Nuevo Script
</mat-button-toggle>
<mat-button-toggle value="existing">
<mat-icon>storage</mat-icon> Script Guardado
</mat-button-toggle>
</mat-button-toggle-group>
</div>
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div>
<div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<div class="script-content">
<h3>Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
<div class="form-section">
<div class="form-section-title">
<mat-icon>code</mat-icon>
Configuración de script
</div>
<div class="script-params" *ngIf="parameterNames.length > 0 && selectedScript.parameters">
<h3>Ingrese los parámetros:</h3>
<div *ngFor="let paramName of parameterNames">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
</mat-form-field>
<div class="action-chips-container">
<mat-chip-listbox [(ngModel)]="commandType" required class="action-chip-listbox">
<mat-chip-option value="new" class="action-chip create-chip firmware-chip" (click)="commandType = 'new'">
<span>Nuevo Script</span>
</mat-chip-option>
<mat-chip-option value="existing" class="action-chip update-chip firmware-chip" (click)="commandType = 'existing'">
<span>Script Guardado</span>
</mat-chip-option>
</mat-chip-listbox>
</div>
<div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="full-width">
<mat-label>Ingrese el script</mat-label>
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
</mat-form-field>
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div>
<div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label>
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
</mat-select>
</mat-form-field>
</div>
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<div class="script-content">
<h3>Script:</h3>
<div class="script-preview" [innerHTML]="scriptContent"></div>
</div>
<div class="script-params" *ngIf="parameterNames.length > 0 && selectedScript.parameters">
<h3>Ingrese los parámetros:</h3>
<div *ngFor="let paramName of parameterNames">
<mat-form-field appearance="fill" class="full-width">
<mat-label>{{ paramName }}</mat-label>
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
</mat-form-field>
</div>
</div>
</div>
</div>
</mat-card>
</div>
<app-scroll-to-top
[threshold]="200"
targetElement=".header-container"
position="bottom-right"
[showTooltip]="true"
tooltipText="Volver arriba"
tooltipPosition="left">
</app-scroll-to-top>

View File

@ -28,6 +28,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import {MatIconModule} from "@angular/material/icon";
import {MatCardModule} from "@angular/material/card";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
import { MatChipsModule } from "@angular/material/chips";
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http);
@ -63,6 +64,7 @@ describe('RunScriptAssistantComponent', () => {
MatIconModule,
MatCardModule,
MatButtonToggleModule,
MatChipsModule,
ToastrModule.forRoot(),
HttpClientTestingModule,
TranslateModule.forRoot({

View File

@ -7,6 +7,7 @@ import { ActivatedRoute, Router } from "@angular/router";
import { SaveScriptComponent } from "./save-script/save-script.component";
import { MatDialog } from "@angular/material/dialog";
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
@Component({
selector: 'app-run-script-assistant',
@ -53,15 +54,9 @@ export class RunScriptAssistantComponent implements OnInit{
}
});
this.clientId = this.clientData?.length ? this.clientData[0]['@id'] : null;
this.clientData.forEach((client: { selected: boolean; status: string}) => {
if (client.status === 'og-live') {
client.selected = true;
}
});
this.clientData.forEach((client: { selected: boolean; status: string}) => { client.selected = true; });
this.selectedClients = this.clientData.filter(
(client: { status: string }) => client.status === 'og-live'
);
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
this.loadScripts()
}
@ -122,18 +117,12 @@ export class RunScriptAssistantComponent implements OnInit{
}
updateSelectedClients() {
this.selectedClients = this.clientData.filter(
(client: { selected: boolean; status: string }) => client.selected && client.status === "og-live"
);
this.selectedClients = this.clientData.filter((client: { selected: boolean; status: string}) => client.selected);
}
toggleSelectAll() {
this.allSelected = !this.allSelected;
this.clientData.forEach((client: { selected: boolean; status: string }) => {
if (client.status === "og-live") {
client.selected = this.allSelected;
}
});
this.clientData.forEach((client: { selected: boolean; status: string }) => { client.selected = this.allSelected; });
}
getPartitionsTooltip(client: any): string {
@ -179,23 +168,33 @@ export class RunScriptAssistantComponent implements OnInit{
}
save(): void {
this.loading = true;
const dialogRef = this.dialog.open(QueueConfirmationModalComponent, {
width: '400px',
disableClose: true,
hasBackdrop: true
});
this.http.post(`${this.baseUrl}/commands/run-script`, {
clients: this.selectedClients.map((client: any) => client.uuid),
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
}).subscribe(
response => {
this.toastService.success('Script ejecutado correctamente');
this.dataChange.emit();
this.router.navigate(['/commands-logs']);
},
error => {
this.toastService.error('Error al ejecutar el script');
dialogRef.afterClosed().subscribe(result => {
if (result !== undefined) {
this.loading = true;
this.http.post(`${this.baseUrl}/commands/run-script`, {
clients: this.selectedClients.map((client: any) => client.uuid),
script: this.commandType === 'existing' ? this.scriptContent : this.newScript,
queue: result
}).subscribe(
response => {
this.toastService.success('Script ejecutado correctamente');
this.dataChange.emit();
this.router.navigate(['/commands-logs']);
this.loading = false;
},
error => {
this.toastService.error('Error al ejecutar el script');
this.loading = false;
}
);
}
);
this.loading = false;
});
}
openScheduleModal(): void {

View File

@ -92,7 +92,7 @@ export class ShowOrganizationalUnitComponent implements OnInit {
{ property: 'Router', value: this.ou.networkSettings.router },
{ property: 'NTP', value: this.ou.networkSettings.ntp },
{ property: 'Modo P2P', value: this.ou.networkSettings.p2pMode },
{ property: 'Tiempo P2P', value: this.ou.networkSettings.p2pTime },
...(this.ou.networkSettings.p2pMode === 'seeder' ? [{ property: 'Tiempo P2P (minutos)', value: this.ou.networkSettings.p2pTime }] : []),
{ property: 'Mcast IP', value: this.ou.networkSettings.mcastIp },
{ property: 'Mcast Speed', value: this.ou.networkSettings.mcastSpeed },
{ property: 'Mcast Port', value: this.ou.networkSettings.mcastPort },

View File

@ -65,7 +65,7 @@ export class LoginComponent {
this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
this.router.navigateByUrl('/groups');
this.dialog.open(GlobalStatusComponent, {
width: '45vw',
width: '65vw',
height: '80vh',
});
}

View File

@ -2,8 +2,52 @@ mat-toolbar {
/*height: 7vh;*/
min-height: 65px;
min-width: 375px;
background-color: #e2e8f0;
background: rgba(226, 232, 240, 0.8);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: black;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
padding: 0 16px;
}
/* Estilos específicos para el botón del sidebar */
.navbar-icon {
color: #3f51b5;
font-size: 24px;
width: 24px;
height: 24px;
}
/* Asegurar que el botón del sidebar sea visible */
mat-toolbar button[mat-icon-button] {
display: flex;
align-items: center;
justify-content: center;
min-width: 48px;
height: 48px;
border-radius: 50%;
transition: all 0.3s ease;
color: #3f51b5;
background-color: transparent;
border: none;
cursor: pointer;
margin-right: 8px;
}
mat-toolbar button[mat-icon-button]:hover {
background-color: rgba(63, 81, 181, 0.1);
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(63, 81, 181, 0.2);
}
.navbar-actions-row {
@ -11,6 +55,7 @@ mat-toolbar {
justify-content: end;
align-items: center;
flex-grow: 1;
gap: 8px;
}
.navbar-buttons-row {
@ -71,6 +116,7 @@ mat-toolbar {
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.trace-button .mat-icon {
@ -81,3 +127,17 @@ mat-toolbar {
margin-right: 2vh;
}
}
.menu-toggle-right {
position: absolute;
right: 16px;
top: 50%;
transform: translateY(-50%);
z-index: 1100;
}
@media (max-width: 576px) {
.menu-toggle-right {
right: 8px;
}
}

View File

@ -3,12 +3,11 @@
matTooltipShowDelay="1000">
</span>
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral"
matTooltipShowDelay="1000">
<mat-icon class="navbar-icon">menu</mat-icon>
</button>
<div class="navbar-actions-row" *ngIf="!isSmallScreen">
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral"
matTooltipShowDelay="1000">
<mat-icon class="navbar-icon">menu</mat-icon>
</button>
<button routerLink="/commands-logs" mat-button>
<mat-icon class="trace-button" >notifications</mat-icon>
</button>
@ -37,6 +36,10 @@
<!-- Menú desplegable para pantallas pequeñas -->
<div *ngIf="isSmallScreen" class="isSmallScreenButtons">
<button mat-icon-button (click)="onToggleSidebar()" matTooltip="Abrir o cerrar la barra lateral"
matTooltipShowDelay="1000">
<mat-icon class="navbar-icon">menu</mat-icon>
</button>
<button class="trace-button" routerLink="/commands-logs" mat-button>
<mat-icon>notifications</mat-icon>
</button>

View File

@ -42,7 +42,7 @@ export class HeaderComponent implements OnInit {
showGlobalStatus() {
this.dialog.open(GlobalStatusComponent, {
width: '45vw',
width: '5vw',
height: '80vh',
})
}

View File

@ -6,6 +6,8 @@ html, body {
.container {
height: 100%;
padding-top: 65px; /* Asegurar que el contenido no se superponga con el header fijo */
box-sizing: border-box;
}
.sidebar {
@ -28,3 +30,10 @@ html, body {
.mat-list-item:hover {
background-color: #2a2a40;
}
/* Asegurar que el contenido principal tenga el espacio correcto */
.content {
padding-top: 0; /* El padding ya está en el container */
height: calc(100vh - 65px); /* Altura total menos el header */
overflow: auto;
}