fix(diagnostics): Back from a request's lines returns to the view it came from
Every filter change replaced the history entry, so Back after a request or job pivot left the console altogether (the 0.90 notes promised otherwise). A pivot follows a link and is its own entry now; the console reads the landed entry back on popstate. The other filters still adjust in place.
This commit is contained in:
@@ -124,6 +124,42 @@ describe("AdminLogs", () => {
|
|||||||
expect([url.get("lreq"), url.get("lgrp"), url.get("lq")]).toEqual(["req-00000001", null, null]);
|
expect([url.get("lreq"), url.get("lgrp"), url.get("lq")]).toEqual(["req-00000001", null, null]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Back from a request's lines returns to the view the pivot was made from", async () => {
|
||||||
|
window.history.replaceState({ sfPage: "logs" }, "", "/?page=logs&lgrp=error&lq=boom");
|
||||||
|
const depth = window.history.length;
|
||||||
|
logsMock.mockResolvedValue({ items: [row(1)], accounts: {}, next_cursor: null });
|
||||||
|
renderConsole();
|
||||||
|
await waitFor(() => expect(lines()).toHaveLength(1));
|
||||||
|
fireEvent.click(lines()[0]!);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "req-00000001" }));
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(screen.getByText("adminLogs.pivotRequest:req-00000001")).toBeTruthy(),
|
||||||
|
);
|
||||||
|
expect(window.history.length).toBe(depth + 1);
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
const landed = new Promise((resolve) =>
|
||||||
|
window.addEventListener("popstate", resolve, { once: true }),
|
||||||
|
);
|
||||||
|
window.history.back();
|
||||||
|
await landed;
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(logsMock).toHaveBeenLastCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
request_id: undefined,
|
||||||
|
levels: ["ERROR", "CRITICAL"],
|
||||||
|
q: "boom",
|
||||||
|
}),
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(screen.queryByText("adminLogs.pivotRequest:req-00000001")).toBeNull();
|
||||||
|
const url = new URLSearchParams(window.location.search);
|
||||||
|
expect([url.get("lreq"), url.get("lgrp"), url.get("lq")]).toEqual([null, "error", "boom"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps the account in use offered when the facets no longer count it", async () => {
|
it("keeps the account in use offered when the facets no longer count it", async () => {
|
||||||
window.history.replaceState(null, "", "/?page=logs&lu=7");
|
window.history.replaceState(null, "", "/?page=logs&lu=7");
|
||||||
logsMock.mockResolvedValue({ items: [], accounts: {}, next_cursor: null });
|
logsMock.mockResolvedValue({ items: [], accounts: {}, next_cursor: null });
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
keepPreviousData,
|
keepPreviousData,
|
||||||
@@ -11,6 +11,7 @@ import { Copy, Radio, Search, X } from "lucide-react";
|
|||||||
import { api, type LogFacet, type LogQuery, type ServerLogRow } from "../lib/api";
|
import { api, type LogFacet, type LogQuery, type ServerLogRow } from "../lib/api";
|
||||||
import { copyText } from "../lib/clipboard";
|
import { copyText } from "../lib/clipboard";
|
||||||
import { formatDateTime } from "../lib/format";
|
import { formatDateTime } from "../lib/format";
|
||||||
|
import { nextNavId, notifyNav, rememberNavPosition } from "../lib/history";
|
||||||
import { LEVEL_GROUPS, type LevelGroup } from "../lib/log";
|
import { LEVEL_GROUPS, type LevelGroup } from "../lib/log";
|
||||||
import {
|
import {
|
||||||
DEFAULT_LOGS_STATE,
|
DEFAULT_LOGS_STATE,
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
import { notify } from "../lib/notifications";
|
import { notify } from "../lib/notifications";
|
||||||
import { qk } from "../lib/queryKeys";
|
import { qk } from "../lib/queryKeys";
|
||||||
import { useDebounced } from "../lib/useDebounced";
|
import { useDebounced } from "../lib/useDebounced";
|
||||||
|
import { urlWithParams } from "../lib/urlState";
|
||||||
import { useUrlMirror } from "../lib/useUrlMirror";
|
import { useUrlMirror } from "../lib/useUrlMirror";
|
||||||
import LogLine from "./LogLine";
|
import LogLine from "./LogLine";
|
||||||
import NavGrid from "./ui/NavGrid";
|
import NavGrid from "./ui/NavGrid";
|
||||||
@@ -82,6 +84,15 @@ export default function AdminLogs() {
|
|||||||
const [open, setOpen] = useState<number | null>(null);
|
const [open, setOpen] = useState<number | null>(null);
|
||||||
const q = useDebounced(state.q, 300);
|
const q = useDebounced(state.q, 300);
|
||||||
useUrlMirror(logsParamUpdates(state));
|
useUrlMirror(logsParamUpdates(state));
|
||||||
|
// Back/Forward lands on another view of the console (a pivot pushed one, below): read it back.
|
||||||
|
useEffect(() => {
|
||||||
|
const onPop = () => {
|
||||||
|
setOpen(null);
|
||||||
|
setState(logsParamsToState(new URLSearchParams(window.location.search)));
|
||||||
|
};
|
||||||
|
window.addEventListener("popstate", onPop);
|
||||||
|
return () => window.removeEventListener("popstate", onPop);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const update = (patch: Partial<LogsUrlState>) => {
|
const update = (patch: Partial<LogsUrlState>) => {
|
||||||
setOpen(null);
|
setOpen(null);
|
||||||
@@ -184,9 +195,18 @@ export default function AdminLogs() {
|
|||||||
return facets.data ? sum(levels.filter((f) => inGroup.includes(f.value))) : undefined;
|
return facets.data ? sum(levels.filter((f) => inGroup.includes(f.value))) : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// A pivot follows a link to another view, so it is its own history entry and Back returns to the
|
||||||
|
// view it was made from. The other filters are adjustments of the view on screen and replace it.
|
||||||
const pivot = (patch: Pick<LogsUrlState, "requestId" | "jobId">) => {
|
const pivot = (patch: Pick<LogsUrlState, "requestId" | "jobId">) => {
|
||||||
setLive(false);
|
setLive(false);
|
||||||
update({ ...PIVOT_RESET, ...patch });
|
const next = { ...state, ...PIVOT_RESET, ...patch };
|
||||||
|
window.history.pushState(
|
||||||
|
{ ...rememberNavPosition(), sfScroll: nextNavId() },
|
||||||
|
"",
|
||||||
|
urlWithParams(logsParamUpdates(next)),
|
||||||
|
);
|
||||||
|
notifyNav();
|
||||||
|
update(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
// "Any", then what the facet counted. The value in use stays offered even at zero — under the
|
// "Any", then what the facet counted. The value in use stays offered even at zero — under the
|
||||||
|
|||||||
Reference in New Issue
Block a user