diff --git a/CHANGELOG.md b/CHANGELOG.md index 740c5dc..b14d38d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [1.8.2] - 2026-07-27 + +### Added +- Task Logs (`task-logs.component`): Consume `output` and `finishedAt` properties from Mercure events to update trace outputs in real-time without reloading the page. + +### Fixed +- Creación de imágenes (`create-image.component`): Al seleccionar la acción "Actualizar Imagen", ahora se filtran correctamente las imágenes para mostrar solo aquellas que pertenecen al repositorio objetivo asignado al cliente. +- Menú de opciones de cliente (`groups.component.html`): Unificado el menú de acciones (tres puntos verticales) usando `matMenuContent`. Se elimina la duplicidad de código que causaba divergencias de comportamiento entre la vista tarjeta y la vista lista. Ahora la opción de "Logs en tiempo real" está siempre disponible en ambas vistas, al igual que los bloqueos al sincronizar. +- Detalles de tareas (`OutputDialogComponent`): Ampliado el ancho de la ventana modal de detalles a `70vw` para facilitar la lectura de trazas largas y comandos de OGAgent/OGLive. +- Repositorios Git (`show-git-images.component`): Corregido el cierre abrupto de la ventana modal al borrar un repositorio. Ahora, la interfaz recarga la lista de repositorios, permitiendo seguir operando sin tener que volver a abrir el gestor. + +## [1.8.1] - 2026-07-17 + +### Added +- Formulario de despliegue (`Deploy Image`): Añadido selector de repositorio origen (OG Repository) al menú de despliegue para permitir al usuario elegir desde dónde descargar la imagen. + +### Changed +- Modal de partición de arranque (`boot-so-partition`): Refactorizado para usar el componente compartido `app-disk-partition-selector`, eliminando código duplicado y unificando el diseño y comportamiento de la selección de discos y particiones con el resto de modales. + +### Fixed +- Detalles de tareas (`OutputDialogComponent`): Corregido el formateo de los detalles mostrados desde los agentes (OGAgent) para renderizar el texto en crudo manteniendo los saltos de línea (usando pre-wrap por CSS y
 tag), y dándole formato monoespaciado en lugar de intentar forzar un parseo estricto JSON que rompía la salida de comandos.
+- Formulario de despliegue (`Deploy Image`): Solucionado el problema que ocultaba las comandos (instructions) cuando el despliegue era en modo caché. El código ha sido refactorizado y limpiado usando directivas estructurales estándar de Angular (`*ngIf`, `*ngFor`).
+- Roles: Permisos actualizados en vistas para reflejar recientes cambios de backend en la gestión de perfiles de software.
+
 ## [1.8.0] - 2026-06-16
 
 ### Added
diff --git a/ogWebconsole/e2e/permissions/ou-operator.spec.ts b/ogWebconsole/e2e/permissions/ou-operator.spec.ts
index 75ced9d..67cf796 100644
--- a/ogWebconsole/e2e/permissions/ou-operator.spec.ts
+++ b/ogWebconsole/e2e/permissions/ou-operator.spec.ts
@@ -54,6 +54,11 @@ test.describe('OU Operator — solo grupos y operaciones de equipos', () => {
       const finalUrl = await navigateTo(page, '/clients/run-script');
       expect(finalUrl).toContain('/clients/run-script');
     });
+
+    test('accede a /commands-logs (trazas de sus acciones)', async ({ page }) => {
+      const finalUrl = await navigateTo(page, '/commands-logs');
+      expect(finalUrl).toContain('/commands-logs');
+    });
   });
 
   // ── GUI en /groups: botones de crear OU/cliente ocultos ─────────────────────
diff --git a/ogWebconsole/src/app/app-routing.module.ts b/ogWebconsole/src/app/app-routing.module.ts
index a4fe9fe..3465323 100644
--- a/ogWebconsole/src/app/app-routing.module.ts
+++ b/ogWebconsole/src/app/app-routing.module.ts
@@ -69,7 +69,7 @@ const routes: Routes = [
       { path: 'commands', component: CommandsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
       { path: 'commands-groups', component: CommandsGroupsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
       { path: 'commands-task', component: CommandsTaskComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
-      { path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
+      { path: 'commands-logs', component: TaskLogsComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] } },
       { path: 'calendars', component: CalendarComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin'] } },
       { path: 'clients/deploy-image', component: DeployImageComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] }  },
       { path: 'clients/partition-assistant', component: PartitionAssistantComponent, canActivate: [roleGuard], data: { allowedRoles: ['super-admin', 'ou-admin', 'ou-operator'] }  },
diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts
index 0af94eb..d6b298c 100644
--- a/ogWebconsole/src/app/app.module.ts
+++ b/ogWebconsole/src/app/app.module.ts
@@ -172,12 +172,14 @@ import { ShowTaskScheduleComponent } from './components/commands/commands-task/s
 import { ShowTaskScriptComponent } from './components/commands/commands-task/show-task-script/show-task-script.component';
 import { CreateTaskScriptComponent } from './components/commands/commands-task/create-task-script/create-task-script.component';
 import { ViewParametersModalComponent } from './components/commands/commands-task/show-task-script/view-parameters-modal/view-parameters-modal.component';
+import { ViewScriptModalComponent } from './components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component';
 import { OutputDialogComponent } from './components/task-logs/output-dialog/output-dialog.component';
 import { ClientTaskLogsComponent } from './components/task-logs/client-task-logs/client-task-logs.component';
 import { BootSoPartitionComponent } from './components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component';
 import { RemoveCacheImageComponent } from './components/commands/main-commands/execute-command/remove-cache-image/remove-cache-image.component';
 import { ChangeParentComponent } from './components/groups/shared/change-parent/change-parent.component';
 import { SoftwareProfilePartitionComponent } from './components/commands/main-commands/execute-command/software-profile-partition/software-profile-partition.component';
+import { ExecuteTaskNowComponent } from './components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component';
 import { ClientPendingTasksComponent } from './components/task-logs/client-pending-tasks/client-pending-tasks.component';
 import { QueueConfirmationModalComponent } from './shared/queue-confirmation-modal/queue-confirmation-modal.component';
 import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component';
@@ -316,12 +318,14 @@ registerLocaleData(localeEs, 'es-ES');
     ShowTaskScriptComponent,
     CreateTaskScriptComponent,
     ViewParametersModalComponent,
+    ViewScriptModalComponent,
     OutputDialogComponent,
     ClientTaskLogsComponent,
     BootSoPartitionComponent,
     RemoveCacheImageComponent,
     ChangeParentComponent,
     SoftwareProfilePartitionComponent,
+    ExecuteTaskNowComponent,
     ClientPendingTasksComponent,
     ModalOverlayComponent,
     ScrollToTopComponent,
diff --git a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html
index 6f90c11..9acb0a0 100644
--- a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html
+++ b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.html
@@ -15,30 +15,48 @@
     
       {{ 'labelRole' | translate }}
       
-        
-          {{ group.name }}
-        
+        @for (group of userGroups; track group['@id']) {
+          
+            {{ group.name }}
+          
+        }
       
     
 
     
       {{ 'labelOrganizationalUnit' | translate }}
       
-        
-          {{ unit.name }}
-        
+        @for (unit of organizationalUnits; track unit['@id']) {
+          
+            {{ unit.name }}
+          
+        }
       
     
 
     
       {{ 'cardView' | translate }}
       
-        
-          {{ option.name }}
-        
+        @for (option of views; track option.value) {
+          
+            {{ option.name }}
+          
+        }
       
     
 
+    
+      {{ 'userLanguage' | translate }}
+      
+        @for (lang of availableLanguages; track lang.code) {
+          
+            {{ lang.label }}
+          
+        }
+      
+      {{ 'userLanguageHint' | translate }}
+    
+
     
       {{ 'clientsPerPage' | translate }}
       
diff --git a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts
index 308da61..bceaa5b 100644
--- a/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts
+++ b/ogWebconsole/src/app/components/admin/users/users/add-user-modal/add-user-modal.component.ts
@@ -36,6 +36,8 @@ export class AddUserModalComponent implements OnInit {
     { value: 'list', name: 'Listado' },
   ];
 
+  readonly availableLanguages: { code: string; label: string }[];
+
   constructor(
     public dialogRef: MatDialogRef,
     @Inject(MAT_DIALOG_DATA) public data: any,
@@ -46,12 +48,15 @@ export class AddUserModalComponent implements OnInit {
     private configService: ConfigService
   ) {
     this.baseUrl = this.configService.apiUrl;
+    const langs = this.configService.languages;
+    this.availableLanguages = Object.keys(langs).map(code => ({ code, label: langs[code] }));
     this.userForm = this.fb.group({
       username: ['', Validators.required],
       password: ['', Validators.required],
       role: ['', Validators.required],
       groupsView: ['card', Validators.required],
       clientsPerPage: [null, [Validators.min(1)]],
+      language: ['es'],
       organizationalUnits: [[]]
     });
 
@@ -87,7 +92,8 @@ export class AddUserModalComponent implements OnInit {
           role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
           organizationalUnits: organizationalUnitIds,
           groupsView: response.groupsView,
-          clientsPerPage: response.clientsPerPage ?? null
+          clientsPerPage: response.clientsPerPage ?? null,
+          language: response.language ?? 'es'
         });
 
         this.userId = response['@id'];
@@ -114,7 +120,8 @@ export class AddUserModalComponent implements OnInit {
         enabled: true,
         userGroups: [this.userForm.value.role],
         groupsView: this.userForm.value.groupsView,
-        clientsPerPage: this.userForm.value.clientsPerPage || null
+        clientsPerPage: this.userForm.value.clientsPerPage || null,
+        language: this.userForm.value.language || null
       };
 
       if (!this.userId && this.userForm.value.password) {
diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css
index abebd3a..c83c2c2 100644
--- a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css
+++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.css
@@ -321,18 +321,21 @@ mat-spinner {
   color: #666;
 }
 
-/* Contenido del script en celda */
-.script-content-cell {
-  background-color: #f5f5f5;
-  padding: 8px 12px;
-  border: 1px solid #e0e0e0;
-  border-radius: 4px;
-  font-family: 'Monaco', 'Menlo', monospace;
-  white-space: pre-wrap;
-  font-size: 12px;
-  max-width: 300px;
+/* Vista previa del script en celda (una sola línea, altura de fila fija) */
+.script-preview-cell {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  max-width: 260px;
+}
+
+.script-preview-text {
+  white-space: nowrap;
   overflow: hidden;
   text-overflow: ellipsis;
+  font-family: 'Monaco', 'Menlo', monospace;
+  font-size: 12px;
+  color: #555;
 }
 
 .no-content-hint {
diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html
index d6b8dcc..73af703 100644
--- a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html
+++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.html
@@ -50,8 +50,11 @@
     
       {{ 'labelScript' | translate }}
       
-        
- {{ item.content }} +
+ {{ item.content }} +
({{ 'viewParameters' | translate }}) diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts index 65eac9c..8b2e32c 100644 --- a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/show-task-script.component.ts @@ -10,6 +10,7 @@ import { CreateTaskScheduleComponent } from "../create-task-schedule/create-task import { CreateTaskScriptComponent } from "../create-task-script/create-task-script.component"; import { DeleteModalComponent } from "../../../../shared/delete_modal/delete-modal/delete-modal.component"; import { ViewParametersModalComponent } from "./view-parameters-modal/view-parameters-modal.component"; +import { ViewScriptModalComponent } from "./view-script-modal/view-script-modal.component"; @Component({ selector: 'app-show-task-script', @@ -119,6 +120,13 @@ export class ShowTaskScriptComponent implements OnInit { }); } + openScriptModal(content: string): void { + this.dialog.open(ViewScriptModalComponent, { + width: '700px', + data: content + }); + } + createNewScript(): void { const dialogRef = this.dialog.open(CreateTaskScriptComponent, { width: '800px', diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.css b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.css new file mode 100644 index 0000000..d5fcf86 --- /dev/null +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.css @@ -0,0 +1,12 @@ +.script-full-content { + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + font-size: 13px; + white-space: pre-wrap; + word-break: break-word; + background: #f5f5f5; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 12px; + max-height: 60vh; + overflow-y: auto; +} diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.html b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.html new file mode 100644 index 0000000..54c9fea --- /dev/null +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.html @@ -0,0 +1,7 @@ +

{{ 'labelScript' | translate }}

+ +
{{ data }}
+
+ + + diff --git a/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.ts b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.ts new file mode 100644 index 0000000..392d17c --- /dev/null +++ b/ogWebconsole/src/app/components/commands/commands-task/show-task-script/view-script-modal/view-script-modal.component.ts @@ -0,0 +1,18 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'app-view-script-modal', + templateUrl: './view-script-modal.component.html', + styleUrl: './view-script-modal.component.css' +}) +export class ViewScriptModalComponent { + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any + ) {} + + close(): void { + this.dialogRef.close(); + } +} diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.html b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.html index e5af2c8..c6bc6b2 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.html +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.html @@ -53,42 +53,13 @@
- - - - - - - - - - - - - -
{{ 'buttonSelectPartition' | translate }} - - - - - {{ column.header | translate }} - - {{ column.cell(image) }} - - - - - -
- {{ image.size }} MB - {{ image.size / 1024 }} GB -
-
- -
+ +
diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts index 356aba4..783f574 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/boot-so-partition/boot-so-partition.component.ts @@ -59,6 +59,10 @@ export class BootSoPartitionComponent implements OnInit { displayedColumns = ['select', ...this.columns.map(column => column.columnDef)]; + isPartitionDisabled = (partition: any): boolean => { + return !partition.operativeSystem; + }; + constructor( @Inject(MAT_DIALOG_DATA) public data: { clients: any }, private dialogRef: MatDialogRef, diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts index 876b557..a7c89ba 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts @@ -11,6 +11,7 @@ import { RemoveCacheImageComponent } from "./remove-cache-image/remove-cache-ima import { AuthService } from '@services/auth.service'; import {SoftwareProfilePartitionComponent} from "./software-profile-partition/software-profile-partition.component"; import { QueueConfirmationModalComponent } from '../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component'; +import { ExecuteTaskNowComponent } from './execute-task-now/execute-task-now.component'; @Component({ selector: 'app-execute-command', @@ -43,6 +44,7 @@ export class ExecuteCommandComponent implements OnInit { { translationKey: 'executeCommands.runScript', slug: 'run-script', disabled: false }, { translationKey: 'executeCommands.refresh', slug: 'refresh', disabled: false }, { translationKey: 'executeCommands.deleteLease', slug: 'delete-lease', disabled: false }, + { translationKey: 'executeCommands.executeTask', slug: 'execute-task', disabled: false }, ]; client: any = {}; @@ -87,7 +89,8 @@ export class ExecuteCommandComponent implements OnInit { 'remove-cache-image', 'partition', 'refresh', - 'delete-lease' + 'delete-lease', + 'execute-task' ], 'ou-minimal': [ 'power-on', @@ -138,6 +141,10 @@ export class ExecuteCommandComponent implements OnInit { command.disabled = !states.every(state => state === 'og-live'); } + if (command.slug === 'execute-task') { + command.disabled = this.clientData.length === 0; + } + return command; }); } @@ -194,6 +201,18 @@ export class ExecuteCommandComponent implements OnInit { if (action === 'delete-lease') { this.deleteClientLease(); } + + if (action === 'execute-task') { + this.openExecuteTaskModal(); + } + } + + openExecuteTaskModal(): void { + this.dialog.open(ExecuteTaskNowComponent, { + width: '500px', + height: 'auto', + data: { clients: this.clientData } + }); } rebootClient(): void { diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.css b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.css new file mode 100644 index 0000000..a50deed --- /dev/null +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.css @@ -0,0 +1,41 @@ +.form-container { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; +} + +.full-width { + width: 100%; +} + +.hint-text { + margin: 0; + font-size: 12px; + color: rgba(0, 0, 0, 0.6); +} + +.mat-dialog-title { + font-size: 20px; + font-weight: 600; + margin-bottom: 12px; +} + +.mat-dialog-content { + max-height: 60vh; + overflow-y: auto; +} + +.mat-dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding: 16px; +} + +.action-container { + display: flex; + justify-content: flex-end; + gap: 1em; + padding: 1.5em; +} diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.html b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.html new file mode 100644 index 0000000..cc8a12f --- /dev/null +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.html @@ -0,0 +1,34 @@ +

{{ 'executeTaskNowModalTitle' | translate }}

+ + + + {{ 'selectTask' | translate }} + + + + + + + {{ task.name }} + + + + +

{{ 'loadingDots' | translate }}

+

{{ 'noTasksAvailableMessage' | translate }}

+
+ + + + + diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.ts new file mode 100644 index 0000000..fcf3d75 --- /dev/null +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-task-now/execute-task-now.component.ts @@ -0,0 +1,105 @@ +import { inject } from '@angular/core'; +import { TranslateService } from '@ngx-translate/core'; +import { Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { HttpClient } from '@angular/common/http'; +import { FormControl } from '@angular/forms'; +import { BehaviorSubject, combineLatest, Observable } from 'rxjs'; +import { map, startWith } from 'rxjs/operators'; +import { ToastrService } from 'ngx-toastr'; +import { ConfigService } from '@services/config.service'; + +@Component({ + selector: 'app-execute-task-now', + templateUrl: './execute-task-now.component.html', + styleUrls: ['./execute-task-now.component.css'] +}) +export class ExecuteTaskNowComponent implements OnInit { + public translate = inject(TranslateService); + baseUrl: string; + tasks: any[] = []; + tasksLoading = true; + taskControl = new FormControl(); + filteredTasks$!: Observable; + selectedTask: any = null; + private tasks$ = new BehaviorSubject([]); + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + private http: HttpClient, + private configService: ConfigService, + private toastService: ToastrService, + ) { + this.baseUrl = this.configService.apiUrl; + } + + ngOnInit(): void { + this.loadTasks(); + + this.filteredTasks$ = combineLatest([ + this.taskControl.valueChanges.pipe(startWith('')), + this.tasks$ + ]).pipe( + map(([value]) => this._filterTasks(typeof value === 'string' ? value : value?.name)) + ); + } + + loadTasks(): void { + this.http.get(`${this.baseUrl}/command-tasks?page=1&itemsPerPage=100`).subscribe( + response => { + this.tasks = response['hydra:member'] || []; + this.tasksLoading = false; + this.tasks$.next(this.tasks); + }, + error => { + this.tasksLoading = false; + this.toastService.error(this.translate.instant('errorLoadingTasks'), error); + } + ); + } + + private _filterTasks(value: string): any[] { + const filterValue = (value || '').toLowerCase(); + return this.tasks.filter(task => task.name.toLowerCase().includes(filterValue)); + } + + displayFn(task: any): string { + return task && task.name ? task.name : ''; + } + + onTaskSelected(task: any): void { + this.selectedTask = task; + } + + executeNow(): void { + if (!this.selectedTask) { + return; + } + + const clients = this.data?.clients || []; + + if (clients.length === 0) { + this.toastService.error(this.translate.instant('noClientsSelected')); + return; + } + + const payload = { + clients: clients.map((client: any) => client['@id'] || `/clients/${client.uuid}`) + }; + + this.http.post(`${this.baseUrl}/command-tasks/${this.selectedTask.uuid}/execute`, payload).subscribe({ + next: () => { + this.toastService.success(this.translate.instant('taskExecutedSuccessfully')); + this.dialogRef.close(true); + }, + error: () => { + this.toastService.error(this.translate.instant('errorExecutingTaskNow')); + } + }); + } + + closeModal(): void { + this.dialogRef.close(false); + } +} diff --git a/ogWebconsole/src/app/components/global-status/global-status.component.ts b/ogWebconsole/src/app/components/global-status/global-status.component.ts index a373ca2..8c03bc0 100644 --- a/ogWebconsole/src/app/components/global-status/global-status.component.ts +++ b/ogWebconsole/src/app/components/global-status/global-status.component.ts @@ -4,6 +4,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 { AuthService } from '@services/auth.service'; @Component({ selector: 'app-global-status', @@ -67,7 +68,8 @@ export class GlobalStatusComponent implements OnInit { constructor( private configService: ConfigService, private toastService: ToastrService, - private http: HttpClient + private http: HttpClient, + private auth: AuthService ) { this.baseUrl = this.configService.apiUrl; this.dhcpsApiUrl = `${this.baseUrl}/dhcp-servers`; @@ -115,7 +117,18 @@ export class GlobalStatusComponent implements OnInit { }); } + /** + * Las sincronizaciones con la infraestructura (subredes DHCP, plantillas PXE y + * ogLives) están restringidas a ROLE_SUPER_ADMIN en ogCore; para el resto de + * perfiles devuelven "Access Denied", así que ni siquiera se lanzan. + */ + private get canSyncInfrastructure(): boolean { + return this.auth.isSuperAdmin; + } + syncSubnets() { + if (!this.canSyncInfrastructure) return; + this.http.get(`${this.dhcpsApiUrl}?page=1&itemsPerPage=10`).subscribe(data => { const dhcps = data['hydra:member'] || []; if (dhcps.length === 0) return; @@ -155,6 +168,7 @@ export class GlobalStatusComponent implements OnInit { } syncTemplates() { + if (!this.canSyncInfrastructure) return; if (this.bootServers.length === 0) return; let pending = this.bootServers.length; @@ -189,6 +203,7 @@ export class GlobalStatusComponent implements OnInit { } syncOgLives(): void { + if (!this.canSyncInfrastructure) return; if (this.bootServers.length === 0) return; let pending = this.bootServers.length; diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts index 572e6a1..05b4510 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/create-image/create-image.component.ts @@ -152,7 +152,8 @@ export class CreateClientImageComponent implements OnInit{ } loadImages() { - const url = `${this.baseUrl}/images?created=false&type=${this.imageType}&page=1&itemsPerPage=100`; + const repoFilter = this.selectedRepository ? `&repositoryId=${this.selectedRepository.id}` : ''; + const url = `${this.baseUrl}/images?created=false&type=${this.imageType}${repoFilter}&page=1&itemsPerPage=100`; this.http.get(url).subscribe( (response: any) => { this.images = response['hydra:member']; diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html index 6b89b06..90ee6ca 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.html @@ -52,7 +52,8 @@
-
+ @for (client of clientData; track client) { +
@@ -73,6 +74,7 @@
+ }
@@ -99,49 +101,89 @@
-
- + @if (imageType === 'monolithic') { +
+ {{ 'selectDeployMethod' | translate }} - {{ method.name }} + @for (method of allMethods; track method.value) { + {{ method.name }} + } + + + {{ 'selectRepository' | translate }} + + @for (repo of allImageRepositories; track repo.uuid) { + {{ repo.name }} ({{ repo.ip }}) + } + + @if (loadingAllRepositories) { + hourglass_empty + } + + {{ 'selectImagePrompt' | translate }} - + @for (image of images; track image.uuid || $index) { +
{{ image.name }}
{{ image.description }}
+ }
+ } -
- + @if (imageType === 'git') { +
+ + {{ 'selectRepository' | translate }} + + @for (repo of allImageRepositories; track repo.uuid) { + {{ repo.name }} ({{ repo.ip }}) + } + + @if (loadingAllRepositories) { + hourglass_empty + } + + + {{ 'buttonSelectRepository' | translate }} - + @for (repo of repositories; track repo) { + {{ repo }} + } - hourglass_empty + @if (loadingRepositories) { + hourglass_empty + } - + {{ 'buttonSelectBranch' | translate }} - + [disabled]="loadingBranches || !selectedGitRepository"> + @for (branch of branches; track branch) { + {{ branch }} + } - hourglass_empty + @if (loadingBranches) { + hourglass_empty + } - + {{ 'labelTag' | translate }} {{ 'buttonNo' | translate }} @@ -149,6 +191,7 @@
+ } @if (imageType === 'monolithic' && selectedMethod && needsAdvancedConfig(selectedMethod)) { @@ -182,9 +225,11 @@ } -
+ @if (errorMessage) { +
{{ errorMessage }}
+ }
@@ -221,6 +266,22 @@
} + + @if (showInstructions) { +
+ + + {{ 'generatedInstructions' | translate }} + + +
{{ ogInstructions }}
+
+
+
+ } + @if (deployMode !== 'update-cache') {
@@ -229,18 +290,6 @@ {{ 'partitionSelection' | translate }}
-
- - - {{ 'generatedInstructions' | translate }} - - -
{{ ogInstructions }}
-
-
-
-
+ @if (imageType === 'git' && filteredCommits.length > 0) { +
history {{ 'commitSelection' | translate }}
@@ -309,11 +359,15 @@ {{ 'labelTags' | translate }} - {{ tag }} - {{ 'noTags' | translate }} + } + @if (!commit.tags || commit.tags.length === 0) { + {{ 'noTags' | translate }} + } @@ -322,6 +376,7 @@
+ }
diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts index 8fcbf2a..f1c587c 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/deploy-image/deploy-image.component.ts @@ -63,6 +63,8 @@ export class DeployImageComponent implements OnInit { selectedModelClient: any = null; filteredPartitions: any[] = []; + allImageRepositories: any[] = []; + loadingAllRepositories: boolean = false; selectedRepository: any = null; imageType: string = 'monolithic'; @@ -145,6 +147,42 @@ export class DeployImageComponent implements OnInit { this.route.queryParams.subscribe(params => { this.runScriptContext = params['runScriptContext'] ? JSON.parse(params['runScriptContext']) : null; }); + this.loadAllRepositories(); + } + + loadAllRepositories() { + this.loadingAllRepositories = true; + this.http.get(`${this.baseUrl}/image-repositories?page=1&itemsPerPage=10000`).subscribe( + (response: any) => { + this.allImageRepositories = response?.['hydra:member'] || []; + this.loadingAllRepositories = false; + // Si ya había un selectedRepository cargado por el cliente, asegurarse de enlazar la misma referencia del array + if (this.selectedRepository) { + const match = this.allImageRepositories.find(r => r.uuid === this.selectedRepository.uuid); + if (match) { + this.selectedRepository = match; + } + } + }, + (error) => { + console.error(this.translate.instant('errorLoadingRepositories1'), error); + this.loadingAllRepositories = false; + } + ); + } + + compareRepositories(r1: any, r2: any): boolean { + return r1 && r2 ? r1.uuid === r2.uuid : r1 === r2; + } + + onRepositorySelected(repo: any) { + this.selectedRepository = repo; + if (this.imageType === 'git') { + this.selectedGitRepository = ''; + this.selectedBranch = ''; + this.loadGitRepositories(); + } + this.loadImages(); } onImageTypeSelected(event: any) { @@ -217,6 +255,23 @@ export class DeployImageComponent implements OnInit { this.filteredPartitions = fullClientData.partitions; this.selectedRepository = fullClientData.repository ?? null; + if (this.selectedRepository?.uuid && this.allImageRepositories.length > 0) { + const match = this.allImageRepositories.find(r => r.uuid === this.selectedRepository.uuid); + if (match) { + this.selectedRepository = match; + } + } else if (this.selectedRepository?.uuid) { + // Fallback por si la llamada de loadAllRepositories no ha terminado + this.http.get(`${this.baseUrl}/image-repositories/${this.selectedRepository.uuid}`).subscribe( + (repository: any) => { + this.selectedRepository = repository; + }, + (error) => { + console.error(this.translate.instant('errorLoadingRepositories1'), error); + } + ); + } + if (fullClientData.partitions) { this.filteredPartitions = fullClientData.partitions.filter((partition: any) => { return partition.partitionNumber !== 0; @@ -256,7 +311,9 @@ export class DeployImageComponent implements OnInit { if (this.imageType === 'monolithic') { url = `${this.baseUrl}/image-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`; } else { - url = `${this.baseUrl}/git-image-repositories?status=success&repository.id=${repositoryId}&page=1&itemsPerPage=1000`; + // Para 'git', la carga de repositorios y ramas se maneja con loadGitRepositories() y loadGitBranches() + this.images = []; + return; } this.http.get(url).subscribe( @@ -586,9 +643,79 @@ export class DeployImageComponent implements OnInit { }); } + generateOgInstructions() { this.showInstructions = true; - this.ogInstructions = `og-deploy-image --image ${this.selectedImage.name} --partition ${this.selectedPartition?.partitionNumber || 1} --method ${this.selectedMethod}`; + this.ogInstructions = this.imageType === 'git' + ? this.buildGitOgInstructions() + : this.buildMonolithicOgInstructions(); + } + + private getProto(method: string | null): string { + switch (method) { + case 'uftp-direct': + case 'udpcast-direct': + return 'MULTICAST-DIRECT'; + case 'multicast': + case 'uftp': + case 'udpcast': + return 'MULTICAST'; + case 'unicast-direct': + return 'UNICAST-DIRECT'; + case 'unicast': + return 'UNICAST'; + case 'p2p': + return 'TORRENT'; + default: + return (method || '').toUpperCase(); + } + } + + private getProtoOpt(method: string | null): string { + if (method === 'udpcast' || method === 'uftp' || method === 'udpcast-direct') { + const mcastMode = `${this.mcastMode}-duplex`; + const mcastSpeed = `${this.mcastSpeed}M`; + return `${this.mcastPort}:${mcastMode}:${this.mcastIp}:${mcastSpeed}:${this.mcastMaxClients}:${this.mcastMaxTime}`; + } + if (method === 'p2p') { + return `${this.p2pMode}:${this.p2pTime}`; + } + return ''; + } + + private buildMonolithicOgInstructions(): string { + const method = this.selectedMethod; + const proto = this.getProto(method); + const protoOpt = this.getProtoOpt(method); + + const disk = String(this.selectedPartition?.diskNumber || 1); + const par = String(this.selectedPartition?.partitionNumber || 1); + const imgname = this.selectedImage?.name || ''; + const repo = this.selectedRepository?.ip || ''; + + const effectiveDeployMode = this.isDirectMethod() ? 'deploy' : this.deployMode; + const isCache = effectiveDeployMode === 'update-cache'; + + const restaurarArgs = [disk, par, imgname, repo, proto, protoOpt].filter(a => a !== '').join(' '); + const scriptName = isCache ? 'ActualizarCache' : 'RestaurarImagen'; + + const innerCommand = isCache + ? ['updateCache', repo, `/${imgname}.img`, proto, protoOpt].filter(a => a !== '').join(' ') + : ['deployImage', repo, imgname, disk, par, proto, protoOpt].filter(a => a !== '').join(' '); + + return `${scriptName} ${restaurarArgs}\n ↳ ${innerCommand}`; + } + + private buildGitOgInstructions(): string { + const disk = String(this.selectedPartition?.diskNumber || 1); + const par = String(this.selectedPartition?.partitionNumber || 1); + const repo = this.selectedRepository?.ip || ''; + const imageName = this.selectedGitRepository || ''; + const branch = this.selectedBranch || ''; + const commit = this.selectedCommit?.hexsha || ''; + + return `RestaurarImagenGit --disk ${disk} --partition ${par} --repository ${repo} ` + + `--image-name ${imageName} --branch ${branch} --commit ${commit}`; } diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index c937b4d..7252db4 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -1,5 +1,5 @@ - @@ -16,19 +16,23 @@
- - + } + @if (auth.userCategory !== 'ou-minimal') { + + } + }}
- - - - - - + + + + + + +
+ } + + + @if (initialLoading) { +
+
- - -
- -
- - - + } @else {
-
-
@@ -77,33 +80,40 @@ {{ 'searchTree' | translate }} - + @if (treeSearchInput.value) { + + } {{ 'searchClient' | translate }} - + @if (clientSearchInput.value) { + + } - - {{ option.name }} - + @for (option of status; track option) { + + {{ option.name }} + + } - + @if (clientSearchStatusInput.value) { + + } -
- +
+
+ +
+
+

+ account_tree + {{ 'organizationalStructure' | translate }} +

+
+ +
- - -
-
-

- account_tree - {{ 'organizationalStructure' | translate }} -

-
- - +
+
+ + {{ + node.type === 'organizational-unit' ? 'business' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'dns' + : node.type === 'client' ? 'computer' + : 'folder' + }} + + {{ node.name }} +
+
+ @if (node.type === 'client') { + {{ node.ip }} + } + @if (node.children && node.children.length > 0) { + {{ node.children.length }} {{ getNodeCountLabel(node.children.length) }} + } +
+
+
+ +
-
- - - - -
- - -
-
- - {{ - node.type === 'organizational-unit' ? 'business' - : node.type === 'classrooms-group' ? 'meeting_room' - : node.type === 'classroom' ? 'school' - : node.type === 'clients-group' ? 'dns' - : node.type === 'client' ? 'computer' - : 'folder' - }} - - {{ node.name }} -
- -
- - {{ node.ip }} - - - {{ node.children.length }} {{ getNodeCountLabel(node.children.length) }} - -
+ + +
+ +
+
+ + {{ + node.type === 'organizational-unit' ? 'business' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'dns' + : node.type === 'client' ? 'computer' + : 'folder' + }} + + {{ node.name }}
- -
- +
+ @if (node.type === 'client') { + {{ node.ip }} + {{ node.mac }} + + {{ getStatusLabel(node.status) }} + + }
- - - - -
- - -
-
- - {{ - node.type === 'organizational-unit' ? 'business' - : node.type === 'classrooms-group' ? 'meeting_room' - : node.type === 'classroom' ? 'school' - : node.type === 'clients-group' ? 'dns' - : node.type === 'client' ? 'computer' - : 'folder' - }} - - {{ node.name }} -
- -
- - {{ node.ip }} - {{ node.mac }} - - {{ getStatusLabel(node.status) }} - - -
-
- -
- -
-
- -
- - - -
+
+ +
+ + + @for (command of commands; track command) { + - - - - + @if (selectedNode?.type === 'classroom') { + - - - - - - - - - - -
- - - - -
- - -
- - {{ 'labelClients' | translate }} - {{ selectedNode?.name }} - -
- + storage + {{ 'partitions' | translate }} + + + + + +
+ + +
+ + +
+ + {{ 'labelClients' | translate }} + {{ selectedNode?.name }} + +
+ @if (currentView === 'card') { + - - -
- -
- - +
+ +
+ @if (classroomCalendarUuid && !syncingUds) { + - - - - - - - list {{ 'vistalista' | translate }} - - - grid_view {{ 'vistatarjeta' | translate }} - - -
+ delete + + + + list {{ 'vistalista' | translate }} + + + grid_view {{ 'vistatarjeta' | translate }} + +
- - - - -
+ + + @if (!isLoadingClients) { +
-
- -
-
-
- computer -
-
-
{{ totalStats.total }}
-
{{ 'totalClients' | translate }}
-
-
-
-
- wifi_off -
-
-
{{ getStatusCount('off') }}
-
{{ 'offline' | translate }}
-
-
-
-
- wifi -
-
-
{{ getStatusCount('og-live') + getStatusCount('linux') + getStatusCount('windows') }}
-
{{ 'online' | translate }}
-
-
-
-
- hourglass_empty -
-
-
{{ getStatusCount('busy') }}
-
{{ 'busy' | translate }}
-
-
-
- - -
-
-
-
-
- - - Client Icon - -
- {{ client.name }} - {{ client.ip }} - {{ client.mac }} - - computer - RemotePC - -
- - - - - - - - - - - - - - - - - - - - -
-
+ @if (hasClients) { +
+ @if (currentView === 'list') { +
+
+
+ computer +
+
+
{{ totalStats.total }}
+
{{ 'totalClients' | translate }}
+
+
+
+
+ wifi_off +
+
+
{{ getStatusCount('off') }}
+
{{ 'offline' | translate }}
+
+
+
+
+ wifi +
+
+
{{ getStatusCount('og-live') + getStatusCount('linux') + getStatusCount('windows') }}
+
{{ 'online' | translate }}
+
+
+
+
+ hourglass_empty +
+
+
{{ getStatusCount('busy') }}
+
{{ 'busy' | translate }}
-
-
- - -
-
- - -
-
-
- {{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }} + } + + @if (currentView === 'card') { +
+
+
+ @for (client of arrayClients; track client) { +
+
+ + + Client Icon +
+ {{ client.name }} + {{ client.ip }} + {{ client.mac }} + @if (client.isRemotePcActive) { + + computer + RemotePC + + } +
+ + + + + + @if (syncingClientIds.has(client.uuid)) { + + + + } +
+
+
+
+ } +
+
+
+ + +
-
- - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @if (currentView === 'list') { +
+
+
+ {{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }} +
+
+ + +
+
+
+
- - - - - - -
- {{ 'labelState' | translate }} - -
-
-
- Client Icon - - - -
-
-
- {{ 'name' | translate }} - -
-
-
- {{ client.name }} -
-
-
- {{ 'labelIpCaps' | translate }} - -
-
-
- {{ client.ip }} - {{ client.mac }} -
-
-
- {{ 'firmwareType' | translate }} - -
-
- - {{ client.firmwareType }} - - {{ 'ogLiveTitle' | translate }} -
- {{ client.ogLive?.kernel }} - {{ client.ogLive?.date | date }} -
-
{{ 'maintenance' | translate }} {{ client.maintenance }} {{ 'subnet' | translate }} {{ client.subnet }} {{ 'pxeTemplate' | translate }} {{ client.pxeTemplate?.name }} {{ 'labelEditOrgUnitParent' | translate }} {{ client.parentName }} {{ 'actions' | translate }} -
- - - - -
- - - - - - - - - - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -
+ + + + + + +
+ {{ 'labelState' | translate }} + +
+
+
+ Client Icon + @if (syncingClientIds.has(client.uuid)) { + + + + } +
+
+
+ {{ 'name' | translate }} + +
+
+
+ {{ client.name }} +
+
+
+ {{ 'labelIpCaps' | translate }} + +
+
+
+ {{ client.ip }} + {{ client.mac }} +
+
+
+ {{ 'firmwareType' | translate }} + +
+
+ @if (client.firmwareType) { + + {{ client.firmwareType }} + + } + {{ 'ogLiveTitle' | translate }} +
+ {{ client.ogLive?.kernel }} + {{ client.ogLive?.date | date }} +
+
{{ 'maintenance' | translate }} {{ client.maintenance }} {{ 'subnet' | translate }} {{ client.subnet }} {{ 'pxeTemplate' | translate }} {{ client.pxeTemplate?.name }} {{ 'labelEditOrgUnitParent' | translate }} {{ client.parentName }} {{ 'actions' | translate }} +
+ + + + + + + +
+
-
- - + + + + +
+ }
- -
- + } @else { + @if (!initialLoading) { +
+ {{ 'noClients' | translate }} + error_outline +
+ } + } - -
- {{ 'noClients' | translate }} - error_outline -
-
-
- + }
- +
+} + +
+ + + + @if (auth.userCategory !== 'ou-minimal') { + + } + + @if (!syncingClientIds.has(client.uuid)) { + + } + + + + + + @if (auth.userCategory !== 'ou-minimal') { + + } + + + diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 15e6fe5..984718d 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -168,7 +168,7 @@ export class GroupsComponent implements OnInit, OnDestroy { ngOnInit(): void { this.updateGridCols(); - this.refreshData(); + this.refreshData(localStorage.getItem('lastSelectedOU') ?? undefined); window.addEventListener('resize', this.updateGridCols); this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => { @@ -340,12 +340,12 @@ export class GroupsComponent implements OnInit, OnDestroy { this.treeDataSource.data = [...this.originalTreeData]; if (selectedNodeIdOrUuid) { this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid); + } - if (this.selectedNode) { - this.treeControl.collapseAll(); - this.expandPathToNode(this.selectedNode); - this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit); - } + if (selectedNodeIdOrUuid && this.selectedNode) { + this.treeControl.collapseAll(); + this.expandPathToNode(this.selectedNode); + this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit); } else { this.treeControl.collapseAll(); if (this.treeDataSource.data.length > 0) { @@ -419,8 +419,9 @@ export class GroupsComponent implements OnInit, OnDestroy { onNodeClick(event: MouseEvent, node: TreeNode): void { event.stopPropagation(); this.selectedNode = node; + this.persistSelectedNode(node); const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); - + const checkStatusAfterLoad = node.type === NodeType.Classroom || node.type === NodeType.ClientsGroup; this.fetchClientsForNode(node, selectedClientsBeforeEdit, checkStatusAfterLoad); } @@ -428,10 +429,17 @@ export class GroupsComponent implements OnInit, OnDestroy { onMenuClick(event: Event, node: any): void { event.stopPropagation(); this.selectedNode = node; + this.persistSelectedNode(node); const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); this.fetchClientsForNode(node, selectedClientsBeforeEdit); } + private persistSelectedNode(node: TreeNode): void { + if (node.uuid) { + localStorage.setItem('lastSelectedOU', node.uuid); + } + } + diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css index 84c2429..e8e0262 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.css @@ -83,4 +83,22 @@ mat-option .unit-path { display: grid; grid-template-columns: repeat(3, 1fr); column-gap: 20px; +} + +.hardware-profile-box { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 0 12px 0; +} + +.hardware-profile-box .form-field { + flex: 1; + min-width: 0; + width: auto; +} + +.hardware-profile-link { + margin-left: auto; + color: #3f51b5; } \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html index 7dd106b..40559ff 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/manage-client/manage-client.component.html @@ -139,6 +139,26 @@
+ @if (isEditMode) { +
+ {{ 'labelHardwareProfile' | translate }} +
+ + + {{ 'noHardwareProfileAssigned' | translate }} + @for (profile of hardwareProfiles; track profile['@id']) { + {{ profile.description }} + } + + + +
+
+ } + {{ 'labelNotes' | translate }}