feat(commands): add generic plugin command parameter modal and confirmation dialog (1.22.0)
parent
4d755cca1f
commit
f8bd02c316
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "1.20.0",
|
||||
"version": "1.22.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, any>): 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<string, any> = {}): 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<any>(url, {
|
||||
clients: clientIds,
|
||||
queue: queue,
|
||||
target_os: pluginCmd.os
|
||||
target_os: pluginCmd.os,
|
||||
...extraParams
|
||||
}).subscribe({
|
||||
next: (response) => {
|
||||
const msg = response?.message || 'Comando procesado correctamente';
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
<div class="plugin-modal-header">
|
||||
<div class="header-title-row">
|
||||
@if (command.icon) {
|
||||
<mat-icon class="header-icon">{{ command.icon }}</mat-icon>
|
||||
}
|
||||
<h2 mat-dialog-title class="dialog-title">
|
||||
{{ getCommandTitle() }}
|
||||
</h2>
|
||||
</div>
|
||||
@if (getTargetDescription(); as target) {
|
||||
<span class="target-badge">
|
||||
<mat-icon class="target-icon">devices</mat-icon>
|
||||
{{ target }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-dialog-content class="plugin-modal-content">
|
||||
@if (getConfirmMessage(); as confirmMsg) {
|
||||
<div class="confirm-message-box">
|
||||
<mat-icon class="confirm-icon">warning_amber</mat-icon>
|
||||
<p class="confirm-text">{{ confirmMsg }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (command.params && command.params.length > 0) {
|
||||
<div class="params-form-grid">
|
||||
@for (param of command.params; track param.key) {
|
||||
<div class="param-row">
|
||||
@switch (param.type) {
|
||||
@case ('text') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label>{{ getParamLabel(param) }}</mat-label>
|
||||
<input matInput [id]="'param-' + param.key" [(ngModel)]="formValues[param.key]" [required]="!!param.required">
|
||||
@if (param.required) {
|
||||
<mat-hint>Campo obligatorio</mat-hint>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@case ('number') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label>{{ getParamLabel(param) }}</mat-label>
|
||||
<input matInput type="number" [id]="'param-' + param.key" [(ngModel)]="formValues[param.key]" [required]="!!param.required">
|
||||
@if (param.required) {
|
||||
<mat-hint>Campo obligatorio</mat-hint>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@case ('select') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-label>{{ getParamLabel(param) }}</mat-label>
|
||||
<mat-select [id]="'param-' + param.key" [(ngModel)]="formValues[param.key]" [required]="!!param.required">
|
||||
@for (opt of param.options; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@case ('checkbox') {
|
||||
<div class="toggle-container">
|
||||
<mat-slide-toggle [id]="'param-' + param.key" color="primary" [(ngModel)]="formValues[param.key]">
|
||||
{{ getParamLabel(param) }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="plugin-modal-actions">
|
||||
<button type="button" class="ordinary-button" (click)="onCancel()">
|
||||
{{ 'buttonCancel' | translate }}
|
||||
</button>
|
||||
<button type="button" class="action-button" [disabled]="!isValid()" (click)="onSubmit()">
|
||||
{{ getCommandTitle() }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
|
@ -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<string, any> = {};
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<PluginCommandModalComponent>,
|
||||
@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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue