feat(plugins): implement declarative field.action contract standard and decouple script generation
parent
063762e1e4
commit
27a9cc3f7f
|
|
@ -7,7 +7,7 @@
|
|||
- Incorporada infraestructura agnóstica para descubrimiento y gestión de plugins desacoplados a través de `/api/plugins`.
|
||||
- Agrupación ordenada de comandos de plugins en submenús dedicados en la consola de comandos (`execute-command`), organizados por plugin para mantener el menú limpio.
|
||||
- Comprobación y filtrado dinámico de compatibilidad por Sistema Operativo (`os: ['windows']`), deshabilitando automáticamente los comandos en puestos con sistemas incompatibles.
|
||||
- Generador dinámico de formularios de configuración basados en esquemas JSON (`app-plugin-settings`) con botón de autogeneración y actualización reactiva de scripts según los parámetros seleccionados.
|
||||
- Estándar de Contrato y Acciones de Campos para Plugins (`PluginSettingsComponent`): Incorporado soporte para acciones declarativas por campo (`field.action`) ejecutadas contra el backend del plugin mediante el contrato estándar `{ config: ... }` -> `{ data: { [key]: value }, message: "..." }`. Los botones de asistencia (como la generación de scripts) se integran directamente en la cabecera de la caja del script y la lógica reside 100% en el backend del plugin, manteniendo el núcleo de `oggui` completamente agnóstico y desacoplado.
|
||||
- Soporte de insignias de estado (badges) contextuales por cliente en las vistas de mosaico y lista de equipos (`groups.component`).
|
||||
- Nuevo elemento condicional "Plugins" en el menú lateral (`sidebar`), visible únicamente cuando existen plugins activos en el sistema.
|
||||
- Resaltador de sintaxis enriquecido y visor de terminal para scripts de configuración con numeración de líneas, colores semánticos (comentarios en verde itálica, comandos, parámetros, cadenas), botón de copiado rápido y alternancia entre vista coloreada y editor manual.
|
||||
|
|
|
|||
|
|
@ -271,6 +271,18 @@
|
|||
border-color: #0284c7;
|
||||
}
|
||||
|
||||
.code-action-btn.action-helper {
|
||||
background: rgba(14, 165, 233, 0.12);
|
||||
color: #38bdf8;
|
||||
border-color: rgba(56, 189, 248, 0.35);
|
||||
}
|
||||
|
||||
.code-action-btn.action-helper:hover {
|
||||
background: rgba(14, 165, 233, 0.25);
|
||||
color: #e0f2fe;
|
||||
border-color: #38bdf8;
|
||||
}
|
||||
|
||||
.action-btn-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
|
|
@ -392,7 +404,7 @@
|
|||
border-top: none;
|
||||
}
|
||||
|
||||
.btn-save, .btn-reset, .btn-regenerate {
|
||||
.btn-save, .btn-reset {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
|
|
|||
|
|
@ -27,12 +27,6 @@
|
|||
<mat-icon>restart_alt</mat-icon>
|
||||
Restaurar por defecto
|
||||
</button>
|
||||
@if (hasScriptFields()) {
|
||||
<button mat-stroked-button color="accent" (click)="regenerateScripts()" class="btn-regenerate">
|
||||
<mat-icon>auto_fix_high</mat-icon>
|
||||
Regenerar Scripts desde Parámetros
|
||||
</button>
|
||||
}
|
||||
<button mat-raised-button color="primary" (click)="save()" class="btn-save">
|
||||
<mat-icon>save</mat-icon>
|
||||
Guardar Configuración
|
||||
|
|
@ -58,7 +52,7 @@
|
|||
@switch (field.type) {
|
||||
@case ('select') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<mat-select [id]="'field-' + field.key" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
<mat-select [id]="'field-' + field.key" [(ngModel)]="configData[field.key]">
|
||||
@for (opt of field.options; track opt) {
|
||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||
}
|
||||
|
|
@ -68,7 +62,7 @@
|
|||
|
||||
@case ('checkbox') {
|
||||
<div class="toggle-wrapper">
|
||||
<mat-slide-toggle [id]="'field-' + field.key" color="primary" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
<mat-slide-toggle [id]="'field-' + field.key" color="primary" [(ngModel)]="configData[field.key]">
|
||||
{{ configData[field.key] ? 'Habilitado' : 'Deshabilitado' }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
|
|
@ -76,13 +70,13 @@
|
|||
|
||||
@case ('number') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<input matInput type="number" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
<input matInput type="number" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]">
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@case ('text') {
|
||||
<mat-form-field appearance="outline" class="w-100">
|
||||
<input matInput type="text" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]" (ngModelChange)="onParamChange()">
|
||||
<input matInput type="text" [id]="'field-' + field.key" [(ngModel)]="configData[field.key]">
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
|
|
@ -96,6 +90,12 @@
|
|||
<span class="code-lang-badge">PowerShell</span>
|
||||
</div>
|
||||
<div class="code-header-actions">
|
||||
@if (field.action) {
|
||||
<button type="button" class="code-action-btn action-helper" (click)="runFieldAction(field)" [title]="field.action.label">
|
||||
<mat-icon class="action-btn-icon">{{ field.action.icon || 'auto_fix_high' }}</mat-icon>
|
||||
<span>{{ field.action.label }}</span>
|
||||
</button>
|
||||
}
|
||||
<button type="button" class="code-action-btn" (click)="copyToClipboard(configData[field.key])" title="Copiar script">
|
||||
<mat-icon class="action-btn-icon">content_copy</mat-icon>
|
||||
<span>Copiar</span>
|
||||
|
|
@ -125,7 +125,6 @@
|
|||
class="code-raw-textarea"
|
||||
rows="9"
|
||||
[(ngModel)]="configData[field.key]"
|
||||
(ngModelChange)="onManualScriptInput(field.key)"
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { PluginManifest, PluginService, SchemaField } from '@services/plugin.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||
import { Subscription } from 'rxjs';
|
||||
|
|
@ -16,10 +18,11 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
schema: SchemaField[] = [];
|
||||
configData: Record<string, any> = {};
|
||||
editingScripts: Record<string, boolean> = {};
|
||||
hasManualScriptEdits: boolean = false;
|
||||
|
||||
private pluginSub?: Subscription;
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
private http = inject(HttpClient);
|
||||
private configService = inject(ConfigService);
|
||||
|
||||
constructor(
|
||||
private pluginService: PluginService,
|
||||
|
|
@ -52,18 +55,26 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
hasScriptFields(): boolean {
|
||||
return this.schema.some(f => f.key === 'freeze_script' || f.key === 'unfreeze_script');
|
||||
}
|
||||
runFieldAction(field: SchemaField): void {
|
||||
if (!field.action || !field.action.endpoint) return;
|
||||
|
||||
onParamChange(): void {
|
||||
if (!this.hasManualScriptEdits) {
|
||||
this.regenerateScripts(false);
|
||||
}
|
||||
}
|
||||
const apiUrl = this.configService.apiUrl || '';
|
||||
const cleanEndpoint = field.action.endpoint.replace(/^\/api/, '');
|
||||
const url = `${apiUrl}${cleanEndpoint}`;
|
||||
|
||||
onManualScriptInput(key: string): void {
|
||||
this.hasManualScriptEdits = true;
|
||||
this.http.post<any>(url, { config: this.configData }).subscribe({
|
||||
next: (response) => {
|
||||
if (response && response.data) {
|
||||
this.configData = { ...this.configData, ...response.data };
|
||||
}
|
||||
const message = response?.message || `${field.action?.label} completado correctamente`;
|
||||
this.toast.info(message, this.plugin?.title || 'Plugin');
|
||||
},
|
||||
error: (err) => {
|
||||
const errorMsg = err?.error?.message || 'Error al ejecutar la acción del plugin';
|
||||
this.toast.error(errorMsg, field.action?.label || 'Plugin');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isEditingScript(key: string): boolean {
|
||||
|
|
@ -142,64 +153,6 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
return this.sanitizer.bypassSecurityTrustHtml(escaped);
|
||||
}
|
||||
|
||||
regenerateScripts(showToast: boolean = true): void {
|
||||
if (!this.pluginName) return;
|
||||
|
||||
if (this.pluginName === 'ogfreeze') {
|
||||
const overlayType = this.configData['overlay_type'] || 'RAM';
|
||||
const overlaySize = this.configData['overlay_size'] || 2048;
|
||||
const protectC = this.configData['protect_c'] !== false;
|
||||
|
||||
// Obtener exclusiones de forma estricta (no usar fallback si el usuario las borró)
|
||||
const rawExcl = this.configData['exclusions'];
|
||||
const exclusionsRaw = (rawExcl !== undefined && rawExcl !== null) ? String(rawExcl).trim() : '';
|
||||
const exclusions = exclusionsRaw.length > 0
|
||||
? exclusionsRaw.split(',').map((s: string) => s.trim()).filter((s: string) => s.length > 0)
|
||||
: [];
|
||||
|
||||
let freezeScript = '';
|
||||
let step = 1;
|
||||
|
||||
// Solo incluir el paso de exclusiones si el usuario ha especificado rutas
|
||||
if (exclusions.length > 0) {
|
||||
freezeScript += `# ${step}. Excluir metadatos y carpetas del filtro UWF\n`;
|
||||
for (const excl of exclusions) {
|
||||
freezeScript += `uwfmgr.exe file add-exclusion "${excl}"\n`;
|
||||
}
|
||||
freezeScript += `\n`;
|
||||
step++;
|
||||
}
|
||||
|
||||
freezeScript += `# ${step}. Configurar buffer temporal de escritura\n`;
|
||||
freezeScript += `uwfmgr.exe overlay set-type ${overlayType}\n`;
|
||||
freezeScript += `uwfmgr.exe overlay set-size ${overlaySize}\n\n`;
|
||||
step++;
|
||||
|
||||
freezeScript += `# ${step}. Proteger volumen y habilitar filtro\n`;
|
||||
if (protectC) {
|
||||
freezeScript += `uwfmgr.exe volume protect C:\n`;
|
||||
}
|
||||
freezeScript += `uwfmgr.exe filter enable\n\n`;
|
||||
step++;
|
||||
|
||||
freezeScript += `# ${step}. Reinicio obligatorio para que entre en vigor\n`;
|
||||
freezeScript += `shutdown /r /t 5 -c "OpenGnsys: Reiniciando para aplicar congelacion de sistema"`;
|
||||
|
||||
this.configData['freeze_script'] = freezeScript;
|
||||
this.configData['unfreeze_script'] = `# Desactivar filtro de protección UWF en el próximo inicio\nuwfmgr.exe filter disable\n\n# Reiniciar para aplicar cambios\nshutdown /r /t 5 -c "OpenGnsys: Reiniciando para descongelar sistema"`;
|
||||
|
||||
this.hasManualScriptEdits = false;
|
||||
|
||||
if (showToast) {
|
||||
this.toast.info('Scripts de PowerShell generados a partir de los parámetros actuales', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
} else {
|
||||
if (showToast) {
|
||||
this.toast.info('Scripts actualizados con los parámetros predeterminados', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.pluginName) return;
|
||||
this.pluginService.savePluginConfig(this.pluginName, this.configData);
|
||||
|
|
@ -209,7 +162,6 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
reset(): void {
|
||||
if (!this.pluginName) return;
|
||||
this.configData = this.pluginService.resetPluginConfig(this.pluginName);
|
||||
this.hasManualScriptEdits = false;
|
||||
this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ export interface PluginBadge {
|
|||
tooltip: string;
|
||||
}
|
||||
|
||||
export interface FieldAction {
|
||||
id: string;
|
||||
label: string;
|
||||
icon?: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export interface SchemaField {
|
||||
key: string;
|
||||
label: string;
|
||||
|
|
@ -29,6 +36,7 @@ export interface SchemaField {
|
|||
type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea';
|
||||
options?: string[];
|
||||
default: any;
|
||||
action?: FieldAction;
|
||||
}
|
||||
|
||||
export interface PluginManifest {
|
||||
|
|
|
|||
Loading…
Reference in New Issue