Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/features/packets/PacketList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export function PacketList({ wsManager, onAnalyze, onViewPath, selectedObservati
acknowledgeNewPackets,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
isLoading,
isError,
Expand Down Expand Up @@ -198,7 +199,7 @@ export function PacketList({ wsManager, onAnalyze, onViewPath, selectedObservati
key={listResetKey}
packets={packets}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
isFetching={isFetching}
fetchNextPage={fetchNextPage}
onScrollAwayFromTop={setIsScrolledAway}
onAtTopChange={setIsAtTop}
Expand Down
23 changes: 19 additions & 4 deletions src/features/packets/PacketVirtualList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
interface PacketVirtualListProps {
packets: PacketSummary[];
hasNextPage: boolean;
isFetchingNextPage: boolean;
isFetching: boolean;
fetchNextPage: () => void;
onScrollAwayFromTop: (isAway: boolean) => void;
onAtTopChange: (isAtTop: boolean) => void;
Expand All @@ -34,7 +34,7 @@ interface PacketVirtualListProps {
export function PacketVirtualList({
packets,
hasNextPage,
isFetchingNextPage,
isFetching,
fetchNextPage,
onScrollAwayFromTop,
onAtTopChange,
Expand Down Expand Up @@ -71,13 +71,13 @@ export function PacketVirtualList({
onScrollAwayFromTop(el.scrollTop > SCROLL_TOP_THRESHOLD_PX);
onAtTopChange(atTopRef.current);

if (hasNextPage && !isFetchingNextPage) {
if (hasNextPage && !isFetching) {
const distFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
if (distFromBottom < SCROLL_BOTTOM_THRESHOLD_PX) {
fetchNextPage();
}
}
}, [hasNextPage, isFetchingNextPage, fetchNextPage, onScrollAwayFromTop, onAtTopChange]);
}, [hasNextPage, isFetching, fetchNextPage, onScrollAwayFromTop, onAtTopChange]);

// When rows are prepended at the top (a reveal on return-to-top, or a live packet while already
// at the top), TanStack keeps the previously-top row anchored — which drifts the view off the
Expand Down Expand Up @@ -152,6 +152,21 @@ export function PacketVirtualList({
);
})}
</div>
{packets.length === 0 && (
<p className="py-4 text-center text-xs font-mono text-text-muted">No matching packets loaded.</p>
)}
{hasNextPage && (
<div className="flex justify-center py-4">
<button
type="button"
disabled={isFetching}
onClick={() => fetchNextPage()}
className="rounded border border-border px-3 py-1.5 text-xs font-mono text-text-normal hover:bg-text-normal/3 disabled:opacity-40 disabled:cursor-not-allowed cursor-pointer"
>
{isFetching ? "Loading packets..." : "Load older packets"}
</button>
</div>
)}
</div>
);
}
2 changes: 2 additions & 0 deletions src/features/packets/usePackets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export function usePackets(frozen: boolean = false, serverFilter: PacketServerFi
data: history,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
isLoading,
isError,
Expand Down Expand Up @@ -251,6 +252,7 @@ export function usePackets(frozen: boolean = false, serverFilter: PacketServerFi
acknowledgeNewPackets,
fetchNextPage,
hasNextPage: hasNextPage ?? false,
isFetching,
isFetchingNextPage,
isLoading,
isError,
Expand Down
1 change: 1 addition & 0 deletions tests/features/packets/PacketList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const basePackets = () => ({
acknowledgeNewPackets: () => {},
fetchNextPage: () => {},
hasNextPage: false,
isFetching: false,
isFetchingNextPage: false,
isLoading: false,
isError: false,
Expand Down
49 changes: 47 additions & 2 deletions tests/features/packets/PacketVirtualList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const many = (n: number) => Array.from({ length: n }, (_, i) => pkt(`AA${i}`));
function makeHandlers() {
return {
hasNextPage: false,
isFetchingNextPage: false,
isFetching: false,
fetchNextPage: vi.fn(),
onScrollAwayFromTop: vi.fn(),
onAtTopChange: vi.fn(),
Expand Down Expand Up @@ -167,6 +167,51 @@ describe("PacketVirtualList header", () => {
});

describe("PacketVirtualList scrolling", () => {
it.each([0, 1])("can manually page with %i visible rows and no scrollbar", (count) => {
const handlers = makeHandlers();
const { rerender } = render(
<PacketVirtualList packets={many(count)} expandedHash={null} {...handlers} hasNextPage />,
);

expect(handlers.fetchNextPage).not.toHaveBeenCalled();
if (count === 0) expect(screen.getByText("No matching packets loaded.")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Load older packets" }));
expect(handlers.fetchNextPage).toHaveBeenCalledTimes(1);

// A still-empty result after the page arrives must not start scanning further pages.
rerender(<PacketVirtualList packets={[]} expandedHash={null} {...handlers} hasNextPage />);
flushResize();
expect(handlers.fetchNextPage).toHaveBeenCalledTimes(1);
fireEvent.click(screen.getByRole("button", { name: "Load older packets" }));
expect(handlers.fetchNextPage).toHaveBeenCalledTimes(2);
});

it("disables manual paging during a request and allows retry afterward", () => {
const handlers = makeHandlers();
const { rerender } = render(
<PacketVirtualList packets={[]} expandedHash={null} {...handlers} hasNextPage isFetching />,
);
const pending = screen.getByRole("button", { name: "Loading packets..." });
expect(pending).toBeDisabled();
fireEvent.click(pending);
expect(handlers.fetchNextPage).not.toHaveBeenCalled();

rerender(<PacketVirtualList packets={[]} expandedHash={null} {...handlers} hasNextPage />);
fireEvent.click(screen.getByRole("button", { name: "Load older packets" }));
expect(handlers.fetchNextPage).toHaveBeenCalledTimes(1);
});

it("removes manual paging when the history cursor is exhausted", () => {
const handlers = makeHandlers();
const { rerender } = render(
<PacketVirtualList packets={[]} expandedHash={null} {...handlers} hasNextPage />,
);
expect(screen.getByRole("button", { name: "Load older packets" })).toBeEnabled();
rerender(<PacketVirtualList packets={[]} expandedHash={null} {...handlers} />);
expect(screen.queryByRole("button", { name: "Load older packets" })).not.toBeInTheDocument();
expect(handlers.fetchNextPage).not.toHaveBeenCalled();
});

it("pages when scrolled near the bottom", () => {
const handlers = makeHandlers();
const { container } = render(
Expand All @@ -183,7 +228,7 @@ describe("PacketVirtualList scrolling", () => {
it("does not page while a page is already in flight", () => {
const handlers = makeHandlers();
const { container } = render(
<PacketVirtualList packets={many(30)} expandedHash={null} {...handlers} hasNextPage isFetchingNextPage />,
<PacketVirtualList packets={many(30)} expandedHash={null} {...handlers} hasNextPage isFetching />,
);

const el = scroller(container);
Expand Down
Loading