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;
|
const reads = logsMock.mock.calls.length;
|
||||||
await act(async () => vi.advanceTimersByTimeAsync(12000));
|
await act(async () => vi.advanceTimersByTimeAsync(12000));
|
||||||
expect(logsMock.mock.calls.length).toBe(reads);
|
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 () => {
|
it("copies every loaded line, oldest first", async () => {
|
||||||
|
|||||||
@@ -105,11 +105,26 @@ export default function AdminLogs() {
|
|||||||
placeholderData: keepPreviousData,
|
placeholderData: keepPreviousData,
|
||||||
refetchInterval: (q) => (live && q.state.status !== "error" ? LIVE_MS * 4 : false),
|
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.
|
// A failed read stops the polling (the intervals above); the button shows that, and pressing it
|
||||||
const olderCursor = logs.data?.pages[logs.data.pages.length - 1]?.next_cursor ?? null;
|
// again reads now.
|
||||||
// A read failed while rows (or counts) were already on screen: they are kept, and said to be old.
|
const polling = live && !logs.isError;
|
||||||
const stale = (logs.isError && !!logs.data) || facets.isError;
|
// What "Load older" continues from — read off the data, since `hasNextPage` is off while live; not
|
||||||
const retryStale = () => {
|
// 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();
|
if (logs.isFetchNextPageError) void logs.fetchNextPage();
|
||||||
else if (logs.isError) void logs.refetch();
|
else if (logs.isError) void logs.refetch();
|
||||||
if (facets.isError) void facets.refetch();
|
if (facets.isError) void facets.refetch();
|
||||||
@@ -129,13 +144,17 @@ export default function AdminLogs() {
|
|||||||
id === "none" ? t("adminLogs.noAccount") : (accounts[id] ?? `#${id}`);
|
id === "none" ? t("adminLogs.noAccount") : (accounts[id] ?? `#${id}`);
|
||||||
|
|
||||||
const toggleLive = () => {
|
const toggleLive = () => {
|
||||||
if (!live) {
|
if (polling) {
|
||||||
// Watching the top: drop the older pages now; `liveRef` keeps each tick to the first page.
|
setLive(false);
|
||||||
qc.setQueryData<typeof logs.data>(key, (d) =>
|
return;
|
||||||
d ? { pages: d.pages.slice(0, 1), pageParams: d.pageParams.slice(0, 1) } : d,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
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 = () => {
|
const loadOlder = () => {
|
||||||
@@ -256,10 +275,10 @@ export default function AdminLogs() {
|
|||||||
/>
|
/>
|
||||||
<span className="ml-auto flex gap-1.5">
|
<span className="ml-auto flex gap-1.5">
|
||||||
<Button
|
<Button
|
||||||
variant={live ? "outline-accent" : "outline"}
|
variant={polling ? "outline-accent" : "outline"}
|
||||||
size="xs"
|
size="xs"
|
||||||
icon={<Radio className="h-3.5 w-3.5" />}
|
icon={<Radio className="h-3.5 w-3.5" />}
|
||||||
aria-pressed={live}
|
aria-pressed={polling}
|
||||||
onClick={toggleLive}
|
onClick={toggleLive}
|
||||||
>
|
>
|
||||||
{t("adminLogs.live")}
|
{t("adminLogs.live")}
|
||||||
@@ -296,14 +315,14 @@ export default function AdminLogs() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{stale && (
|
{failure && (
|
||||||
<StateMessage
|
<StateMessage
|
||||||
tone="error"
|
tone="error"
|
||||||
compact
|
compact
|
||||||
onRetry={retryStale}
|
onRetry={retryFailed}
|
||||||
className="border-b border-border"
|
className="border-b border-border"
|
||||||
>
|
>
|
||||||
{t("adminLogs.stale")}
|
{t(`adminLogs.${failure}`)}
|
||||||
</StateMessage>
|
</StateMessage>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"showJob": "Only this job's lines",
|
"showJob": "Only this job's lines",
|
||||||
"empty": "No lines match the filters.",
|
"empty": "No lines match the filters.",
|
||||||
"stale": "The last refresh failed; what you see may be out of date.",
|
"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_one": "{{count}} line loaded · newest first",
|
||||||
"count_other": "{{count}} lines loaded · newest first",
|
"count_other": "{{count}} lines loaded · newest first",
|
||||||
"older": "Load older",
|
"older": "Load older",
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
"showJob": "Csak ennek a jobnak a sorai",
|
"showJob": "Csak ennek a jobnak a sorai",
|
||||||
"empty": "Nincs a szűrésnek megfelelő sor.",
|
"empty": "Nincs a szűrésnek megfelelő sor.",
|
||||||
"stale": "Az utolsó frissítés nem sikerült, amit látsz, elavult lehet.",
|
"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_one": "{{count}} sor betöltve · a legújabb felül",
|
||||||
"count_other": "{{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",
|
"older": "Régebbiek betöltése",
|
||||||
|
|||||||
Reference in New Issue
Block a user