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:
@@ -96,6 +96,10 @@
|
||||
let confirmClearEvents = $state(false);
|
||||
let refreshSeconds = $state(loadRefreshSeconds());
|
||||
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
|
||||
// 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
|
||||
? providers.filter(p => p.type === globalProviderFilter.providerType).length
|
||||
: displayProviders);
|
||||
@@ -675,18 +689,23 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#if status.recent_events.length === 0}
|
||||
{#if eventsLoading}
|
||||
<div class="empty-state"><p>{t('dashboard.loadingEvents')}</p></div>
|
||||
{:else if status.recent_events.length === 0}
|
||||
{:else}
|
||||
<div class="empty-state">
|
||||
<MdiIcon name="mdiCalendarBlank" size={36} />
|
||||
<p>{t('dashboard.noEvents')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{: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)}
|
||||
<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}
|
||||
aria-label={t('events.detailTitle')}>
|
||||
<div class="signal-avatar"
|
||||
@@ -1232,6 +1251,11 @@
|
||||
SIGNAL STREAM — events with routing trail
|
||||
============================================================ */
|
||||
.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 {
|
||||
display: grid;
|
||||
grid-template-columns: 40px 1fr auto;
|
||||
|
||||
Reference in New Issue
Block a user