fix: reactividad Alpine ping — estado en cx directamente

Alpine v3 NO puede rastrear accesos dinámicos pingCargando[cx.ID]
dentro de x-for porque no sabe qué key observar por item.

Solución: estado de ping embebido en cada objeto cx:
- abrirModal inyecta _pingCargando:false y _pingResultado:null en cada cx
- hacerPing muta cx._pingCargando / cx._pingResultado directamente
- template usa cx._pingCargando y cx._pingResultado (proxy reactivo del x-for)

Esto garantiza que el spinner, el botón deshabilitado y el resultado
del ping se actualicen correctamente para cada card.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Lizandro Guarnizo
2026-05-21 12:06:00 -05:00
co-authored by Copilot
parent a484676392
commit 6cca8e6fcd
+30 -24
View File
@@ -286,34 +286,34 @@
</div>
<!-- Resultado del ping -->
<div x-show="pingResultado[cx.ID || cx.id] !== undefined && pingResultado[cx.ID || cx.id] !== null" class="mt-1">
<div :class="pingResultado[cx.ID || cx.id]?.exitoso ? 'bg-green-50 border-green-200 text-green-800' : 'bg-red-50 border-red-200 text-red-800'"
<div x-show="cx._pingResultado !== null" class="mt-1">
<div :class="cx._pingResultado?.exitoso ? 'bg-green-50 border-green-200 text-green-800' : 'bg-red-50 border-red-200 text-red-800'"
class="rounded-lg px-3 py-2 border text-xs flex flex-col gap-1">
<div class="flex items-center gap-2">
<span x-text="pingResultado[cx.ID || cx.id]?.exitoso ? '✓' : '✗'" class="font-bold text-base leading-none"></span>
<span x-show="pingResultado[cx.ID || cx.id]?.exitoso" x-text="'Conectado en ' + pingResultado[cx.ID || cx.id]?.tiempo + ' ms'"></span>
<span x-show="!pingResultado[cx.ID || cx.id]?.exitoso" class="font-medium">Sin conexión</span>
<span x-text="cx._pingResultado?.exitoso ? '✓' : '✗'" class="font-bold text-base leading-none"></span>
<span x-show="cx._pingResultado?.exitoso" x-text="'Conectado en ' + cx._pingResultado?.tiempo + ' ms'"></span>
<span x-show="!cx._pingResultado?.exitoso" class="font-medium">Sin conexión</span>
</div>
<span x-show="pingResultado[cx.ID || cx.id]?.host" class="font-mono opacity-70"
x-text="(pingResultado[cx.ID || cx.id]?.host || '') + ':' + (pingResultado[cx.ID || cx.id]?.puerto || '')"></span>
<span x-show="!pingResultado[cx.ID || cx.id]?.exitoso && pingResultado[cx.ID || cx.id]?.error"
x-text="pingResultado[cx.ID || cx.id]?.error" class="opacity-80 break-words"></span>
<span x-show="cx._pingResultado?.host" class="font-mono opacity-70"
x-text="(cx._pingResultado?.host || '') + ':' + (cx._pingResultado?.puerto || '')"></span>
<span x-show="!cx._pingResultado?.exitoso && cx._pingResultado?.error"
x-text="cx._pingResultado?.error" class="opacity-80 break-words"></span>
</div>
</div>
<!-- Ping + Query Runner buttons -->
<div class="pt-3 border-t border-slate-100 flex items-center justify-between">
<button type="button" @click.stop="hacerPing(cx)"
:disabled="pingCargando[cx.ID || cx.id]"
:disabled="cx._pingCargando"
class="flex items-center gap-1 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
:class="pingCargando[cx.ID || cx.id] ? 'bg-slate-100 text-slate-400 cursor-wait' : 'bg-blue-50 text-blue-700 hover:bg-blue-100'">
<svg x-show="!pingCargando[cx.ID || cx.id]" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
:class="cx._pingCargando ? 'bg-slate-100 text-slate-400 cursor-wait' : 'bg-blue-50 text-blue-700 hover:bg-blue-100'">
<svg x-show="!cx._pingCargando" class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"/>
</svg>
<svg x-show="pingCargando[cx.ID || cx.id]" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<svg x-show="cx._pingCargando" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
<span x-text="pingCargando[cx.ID || cx.id] ? 'Probando...' : 'Verificar'"></span>
<span x-text="cx._pingCargando ? 'Probando...' : 'Verificar'"></span>
</button>
<button type="button" @click="abrirQueryRunner(cx)"
@@ -513,9 +513,16 @@
try {
const r = await fetch(`/app/servidor-dashboard/${servidor.ID}`);
const data = await r.json();
this.servidorSeleccionado._conexiones = data.conexiones || [];
// Inyectar estado de ping directamente en cada conexión para que
// Alpine v3 lo rastreé correctamente dentro del x-for
const conexiones = (data.conexiones || []).map(c => ({
...c,
_pingCargando: false,
_pingResultado: null,
}));
this.servidorSeleccionado._conexiones = conexiones;
const idx = this.servidores.findIndex(s => s.ID === servidor.ID);
if (idx >= 0) this.servidores[idx]._conexiones = data.conexiones || [];
if (idx >= 0) this.servidores[idx]._conexiones = conexiones;
} catch (e) {
console.error('Error cargando conexiones:', e);
this.servidorSeleccionado._conexiones = [];
@@ -581,22 +588,21 @@
async hacerPing(cx) {
const id = cx.ID || cx.id;
if (!id) return;
// Spread-replace para forzar reactividad Alpine v3 con keys nuevas
this.pingCargando = { ...this.pingCargando, [id]: true };
this.pingResultado = { ...this.pingResultado, [id]: null };
await this.$nextTick();
// Mutar cx directamente — Alpine v3 rastrea propiedades del objeto
// dentro de x-for como proxy reactivo, no keys dinámicas de otro objeto
cx._pingCargando = true;
cx._pingResultado = null;
try {
const r = await fetch(`/app/conx-ping/${id}`);
if (!r.ok) {
const txt = await r.text().catch(() => '');
throw new Error(`HTTP ${r.status}${txt ? ': ' + txt.substring(0, 80) : ''}`);
}
const data = await r.json();
this.pingResultado = { ...this.pingResultado, [id]: data };
cx._pingResultado = await r.json();
} catch (e) {
this.pingResultado = { ...this.pingResultado, [id]: { exitoso: false, error: e.message || 'Sin conexión' } };
cx._pingResultado = { exitoso: false, error: e.message || 'Sin conexión' };
} finally {
this.pingCargando = { ...this.pingCargando, [id]: false };
cx._pingCargando = false;
}
},