fix(diagnostics): say which read failed, and let Live restart
- "Load older" is not offered off a loading filter's placeholder rows, whose cursor belongs to the previous filter. - A failed older page, stale rows and missing counts each get their own message (HU/EN). - Live shows as off once a failed read stopped the polling; pressing it again reads now.
This commit is contained in:
@@ -197,6 +197,44 @@ describe("AdminLogs", () => {
|
||||
const reads = logsMock.mock.calls.length;
|
||||
await act(async () => vi.advanceTimersByTimeAsync(12000));
|
||||
expect(logsMock.mock.calls.length).toBe(reads);
|
||||
// The button says polling stopped, and pressing it reads again.
|
||||
const liveButton = screen.getByRole("button", { name: "adminLogs.live" });
|
||||
expect(liveButton.getAttribute("aria-pressed")).toBe("false");
|
||||
fireEvent.click(liveButton);
|
||||
await settle();
|
||||
expect(logsMock.mock.calls.length).toBe(reads + 1);
|
||||
});
|
||||
|
||||
it("a failed older page says so, not that the list is out of date", async () => {
|
||||
logsMock.mockImplementation(async (_q, before) => {
|
||||
if (before !== null) throw new Error("500");
|
||||
return { items: [row(2)], accounts: {}, next_cursor: "c1" };
|
||||
});
|
||||
renderConsole();
|
||||
await waitFor(() => expect(lines()).toHaveLength(1));
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "adminLogs.older" }));
|
||||
|
||||
expect((await screen.findByRole("alert")).textContent).toContain("adminLogs.olderFailed");
|
||||
});
|
||||
|
||||
it("offers no older page while a filter change loads", async () => {
|
||||
let landErrors!: (page: LogPage) => void;
|
||||
logsMock.mockImplementation((q) =>
|
||||
q.levels
|
||||
? new Promise<LogPage>((resolve) => (landErrors = resolve))
|
||||
: Promise.resolve({ items: [row(2)], accounts: {}, next_cursor: "c1" }),
|
||||
);
|
||||
renderConsole();
|
||||
await waitFor(() => expect(lines()).toHaveLength(1));
|
||||
expect(screen.queryByRole("button", { name: "adminLogs.older" })).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /settings\.myLog\.filters\.error/ }));
|
||||
await waitFor(() => expect(landErrors).toBeTypeOf("function"));
|
||||
|
||||
// The rows on screen are the previous filter's: their cursor is not this list's.
|
||||
expect(screen.queryByRole("button", { name: "adminLogs.older" })).toBeNull();
|
||||
await act(async () => landErrors({ items: [row(1)], accounts: {}, next_cursor: null }));
|
||||
});
|
||||
|
||||
it("copies every loaded line, oldest first", async () => {
|
||||
|
||||
@@ -105,11 +105,26 @@ export default function AdminLogs() {
|
||||
placeholderData: keepPreviousData,
|
||||
refetchInterval: (q) => (live && q.state.status !== "error" ? LIVE_MS * 4 : false),
|
||||
});
|
||||
// What "Load older" continues from — read off the data, since `hasNextPage` is off while live.
|
||||
const olderCursor = logs.data?.pages[logs.data.pages.length - 1]?.next_cursor ?? null;
|
||||
// A read failed while rows (or counts) were already on screen: they are kept, and said to be old.
|
||||
const stale = (logs.isError && !!logs.data) || facets.isError;
|
||||
const retryStale = () => {
|
||||
// A failed read stops the polling (the intervals above); the button shows that, and pressing it
|
||||
// again reads now.
|
||||
const polling = live && !logs.isError;
|
||||
// What "Load older" continues from — read off the data, since `hasNextPage` is off while live; not
|
||||
// off placeholder rows, which are the previous filter's.
|
||||
const olderCursor = logs.isPlaceholderData
|
||||
? null
|
||||
: (logs.data?.pages[logs.data.pages.length - 1]?.next_cursor ?? null);
|
||||
// A read that failed while lines were on screen: they are kept, and the message says what the
|
||||
// failure means for them. (With no lines, the list's own error state says it.)
|
||||
const failure = !logs.data
|
||||
? null
|
||||
: logs.isFetchNextPageError
|
||||
? "olderFailed"
|
||||
: logs.isError || (facets.isError && facets.data)
|
||||
? "stale"
|
||||
: facets.isError
|
||||
? "countsFailed"
|
||||
: null;
|
||||
const retryFailed = () => {
|
||||
if (logs.isFetchNextPageError) void logs.fetchNextPage();
|
||||
else if (logs.isError) void logs.refetch();
|
||||
if (facets.isError) void facets.refetch();
|
||||
@@ -129,13 +144,17 @@ export default function AdminLogs() {
|
||||
id === "none" ? t("adminLogs.noAccount") : (accounts[id] ?? `#${id}`);
|
||||
|
||||
const toggleLive = () => {
|
||||
if (!live) {
|
||||
// Watching the top: drop the older pages now; `liveRef` keeps each tick to the first page.
|
||||
qc.setQueryData<typeof logs.data>(key, (d) =>
|
||||
d ? { pages: d.pages.slice(0, 1), pageParams: d.pageParams.slice(0, 1) } : d,
|
||||
);
|
||||
if (polling) {
|
||||
setLive(false);
|
||||
return;
|
||||
}
|
||||
setLive(!live);
|
||||
// Watching the top: drop the older pages now; `liveRef` keeps each tick to the first page.
|
||||
qc.setQueryData<typeof logs.data>(key, (d) =>
|
||||
d ? { pages: d.pages.slice(0, 1), pageParams: d.pageParams.slice(0, 1) } : d,
|
||||
);
|
||||
setLive(true);
|
||||
if (logs.isError) void logs.refetch();
|
||||
if (facets.isError) void facets.refetch();
|
||||
};
|
||||
|
||||
const loadOlder = () => {
|
||||
@@ -256,10 +275,10 @@ export default function AdminLogs() {
|
||||
/>
|
||||
<span className="ml-auto flex gap-1.5">
|
||||
<Button
|
||||
variant={live ? "outline-accent" : "outline"}
|
||||
variant={polling ? "outline-accent" : "outline"}
|
||||
size="xs"
|
||||
icon={<Radio className="h-3.5 w-3.5" />}
|
||||
aria-pressed={live}
|
||||
aria-pressed={polling}
|
||||
onClick={toggleLive}
|
||||
>
|
||||
{t("adminLogs.live")}
|
||||
@@ -296,14 +315,14 @@ export default function AdminLogs() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stale && (
|
||||
{failure && (
|
||||
<StateMessage
|
||||
tone="error"
|
||||
compact
|
||||
onRetry={retryStale}
|
||||
onRetry={retryFailed}
|
||||
className="border-b border-border"
|
||||
>
|
||||
{t("adminLogs.stale")}
|
||||
{t(`adminLogs.${failure}`)}
|
||||
</StateMessage>
|
||||
)}
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"showJob": "Only this job's lines",
|
||||
"empty": "No lines match the filters.",
|
||||
"stale": "The last refresh failed; what you see may be out of date.",
|
||||
"olderFailed": "Couldn't load the older lines.",
|
||||
"countsFailed": "The filter counts didn't load.",
|
||||
"count_one": "{{count}} line loaded · newest first",
|
||||
"count_other": "{{count}} lines loaded · newest first",
|
||||
"older": "Load older",
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
"showJob": "Csak ennek a jobnak a sorai",
|
||||
"empty": "Nincs a szűrésnek megfelelő sor.",
|
||||
"stale": "Az utolsó frissítés nem sikerült, amit látsz, elavult lehet.",
|
||||
"olderFailed": "Nem sikerült betölteni a régebbi sorokat.",
|
||||
"countsFailed": "A szűrők számai nem töltődtek be.",
|
||||
"count_one": "{{count}} sor betöltve · a legújabb felül",
|
||||
"count_other": "{{count}} sor betöltve · a legújabb felül",
|
||||
"older": "Régebbiek betöltése",
|
||||
|
||||
Reference in New Issue
Block a user