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 }}
+
+ visibility
+
({{ '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 }}
+
+
+ {{ 'buttonClose' | translate }}
+
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 }}
+
+
+
+ arrow_drop_down
+
+
+
+
+ {{ task.name }}
+
+
+
+
+ {{ 'loadingDots' | translate }}
+ {{ 'noTasksAvailableMessage' | translate }}
+
+
+
+ {{ 'buttonCancel' | translate }}
+
+ {{ 'executeButton' | 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) {
+
+ }
@@ -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 }}
+ close
+
+
+
+ {{ ogInstructions }}
+
+
+
+ }
+
@if (deployMode !== 'update-cache') {
@@ -229,18 +290,6 @@
{{ 'partitionSelection' | translate }}
-
-
-
- {{ 'generatedInstructions' | translate }}
- close
-
-
-
- {{ ogInstructions }}
-
-
-
- 0" class="form-section compact-section">
+ @if (imageType === 'git' && filteredCommits.length > 0) {
+
+ }
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 @@
-
-
-
-
-
+ } @else {
-
-
@@ -77,33 +80,40 @@
{{ 'searchTree' | translate }}
-
- close
-
+ @if (treeSearchInput.value) {
+
+ close
+
+ }
{{ 'searchClient' | translate }}
-
- close
-
+ @if (clientSearchInput.value) {
+
+ close
+
+ }
-
- {{ option.name }}
-
+ @for (option of status; track option) {
+
+ {{ option.name }}
+
+ }
-
- close
-
+ @if (clientSearchStatusInput.value) {
+
+ close
+
+ }
-
-
+
+
+
+
diff --git a/ogWebconsole/src/app/components/hardware-profile/hardware-profile.component.ts b/ogWebconsole/src/app/components/hardware-profile/hardware-profile.component.ts
index 3ea2a45..06159d5 100644
--- a/ogWebconsole/src/app/components/hardware-profile/hardware-profile.component.ts
+++ b/ogWebconsole/src/app/components/hardware-profile/hardware-profile.component.ts
@@ -37,6 +37,7 @@ export class HardwareProfileComponent {
filteredClients!: Observable;
clientControl = new FormControl();
clients: any[] = [];
+ maxVisibleClients: number = 3;
columns = [
{
columnDef: 'id',
@@ -49,13 +50,9 @@ export class HardwareProfileComponent {
cell: (hardware: any) => hardware.description
},
{
- columnDef: 'client',
- header: 'client',
- cell: (hardware: any) => hardware.client ? {
- name: hardware.client.name || 'N/A',
- ip: hardware.client.ip || 'N/A',
- mac: hardware.client.mac || 'N/A'
- } : 'N/A'
+ columnDef: 'clients',
+ header: 'clients',
+ cell: (hardware: any) => hardware.clients?.length ? hardware.clients : 'N/A'
},
{
columnDef: 'createdAt',
@@ -104,16 +101,24 @@ export class HardwareProfileComponent {
}
onOptionClientSelected(selectedClient: any): void {
- this.filters['client.id'] = selectedClient.id;
+ this.filters['clients.id'] = selectedClient.id;
this.search();
}
clearClientFilter(event: Event, clientSearchClientInput: any): void {
clientSearchClientInput.value = '';
- this.filters['client.id'] = '';
+ this.filters['clients.id'] = '';
this.search();
}
+ getVisibleClients(clients: any): any[] {
+ return (clients || []).slice(0, this.maxVisibleClients);
+ }
+
+ getHiddenClientNames(clients: any): string {
+ return (clients || []).slice(this.maxVisibleClients).map((client: any) => client.name).join(', ');
+ }
+
private _filterClients(value: string): any[] {
const filterValue = value.toLowerCase();
diff --git a/ogWebconsole/src/app/components/images/create-image/create-image.component.html b/ogWebconsole/src/app/components/images/create-image/create-image.component.html
index 26df815..f43bdc0 100644
--- a/ogWebconsole/src/app/components/images/create-image/create-image.component.html
+++ b/ogWebconsole/src/app/components/images/create-image/create-image.component.html
@@ -32,15 +32,6 @@
-
- {{ 'softwareProfile' | translate }}
-
-
- {{ profile.description }}
-
-
-
-
{{ 'labelRemotePc' | translate }}
diff --git a/ogWebconsole/src/app/components/images/create-image/create-image.component.ts b/ogWebconsole/src/app/components/images/create-image/create-image.component.ts
index 99ff396..ae5600c 100644
--- a/ogWebconsole/src/app/components/images/create-image/create-image.component.ts
+++ b/ogWebconsole/src/app/components/images/create-image/create-image.component.ts
@@ -18,7 +18,6 @@ export class CreateImageComponent implements OnInit {
baseUrl: string;
imageForm: FormGroup;
imageId: string | null = null;
- softwareProfiles: any[] = [];
repositories: any[] = [];
loading: boolean = false;
isEditMode: boolean = false;
@@ -41,7 +40,6 @@ export class CreateImageComponent implements OnInit {
comments: [''],
remotePc: [false],
isGlobal: [false],
- softwareProfile: [''],
imageRepositories: [[], Validators.required],
});
}
@@ -54,7 +52,6 @@ export class CreateImageComponent implements OnInit {
} else {
this.loading = false;
}
- this.fetchSoftwareProfiles();
this.fetchRepositories();
}
@@ -67,7 +64,6 @@ export class CreateImageComponent implements OnInit {
comments: response.comments,
remotePc: response.remotePc,
isGlobal: response.isGlobal,
- softwareProfile: response.softwareProfile ? response.softwareProfile['@id'] : null,
imageRepositories: response.imageRepositories ? response.imageRepositories.map((r: any) => r.imageRepository['@id']) : [],
});
this.imageId = response['@id'];
@@ -85,18 +81,6 @@ export class CreateImageComponent implements OnInit {
this.showWarning = this.imageForm.get('isGlobal')?.value;
}
- fetchSoftwareProfiles() {
- const url = `${this.baseUrl}/software-profiles`;
- this.http.get(url).subscribe({
- next: (response: any) => {
- this.softwareProfiles = response['hydra:member'];
- },
- error: (error) => {
- this.toastService.error(this.translate.instant('errorGettingProfiles'));
- }
- });
- }
-
fetchRepositories() {
const url = `${this.baseUrl}/image-repositories`;
this.http.get(url).subscribe({
@@ -117,7 +101,6 @@ export class CreateImageComponent implements OnInit {
remotePc: this.imageForm.value.remotePc,
isGlobal: this.imageForm.value.isGlobal,
imageRepositories: this.imageForm.value.imageRepositories,
- ...(this.imageForm.value.softwareProfile ? { softwareProfile: this.imageForm.value.softwareProfile } : {}),
};
if (this.imageId) {
diff --git a/ogWebconsole/src/app/components/login/login.component.ts b/ogWebconsole/src/app/components/login/login.component.ts
index c5738be..b808a8c 100644
--- a/ogWebconsole/src/app/components/login/login.component.ts
+++ b/ogWebconsole/src/app/components/login/login.component.ts
@@ -64,10 +64,14 @@ export class LoginComponent {
this.auth.refresh();
this.openSnackBar(false, 'Bienvenido ' + this.auth.username);
this.router.navigateByUrl('/groups');
- this.dialog.open(GlobalStatusComponent, {
- width: '65vw',
- height: '80vh',
- });
+ // El estado global consulta y sincroniza infraestructura (ogBoot, DHCP,
+ // repositorios), a la que solo tienen acceso los perfiles de administración.
+ if (this.auth.isSuperAdmin || this.auth.isOUAdmin) {
+ this.dialog.open(GlobalStatusComponent, {
+ width: '65vw',
+ height: '80vh',
+ });
+ }
}
this.isLoading = false;
},
diff --git a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts
index 168b7b8..0588504 100644
--- a/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts
+++ b/ogWebconsole/src/app/components/repositories/show-git-images/show-git-images.component.ts
@@ -110,6 +110,11 @@ export class ShowGitCommitsComponent implements OnInit{
if (this.repositories.length > 0) {
this.selectedRepository = this.repositories[0];
this.loadBranches();
+ } else {
+ this.selectedRepository = '';
+ this.branches = [];
+ this.selectedBranch = '';
+ this.dataSource.data = [];
}
},
error => {
@@ -344,7 +349,7 @@ export class ShowGitCommitsComponent implements OnInit{
this.http.post(`${this.baseUrl}/image-repositories/server/git/${this.data.repositoryUuid}/delete`, payload).subscribe({
next: () => {
this.toastService.success(this.translate.instant('repositoryDeletedSuccessfully'));
- this.dialogRef.close(true);
+ this.loadRepositories();
},
error: (error) => {
console.error(this.translate.instant('errorDeletingRepository1'), error);
diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.css b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.css
index 05c97d3..2f59c42 100644
--- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.css
+++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.css
@@ -108,3 +108,36 @@ table {
margin: 0 auto;
align-self: center;
}
+
+.software-profile-select {
+ width: 150px;
+ font-size: 0.8em;
+}
+
+.software-profile-select ::ng-deep .mat-mdc-text-field-wrapper {
+ padding: 0 8px;
+}
+
+.software-profile-select ::ng-deep .mat-mdc-form-field-infix {
+ min-height: unset;
+ padding-top: 8px;
+ padding-bottom: 8px;
+}
+
+.software-profile-caret {
+ cursor: pointer;
+ color: rgba(0, 0, 0, 0.54);
+ font-size: 20px;
+ width: 20px;
+ height: 20px;
+}
+
+::ng-deep .software-profile-panel {
+ max-width: 320px;
+}
+
+::ng-deep .software-profile-panel .mat-mdc-option {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html
index df7b830..4e973a5 100644
--- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html
+++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.html
@@ -83,8 +83,25 @@
{{ getStatusLabel(image[column.columnDef]) }}
+
+
+
+
+ arrow_drop_down
+
+ {{ 'noSoftwareProfileAssigned' | translate }}
+
+ {{ profile.description }}
+
+
+
+
+
+ *ngIf="column.columnDef !== 'remotePc' && column.columnDef !== 'status' && column.columnDef !== 'isGlobal' && column.columnDef !== 'name' && column.columnDef !== 'datasize' && column.columnDef !== 'softwareProfile'">
{{ column.cell(image) }}
diff --git a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts
index fa2ba97..8f89564 100644
--- a/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts
+++ b/ogWebconsole/src/app/components/repositories/show-monolitic-images/show-monolitic-images.component.ts
@@ -37,6 +37,7 @@ export class ShowMonoliticImagesComponent implements OnInit {
filters: { [key: string]: string } = {};
alertMessage: string | null = null;
repository: any = {};
+ softwareProfiles: any[] = [];
datePipe: DatePipe = new DatePipe('es-ES');
columns = [
{
@@ -70,9 +71,9 @@ export class ShowMonoliticImagesComponent implements OnInit {
cell: (image: any) => this.formatSize(image.datasize)
},
{
- columnDef: 'description',
- header: 'labelDescription',
- cell: (image: any) => image.description
+ columnDef: 'softwareProfile',
+ header: 'softwareProfile',
+ cell: (image: any) => image.softwareProfile ? image.softwareProfile['@id'] : null
},
{
columnDef: 'createdAt',
@@ -100,13 +101,63 @@ export class ShowMonoliticImagesComponent implements OnInit {
if (this.data) {
this.loadData();
}
+ this.loadSoftwareProfiles();
+ }
+
+ loadSoftwareProfiles(): void {
+ this.http.get(`${this.baseUrl}/software-profiles?page=1&itemsPerPage=10000`).subscribe({
+ next: (response) => {
+ this.softwareProfiles = response['hydra:member'];
+ },
+ error: (error) => {
+ console.error(this.translate.instant('errorGettingProfiles'), error);
+ }
+ });
+ }
+
+ filterProfiles(value: any): any[] {
+ const query = typeof value === 'string' ? value : (value?.description || '');
+ const normalized = query.toLowerCase();
+ return this.softwareProfiles.filter(profile => (profile.description || '').toLowerCase().includes(normalized));
+ }
+
+ displayProfile(value: any): string {
+ return value?.description || '';
+ }
+
+ onProfileSelected(image: any, selectedProfile: any | null): void {
+ const previousProfile = image.softwareProfile;
+
+ // Optimistic update: reflect the choice immediately so the blur/reset
+ // handler (which reads image.softwareProfile) doesn't race the PATCH
+ // response and briefly flash back to the old value.
+ image.softwareProfile = selectedProfile;
+ image.profileValue = selectedProfile;
+
+ this.http.patch(`${this.baseUrl}${image['@id']}`, { softwareProfile: selectedProfile ? selectedProfile['@id'] : null }).subscribe({
+ next: () => {
+ this.toastService.success(this.translate.instant('successfullyEditedSoftware'));
+ },
+ error: (error) => {
+ image.softwareProfile = previousProfile;
+ image.profileValue = previousProfile;
+ this.toastService.error(error.error['hydra:description']);
+ }
+ });
+ }
+
+ resetProfileSearchIfInvalid(image: any): void {
+ image.profileValue = image.softwareProfile || null;
}
loadData(): void {
this.loading = true;
this.http.get(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}&repository.id=${this.data.repositoryId}`, { params: this.filters }).subscribe(
data => {
- this.dataSource.data = data['hydra:member'];
+ this.dataSource.data = (data['hydra:member'] || []).map((item: any) => ({
+ ...item,
+ profileValue: item.softwareProfile || null
+ }));
this.length = data['hydra:totalItems'];
this.loading = false;
},
diff --git a/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.css b/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.css
index 0ec67f1..f2e2a3b 100644
--- a/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.css
+++ b/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.css
@@ -63,4 +63,28 @@
justify-content: flex-end;
gap: 1em;
padding: 0em 1.5em 1.5em 1.5em;
+}
+
+.software-tab-hint {
+ color: #666;
+ font-size: 0.85em;
+ margin: 0 0 12px 0;
+}
+
+.software-list-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-top: 8px;
+ font-size: 0.85em;
+ color: #555;
+}
+
+.clear-selection-link {
+ background: none;
+ border: none;
+ color: #3f51b5;
+ cursor: pointer;
+ text-decoration: underline;
+ padding: 0;
}
\ No newline at end of file
diff --git a/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.html b/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.html
index d19dfed..7f56836 100644
--- a/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.html
+++ b/ogWebconsole/src/app/components/software-profile/create-software-profile/create-software-profile.component.html
@@ -12,14 +12,6 @@
{{ 'labelComments' | translate }}
-
- {{ 'selectOrganizationalUnit' | translate }}
-
-
- {{ ou.name }}
-
-
-
{{ 'selectOperativeSystem' | translate }}
@@ -33,11 +25,13 @@