admin: reclaim the desktop header, move pause and log out to the sidebar

the header row survives only on narrow screens; on desktop its lone occupant, log out, joins pause in the sidebar footer, both full width. pause leaves the query detail page's related actions, where a global control had no business, and its hand-rolled duration dropdown becomes a react-aria menu with real keyboard navigation, dismissal and positioning.
This commit is contained in:
2026-08-29 11:23:17 +02:00
parent d5613ee718
commit 79578e1d2a
9 changed files with 164 additions and 150 deletions
+35 -2
View File
@@ -214,12 +214,45 @@ test("mount probe reveals the logout button and a failed logout surfaces inline"
renderShell();
fireEvent.click(await screen.findByRole("button", { name: "Log out" }));
// Both renderings are mounted; the viewport paints the sidebar one on WIDE
// and the header one below it.
await waitFor(() => expect(screen.getAllByRole("button", { name: "Log out" })).toHaveLength(2));
const aside = document.querySelector("aside") as HTMLElement;
fireEvent.click(within(aside).getByRole("button", { name: "Log out" }));
await screen.findByText("Rate limited. Try again in 7s.");
await within(aside).findByText("Rate limited. Try again in 7s.");
expect(screen.getByRole("heading", { name: "Overview" })).toBeTruthy();
});
test("Log out sits in the sidebar on wide, and only in the header below it", async () => {
stubFetch((url) =>
url === "/api/auth/login"
? new Response(JSON.stringify({ error: "password required" }), {
status: 401,
headers: { "content-type": "application/json" },
})
: null,
);
renderShell();
await screen.findByRole("heading", { name: "Overview" });
const aside = document.querySelector("aside") as HTMLElement;
const header = document.querySelector("header") as HTMLElement;
await waitFor(() => expect(within(aside).getByRole("button", { name: "Log out" })).toBeTruthy());
expect(within(header).getByRole("button", { name: "Log out" })).toBeTruthy();
// The header is the narrow rendering now: the Menu button is its only nav.
expect(within(header).getByRole("button", { name: "Menu" })).toBeTruthy();
// In the sidebar the control precedes the version label rather than crowding it.
const version = within(aside).getByText(/^nxdns v/);
const logout = within(aside).getByRole("button", { name: "Log out" });
expect(logout.compareDocumentPosition(version) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
// The drawer keeps no third copy: the header already carries the narrow one.
fireEvent.click(within(header).getByRole("button", { name: "Menu" }));
const drawer = document.getElementById("mobile-nav") as HTMLElement;
expect(within(drawer).queryByRole("button", { name: "Log out" })).toBeNull();
});
test("the header carries no protection display at all any more", async () => {
renderShell();
await screen.findByRole("heading", { name: "Overview" });
+16 -3
View File
@@ -110,6 +110,17 @@ const styles = stylex.create({
alignItems: "center",
gap: "0.5rem",
},
/**
* In a 14rem column the button and its error stack, so the button spans the
* sidebar and matches the Pause trigger below it.
*/
sidebarLogout: {
flexDirection: "column",
alignItems: "stretch",
gap: "0.25rem",
paddingInline: "1rem",
paddingTop: "0.75rem",
},
shell: {
minHeight: "100dvh",
height: { default: null, [WIDE]: "100dvh" },
@@ -147,8 +158,9 @@ const styles = stylex.create({
minWidth: { default: null, [WIDE]: 0 },
flexDirection: "column",
},
/** Narrow only: on WIDE the sidebar carries everything this row held. */
header: {
display: "flex",
display: { default: "flex", [WIDE]: "none" },
alignItems: "center",
gap: "0.75rem",
borderBottomWidth: 1,
@@ -284,13 +296,13 @@ function VersionFooter() {
);
}
function LogoutButton() {
function LogoutButton({ style }: { style?: stylex.StyleXStyles }) {
const { authRequired, logout } = useAuth();
const navigate = useNavigate();
const [error, setError] = useState<unknown>(null);
if (authRequired !== true) return null;
return (
<div {...stylex.props(styles.logoutRow)}>
<div {...stylex.props(styles.logoutRow, style)}>
<button
type="button"
onClick={() => {
@@ -318,6 +330,7 @@ export default function AppShell() {
<nav aria-label="Main" {...stylex.props(styles.sidebarNav)}>
<NavLinks />
</nav>
<LogoutButton style={styles.sidebarLogout} />
<SidebarFooter />
</aside>
<div {...stylex.props(styles.column)}>