feat: soporte para activar y desactivar plugins en caliente (1.19.0)
parent
f10613af82
commit
2aea3beab8
|
|
@ -1,5 +1,14 @@
|
|||
# Changelog
|
||||
|
||||
## [1.19.0] - 2026-10-23
|
||||
|
||||
### Added
|
||||
- **Activación y Desactivación Dinámica de Plugins (`PluginService`, `plugins.component`, `plugin-settings.component`)**:
|
||||
- Incorporada la capacidad de activar y desactivar plugins en caliente sin desinstalarlos del sistema.
|
||||
- Al desactivar un plugin, sus comandos contextuales y badges se ocultan automáticamente de las tarjetas y menús de clientes en toda la consola web, preservando íntegra su configuración en base de datos.
|
||||
- En la vista de administración `/plugins`, se integró un interruptor deslizante (`mat-slide-toggle`) en la cabecera de configuración para conmutar entre `Activo` e `Inactivo` con notificación toast y actualización reactiva.
|
||||
- Añadido indicador visual de estado y conteo en la cabecera del panel (`X Activo(s) / Y Instalado(s)`) y badge `Inactivo` en las pestañas de los plugins deshabilitados.
|
||||
|
||||
## [1.18.0] - 2026-10-23
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "1.18.0",
|
||||
"version": "1.19.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
|
|
|||
|
|
@ -70,6 +70,20 @@
|
|||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.plugin-status-toggle {
|
||||
margin-left: auto;
|
||||
font-weight: 600;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.plugin-status-toggle.active {
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.plugin-status-toggle.inactive {
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.plugin-description {
|
||||
margin: 6px 0 0 0;
|
||||
color: #475569;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@
|
|||
<div class="plugin-title-row">
|
||||
<h2 class="plugin-title">{{ plugin.titleKey ? (plugin.titleKey | translate) : plugin.title }}</h2>
|
||||
<span class="plugin-version-tag">v{{ plugin.version }}</span>
|
||||
<span class="plugin-status-tag">Activo</span>
|
||||
<mat-slide-toggle
|
||||
[checked]="plugin.enabled"
|
||||
(change)="toggleEnabled($event.checked)"
|
||||
color="primary"
|
||||
class="plugin-status-toggle"
|
||||
[class.active]="plugin.enabled"
|
||||
[class.inactive]="!plugin.enabled">
|
||||
{{ plugin.enabled ? 'Activo' : 'Inactivo' }}
|
||||
</mat-slide-toggle>
|
||||
</div>
|
||||
<p class="plugin-description">{{ plugin.descriptionKey ? (plugin.descriptionKey | translate) : plugin.description }}</p>
|
||||
<span class="plugin-author">Autor: {{ plugin.author }}</span>
|
||||
|
|
|
|||
|
|
@ -55,6 +55,20 @@ export class PluginSettingsComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
}
|
||||
|
||||
toggleEnabled(checked: boolean): void {
|
||||
if (!this.pluginName) return;
|
||||
this.pluginService.togglePlugin(this.pluginName, checked).subscribe({
|
||||
next: () => {
|
||||
const statusText = checked ? 'activado' : 'desactivado';
|
||||
this.toast.success(`Plugin ${statusText} correctamente`, this.plugin?.title || 'Plugin');
|
||||
},
|
||||
error: () => {
|
||||
this.toast.error('Error al cambiar el estado del plugin', this.plugin?.title || 'Plugin');
|
||||
this.loadPluginData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
runFieldAction(field: SchemaField): void {
|
||||
if (!field.action || !field.action.endpoint) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -148,3 +148,14 @@
|
|||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tab-disabled-badge {
|
||||
margin-left: 8px;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<mat-icon class="header-title-icon">extension</mat-icon>
|
||||
<h1 class="header-main-title">Plugins y Extensiones</h1>
|
||||
@if (!isLoading) {
|
||||
<span class="plugins-count-badge">{{ plugins.length }} Activo(s)</span>
|
||||
<span class="plugins-count-badge">{{ activePluginsCount }} Activo(s) / {{ plugins.length }} Instalado(s)</span>
|
||||
}
|
||||
</div>
|
||||
<p class="header-subtitle">
|
||||
|
|
@ -27,6 +27,9 @@
|
|||
<ng-template mat-tab-label>
|
||||
<mat-icon class="tab-plugin-icon">{{ plugin.icon }}</mat-icon>
|
||||
<span>{{ plugin.titleKey ? (plugin.titleKey | translate) : plugin.title }}</span>
|
||||
@if (!plugin.enabled) {
|
||||
<span class="tab-disabled-badge">Inactivo</span>
|
||||
}
|
||||
</ng-template>
|
||||
|
||||
<div class="tab-inner-content">
|
||||
|
|
|
|||
|
|
@ -14,11 +14,15 @@ export class PluginsComponent implements OnInit, OnDestroy {
|
|||
private pluginSub?: Subscription;
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
|
||||
get activePluginsCount(): number {
|
||||
return this.plugins.filter(p => p.enabled).length;
|
||||
}
|
||||
|
||||
constructor(public pluginService: PluginService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
// 1. Asignar estado actual en memoria si ya existe
|
||||
this.plugins = this.pluginService.getPlugins();
|
||||
this.plugins = this.pluginService.getAllPlugins();
|
||||
if (this.plugins.length > 0) {
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export class PluginService {
|
|||
private configService = inject(ConfigService);
|
||||
private translateService = inject(TranslateService);
|
||||
|
||||
private allPlugins: PluginManifest[] = [];
|
||||
private activePlugins: PluginManifest[] = [];
|
||||
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
|
||||
public plugins$ = this.pluginsSubject.asObservable();
|
||||
|
|
@ -147,18 +148,18 @@ export class PluginService {
|
|||
return of([]);
|
||||
}
|
||||
|
||||
return this.http.get<PluginManifest[]>(`${apiUrl}/plugins`).pipe(
|
||||
return this.http.get<PluginManifest[]>(`${apiUrl}/plugins?all=true`).pipe(
|
||||
catchError(() => of([])),
|
||||
switchMap((plugins) => {
|
||||
const active = (plugins || []).filter(p => p.enabled);
|
||||
if (active.length === 0) {
|
||||
if (!plugins || plugins.length === 0) {
|
||||
this.allPlugins = [];
|
||||
this.activePlugins = [];
|
||||
this.pluginsSubject.next([]);
|
||||
return of([]);
|
||||
}
|
||||
|
||||
// Descubrir y fusionar manifiestos extendidos de cada plugin activo
|
||||
const manifestRequests = active.map(p =>
|
||||
// Descubrir y fusionar manifiestos extendidos de cada plugin
|
||||
const manifestRequests = plugins.map(p =>
|
||||
this.http.get<Partial<PluginManifest>>(`${apiUrl}/plugins/${p.name}/manifest`).pipe(
|
||||
catchError(() => of({})),
|
||||
map(extra => ({ ...p, ...extra }))
|
||||
|
|
@ -167,8 +168,9 @@ export class PluginService {
|
|||
|
||||
return forkJoin(manifestRequests).pipe(
|
||||
tap((fullPlugins) => {
|
||||
this.activePlugins = fullPlugins;
|
||||
this.pluginsSubject.next(this.activePlugins);
|
||||
this.allPlugins = fullPlugins;
|
||||
this.activePlugins = fullPlugins.filter(p => p.enabled);
|
||||
this.pluginsSubject.next(this.allPlugins);
|
||||
this.loadPluginTranslations();
|
||||
this.loadActivePluginsStatus().subscribe();
|
||||
})
|
||||
|
|
@ -177,6 +179,14 @@ export class PluginService {
|
|||
);
|
||||
}
|
||||
|
||||
togglePlugin(name: string, enabled?: boolean): Observable<any> {
|
||||
const apiUrl = this.configService.apiUrl;
|
||||
if (!apiUrl) return of(null);
|
||||
return this.http.post(`${apiUrl}/plugins/${name}/toggle`, enabled !== undefined ? { enabled } : {}).pipe(
|
||||
switchMap(() => this.refreshPlugins())
|
||||
);
|
||||
}
|
||||
|
||||
public loadPluginTranslations(lang?: string): void {
|
||||
const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es';
|
||||
const apiUrl = this.configService.apiUrl;
|
||||
|
|
@ -250,6 +260,10 @@ export class PluginService {
|
|||
return this.activePlugins.filter(p => p.enabled);
|
||||
}
|
||||
|
||||
getAllPlugins(): PluginManifest[] {
|
||||
return this.allPlugins;
|
||||
}
|
||||
|
||||
hasPlugins(): boolean {
|
||||
return this.getPlugins().length > 0;
|
||||
}
|
||||
|
|
@ -259,7 +273,7 @@ export class PluginService {
|
|||
}
|
||||
|
||||
getPlugin(name: string): PluginManifest | undefined {
|
||||
return this.activePlugins.find(p => p.name === name);
|
||||
return this.allPlugins.find(p => p.name === name);
|
||||
}
|
||||
|
||||
getPluginGroups(): PluginGroup[] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue