fix(events): load older logs on terminal scroll
This commit is contained in:
@@ -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,7 +125,6 @@ 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 && (
|
||||||
@@ -141,6 +134,7 @@ export function EventsPage() {
|
|||||||
)}
|
)}
|
||||||
{!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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user