fix(activity): an old audit/log link opens its tab, times read relative, one name for admin actions

Seen in the browser, not in the page's own tests: the navigation provider rewrites a retired page id
to `activity` before the page's chunk reads the URL, so `?page=audit` landed on the Runs tab. The
retired-page map now carries the params that open the successor's part (`at`), and the provider
writes them as it rewrites the page — on load and on Back alike.

A list time is relative with the exact second on hover (the DESIGN copy rule); a run moved over from
the audit log, whose start and end are one instant, shows no length instead of an invented "1 sec".
The audit log is called "admin actions" everywhere it is named (the clear button and its confirm, the
Configuration group, the cleanup job, which also prunes the run history its description left out).
This commit is contained in:
2026-10-18 04:00:38 +02:00
parent fe6308d844
commit c288ba147b
18 changed files with 133 additions and 92 deletions
+21 -4
View File
@@ -3,7 +3,7 @@
// The activity page around its run and audit lists (the server-log tab has its own file): the view
// is the URL — a link narrows the runs to one job, a status chip narrows them further; a run opens
// to its log lines and an admin action to its request's, each a new history entry that Back undoes;
// a link to the retired audit page opens the admin tab; the account filter reaches the quota tab; a
// the account filter reaches the quota tab; a
// failed read is an error with a retry, never an empty list.
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { act, cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
@@ -168,11 +168,10 @@ describe("Activity", () => {
expect(screen.getByText("quotaExceeded")).toBeTruthy();
});
it("a link to the retired audit page opens the admin tab, and an action opens its request's lines", async () => {
window.history.replaceState(null, "", "/?page=audit");
it("the admin tab lists the actions, and an action opens its request's lines", async () => {
window.history.replaceState(null, "", "/?page=activity&at=admin");
renderPage();
await waitFor(() => expect(screen.getByText("auditActions.scheduler_interval")).toBeTruthy());
expect(url().get("page")).toBe("activity");
expect(runsMock).not.toHaveBeenCalled();
fireEvent.click(rowButton(/auditActions\.scheduler_interval/));
@@ -197,6 +196,24 @@ describe("Activity", () => {
expect(Math.round(days)).toBe(30);
});
// The runs moved over from the audit log kept one instant for start and end: their length is
// unknown, and a "1 sec" would be invented.
it("a run whose start and end are one instant has no length", async () => {
window.history.replaceState(null, "", "/?page=activity");
runsMock.mockResolvedValue(
page([
{ ...RUN, id: 1, status: "ok", error: null, finished_at: RUN.started_at, summary: "moved" },
{ ...RUN, id: 2, status: "ok", error: null, summary: "timed" },
]),
);
renderPage();
await waitFor(() => expect(screen.getByText("moved")).toBeTruthy());
const cells = (text: string) =>
[...rowButton(new RegExp(text)).children].map((c) => c.textContent);
expect(cells("moved")).toContain("—");
expect(cells("timed")).not.toContain("—");
});
it("a failed read is an error with a retry, not an empty list", async () => {
window.history.replaceState(null, "", "/?page=activity");
runsMock.mockRejectedValue(new Error("502"));