fix(alerts): preserve offsets when large records shrink a page

This commit is contained in:
moe-berri 2026-11-08 15:12:24 +08:00
parent 616f3e4c8a
commit 74ba586a1c
2 changed files with 44 additions and 4 deletions

View file

@ -30,6 +30,7 @@ let refreshBarrier: Promise<void> | undefined;
let holdReads = false;
let failList = false;
let byteLimitedPage = false;
let mixedPageSizes = false;
let failActivity = false;
let staleStatus = false;
let empty = false;
@ -88,6 +89,16 @@ function listResponse(query: URLSearchParams) {
}));
return json({ data, total: 6, limit: 2, offset });
}
if (mixedPageSizes) {
const limit = Math.min(Number(query.get("limit") ?? 25), offset < 25 ? 25 : 2);
const data = Array.from({ length: Math.min(limit, 30 - offset) }, (_, index) => ({
...SPEND_ISSUE,
id: `mixed-${offset + index}`,
scope_label: `Mixed key ${offset + index}`,
}));
const page = { data, total: 30, limit, offset };
return json(page);
}
const emptyPage = { data: [], total: 0, limit: 25, offset };
if (empty) return json(emptyPage);
const next = { ...SPEND_ISSUE, id: "second-page", scope_label: "Second key", title: "Second key spend increased" };
@ -119,6 +130,7 @@ describe("unified issue investigations", () => {
holdReads = false;
failList = false;
byteLimitedPage = false;
mixedPageSizes = false;
failActivity = false;
staleStatus = false;
empty = false;
@ -197,6 +209,33 @@ describe("unified issue investigations", () => {
expect(calls.some((call) => call.path === "/issues" && call.query.get("offset") === "4")).toBe(true);
});
it.each([false, true])("preserves the exact offset when later pages shrink (deep link %s)", async (deepLink) => {
mixedPageSizes = true;
const onUrlUpdate = vi.fn();
renderWithProviders(<IssuesInbox />, { searchParams: deepLink ? "page=2" : "", onUrlUpdate });
if (!deepLink) {
await screen.findByRole("heading", { name: "Spend on Mixed key 0 rose to 20× normal" });
await userEvent.click(screen.getByRole("button", { name: "Go to next page" }));
}
await waitFor(() =>
expect(
calls
.filter((call) => call.path === "/issues")
.at(-1)
?.query.get("limit"),
).toBe("1"),
);
expect(await screen.findByRole("heading", { name: "Spend on Mixed key 25 rose to 20× normal" })).toBeVisible();
expect(screen.queryByRole("heading", { name: "Spend on Mixed key 24 rose to 20× normal" })).not.toBeInTheDocument();
expect(onUrlUpdate.mock.calls.at(-1)?.[0].queryString).toContain("page=26");
await userEvent.click(screen.getByRole("button", { name: "Go to next page" }));
expect(await screen.findByRole("heading", { name: "Spend on Mixed key 26 rose to 20× normal" })).toBeVisible();
await userEvent.click(screen.getByRole("button", { name: "Go to previous page" }));
expect(await screen.findByRole("heading", { name: "Spend on Mixed key 25 rose to 20× normal" })).toBeVisible();
await userEvent.click(screen.getByRole("button", { name: "Go to first page" }));
expect(await screen.findByRole("heading", { name: "Spend on Mixed key 0 rose to 20× normal" })).toBeVisible();
});
it("shows retirement and key spend in one server-paginated inbox with issue destinations", async () => {
const user = userEvent.setup();
renderWithProviders(<IssuesInbox />);

View file

@ -71,10 +71,11 @@ export default function IssuesInbox() {
const { onPaginationChange } = table;
useEffect(() => {
if (query.data && query.data.limit < pageSize) {
onPaginationChange({
pageIndex: Math.floor((pageIndex * pageSize) / query.data.limit),
pageSize: query.data.limit,
});
const offset = pageIndex * pageSize;
const limit = query.data.limit;
const alignedSize =
Array.from({ length: limit }, (_, index) => limit - index).find((size) => offset % size === 0) ?? 1;
onPaginationChange({ pageIndex: offset / alignedSize, pageSize: alignedSize });
} else if (query.data && pageIndex > lastPage) onPaginationChange({ pageIndex: lastPage, pageSize });
}, [query.data, pageIndex, pageSize, lastPage, onPaginationChange]);
const changeStatus = (next: IssueStatus) => {