Files
siftlode/frontend/src/lib/activityUrlState.test.ts
T
peter 24448e7c6f feat(activity): one Activity page — runs, admin actions, server log and quota under one filter bar
The audit log, the server log and the admin quota view were three pages (the quota one a Stats tab),
each with its own filters, and the background runs had no list at all. Now one page ("Napló" /
"Activity") reads all four, each still its own table on the server: a shared bar (text, a period of
1 h to 90 days defaulting to 7, an account) and each tab's own filters, the whole view in the URL.
The old page ids open their tab.

Server side: the audit log and the new run list (`/api/admin/runs`) filter and page like the server
log, through one keyset helper (`routes/_keyset`) the log now shares; an audit row keeps the request
it was made in (migration 0111), so its log lines are a pivot away, as a run's are (`lrun`). The
quota breakdown takes since/until and one account. The Scheduler rows get a history button to their
job's runs.

Also fixed on the way: re-saving the value in force on the Configuration page staged its override
without committing it (the commit sat inside the audit branch), so the save was lost.
2026-10-18 03:45:18 +02:00

65 lines
2.4 KiB
TypeScript

// The activity page's view in the address bar: a link carries the tab and every filter, a hand-edited
// value the API would refuse degrades to "no filter", a filter at its default leaves no key behind,
// and a link to a page this one replaced opens that page's tab.
import { describe, expect, it } from "vitest";
import {
ACTIVITY_KEYS,
activityParamsToState,
activityParamUpdates,
DEFAULT_ACTIVITY_STATE,
sinceOf,
type ActivityUrlState,
} from "./activityUrlState";
const params = (updates: Record<string, string | null>) =>
new URLSearchParams(Object.entries(updates).filter((e): e is [string, string] => e[1] !== null));
describe("activityUrlState", () => {
it("round-trips the tab and every filter", () => {
const state: ActivityUrlState = {
tab: "admin",
window: "30d",
user: "none",
q: "100% done",
job: "channel_backfill",
status: "interrupted",
action: "config_set",
};
expect(activityParamsToState(params(activityParamUpdates(state)))).toEqual(state);
});
it("leaves no key for a filter at its default, and opens on the runs of the last 7 days", () => {
expect(
Object.values(activityParamUpdates(DEFAULT_ACTIVITY_STATE)).every((v) => v === null),
).toBe(true);
expect([DEFAULT_ACTIVITY_STATE.tab, DEFAULT_ACTIVITY_STATE.window]).toEqual(["runs", "7d"]);
});
it("drops a value the API would refuse instead of sending it", () => {
const state = activityParamsToState(
new URLSearchParams(
"at=everything&aw=1y&au=me&rjob=Not-A-Job&rst=finished&aact=DROP%20TABLE",
),
);
expect(state).toEqual(DEFAULT_ACTIVITY_STATE);
});
it("opens the tab of the page a link still names", () => {
expect(activityParamsToState(new URLSearchParams("page=audit")).tab).toBe("admin");
expect(activityParamsToState(new URLSearchParams("page=logs")).tab).toBe("logs");
// A tab the link names wins over the retired page's.
expect(activityParamsToState(new URLSearchParams("page=logs&at=quota")).tab).toBe("quota");
});
it("clears its own keys and the server log's on a page change", () => {
expect(ACTIVITY_KEYS).toEqual(expect.arrayContaining(["at", "aw", "au", "aq", "rjob", "lreq"]));
});
it("starts the window that many hours back", () => {
const now = Date.UTC(2026, 9, 18, 12);
expect(sinceOf("24h", now)).toBe("2026-10-17T12:00:00.000Z");
expect(sinceOf("90d", now)).toBe("2026-07-20T12:00:00.000Z");
});
});