diff --git a/CHANGELOG.md b/CHANGELOG.md index 248a73e..9dcc555 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [1.22.0] - 2026-10-27 + +### Added +- **Diálogos Dinámicos de Parámetros y Confirmación en Comandos de Plugins (`PluginCommandModalComponent`, `ExecuteCommandComponent`)**: + - Incorporada especificación de parámetros interactivos (`params?: CommandParam[]`) y mensajes de confirmación (`confirm?: string; confirmKey?: string`) en la interfaz declarativa `PluginCommand`. + - Creado el componente modal genérico `PluginCommandModalComponent` que renderiza dinámicamente formularios reactivos con soporte para campos de texto, números, selectores y conmutadores (`checkbox`), respetando validaciones de obligatoriedad (`required`) e internacionalización con `@ngx-translate`. + - En `ExecuteCommandComponent`, al disparar un comando de cualquier plugin que requiera parámetros o confirmación, se abre automáticamente el modal interactivo, inyectando los valores resultantes en el cuerpo de la petición POST enviada al endpoint del plugin de forma 100% desacoplada. + - Total adhesión a la sintaxis moderna de control de flujo Angular (`@if`, `@for`, `@switch`) y separación estricta de estilos sin atributos inline. + ## [1.21.1] - 2026-10-27 ### Fixed diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index 4ace8a4..56cae83 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -1,6 +1,6 @@ { "name": "og-webconsole", - "version": "1.20.0", + "version": "1.22.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/ogWebconsole/src/app/app.module.ts b/ogWebconsole/src/app/app.module.ts index 9e1792c..758e4b8 100644 --- a/ogWebconsole/src/app/app.module.ts +++ b/ogWebconsole/src/app/app.module.ts @@ -184,6 +184,7 @@ import { SoftwareProfilePartitionComponent } from './components/commands/main-co 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 { PluginCommandModalComponent } from './shared/plugin-command-modal/plugin-command-modal.component'; import { ModalOverlayComponent } from './shared/modal-overlay/modal-overlay.component'; import { ScrollToTopComponent } from './shared/scroll-to-top/scroll-to-top.component'; import { CreateTagModalComponent } from './components/repositories/show-git-images/create-tag-modal/create-tag-modal.component'; @@ -239,6 +240,7 @@ registerLocaleData(localeEs, 'es-ES'); DeleteModalComponent, DeleteResultsModalComponent, QueueConfirmationModalComponent, + PluginCommandModalComponent, ClassroomViewComponent, ClientViewComponent, ShowOrganizationalUnitComponent, 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 a393880..6541347 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 { PluginCommandModalComponent } from '../../../../shared/plugin-command-modal/plugin-command-modal.component'; import { ExecuteTaskNowComponent } from './execute-task-now/execute-task-now.component'; import { PluginService, PluginCommand } from '@services/plugin.service'; @@ -542,6 +543,32 @@ export class ExecuteCommandComponent implements OnInit { return; } + const hasParams = !!(pluginCmd.params && pluginCmd.params.length > 0); + const hasConfirm = !!(pluginCmd.confirm || pluginCmd.confirmKey); + + if (hasParams || hasConfirm) { + const dialogRef = this.dialog.open(PluginCommandModalComponent, { + width: '450px', + data: { + command: pluginCmd, + clients: this.clientData + } + }); + + dialogRef.afterClosed().subscribe((result: any) => { + if (!result || !result.confirmed) { + return; // Cancelado por el usuario + } + const customParams = result.params || {}; + this.promptQueueAndSendPluginCommand(pluginCmd, customParams); + }); + return; + } + + this.promptQueueAndSendPluginCommand(pluginCmd, {}); + } + + private promptQueueAndSendPluginCommand(pluginCmd: PluginCommand, extraParams: Record): void { const requiredOs = (pluginCmd.os || []).map(os => os.toLowerCase()); // 1. Equipos en un sistema operativo no coincidente (ej. Linux u og-live cuando se requiere Windows) @@ -571,16 +598,16 @@ export class ExecuteCommandComponent implements OnInit { return; } - this.sendPluginCommandRequest(pluginCmd, queue); + this.sendPluginCommandRequest(pluginCmd, queue, extraParams); }); return; } // 3. Todos los equipos están en línea en el sistema operativo correcto - this.sendPluginCommandRequest(pluginCmd, false); + this.sendPluginCommandRequest(pluginCmd, false, extraParams); } - private sendPluginCommandRequest(pluginCmd: PluginCommand, queue: boolean): void { + private sendPluginCommandRequest(pluginCmd: PluginCommand, queue: boolean, extraParams: Record = {}): void { if (!pluginCmd.endpoint) { this.toastService.error('El comando no especifica un endpoint de ejecución', pluginCmd.label); return; @@ -593,7 +620,8 @@ export class ExecuteCommandComponent implements OnInit { this.http.post(url, { clients: clientIds, queue: queue, - target_os: pluginCmd.os + target_os: pluginCmd.os, + ...extraParams }).subscribe({ next: (response) => { const msg = response?.message || 'Comando procesado correctamente'; diff --git a/ogWebconsole/src/app/services/plugin.service.ts b/ogWebconsole/src/app/services/plugin.service.ts index 1ceb07c..8d88431 100644 --- a/ogWebconsole/src/app/services/plugin.service.ts +++ b/ogWebconsole/src/app/services/plugin.service.ts @@ -5,6 +5,16 @@ import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { ConfigService } from './config.service'; +export interface CommandParam { + key: string; + label: string; + labelKey?: string; + type: 'text' | 'number' | 'select' | 'checkbox'; + options?: string[]; + default?: any; + required?: boolean; +} + export interface PluginCommand { slug: string; translationKey?: string; @@ -14,6 +24,9 @@ export interface PluginCommand { disabled: boolean; endpoint?: string; os?: string[]; // Ej: ['windows'], ['linux'] + params?: CommandParam[]; + confirm?: string; + confirmKey?: string; } export interface PluginBadgeRule { diff --git a/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.css b/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.css new file mode 100644 index 0000000..6bd7dd3 --- /dev/null +++ b/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.css @@ -0,0 +1,141 @@ +.plugin-modal-header { + padding: 1.25rem 1.5rem 0.5rem 1.5rem; +} + +.header-title-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.header-icon { + color: #1976d2; + font-size: 1.75rem; + width: 1.75rem; + height: 1.75rem; +} + +.dialog-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-primary, #1e293b); +} + +.target-badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-top: 0.35rem; + padding: 0.2rem 0.6rem; + background-color: var(--badge-bg, #f1f5f9); + color: var(--badge-color, #475569); + border-radius: 9999px; + font-size: 0.8rem; + font-weight: 500; +} + +.target-icon { + font-size: 1rem; + width: 1rem; + height: 1rem; + color: #64748b; +} + +.plugin-modal-content { + padding: 1rem 1.5rem; + min-width: 360px; +} + +.confirm-message-box { + display: flex; + align-items: flex-start; + gap: 0.75rem; + padding: 0.85rem 1rem; + background-color: #fffbeb; + border-left: 4px solid #f59e0b; + border-radius: 6px; + margin-bottom: 1.25rem; +} + +.confirm-icon { + color: #d97706; + font-size: 1.5rem; + width: 1.5rem; + height: 1.5rem; + flex-shrink: 0; + margin-top: 0.1rem; +} + +.confirm-text { + margin: 0; + font-size: 0.95rem; + color: #92400e; + line-height: 1.4; +} + +.params-form-grid { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.param-row { + display: flex; + flex-direction: column; +} + +.w-100 { + width: 100%; +} + +.toggle-container { + padding: 0.5rem 0; +} + +.plugin-modal-actions { + display: flex; + justify-content: flex-end; + gap: 0.75rem; + padding: 1rem 1.5rem 1.5rem 1.5rem; +} + +.action-button { + background-color: #1976d2; + color: #ffffff; + padding: 0.5rem 1.25rem; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 0.95rem; + font-weight: 500; + transition: background-color 0.2s ease; + font-family: inherit; +} + +.action-button:hover:not(:disabled) { + background-color: #1565c0; +} + +.action-button:disabled { + background-color: #cbd5e1; + color: #94a3b8; + cursor: not-allowed; +} + +.ordinary-button { + background-color: #f1f5f9; + color: #334155; + padding: 0.5rem 1.25rem; + border: 1px solid #cbd5e1; + border-radius: 6px; + cursor: pointer; + font-size: 0.95rem; + font-weight: 500; + transition: background-color 0.2s ease; + font-family: inherit; +} + +.ordinary-button:hover:not(:disabled) { + background-color: #e2e8f0; +} diff --git a/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.html b/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.html new file mode 100644 index 0000000..319e9f2 --- /dev/null +++ b/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.html @@ -0,0 +1,83 @@ +
+
+ @if (command.icon) { + {{ command.icon }} + } +

+ {{ getCommandTitle() }} +

+
+ @if (getTargetDescription(); as target) { + + devices + {{ target }} + + } +
+ + + @if (getConfirmMessage(); as confirmMsg) { +
+ warning_amber +

{{ confirmMsg }}

+
+ } + + @if (command.params && command.params.length > 0) { +
+ @for (param of command.params; track param.key) { +
+ @switch (param.type) { + @case ('text') { + + {{ getParamLabel(param) }} + + @if (param.required) { + Campo obligatorio + } + + } + + @case ('number') { + + {{ getParamLabel(param) }} + + @if (param.required) { + Campo obligatorio + } + + } + + @case ('select') { + + {{ getParamLabel(param) }} + + @for (opt of param.options; track opt) { + {{ opt }} + } + + + } + + @case ('checkbox') { +
+ + {{ getParamLabel(param) }} + +
+ } + } +
+ } +
+ } +
+ + + + + diff --git a/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.ts b/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.ts new file mode 100644 index 0000000..faca1a8 --- /dev/null +++ b/ogWebconsole/src/app/shared/plugin-command-modal/plugin-command-modal.component.ts @@ -0,0 +1,102 @@ +import { Component, Inject, OnInit, inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TranslateService } from '@ngx-translate/core'; +import { PluginCommand, CommandParam } from '../../services/plugin.service'; + +export interface PluginCommandModalData { + command: PluginCommand; + clients: any[]; +} + +@Component({ + selector: 'app-plugin-command-modal', + templateUrl: './plugin-command-modal.component.html', + styleUrl: './plugin-command-modal.component.css' +}) +export class PluginCommandModalComponent implements OnInit { + private translate = inject(TranslateService); + + command: PluginCommand; + clients: any[] = []; + formValues: Record = {}; + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: PluginCommandModalData + ) { + this.command = data.command; + this.clients = data.clients || []; + } + + ngOnInit(): void { + if (this.command.params) { + for (const param of this.command.params) { + this.formValues[param.key] = param.default !== undefined ? param.default : (param.type === 'checkbox' ? false : ''); + } + } + } + + getCommandTitle(): string { + const key = this.command.translationKey || this.command.labelKey; + if (key) { + const trans = this.translate.instant(key); + if (trans && trans !== key) return trans; + } + return this.command.label || this.command.slug; + } + + getConfirmMessage(): string | null { + if (this.command.confirmKey) { + const trans = this.translate.instant(this.command.confirmKey); + if (trans && trans !== this.command.confirmKey) return trans; + } + return this.command.confirm || null; + } + + getParamLabel(param: CommandParam): string { + if (param.labelKey) { + const trans = this.translate.instant(param.labelKey); + if (trans && trans !== param.labelKey) return trans; + } + return param.label || param.key; + } + + getTargetDescription(): string { + if (!this.clients || this.clients.length === 0) { + return ''; + } + if (this.clients.length === 1) { + return this.clients[0].name || this.clients[0].ip || 'Equipo seleccionado'; + } + return `${this.clients.length} equipos seleccionados`; + } + + isValid(): boolean { + if (!this.command.params || this.command.params.length === 0) { + return true; + } + for (const param of this.command.params) { + if (param.required) { + const val = this.formValues[param.key]; + if (val === undefined || val === null || val === '') { + return false; + } + } + } + return true; + } + + onCancel(): void { + this.dialogRef.close(); + } + + onSubmit(): void { + if (!this.isValid()) { + return; + } + this.dialogRef.close({ + confirmed: true, + params: { ...this.formValues } + }); + } +}