Merge pull request 'New version 1.1.0' (#50) from develop into main
oggui-debian-package/pipeline/head There was a failure building this commit Details
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/tag There was a failure building this commit Details

Reviewed-on: #50
pull/51/head^2 1.1.0
Manuel Aranda Rosales 2025-10-17 12:33:00 +02:00
commit e85e1db2db
18 changed files with 598 additions and 49 deletions

View File

@ -1,4 +1,16 @@
# Changelog
## [1.1.0] - 2025-10-17
### Added
- Se han incorporado opciones para poder seleccionar tamaños de pantalla para visualizar en los menu ogBrowser
### Improved
- Se han incluido cambios en el particionador que mejoran la experiencia de usuario.
- Se ha incluido un parametro en el "config.json" para poder integrar los logs desde el instalador.
### Fixed.
- Se han corrgido los test que no se realizaban correctamente.
---
## [1.0.0] - 2025-10-09
### Added
- Se ha añadido nuevo readme

View File

@ -0,0 +1,289 @@
# Documentación Funcional - OgWebconsole
## Descripción General
**OgWebconsole** es una aplicación web desarrollada en Angular 18 que proporciona una interfaz de gestión integral para sistemas de computación distribuida, especialmente diseñada para entornos educativos y laboratorios. La aplicación permite administrar clientes, unidades organizativas, comandos, imágenes, calendarios y configuraciones de red de manera centralizada.
**URL de Acceso:** https://localhost:4200/
## Arquitectura del Sistema
### Tecnologías Utilizadas
- **Frontend:** Angular 18 con Angular Material
- **Autenticación:** JWT (JSON Web Tokens)
- **Internacionalización:** ngx-translate (Español/Inglés)
- **Gráficos:** ngx-charts
- **Notificaciones:** ngx-toastr
- **Tours Guiados:** ngx-joyride
### Sistema de Roles
La aplicación implementa un sistema jerárquico de roles con diferentes niveles de acceso:
1. **Super Admin** (`super-admin`): Acceso completo a todas las funcionalidades
2. **OU Admin** (`ou-admin`): Administrador de unidad organizativa
3. **OU Operator** (`ou-operator`): Operador de unidad organizativa
4. **OU Minimal** (`ou-minimal`): Acceso mínimo de solo lectura
5. **User** (`user`): Usuario básico
## Módulos Principales
### 1. Gestión de Grupos y Unidades Organizativas
**Ruta:** `/groups`
**Descripción:** Módulo central para la gestión de la estructura organizativa jerárquica.
**Funcionalidades:**
- Visualización en árbol de unidades organizativas (Centros, Aulas, Grupos)
- Gestión de clientes individuales y masiva
- Filtros avanzados por nombre, IP, MAC y estado
- Vista de lista y tarjeta para clientes
- Estadísticas en tiempo real de clientes (Online, Offline, Ocupados)
**Casos de Uso:**
- **Crear Estructura Organizativa:** Un administrador puede crear centros, aulas y grupos de clientes
- **Gestionar Clientes:** Añadir, editar, eliminar y mover clientes entre unidades organizativas
- **Monitoreo de Estado:** Visualizar el estado en tiempo real de todos los clientes conectados
- **Búsqueda Avanzada:** Localizar clientes específicos por múltiples criterios
### 2. Administración de Usuarios y Roles
**Ruta:** `/users`, `/roles`, `/env-vars`
**Descripción:** Gestión completa del sistema de usuarios y permisos.
**Funcionalidades:**
- CRUD de usuarios con asignación de roles
- Gestión de roles con permisos granulares
- Variables de entorno del sistema
- Cambio de contraseñas
**Casos de Uso:**
- **Gestión de Usuarios:** Crear cuentas para administradores y operadores
- **Control de Acceso:** Asignar roles específicos según responsabilidades
- **Configuración del Sistema:** Modificar variables de entorno críticas
### 3. Gestión de Comandos
**Rutas:** `/commands`, `/commands-groups`, `/commands-task`, `/commands-logs`
**Descripción:** Sistema completo de ejecución y programación de comandos.
**Funcionalidades:**
- Creación y edición de comandos personalizados
- Agrupación de comandos para ejecución masiva
- Programación de tareas con calendario
- Trazabilidad completa de ejecuciones
- Logs detallados de todas las operaciones
**Comandos Predefinidos:**
- Encender/Apagar equipos
- Reiniciar sistemas
- Crear/Desplegar imágenes
- Particionar y formatear discos
- Inventarios de software y hardware
- Ejecución de scripts personalizados
**Casos de Uso:**
- **Mantenimiento Programado:** Ejecutar tareas de mantenimiento en horarios específicos
- **Despliegue Masivo:** Aplicar configuraciones a múltiples clientes simultáneamente
- **Monitoreo de Ejecución:** Seguir el progreso de comandos en tiempo real
- **Auditoría:** Revisar historial completo de operaciones realizadas
### 4. Gestión de Imágenes y Repositorios
**Rutas:** `/images`, `/repositories`, `/pxe-images`
**Descripción:** Administración de imágenes del sistema y repositorios.
**Funcionalidades:**
- Gestión de imágenes OgLive
- Repositorios Git y monolíticos
- Plantillas PXE para arranque de red
- Importación y exportación de imágenes
- Conversión entre formatos de imagen
**Casos de Uso:**
- **Despliegue de Sistemas:** Crear y distribuir imágenes de sistemas operativos
- **Gestión de Versiones:** Control de versiones de imágenes mediante Git
- **Configuración de Arranque:** Configurar plantillas PXE para diferentes tipos de arranque
- **Backup y Restauración:** Crear copias de seguridad de configuraciones
### 5. Configuración de Red y DHCP
**Rutas:** `/subnets`, `/ogdhcp-status`
**Descripción:** Gestión de configuración de red y servicios DHCP.
**Funcionalidades:**
- Configuración de subredes
- Gestión de clientes DHCP
- Monitoreo del estado del servicio DHCP
- Configuración de parámetros de red
**Casos de Uso:**
- **Configuración de Red:** Definir subredes y rangos de IP
- **Gestión de Clientes:** Asignar direcciones IP automáticamente
- **Monitoreo de Red:** Supervisar el estado de los servicios de red
### 6. Gestión de Calendarios
**Ruta:** `/calendars`
**Descripción:** Sistema de calendarios para control de disponibilidad de equipos.
**Funcionalidades:**
- Creación de calendarios personalizados
- Configuración de horarios de disponibilidad
- Excepciones y reglas especiales
- Integración con Remote PC
**Casos de Uso:**
- **Control de Acceso:** Definir horarios de uso de laboratorios
- **Mantenimiento Programado:** Bloquear equipos durante mantenimiento
- **Gestión de Recursos:** Optimizar el uso de recursos informáticos
### 7. Gestión de Software y Hardware
**Rutas:** `/software`, `/software-profiles`, `/operative-systems`, `/hardware`, `/hardware-profiles`, `/hardware-types`
**Descripción:** Administración de configuraciones de software y hardware.
**Funcionalidades:**
- Catálogo de software disponible
- Perfiles de software para diferentes entornos
- Configuraciones de sistemas operativos
- Gestión de hardware y perfiles de hardware
- Tipos de hardware y configuraciones específicas
**Casos de Uso:**
- **Configuración de Entornos:** Crear perfiles específicos para diferentes tipos de laboratorios
- **Gestión de Software:** Mantener catálogo actualizado de software disponible
- **Configuración de Hardware:** Definir configuraciones óptimas para diferentes tipos de equipos
### 8. Estado Global del Sistema
**Ruta:** `/global-status`
**Descripción:** Dashboard centralizado con métricas del sistema.
**Funcionalidades:**
- Uso de CPU y RAM en tiempo real
- Estado de servicios críticos
- Estadísticas de equipos conectados
- Monitoreo de procesos del sistema
**Casos de Uso:**
- **Monitoreo del Sistema:** Supervisar el rendimiento general del sistema
- **Detección de Problemas:** Identificar rápidamente fallos o sobrecargas
- **Planificación de Recursos:** Analizar tendencias de uso para planificación
## Flujos de Trabajo Principales
### 1. Configuración Inicial del Sistema
1. **Acceso como Super Admin**
2. **Configuración de Variables de Entorno** (`/env-vars`)
3. **Creación de Roles y Usuarios** (`/roles`, `/users`)
4. **Configuración de Red** (`/subnets`)
5. **Creación de Estructura Organizativa** (`/groups`)
### 2. Gestión Diaria de Clientes
1. **Acceso a Gestión de Grupos** (`/groups`)
2. **Selección de Unidad Organizativa**
3. **Visualización de Clientes**
4. **Ejecución de Comandos** (individual o masivo)
5. **Monitoreo de Estado** en tiempo real
### 3. Despliegue de Imágenes
1. **Preparación de Imagen** (`/images` o `/repositories`)
2. **Configuración de Plantilla PXE** (`/pxe-templates`)
3. **Selección de Clientes Objetivo** (`/groups`)
4. **Ejecución de Despliegue** (comando `deployImage`)
5. **Verificación de Resultados** (`/commands-logs`)
### 4. Mantenimiento Programado
1. **Creación de Tarea** (`/commands-task`)
2. **Selección de Comandos** (`/commands` o `/commands-groups`)
3. **Programación de Ejecución** (fecha y hora)
4. **Selección de Clientes** (`/groups`)
5. **Monitoreo de Ejecución** (`/commands-logs`)
## Características Técnicas Avanzadas
### 1. Sistema de Autenticación y Autorización
- Autenticación basada en JWT
- Guards de ruta para control de acceso
- Roles jerárquicos con permisos granulares
- Sesiones persistentes con refresh automático
### 2. Interfaz de Usuario
- Diseño responsive con Angular Material
- Tours guiados integrados (ngx-joyride)
- Internacionalización completa (ES/EN)
- Notificaciones en tiempo real
- Modales y diálogos contextuales
### 3. Gestión de Estado
- Servicios centralizados para cada módulo
- Interceptores HTTP para manejo de errores
- Caché inteligente de datos
- Actualizaciones en tiempo real
### 4. Monitoreo y Logging
- Trazabilidad completa de operaciones
- Logs detallados de ejecución
- Métricas de rendimiento
- Exportación de datos (CSV)
## Casos de Uso Específicos por Rol
### Super Admin
- Configuración completa del sistema
- Gestión de todos los usuarios y roles
- Acceso a todas las funcionalidades
- Configuración de variables de entorno
- Monitoreo global del sistema
### OU Admin
- Gestión de unidades organizativas asignadas
- Creación y gestión de clientes
- Ejecución de comandos y tareas
- Gestión de imágenes y repositorios
- Configuración de calendarios
### OU Operator
- Ejecución de comandos predefinidos
- Monitoreo de estado de clientes
- Visualización de logs y trazas
- Gestión básica de clientes
### OU Minimal
- Solo visualización de información
- Sin permisos de modificación
- Acceso limitado a funcionalidades de consulta
## Consideraciones de Seguridad
1. **Autenticación Robusta:** Sistema JWT con refresh tokens
2. **Control de Acceso:** Guards de ruta y validación de roles
3. **Validación de Datos:** Validación tanto en frontend como backend
4. **Auditoría:** Logs completos de todas las operaciones
5. **HTTPS:** Comunicación segura en toda la aplicación
## Escalabilidad y Rendimiento
1. **Paginación:** Implementada en todas las listas grandes
2. **Lazy Loading:** Carga diferida de componentes
3. **Caché Inteligente:** Reducción de llamadas al servidor
4. **Optimización de Consultas:** Filtros y búsquedas eficientes
5. **Responsive Design:** Adaptable a diferentes dispositivos
## Conclusión
OgWebconsole es una solución integral para la gestión de laboratorios de computación que combina facilidad de uso con potentes funcionalidades de administración. Su arquitectura modular y sistema de roles permite adaptarse a diferentes entornos y necesidades organizativas, proporcionando una herramienta centralizada para la gestión eficiente de recursos informáticos distribuidos.
La aplicación está diseñada para ser intuitiva para usuarios finales mientras proporciona las herramientas avanzadas necesarias para administradores de sistemas, haciendo que sea una solución ideal para entornos educativos, laboratorios de investigación y cualquier organización que requiera gestión centralizada de múltiples equipos informáticos.

View File

@ -30,6 +30,7 @@ import {JoyrideModule} from "ngx-joyride";
import {ConfigService} from "@services/config.service";
import {ActivatedRoute} from "@angular/router";
import {MatButtonToggleModule} from "@angular/material/button-toggle";
import {MatRadioModule} from "@angular/material/radio";
describe('CreateTaskScriptComponent', () => {
let component: CreateTaskScriptComponent;
@ -66,6 +67,7 @@ describe('CreateTaskScriptComponent', () => {
MatCardModule,
MatMenuModule,
MatButtonToggleModule,
MatRadioModule,
MatTreeModule,
TranslateModule.forRoot(),
JoyrideModule.forRoot(),

View File

@ -133,7 +133,7 @@
</div>
<div class="logs-container">
<iframe
src="https://localhost:3030/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=ogrepository2&theme=dark&panelId=1&__feature.dashboardSceneSolo"
[src]="getRepositoryIframeUrl()"
width="100%"
height="600"
frameborder="0"
@ -187,7 +187,7 @@
</div>
<div class="logs-container">
<iframe
src="https://localhost:3030/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo"
[src]="getOgBootIframeUrl()"
width="100%"
height="800"
frameborder="0"
@ -237,7 +237,7 @@
</div>
<div class="logs-container">
<iframe
src="https://localhost:3030/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo"
[src]="getDhcpIframeUrl()"
width="100%"
height="800"
frameborder="0"
@ -278,7 +278,7 @@
</div>
<div class="logs-container">
<iframe
src="https://localhost:3030/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser"
[src]="getOgCoreIframeUrl()"
width="100%"
height="800"
frameborder="0"

View File

@ -2,6 +2,7 @@ import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import { ConfigService } from '@services/config.service';
import { ToastrService } from "ngx-toastr";
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-global-status',
@ -56,7 +57,8 @@ export class GlobalStatusComponent implements OnInit {
constructor(
private configService: ConfigService,
private toastService: ToastrService,
private http: HttpClient
private http: HttpClient,
private sanitizer: DomSanitizer
) {
this.baseUrl = this.configService.apiUrl;
this.ogBootApiUrl = `${this.baseUrl}/og-boot/status`;
@ -213,9 +215,7 @@ export class GlobalStatusComponent implements OnInit {
this.http.get<any>(`${this.repositoriesUrl}?page=1&itemsPerPage=10`).subscribe(
data => {
console.log('Respuesta de repositorios:', data); // Debug log
this.repositories = data['hydra:member'] || [];
console.log('Repositorios cargados:', this.repositories); // Debug log
if (this.repositories.length === 0) {
if (showLoading) {
@ -229,7 +229,6 @@ export class GlobalStatusComponent implements OnInit {
if (showLoading) {
this.loading = false;
}
// Marcar como error los repositorios que no han respondido
this.repositories.forEach(repository => {
if (!(repository.uuid in this.errorRepositories)) {
this.errorRepositories[repository.uuid] = true;
@ -253,7 +252,6 @@ export class GlobalStatusComponent implements OnInit {
});
},
error => {
console.error('Error al cargar repositorios:', error); // Debug log
this.loading = false;
this.repositories = [];
this.toastService.error('Error al cargar la lista de repositorios');
@ -262,18 +260,15 @@ export class GlobalStatusComponent implements OnInit {
}
loadRepositoryStatus(repositoryUuid: string, callback: (errorOccurred: boolean) => void): void {
console.log(`Cargando estado del repositorio: ${repositoryUuid}`); // Debug log
console.log(`Cargando estado del repositorio: ${repositoryUuid}`);
const timeoutId = setTimeout(() => {
console.log(`Timeout al cargar estado del repositorio: ${repositoryUuid}`); // Debug log
callback(true);
}, 5000);
this.http.get<any>(`${this.baseUrl}/image-repositories/server/${repositoryUuid}/status`).subscribe(
data => {
console.log(`Estado del repositorio ${repositoryUuid}:`, data); // Debug log
if (!data.success) {
console.error(`Error en la respuesta del repositorio ${repositoryUuid}:`, data);
clearTimeout(timeoutId);
callback(true);
return;
@ -307,7 +302,6 @@ export class GlobalStatusComponent implements OnInit {
callback(false);
},
error => {
console.error(`Error al cargar estado del repositorio ${repositoryUuid}:`, error); // Debug log
this.toastService.error(error.error['hydra:description'] || 'Error al cargar el estado del repositorio');
clearTimeout(timeoutId);
callback(true);
@ -326,9 +320,6 @@ export class GlobalStatusComponent implements OnInit {
}
onRepositoryChange(repositoryUuid: string): void {
this.selectedRepositoryUuid = repositoryUuid;
if (repositoryUuid && !this.repositoryStatuses[repositoryUuid]) {
@ -345,13 +336,29 @@ export class GlobalStatusComponent implements OnInit {
return selectedRepo ? selectedRepo.name : '';
}
getRepositoryIframeUrl(): string {
getRepositoryIframeUrl(): SafeResourceUrl {
const repositoryName = this.getSelectedRepositoryName();
if (repositoryName) {
const encodedName = encodeURIComponent(repositoryName);
return `https://localhost:3030/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
const url = `${this.configService.logUrl}/d-solo/ogrepo-logs/ogrepo-logs?orgId=1&timezone=browser&refresh=5s&var-query0=&editIndex=0&var-hostname=${encodedName}&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
return '';
return this.sanitizer.bypassSecurityTrustResourceUrl('');
}
getOgBootIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogboot-logs/ogboot-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
getDhcpIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogdhcp-logs/ogdhcp-logs?orgId=1&timezone=browser&refresh=5s&theme=dark&panelId=1&__feature.dashboardSceneSolo`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
getOgCoreIframeUrl(): SafeResourceUrl {
const url = `${this.configService.logUrl}/d-solo/ogcore-logs/ogcore-logs?orgId=1&tab=transformations&theme=dark&panelId=1&__feature.dashboardSceneSolo&now-5m&to=now&timezone=browser`;
return this.sanitizer.bypassSecurityTrustResourceUrl(url);
}
refreshAll(): void {
@ -414,7 +421,6 @@ export class GlobalStatusComponent implements OnInit {
}
break;
case 'ogcore':
// No se necesita cargar nada, solo mostrar los logs
break;
}

View File

@ -152,7 +152,7 @@
<div class="partition-assistant" *ngIf="selectedDisk">
<div class="row-button">
<button class="action-button" [disabled]="partitionCode === 'MSDOS'" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
<button class="action-button" (click)="addPartition(selectedDisk.diskNumber)">Añadir partición</button>
<div class="info-badge" *ngIf="selectedModelClient.firmwareType">
<mat-icon class="info-icon">memory</mat-icon>
@ -238,32 +238,32 @@
<tr *ngIf="!partition.removed">
<td>{{ partition.partitionNumber }}</td>
<td>
<select [(ngModel)]="partition.partitionCode" required [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT' && selectedDisk.diskNumber === 1">
<select [(ngModel)]="partition.partitionCode" required>
<option *ngFor="let type of partitionTypes" [value]="type.name">
{{ type.name }}
</option>
</select>
</td>
<td>
<select [(ngModel)]="partition.filesystem" required [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT' && selectedDisk.diskNumber === 1">
<select [(ngModel)]="partition.filesystem" required>
<option *ngFor="let type of filesystemTypes" [value]="type.name">
{{ type.name }}
</option>
</select>
</td>
<td>
<input [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT' && selectedDisk.diskNumber === 1" type="number" [(ngModel)]="partition.size" required
<input type="number" [(ngModel)]="partition.size" required
(input)="updatePartitionSize(selectedDisk.diskNumber, j, partition.size)" />
</td>
<td>
<input [disabled]="partition.partitionNumber === 1 && partitionCode === 'GPT' && selectedDisk.diskNumber === 1" type="number" [(ngModel)]="partition.percentage"
<input type="number" [(ngModel)]="partition.percentage"
(input)="updatePartitionSizeFromPercentage(selectedDisk.diskNumber, j, partition.percentage)" />
</td>
<td>
<mat-checkbox type="checkbox" [(ngModel)]="partition.format" />
</td>
<td>
<button mat-button *ngIf="partitionCode !== 'MSDOS'" (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
<button mat-button (click)="removePartition(selectedDisk.diskNumber, partition)" class="remove-btn">X</button>
</td>
</tr>
</ng-container>

View File

@ -65,7 +65,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
partitionValidationStatus: 'idle' | 'loading' | 'success' | 'error' = 'idle';
partitionValidationMessage: string = '';
private validationDebounceTime = 500;
private validationDebounceTime = 1000;
private validationSubject = new Subject<void>();
displayedColumns: string[] = ['partitionNumber', 'partitionCode', 'filesystem', 'size', 'percentage', 'format', 'actions'];
@ -382,13 +382,21 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
removedPartitions.length > 0 ? Math.max(...removedPartitions.map((p) => p.partitionNumber)) : 0;
const newPartitionNumber = maxPartitionNumber + 1;
// Verificar si es UEFI y es la primera partición
const isUEFI = this.selectedModelClient?.firmwareType === 'UEFI';
const isFirstPartition = removedPartitions.length === 0;
const isFirstDisk = diskNumber === 1;
// Si es UEFI, primera partición y primer disco, crear partición EFI
const shouldCreateEFI = isUEFI && isFirstPartition && isFirstDisk;
disk.partitions.push({
partitionNumber: newPartitionNumber,
size: 0,
partitionCode: 'EMPTY',
filesystem: 'EXT4',
size: shouldCreateEFI ? 512 : 0,
partitionCode: shouldCreateEFI ? 'EFI' : 'EMPTY',
filesystem: shouldCreateEFI ? 'FAT32' : 'EXT4',
memoryUsage: 0,
sizeBytes: 0,
sizeBytes: shouldCreateEFI ? 512 : 0,
format: false,
color: this.getNextPartitionColor(),
percentage: 0,
@ -397,6 +405,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
this.updateDiskChart(disk);
this.resetValidationStatus();
this.validationSubject.next();
setTimeout(() => {
@ -427,6 +436,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
this.updateDiskChart(disk);
this.resetValidationStatus();
this.validationSubject.next();
}
}
@ -519,12 +529,15 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
partitionToRemove.removed = true;
}
this.recalculatePartitionNumbers(disk.partitions);
disk.used = this.calculateUsedSpace(disk.partitions);
disk.percentage = (disk.used / disk.totalDiskSize) * 100;
this.updateDiskChart(disk);
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
this.resetValidationStatus();
this.validationSubject.next();
setTimeout(() => {
@ -543,10 +556,16 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
this.updateDiskChart(disk);
this.updatePartitionPercentages(disk.partitions, disk.totalDiskSize);
this.resetValidationStatus();
this.validationSubject.next();
}
}
private resetValidationStatus(): void {
this.partitionValidationStatus = 'loading';
this.partitionValidationMessage = '';
}
validatePartitionSizes(): void {
if (!this.selectedModelClient || !this.selectedDisk) {
return;
@ -772,6 +791,7 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
if (this.selectedDiskNumber) {
this.scrollToPartitionTable();
this.resetValidationStatus();
this.validationSubject.next();
setTimeout(() => {
@ -865,4 +885,15 @@ export class PartitionAssistantComponent implements OnInit, AfterViewInit, OnDes
const isValidCode = this.partitionTypes.some(type => type.name === partitionCode);
return isValidCode ? partitionCode : 'EMPTY';
}
private recalculatePartitionNumbers(partitions: Partition[]): void {
const activePartitions = partitions.filter(partition => !partition.removed);
activePartitions.sort((a, b) => a.partitionNumber - b.partitionNumber);
activePartitions.forEach((partition, index) => {
partition.partitionNumber = index + 1;
});
}
}

View File

@ -1,5 +1,7 @@
import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { ConfigService } from '@services/config.service';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
@Component({
selector: 'app-client-logs-modal',
@ -7,16 +9,19 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
styleUrls: ['./client-logs-modal.component.css']
})
export class ClientLogsModalComponent {
iframeUrl: string;
iframeUrl: SafeResourceUrl;
grafanaUrl: string;
constructor(
public dialogRef: MatDialogRef<ClientLogsModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: { client: any }
@Inject(MAT_DIALOG_DATA) public data: { client: any },
private configService: ConfigService,
private sanitizer: DomSanitizer
) {
const mac = this.data.client.mac || '';
this.iframeUrl = `https://localhost:3030/d-solo/opengnsys-clients/filebeat-clients?orgId=1&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&panelId=1&__feature.dashboardSceneSolo`;
this.grafanaUrl = `https://localhost:3030/d/opengnsys-clients/filebeat-clients?orgId=1&from=now-5m&to=now&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&viewPanel=panel-1`;
const iframeUrlString = `${this.configService.logUrl}/d-solo/opengnsys-clients/filebeat-clients?orgId=1&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&panelId=1&__feature.dashboardSceneSolo`;
this.iframeUrl = this.sanitizer.bypassSecurityTrustResourceUrl(iframeUrlString);
this.grafanaUrl = `${this.configService.logUrl}/d/opengnsys-clients/filebeat-clients?orgId=1&from=now-5m&to=now&timezone=browser&var-hostname=$__all&var-mac=${mac}&refresh=5s&viewPanel=panel-1`;
}
closeModal(): void {

View File

@ -84,6 +84,25 @@
<mat-error>{{ 'menuError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'resolutionLabel' | translate }}</mat-label>
<mat-select formControlName="resolution" required>
<mat-option value="769">640×480 (8-bit - 256 colores)</mat-option>
<mat-option value="771">800×600 (8-bit - 256 colores)</mat-option>
<mat-option value="773">1024×768 (8-bit - 256 colores)</mat-option>
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
<mat-option value="784">640×480 (16-bit - 65k colores)</mat-option>
<mat-option value="787">800×600 (16-bit - 65k colores)</mat-option>
<mat-option value="791">1024×768 (16-bit - 65k colores)</mat-option>
<mat-option value="794">1280×1024 (16-bit - 65k colores)</mat-option>
<mat-option value="785">640×480 (24-bit - 16M colores)</mat-option>
<mat-option value="788">800×600 (24-bit - 16M colores)</mat-option>
<mat-option value="792">1024×768 (24-bit - 16M colores)</mat-option>
<mat-option value="795">1280×1024 (24-bit - 16M colores)</mat-option>
</mat-select>
<mat-error>{{ 'resolutionError' | translate }}</mat-error>
</mat-form-field>
<mat-checkbox formControlName="maintenance">
{{ 'maintenance' | translate }}
</mat-checkbox>

View File

@ -58,9 +58,11 @@ export class ManageClientComponent implements OnInit {
Promise.all(observables).then(() => {
if (this.isEditMode) {
this.loadData(this.data.uuid).then(() => {
this.setMenuResolutionIfEmpty();
this.loading = false;
});
} else {
this.setMenuResolutionIfEmpty();
this.loading = false;
}
}).catch(error => {
@ -85,7 +87,8 @@ export class ManageClientComponent implements OnInit {
ogLive: [null],
repository: [null],
menu: [null],
maintenance: [false]
maintenance: [false],
resolution: [null]
});
}
@ -105,6 +108,7 @@ export class ManageClientComponent implements OnInit {
ogLive: unit.networkSettings?.ogLive?.['@id'],
menu: unit.networkSettings?.menu?.['@id'],
pxeTemplate: unit.networkSettings?.pxeTemplate?.['@id'],
resolution: unit.networkSettings?.resolution || null
}));
const initialUnitId = this.clientForm.get('organizationalUnit')?.value;
@ -199,6 +203,20 @@ export class ManageClientComponent implements OnInit {
this.setOrganizationalUnitDefaults(event.value);
}
setMenuResolutionIfEmpty(): void {
const selectedMenuId = this.clientForm.get('menu')?.value;
const currentResolution = this.clientForm.get('resolution')?.value;
if (selectedMenuId && !currentResolution) {
const selectedMenu = this.menus.find(menu => menu['@id'] === selectedMenuId);
if (selectedMenu && selectedMenu.resolution) {
this.clientForm.patchValue({
resolution: selectedMenu.resolution
});
}
}
}
setOrganizationalUnitDefaults(unitId: string): void {
const selectedUnit: any = this.parentUnitsWithPaths.find(unit => unit.id === unitId);
if (selectedUnit) {
@ -207,7 +225,8 @@ export class ManageClientComponent implements OnInit {
ogLive: selectedUnit.ogLive || null,
menu: selectedUnit.menu || null,
netiface: selectedUnit.netiface || null,
pxeTemplate: selectedUnit.pxeTemplate || null
pxeTemplate: selectedUnit.pxeTemplate || null,
resolution: selectedUnit.resolution || null
});
}
}
@ -230,7 +249,8 @@ export class ManageClientComponent implements OnInit {
ogLive: data.ogLive ? data.ogLive['@id'] : null,
pxeTemplate: data.pxeTemplate ? data.pxeTemplate['@id'] : null,
menu: data.menu ? data.menu['@id'] : null,
maintenance: data.maintenance
maintenance: data.maintenance,
resolution: data.resolution
});
resolve();
},

View File

@ -1,5 +1,10 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatTableModule } from '@angular/material/table';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { HardwareCollectionModalComponent } from './hardware-collection-modal.component';
describe('HardwareCollectionModalComponent', () => {
@ -9,6 +14,14 @@ describe('HardwareCollectionModalComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [HardwareCollectionModalComponent],
imports: [
MatDialogModule,
MatTableModule,
MatIconModule,
MatButtonModule,
BrowserAnimationsModule,
TranslateModule.forRoot()
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: {} }

View File

@ -1,5 +1,22 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatExpansionModule } from '@angular/material/expansion';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
import { HardwareProfileComponent } from './hardware-profile.component';
describe('HardwareProfileComponent', () => {
@ -7,8 +24,36 @@ describe('HardwareProfileComponent', () => {
let fixture: ComponentFixture<HardwareProfileComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [HardwareProfileComponent]
declarations: [HardwareProfileComponent],
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
MatTableModule,
MatPaginatorModule,
MatDialogModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatExpansionModule,
MatAutocompleteModule,
FormsModule,
ReactiveFormsModule
],
providers: [
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();

View File

@ -1,5 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatExpansionModule } from '@angular/material/expansion';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
import { HardwareTypeComponent } from './hardware-type.component';
describe('HardwareTypeComponent', () => {
@ -7,8 +23,34 @@ describe('HardwareTypeComponent', () => {
let fixture: ComponentFixture<HardwareTypeComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [HardwareTypeComponent]
declarations: [HardwareTypeComponent],
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
MatTableModule,
MatPaginatorModule,
MatDialogModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatExpansionModule,
FormsModule
],
providers: [
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();

View File

@ -1,5 +1,21 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { MatTableModule } from '@angular/material/table';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatExpansionModule } from '@angular/material/expansion';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ToastrModule } from 'ngx-toastr';
import { TranslateModule } from '@ngx-translate/core';
import { JoyrideModule } from 'ngx-joyride';
import { ConfigService } from '@services/config.service';
import { HardwareComponent } from './hardware.component';
describe('HardwareComponent', () => {
@ -7,8 +23,34 @@ describe('HardwareComponent', () => {
let fixture: ComponentFixture<HardwareComponent>;
beforeEach(async () => {
const mockConfigService = {
apiUrl: 'http://mock-api-url',
mercureUrl: 'http://mock-mercure-url'
};
await TestBed.configureTestingModule({
declarations: [HardwareComponent]
declarations: [HardwareComponent],
imports: [
HttpClientTestingModule,
BrowserAnimationsModule,
ToastrModule.forRoot(),
TranslateModule.forRoot(),
JoyrideModule.forRoot(),
MatTableModule,
MatPaginatorModule,
MatDialogModule,
MatIconModule,
MatButtonModule,
MatTooltipModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatExpansionModule,
FormsModule
],
providers: [
{ provide: ConfigService, useValue: mockConfigService }
]
})
.compileComponents();

View File

@ -17,7 +17,20 @@
<mat-form-field appearance="fill" class="form-field">
<mat-label>Resolución</mat-label>
<input matInput formControlName="resolution" name="resolution">
<mat-select formControlName="resolution" name="resolution">
<mat-option value="769">640×480 (8-bit - 256 colores)</mat-option>
<mat-option value="771">800×600 (8-bit - 256 colores)</mat-option>
<mat-option value="773">1024×768 (8-bit - 256 colores)</mat-option>
<mat-option value="775">1280×1024 (8-bit - 256 colores)</mat-option>
<mat-option value="784">640×480 (16-bit - 65k colores)</mat-option>
<mat-option value="787">800×600 (16-bit - 65k colores)</mat-option>
<mat-option value="791">1024×768 (16-bit - 65k colores)</mat-option>
<mat-option value="794">1280×1024 (16-bit - 65k colores)</mat-option>
<mat-option value="785">640×480 (24-bit - 16M colores)</mat-option>
<mat-option value="788">800×600 (24-bit - 16M colores)</mat-option>
<mat-option value="792">1024×768 (24-bit - 16M colores)</mat-option>
<mat-option value="795">1280×1024 (24-bit - 16M colores)</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="form-field">

View File

@ -29,4 +29,8 @@ export class ConfigService {
get mercureUrl(): string {
return this.config.mercureUrl;
}
get logUrl(): string {
return this.config.logUrl;
}
}

View File

@ -593,5 +593,8 @@
"hardwareTypes": "Types",
"TOOLTIP_HARDWARE_TYPES": "Manage hardware types",
"manageHardwareProfiles": "Manage hardware profiles",
"manageHardwareTypes": "Manage hardware types"
"manageHardwareTypes": "Manage hardware types",
"resolution": "Resolution",
"resolutionError": "Resolution is required",
"resolutionLabel": "Select resolution"
}

View File

@ -598,6 +598,9 @@
"hardwareTypes": "Tipos",
"TOOLTIP_HARDWARE_TYPES": "Gestionar tipos de hardware",
"manageHardwareProfiles": "Administrar perfiles de hardware",
"manageHardwareTypes": "Administrar tipos de hardware"
"manageHardwareTypes": "Administrar tipos de hardware",
"resolution": "Resolución",
"resolutionError": "La resolución es requerida",
"resolutionLabel": "Seleccionar resolución"
}