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`.
|
- 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.
|
- 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.
|
- 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`).
|
- 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.
|
- 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.
|
- 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;
|
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 {
|
.action-btn-icon {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
|
|
@ -392,7 +404,7 @@
|
||||||
border-top: none;
|
border-top: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-save, .btn-reset, .btn-regenerate {
|
.btn-save, .btn-reset {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,6 @@
|
||||||
<mat-icon>restart_alt</mat-icon>
|
<mat-icon>restart_alt</mat-icon>
|
||||||
Restaurar por defecto
|
Restaurar por defecto
|
||||||
</button>
|
</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">
|
<button mat-raised-button color="primary" (click)="save()" class="btn-save">
|
||||||
<mat-icon>save</mat-icon>
|
<mat-icon>save</mat-icon>
|
||||||
Guardar Configuración
|
Guardar Configuración
|
||||||
|
|
@ -58,7 +52,7 @@
|
||||||
@switch (field.type) {
|
@switch (field.type) {
|
||||||
@case ('select') {
|
@case ('select') {
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<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) {
|
@for (opt of field.options; track opt) {
|
||||||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||||||
}
|
}
|
||||||
|
|
@ -68,7 +62,7 @@
|
||||||
|
|
||||||
@case ('checkbox') {
|
@case ('checkbox') {
|
||||||
<div class="toggle-wrapper">
|
<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' }}
|
{{ configData[field.key] ? 'Habilitado' : 'Deshabilitado' }}
|
||||||
</mat-slide-toggle>
|
</mat-slide-toggle>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -76,13 +70,13 @@
|
||||||
|
|
||||||
@case ('number') {
|
@case ('number') {
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<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>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
|
|
||||||
@case ('text') {
|
@case ('text') {
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<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>
|
</mat-form-field>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -96,6 +90,12 @@
|
||||||
<span class="code-lang-badge">PowerShell</span>
|
<span class="code-lang-badge">PowerShell</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="code-header-actions">
|
<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">
|
<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>
|
<mat-icon class="action-btn-icon">content_copy</mat-icon>
|
||||||
<span>Copiar</span>
|
<span>Copiar</span>
|
||||||
|
|
@ -125,7 +125,6 @@
|
||||||
class="code-raw-textarea"
|
class="code-raw-textarea"
|
||||||
rows="9"
|
rows="9"
|
||||||
[(ngModel)]="configData[field.key]"
|
[(ngModel)]="configData[field.key]"
|
||||||
(ngModelChange)="onManualScriptInput(field.key)"
|
|
||||||
spellcheck="false"
|
spellcheck="false"
|
||||||
></textarea>
|
></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
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 { PluginManifest, PluginService, SchemaField } from '@services/plugin.service';
|
||||||
|
import { ConfigService } from '@services/config.service';
|
||||||
import { ToastrService } from 'ngx-toastr';
|
import { ToastrService } from 'ngx-toastr';
|
||||||
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
import { DomSanitizer, SafeHtml } from '@angular/platform-browser';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
|
|
@ -16,10 +18,11 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
schema: SchemaField[] = [];
|
schema: SchemaField[] = [];
|
||||||
configData: Record<string, any> = {};
|
configData: Record<string, any> = {};
|
||||||
editingScripts: Record<string, boolean> = {};
|
editingScripts: Record<string, boolean> = {};
|
||||||
hasManualScriptEdits: boolean = false;
|
|
||||||
|
|
||||||
private pluginSub?: Subscription;
|
private pluginSub?: Subscription;
|
||||||
private sanitizer = inject(DomSanitizer);
|
private sanitizer = inject(DomSanitizer);
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private configService = inject(ConfigService);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private pluginService: PluginService,
|
private pluginService: PluginService,
|
||||||
|
|
@ -52,18 +55,26 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hasScriptFields(): boolean {
|
runFieldAction(field: SchemaField): void {
|
||||||
return this.schema.some(f => f.key === 'freeze_script' || f.key === 'unfreeze_script');
|
if (!field.action || !field.action.endpoint) return;
|
||||||
}
|
|
||||||
|
|
||||||
onParamChange(): void {
|
const apiUrl = this.configService.apiUrl || '';
|
||||||
if (!this.hasManualScriptEdits) {
|
const cleanEndpoint = field.action.endpoint.replace(/^\/api/, '');
|
||||||
this.regenerateScripts(false);
|
const url = `${apiUrl}${cleanEndpoint}`;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onManualScriptInput(key: string): void {
|
this.http.post<any>(url, { config: this.configData }).subscribe({
|
||||||
this.hasManualScriptEdits = true;
|
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 {
|
isEditingScript(key: string): boolean {
|
||||||
|
|
@ -142,64 +153,6 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
return this.sanitizer.bypassSecurityTrustHtml(escaped);
|
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 {
|
save(): void {
|
||||||
if (!this.pluginName) return;
|
if (!this.pluginName) return;
|
||||||
this.pluginService.savePluginConfig(this.pluginName, this.configData);
|
this.pluginService.savePluginConfig(this.pluginName, this.configData);
|
||||||
|
|
@ -209,7 +162,6 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
reset(): void {
|
reset(): void {
|
||||||
if (!this.pluginName) return;
|
if (!this.pluginName) return;
|
||||||
this.configData = this.pluginService.resetPluginConfig(this.pluginName);
|
this.configData = this.pluginService.resetPluginConfig(this.pluginName);
|
||||||
this.hasManualScriptEdits = false;
|
|
||||||
this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin');
|
this.toast.info('Configuración restaurada a valores por defecto', this.plugin?.title || 'Plugin');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,13 @@ export interface PluginBadge {
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FieldAction {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
endpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SchemaField {
|
export interface SchemaField {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
|
|
@ -29,6 +36,7 @@ export interface SchemaField {
|
||||||
type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea';
|
type: 'text' | 'number' | 'select' | 'checkbox' | 'textarea';
|
||||||
options?: string[];
|
options?: string[];
|
||||||
default: any;
|
default: any;
|
||||||
|
action?: FieldAction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginManifest {
|
export interface PluginManifest {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue