fix(frontend): stop event-log flicker on pagination

Pagination/filter reloads were collapsing the panel into a "Loading
events…" placeholder and then replaying the stagger entry animation,
which read as the whole section being reconstructed. Keep the existing
rows + paginator mounted during reload (with a soft dim) and only run
the aurora-rise cascade on the very first non-empty render.
This commit is contained in:
2026-05-09 14:47:12 +03:00
parent 757271dadf
commit 87cb33cffe
+27 -3
View File
@@ -96,6 +96,10 @@
let confirmClearEvents = $state(false); let confirmClearEvents = $state(false);
let refreshSeconds = $state(loadRefreshSeconds()); let refreshSeconds = $state(loadRefreshSeconds());
let selectedEvent = $state<EventLog | null>(null); let selectedEvent = $state<EventLog | null>(null);
// Stagger entry animation should play once on initial load only —
// without this, every pagination/filter change re-runs the cascade
// (~600ms of fade-up per row) which reads as the panel "reconstructing".
let eventsAnimated = $state(false);
// Auto-refresh ticker — re-creates the interval whenever the user // Auto-refresh ticker — re-creates the interval whenever the user
// changes the cadence. ``$effect`` returns a cleanup that fires on // changes the cadence. ``$effect`` returns a cleanup that fires on
@@ -279,6 +283,16 @@
} }
} }
// Disable stagger entry animation once the first non-empty list has
// rendered + had time to play. Subsequent pagination/filter reloads
// then settle in place instead of re-running the cascade.
$effect(() => {
if (eventsAnimated) return;
if (!status?.recent_events?.length) return;
const handle = setTimeout(() => { eventsAnimated = true; }, 700);
return () => clearTimeout(handle);
});
const filteredProviderCount = $derived(globalProviderFilter.providerType const filteredProviderCount = $derived(globalProviderFilter.providerType
? providers.filter(p => p.type === globalProviderFilter.providerType).length ? providers.filter(p => p.type === globalProviderFilter.providerType).length
: displayProviders); : displayProviders);
@@ -675,18 +689,23 @@
</div> </div>
{/snippet} {/snippet}
{#if status.recent_events.length === 0}
{#if eventsLoading} {#if eventsLoading}
<div class="empty-state"><p>{t('dashboard.loadingEvents')}</p></div> <div class="empty-state"><p>{t('dashboard.loadingEvents')}</p></div>
{:else if status.recent_events.length === 0} {:else}
<div class="empty-state"> <div class="empty-state">
<MdiIcon name="mdiCalendarBlank" size={36} /> <MdiIcon name="mdiCalendarBlank" size={36} />
<p>{t('dashboard.noEvents')}</p> <p>{t('dashboard.noEvents')}</p>
</div> </div>
{/if}
{:else} {:else}
<div class="signal-list stagger-children"> <div class="signal-list"
class:stagger-children={!eventsAnimated}
class:signal-list--reloading={eventsLoading}
aria-busy={eventsLoading}>
{#each status.recent_events as event, i (event.id)} {#each status.recent_events as event, i (event.id)}
<button type="button" class="signal-row signal-row--clickable" <button type="button" class="signal-row signal-row--clickable"
style="animation-delay: {i * 60}ms;" style={eventsAnimated ? '' : `animation-delay: ${i * 60}ms;`}
onclick={() => selectedEvent = event} onclick={() => selectedEvent = event}
aria-label={t('events.detailTitle')}> aria-label={t('events.detailTitle')}>
<div class="signal-avatar" <div class="signal-avatar"
@@ -1232,6 +1251,11 @@
SIGNAL STREAM — events with routing trail SIGNAL STREAM — events with routing trail
============================================================ */ ============================================================ */
.signal-list { position: relative; z-index: 1; padding-bottom: 0.25rem; } .signal-list { position: relative; z-index: 1; padding-bottom: 0.25rem; }
/* Soft dim while a page change / filter reload is in flight. We keep
the previous rows mounted (avoids the layout collapsing to a tiny
"Loading…" placeholder) and just nudge opacity so the swap feels
like a refresh rather than a teardown. */
.signal-list--reloading { opacity: 0.55; pointer-events: none; transition: opacity 0.15s ease; }
.signal-row { .signal-row {
display: grid; display: grid;
grid-template-columns: 40px 1fr auto; grid-template-columns: 40px 1fr auto;