feat: soporte para activar y desactivar plugins en caliente (1.19.0)
parent
f10613af82
commit
2aea3beab8
|
|
@ -1,5 +1,14 @@
|
||||||
# Changelog
|
# 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
|
## [1.18.0] - 2026-10-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "og-webconsole",
|
"name": "og-webconsole",
|
||||||
"version": "1.18.0",
|
"version": "1.19.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,20 @@
|
||||||
border-radius: 12px;
|
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 {
|
.plugin-description {
|
||||||
margin: 6px 0 0 0;
|
margin: 6px 0 0 0;
|
||||||
color: #475569;
|
color: #475569;
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,15 @@
|
||||||
<div class="plugin-title-row">
|
<div class="plugin-title-row">
|
||||||
<h2 class="plugin-title">{{ plugin.titleKey ? (plugin.titleKey | translate) : plugin.title }}</h2>
|
<h2 class="plugin-title">{{ plugin.titleKey ? (plugin.titleKey | translate) : plugin.title }}</h2>
|
||||||
<span class="plugin-version-tag">v{{ plugin.version }}</span>
|
<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>
|
</div>
|
||||||
<p class="plugin-description">{{ plugin.descriptionKey ? (plugin.descriptionKey | translate) : plugin.description }}</p>
|
<p class="plugin-description">{{ plugin.descriptionKey ? (plugin.descriptionKey | translate) : plugin.description }}</p>
|
||||||
<span class="plugin-author">Autor: {{ plugin.author }}</span>
|
<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 {
|
runFieldAction(field: SchemaField): void {
|
||||||
if (!field.action || !field.action.endpoint) return;
|
if (!field.action || !field.action.endpoint) return;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -148,3 +148,14 @@
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
margin: 0;
|
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>
|
<mat-icon class="header-title-icon">extension</mat-icon>
|
||||||
<h1 class="header-main-title">Plugins y Extensiones</h1>
|
<h1 class="header-main-title">Plugins y Extensiones</h1>
|
||||||
@if (!isLoading) {
|
@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>
|
</div>
|
||||||
<p class="header-subtitle">
|
<p class="header-subtitle">
|
||||||
|
|
@ -27,6 +27,9 @@
|
||||||
<ng-template mat-tab-label>
|
<ng-template mat-tab-label>
|
||||||
<mat-icon class="tab-plugin-icon">{{ plugin.icon }}</mat-icon>
|
<mat-icon class="tab-plugin-icon">{{ plugin.icon }}</mat-icon>
|
||||||
<span>{{ plugin.titleKey ? (plugin.titleKey | translate) : plugin.title }}</span>
|
<span>{{ plugin.titleKey ? (plugin.titleKey | translate) : plugin.title }}</span>
|
||||||
|
@if (!plugin.enabled) {
|
||||||
|
<span class="tab-disabled-badge">Inactivo</span>
|
||||||
|
}
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<div class="tab-inner-content">
|
<div class="tab-inner-content">
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,15 @@ export class PluginsComponent implements OnInit, OnDestroy {
|
||||||
private pluginSub?: Subscription;
|
private pluginSub?: Subscription;
|
||||||
private cdr = inject(ChangeDetectorRef);
|
private cdr = inject(ChangeDetectorRef);
|
||||||
|
|
||||||
|
get activePluginsCount(): number {
|
||||||
|
return this.plugins.filter(p => p.enabled).length;
|
||||||
|
}
|
||||||
|
|
||||||
constructor(public pluginService: PluginService) {}
|
constructor(public pluginService: PluginService) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// 1. Asignar estado actual en memoria si ya existe
|
// 1. Asignar estado actual en memoria si ya existe
|
||||||
this.plugins = this.pluginService.getPlugins();
|
this.plugins = this.pluginService.getAllPlugins();
|
||||||
if (this.plugins.length > 0) {
|
if (this.plugins.length > 0) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ export class PluginService {
|
||||||
private configService = inject(ConfigService);
|
private configService = inject(ConfigService);
|
||||||
private translateService = inject(TranslateService);
|
private translateService = inject(TranslateService);
|
||||||
|
|
||||||
|
private allPlugins: PluginManifest[] = [];
|
||||||
private activePlugins: PluginManifest[] = [];
|
private activePlugins: PluginManifest[] = [];
|
||||||
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
|
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
|
||||||
public plugins$ = this.pluginsSubject.asObservable();
|
public plugins$ = this.pluginsSubject.asObservable();
|
||||||
|
|
@ -147,18 +148,18 @@ export class PluginService {
|
||||||
return of([]);
|
return of([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.http.get<PluginManifest[]>(`${apiUrl}/plugins`).pipe(
|
return this.http.get<PluginManifest[]>(`${apiUrl}/plugins?all=true`).pipe(
|
||||||
catchError(() => of([])),
|
catchError(() => of([])),
|
||||||
switchMap((plugins) => {
|
switchMap((plugins) => {
|
||||||
const active = (plugins || []).filter(p => p.enabled);
|
if (!plugins || plugins.length === 0) {
|
||||||
if (active.length === 0) {
|
this.allPlugins = [];
|
||||||
this.activePlugins = [];
|
this.activePlugins = [];
|
||||||
this.pluginsSubject.next([]);
|
this.pluginsSubject.next([]);
|
||||||
return of([]);
|
return of([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Descubrir y fusionar manifiestos extendidos de cada plugin activo
|
// Descubrir y fusionar manifiestos extendidos de cada plugin
|
||||||
const manifestRequests = active.map(p =>
|
const manifestRequests = plugins.map(p =>
|
||||||
this.http.get<Partial<PluginManifest>>(`${apiUrl}/plugins/${p.name}/manifest`).pipe(
|
this.http.get<Partial<PluginManifest>>(`${apiUrl}/plugins/${p.name}/manifest`).pipe(
|
||||||
catchError(() => of({})),
|
catchError(() => of({})),
|
||||||
map(extra => ({ ...p, ...extra }))
|
map(extra => ({ ...p, ...extra }))
|
||||||
|
|
@ -167,8 +168,9 @@ export class PluginService {
|
||||||
|
|
||||||
return forkJoin(manifestRequests).pipe(
|
return forkJoin(manifestRequests).pipe(
|
||||||
tap((fullPlugins) => {
|
tap((fullPlugins) => {
|
||||||
this.activePlugins = fullPlugins;
|
this.allPlugins = fullPlugins;
|
||||||
this.pluginsSubject.next(this.activePlugins);
|
this.activePlugins = fullPlugins.filter(p => p.enabled);
|
||||||
|
this.pluginsSubject.next(this.allPlugins);
|
||||||
this.loadPluginTranslations();
|
this.loadPluginTranslations();
|
||||||
this.loadActivePluginsStatus().subscribe();
|
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 {
|
public loadPluginTranslations(lang?: string): void {
|
||||||
const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es';
|
const currentLang = lang || this.translateService.currentLang || this.translateService.defaultLang || 'es';
|
||||||
const apiUrl = this.configService.apiUrl;
|
const apiUrl = this.configService.apiUrl;
|
||||||
|
|
@ -250,6 +260,10 @@ export class PluginService {
|
||||||
return this.activePlugins.filter(p => p.enabled);
|
return this.activePlugins.filter(p => p.enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getAllPlugins(): PluginManifest[] {
|
||||||
|
return this.allPlugins;
|
||||||
|
}
|
||||||
|
|
||||||
hasPlugins(): boolean {
|
hasPlugins(): boolean {
|
||||||
return this.getPlugins().length > 0;
|
return this.getPlugins().length > 0;
|
||||||
}
|
}
|
||||||
|
|
@ -259,7 +273,7 @@ export class PluginService {
|
||||||
}
|
}
|
||||||
|
|
||||||
getPlugin(name: string): PluginManifest | undefined {
|
getPlugin(name: string): PluginManifest | undefined {
|
||||||
return this.activePlugins.find(p => p.name === name);
|
return this.allPlugins.find(p => p.name === name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getPluginGroups(): PluginGroup[] {
|
getPluginGroups(): PluginGroup[] {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue