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 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;
|
||||||
|
|||||||
Reference in New Issue
Block a user