fix(events): load older logs on terminal scroll

This commit is contained in:
2026-06-21 09:14:32 +03:30
parent c72ca7203f
commit 496b18fc22
2 changed files with 48 additions and 23 deletions

View File

@@ -36,6 +36,7 @@ export function EventsPage() {
const [loadingMore, setLoadingMore] = useState(false); const [loadingMore, setLoadingMore] = useState(false);
const [lastRefreshAt, setLastRefreshAt] = useState<string | null>(null); const [lastRefreshAt, setLastRefreshAt] = useState<string | null>(null);
const sentinelRef = useRef<HTMLDivElement | null>(null); const sentinelRef = useRef<HTMLDivElement | null>(null);
const feedRef = useRef<HTMLDivElement | null>(null);
const refreshFirstPage = useCallback(async (replace = false) => { const refreshFirstPage = useCallback(async (replace = false) => {
if (replace) setLoading(true); if (replace) setLoading(true);
@@ -74,19 +75,12 @@ export function EventsPage() {
} }
}, [loadingMore, nextPageUrl]); }, [loadingMore, nextPageUrl]);
useEffect(() => { const handleFeedScroll = useCallback(() => {
const node = sentinelRef.current; const node = feedRef.current;
if (!node || !nextPageUrl) return undefined; if (!node || !nextPageUrl || loadingMore) return;
const distanceFromBottom = node.scrollHeight - node.scrollTop - node.clientHeight;
const observer = new IntersectionObserver( if (distanceFromBottom <= 140) void loadMore();
(entries) => { }, [loadMore, loadingMore, nextPageUrl]);
if (entries.some((entry) => entry.isIntersecting)) void loadMore();
},
{ rootMargin: "280px 0px" }
);
observer.observe(node);
return () => observer.disconnect();
}, [loadMore, nextPageUrl]);
const newestFirst = useMemo(() => [...events].sort((a, b) => b.id - a.id), [events]); const newestFirst = useMemo(() => [...events].sort((a, b) => b.id - a.id), [events]);
@@ -113,7 +107,7 @@ export function EventsPage() {
<code>job-events.log</code> <code>job-events.log</code>
<span>refreshed {lastRefreshAt ? new Date(lastRefreshAt).toLocaleTimeString() : "-"}</span> <span>refreshed {lastRefreshAt ? new Date(lastRefreshAt).toLocaleTimeString() : "-"}</span>
</div> </div>
<div className="terminal-feed" role="log" aria-live="polite"> <div className="terminal-feed" role="log" aria-live="polite" onScroll={handleFeedScroll} ref={feedRef}>
{newestFirst.map((event) => ( {newestFirst.map((event) => (
<article className={`terminal-line ${eventTone(event.type)}`} key={event.id}> <article className={`terminal-line ${eventTone(event.type)}`} key={event.id}>
<time className="terminal-time" dateTime={event.created_at}> <time className="terminal-time" dateTime={event.created_at}>
@@ -131,15 +125,15 @@ export function EventsPage() {
))} ))}
{loading && !newestFirst.length && <EmptyState title="Loading events" />} {loading && !newestFirst.length && <EmptyState title="Loading events" />}
{!loading && !newestFirst.length && <EmptyState title="No events yet" />} {!loading && !newestFirst.length && <EmptyState title="No events yet" />}
</div> <div className="infinite-sentinel" ref={sentinelRef}>
<div className="infinite-sentinel" ref={sentinelRef}> {loadingMore && <span>Loading older events...</span>}
{loadingMore && <span>Loading older events...</span>} {!loadingMore && nextPageUrl && (
{!loadingMore && nextPageUrl && ( <button className="secondary-button" type="button" onClick={() => void loadMore()}>
<button className="secondary-button" type="button" onClick={() => void loadMore()}> Load older events
Load older events </button>
</button> )}
)} {!loadingMore && !nextPageUrl && newestFirst.length > 0 && <span>End of event history</span>}
{!loadingMore && !nextPageUrl && newestFirst.length > 0 && <span>End of event history</span>} </div>
</div> </div>
</section> </section>
</div> </div>

View File

@@ -35,6 +35,9 @@
--panel-raised: #f9fbfc; --panel-raised: #f9fbfc;
--background: #f4f7f9; --background: #f4f7f9;
--shadow: 0 18px 46px rgb(35 36 38 / 8%); --shadow: 0 18px 46px rgb(35 36 38 / 8%);
--terminal-scroll-thumb: #0bb3f0;
--terminal-scroll-thumb-hover: #f0bb0b;
--terminal-scroll-track: #171b1f;
color: var(--ink); color: var(--ink);
background: var(--background); background: var(--background);
font-synthesis: none; font-synthesis: none;
@@ -60,6 +63,9 @@
--panel-raised: #25282b; --panel-raised: #25282b;
--background: #141618; --background: #141618;
--shadow: 0 18px 46px rgb(0 0 0 / 28%); --shadow: 0 18px 46px rgb(0 0 0 / 28%);
--terminal-scroll-thumb: #f0bb0b;
--terminal-scroll-thumb-hover: #55c8ff;
--terminal-scroll-track: #101316;
} }
* { * {
@@ -1250,6 +1256,31 @@ label {
min-height: 420px; min-height: 420px;
overflow: auto; overflow: auto;
padding: 8px; padding: 8px;
scrollbar-color: var(--terminal-scroll-thumb) var(--terminal-scroll-track);
scrollbar-width: thin;
}
.terminal-feed::-webkit-scrollbar {
height: 10px;
width: 10px;
}
.terminal-feed::-webkit-scrollbar-track {
background: var(--terminal-scroll-track);
}
.terminal-feed::-webkit-scrollbar-thumb {
background: var(--terminal-scroll-thumb);
border: 2px solid var(--terminal-scroll-track);
border-radius: 999px;
}
.terminal-feed::-webkit-scrollbar-thumb:hover {
background: var(--terminal-scroll-thumb-hover);
}
.terminal-feed::-webkit-scrollbar-corner {
background: var(--terminal-scroll-track);
} }
.terminal-line { .terminal-line {