fix(tooltips): actualizacion reactiva de tooltips y badges de plugins en tiempo real (v1.24.2)
parent
dbea3d2cd6
commit
8a82c37d48
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -1,5 +1,15 @@
|
|||
# Changelog
|
||||
|
||||
## [1.24.2] - 2026-10-30
|
||||
|
||||
### Fixed
|
||||
- **Actualización Reactiva en Tiempo Real de Tooltips y Badges de Plugins (`PluginService`, `groups.component`, `execute-command.component`)**:
|
||||
- Solucionado el problema por el cual el tiempo restante en el tooltip de los badges no se actualizaba en vivo al recibir eventos Mercure o tras ejecutar acciones como "Renovar tiempo", requiriendo recargar la página manualmente.
|
||||
- La recepción de eventos SSE de Mercure en `PluginService` se ejecuta ahora dentro de `NgZone.run()`, emitiendo a través del nuevo flujo observable reactivo `statusUpdates$`.
|
||||
- Suscripción reactiva en `GroupsComponent` a `pluginService.statusUpdates$`, activando la detección de cambios (`cd.detectChanges()`) de manera inmediata ante cualquier actualización de estado o metadatos de puestos.
|
||||
- En `groups.component.html` y `client-details.component.html`, la expresión de seguimiento del control de flujo (`@for`) incorpora ahora el tooltip (`track ... + (badge.tooltip || '')`), forzando la actualización instantánea del enlace `[matTooltip]` en el DOM al cambiar los minutos restantes o datos del alumno.
|
||||
- Tras procesar con éxito un comando de plugin en `ExecuteCommandComponent`, se dispara automáticamente una recarga de estados en segundo plano (`loadActivePluginsStatus()`).
|
||||
|
||||
## [1.24.1] - 2026-10-30
|
||||
|
||||
### Fixed
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "1.24.1",
|
||||
"version": "1.24.2",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
|
|
|||
|
|
@ -626,6 +626,7 @@ export class ExecuteCommandComponent implements OnInit {
|
|||
next: (response) => {
|
||||
const msg = response?.message || 'Comando procesado correctamente';
|
||||
this.toastService.success(msg, pluginCmd.label);
|
||||
this.pluginService.loadActivePluginsStatus().subscribe();
|
||||
},
|
||||
error: (error) => {
|
||||
const errorMsg = error?.error?.message || error?.error?.['hydra:description'] || 'Error al comunicar con el servidor';
|
||||
|
|
|
|||
|
|
@ -423,7 +423,7 @@
|
|||
RemotePC
|
||||
</span>
|
||||
}
|
||||
@for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label) {
|
||||
@for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label + '-' + (badge.tooltip || '')) {
|
||||
<span class="plugin-badge badge-{{badge.type}}" [matTooltip]="badge.tooltip">
|
||||
<mat-icon class="plugin-badge-icon">{{ badge.icon }}</mat-icon> {{ badge.label }}
|
||||
</span>
|
||||
|
|
@ -554,7 +554,7 @@
|
|||
RemotePC
|
||||
</span>
|
||||
}
|
||||
@for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label) {
|
||||
@for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label + '-' + (badge.tooltip || '')) {
|
||||
<span class="plugin-badge badge-{{badge.type}} list-view-badge" [matTooltip]="badge.tooltip">
|
||||
<mat-icon class="plugin-badge-icon">{{ badge.icon }}</mat-icon> {{ badge.label }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -270,7 +270,13 @@ export class GroupsComponent implements OnInit, OnDestroy {
|
|||
|
||||
this.breakpointObserver.observe(['(max-width: 992px)']).subscribe((result) => {
|
||||
this.isSmallScreen = result.matches;
|
||||
})
|
||||
});
|
||||
|
||||
this.subscriptions.add(
|
||||
this.pluginService.statusUpdates$.subscribe(() => {
|
||||
this.cd.detectChanges();
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private updateClientStatus(clientUuid: string, status: string): void {
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
<span class="image-date">Desplegada: {{ image.deployDate | date:'dd/MM/yyyy HH:mm' }}</span>
|
||||
}
|
||||
<div class="partition-badges-row">
|
||||
@for (badge of getPartitionBadges(disk.diskNumber, image.partitionNumber); track badge.id + '-' + badge.type + '-' + badge.label) {
|
||||
@for (badge of getPartitionBadges(disk.diskNumber, image.partitionNumber); track badge.id + '-' + badge.type + '-' + badge.label + '-' + (badge.tooltip || '')) {
|
||||
<span class="plugin-badge badge-{{badge.type}}" [matTooltip]="badge.tooltip">
|
||||
<mat-icon class="plugin-badge-icon">{{ badge.icon }}</mat-icon> {{ badge.label }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Injectable, inject } from '@angular/core';
|
||||
import { Injectable, NgZone, inject } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, forkJoin, of } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, Subject, forkJoin, of } from 'rxjs';
|
||||
import { catchError, map, switchMap, tap } from 'rxjs/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ConfigService } from './config.service';
|
||||
|
|
@ -115,12 +115,16 @@ export class PluginService {
|
|||
private configService = inject(ConfigService);
|
||||
private translateService = inject(TranslateService);
|
||||
private authService = inject(AuthService);
|
||||
private ngZone = inject(NgZone);
|
||||
|
||||
private allPlugins: PluginManifest[] = [];
|
||||
private activePlugins: PluginManifest[] = [];
|
||||
private pluginsSubject = new BehaviorSubject<PluginManifest[]>([]);
|
||||
public plugins$ = this.pluginsSubject.asObservable();
|
||||
|
||||
private statusUpdatesSubject = new Subject<{ plugin: string; clientId?: string; status: string; metadata?: any }>();
|
||||
public statusUpdates$ = this.statusUpdatesSubject.asObservable();
|
||||
|
||||
// Dynamic state cache: key is `${pluginName}:${clientIdOrUuid}` -> status string
|
||||
private clientPluginStatusMap = new Map<string, string>();
|
||||
// Dynamic metadata cache: key is `${pluginName}:${clientIdOrUuid}` -> extra attributes (e.g. dni, remaining_minutes)
|
||||
|
|
@ -184,95 +188,104 @@ export class PluginService {
|
|||
try {
|
||||
this.mercureEventSource = new EventSource(`${mercureUrl}?topic=` + encodeURIComponent('plugins'));
|
||||
this.mercureEventSource.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data.plugin && data.status) {
|
||||
const disk = data.disk ?? data.disk_number ?? data.diskNumber;
|
||||
const part = data.partition ?? data.partition_number ?? data.partitionNumber;
|
||||
this.ngZone.run(() => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data && data.plugin && data.status) {
|
||||
const disk = data.disk ?? data.disk_number ?? data.diskNumber;
|
||||
const part = data.partition ?? data.partition_number ?? data.partitionNumber;
|
||||
|
||||
const clearEntriesForPrefix = (prefix: string) => {
|
||||
for (const key of Array.from(this.clientPluginStatusMap.keys())) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.clientPluginStatusMap.delete(key);
|
||||
const clearEntriesForPrefix = (prefix: string) => {
|
||||
for (const key of Array.from(this.clientPluginStatusMap.keys())) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.clientPluginStatusMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of Array.from(this.clientPluginMetadataMap.keys())) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.clientPluginMetadataMap.delete(key);
|
||||
for (const key of Array.from(this.clientPluginMetadataMap.keys())) {
|
||||
if (key.startsWith(prefix)) {
|
||||
this.clientPluginMetadataMap.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
if (data.status === 'NONE') {
|
||||
if (data.clientId !== undefined && data.clientId !== null) {
|
||||
const idStr = String(data.clientId);
|
||||
this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, idStr));
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, idStr, Number(disk), Number(part)));
|
||||
const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, idStr));
|
||||
if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) {
|
||||
if (data.status === 'NONE') {
|
||||
if (data.clientId !== undefined && data.clientId !== null) {
|
||||
const idStr = String(data.clientId);
|
||||
this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, idStr));
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, idStr, Number(disk), Number(part)));
|
||||
const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, idStr));
|
||||
if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) {
|
||||
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr));
|
||||
}
|
||||
} else if (disk !== undefined && disk !== null) {
|
||||
clearEntriesForPrefix(`${data.plugin}:${idStr}:${disk}:`);
|
||||
} else {
|
||||
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr));
|
||||
clearEntriesForPrefix(`${data.plugin}:${idStr}:`);
|
||||
}
|
||||
} else if (disk !== undefined && disk !== null) {
|
||||
clearEntriesForPrefix(`${data.plugin}:${idStr}:${disk}:`);
|
||||
} else {
|
||||
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr));
|
||||
clearEntriesForPrefix(`${data.plugin}:${idStr}:`);
|
||||
}
|
||||
}
|
||||
if (data.clientUuid) {
|
||||
const uuidStr = data.clientUuid;
|
||||
this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, uuidStr, Number(disk), Number(part)));
|
||||
const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, uuidStr));
|
||||
if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) {
|
||||
if (data.clientUuid) {
|
||||
const uuidStr = data.clientUuid;
|
||||
this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, uuidStr, Number(disk), Number(part)));
|
||||
const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, uuidStr));
|
||||
if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) {
|
||||
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
||||
}
|
||||
} else if (disk !== undefined && disk !== null) {
|
||||
clearEntriesForPrefix(`${data.plugin}:${uuidStr}:${disk}:`);
|
||||
} else {
|
||||
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
||||
clearEntriesForPrefix(`${data.plugin}:${uuidStr}:`);
|
||||
}
|
||||
} else if (disk !== undefined && disk !== null) {
|
||||
clearEntriesForPrefix(`${data.plugin}:${uuidStr}:${disk}:`);
|
||||
} else {
|
||||
this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr));
|
||||
clearEntriesForPrefix(`${data.plugin}:${uuidStr}:`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Extraer metadatos contextuales (p. ej. dni, remainingMinutes, remaining_minutes)
|
||||
const metadata: any = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (!['plugin', 'status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp'].includes(k) && v !== null && v !== undefined) {
|
||||
metadata[k] = v;
|
||||
this.statusUpdatesSubject.next({ plugin: data.plugin, status: 'NONE' });
|
||||
} else {
|
||||
// Extraer metadatos contextuales (p. ej. dni, remainingMinutes, remaining_minutes)
|
||||
const metadata: any = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (!['plugin', 'status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp'].includes(k) && v !== null && v !== undefined) {
|
||||
metadata[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
const hasMeta = Object.keys(metadata).length > 0;
|
||||
const hasMeta = Object.keys(metadata).length > 0;
|
||||
|
||||
if (data.clientId !== undefined && data.clientId !== null) {
|
||||
const idStr = String(data.clientId);
|
||||
this.setPluginStatus(data.plugin, idStr, data.status);
|
||||
if (hasMeta && data.status !== 'FREE') {
|
||||
this.setPluginMetadata(data.plugin, idStr, metadata);
|
||||
} else if (data.status === 'FREE') {
|
||||
this.clearPluginMetadata(data.plugin, idStr);
|
||||
if (data.clientId !== undefined && data.clientId !== null) {
|
||||
const idStr = String(data.clientId);
|
||||
this.setPluginStatus(data.plugin, idStr, data.status);
|
||||
if (hasMeta && data.status !== 'FREE') {
|
||||
this.setPluginMetadata(data.plugin, idStr, metadata);
|
||||
} else if (data.status === 'FREE') {
|
||||
this.clearPluginMetadata(data.plugin, idStr);
|
||||
}
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.setPartitionPluginStatus(data.plugin, idStr, Number(disk), Number(part), data.status);
|
||||
}
|
||||
}
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.setPartitionPluginStatus(data.plugin, idStr, Number(disk), Number(part), data.status);
|
||||
}
|
||||
}
|
||||
if (data.clientUuid) {
|
||||
this.setPluginStatus(data.plugin, data.clientUuid, data.status);
|
||||
if (hasMeta && data.status !== 'FREE') {
|
||||
this.setPluginMetadata(data.plugin, data.clientUuid, metadata);
|
||||
} else if (data.status === 'FREE') {
|
||||
this.clearPluginMetadata(data.plugin, data.clientUuid);
|
||||
}
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.setPartitionPluginStatus(data.plugin, data.clientUuid, Number(disk), Number(part), data.status);
|
||||
if (data.clientUuid) {
|
||||
this.setPluginStatus(data.plugin, data.clientUuid, data.status);
|
||||
if (hasMeta && data.status !== 'FREE') {
|
||||
this.setPluginMetadata(data.plugin, data.clientUuid, metadata);
|
||||
} else if (data.status === 'FREE') {
|
||||
this.clearPluginMetadata(data.plugin, data.clientUuid);
|
||||
}
|
||||
if (disk !== undefined && disk !== null && part !== undefined && part !== null) {
|
||||
this.setPartitionPluginStatus(data.plugin, data.clientUuid, Number(disk), Number(part), data.status);
|
||||
}
|
||||
}
|
||||
this.statusUpdatesSubject.next({
|
||||
plugin: data.plugin,
|
||||
clientId: data.clientId ? String(data.clientId) : data.clientUuid,
|
||||
status: data.status,
|
||||
metadata
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
});
|
||||
};
|
||||
} catch {
|
||||
}
|
||||
|
|
@ -422,7 +435,12 @@ export class PluginService {
|
|||
);
|
||||
});
|
||||
|
||||
return forkJoin(requests).pipe(map(() => undefined));
|
||||
return forkJoin(requests).pipe(
|
||||
tap(() => {
|
||||
this.statusUpdatesSubject.next({ plugin: 'all', status: 'REFRESHED' });
|
||||
}),
|
||||
map(() => undefined)
|
||||
);
|
||||
}
|
||||
|
||||
getPlugins(): PluginManifest[] {
|
||||
|
|
|
|||
Loading…
Reference in New Issue